From fc6a1824a5b587d7881dac71e429ffcc4859f786 Mon Sep 17 00:00:00 2001 From: gillian Date: Fri, 20 Feb 2026 16:38:27 -0500 Subject: [PATCH 01/18] Added change log for future code from utils --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5bd70e3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## [Unreleased] - 2026-02-20 + +### Added +- **Dual-Threshold Detection:** Logic to capture the start and end of signals, not just the peak. +- **Signal Smoothing & Noise Filters:** Prevents detections from breaking into fragments and ignores short interference spikes. +- **Auto-Frequency Calculation:** Automatically adjusts bounding boxes to fit signal frequency ranges tightly. + +### Changed +- **Signal Power Detection:** Switched from raw signal strength to power for improved accuracy. +- **CLI Workflow:** `Clear` and `Remove` commands now modify files directly (in-place) to avoid redundant copies. +- **Metadata Logic:** Updated labels to show detection percentages and overhauled internal metadata cleaning. +- **Viewer UI:** Moved legend outside the plot, added a black background, and adjusted transparency for better spectrogram visibility. + +### Fixed +- Prevented redundant `_annotated` suffixes in file naming patterns. +- Simplified internal math to increase processing speed and precision. \ No newline at end of file From f7eedfa2bd6bca7cdd3d0563858542a651af03fc Mon Sep 17 00:00:00 2001 From: gillian Date: Mon, 23 Feb 2026 13:48:46 -0500 Subject: [PATCH 02/18] Annotate added to cli --- .../ria_toolkit_oss/annotate.py | 813 ++++++++++++++++++ 1 file changed, 813 insertions(+) create mode 100644 src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py new file mode 100644 index 0000000..74e1370 --- /dev/null +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -0,0 +1,813 @@ +"""Annotate command - Automatic detection and manual annotation management.""" + +import json +from pathlib import Path + +import click + +from ria_toolkit_oss.annotations import ( + annotate_with_cusum, + detect_signals_energy, + split_recording_annotations, + threshold_qualifier, +) +from ria_toolkit_oss.data import Annotation +from ria_toolkit_oss.data.recording import Recording +from ria_toolkit_oss.io import load_recording, to_blue, to_npy, to_sigmf, to_wav +from ria_toolkit_oss_cli.common import format_frequency, format_sample_count + + +def normalize_sigmf_path(filepath): + """Normalize SigMF path to base name without extension.""" + path = Path(filepath) + + # Handle .sigmf-data, .sigmf-meta, or .sigmf + if ".sigmf" in path.suffix: + # Remove the suffix to get base name + return path.with_suffix("") + else: + return path + + +def detect_input_format(filepath): + """Detect file format from extension.""" + path = Path(filepath) + ext = path.suffix.lower() + + if ext in [".sigmf-data", ".sigmf-meta"]: + return "sigmf" + elif path.name.endswith(".sigmf"): + return "sigmf" + elif ext == ".npy": + return "npy" + elif ext == ".wav": + return "wav" + elif ext == ".blue": + return "blue" + else: + raise click.ClickException(f"Unknown format for '{filepath}'. Supported: .sigmf, .npy, .wav, .blue") + + +def determine_output_path(input_path, output_path, fmt, quiet, overwrite): + input_path = Path(input_path) + + if output_path: + target = Path(output_path) + final_path = target + else: + annotated_name = f"{input_path.stem}_annotated" + target = input_path.with_name(f"{annotated_name}{input_path.suffix}") + + if fmt == "sigmf": + final_path = normalize_sigmf_path(target) + if not quiet: + click.echo(f"Saving SigMF metadata to: {final_path}") + else: + final_path = target + if not quiet: + click.echo(f"Saving to: {final_path}") + + if final_path.exists() and not overwrite and final_path != input_path: + click.echo(f"Error: {final_path} already exists. Use --overwrite to replace it.", err=True) + return None + + return final_path + + +def save_recording_auto(recording, output_path, input_path, quiet=False, overwrite=False): + """Save recording, auto-detecting format from extension. + + For SigMF: Only overwrites metadata file, data file is unchanged + For other formats: Creates _annotated copy by default, unless overwrite=True + """ + input_path = Path(input_path) + fmt = detect_input_format(input_path) + + # Determine output path + output_path = determine_output_path( + input_path=input_path, output_path=output_path, fmt=fmt, quiet=quiet, overwrite=overwrite + ) + + if fmt == "sigmf": + # Normalize path for SigMF + base_path = output_path + stem = base_path.name + parent = base_path.parent + + # For SigMF: only save metadata, copy data if needed + meta_path = parent / f"{stem}.sigmf-meta" + data_path = parent / f"{stem}.sigmf-data" + + # If output is different from input, copy data file + input_base = normalize_sigmf_path(input_path) + if input_base != base_path: + import shutil + + # Construct input data path correctly + # input_base is like /path/to/recording or /path/to/recording.sigmf + # We need /path/to/recording.sigmf-data + if str(input_base).endswith(".sigmf"): + input_data = Path(str(input_base).replace(".sigmf", ".sigmf-data")) + else: + input_data = input_base.parent / f"{input_base.name}.sigmf-data" + if not quiet: + click.echo(f" Copying: {data_path}") + shutil.copy2(input_data, data_path) + + # Always save metadata (this is the whole point) + to_sigmf(recording, filename=stem, path=parent, overwrite=True) + + if not quiet: + click.echo(f" Updated: {meta_path}") + if input_base != base_path: + click.echo(f" Created: {data_path}") + + elif fmt == "npy": + to_npy(recording, filename=output_path.stem, path=output_path.parent, overwrite=True) + if not quiet: + click.echo(f" Created: {output_path}") + elif fmt == "wav": + to_wav(recording, filename=output_path.stem, path=output_path.parent, overwrite=True) + if not quiet: + click.echo(f" Created: {output_path}") + elif fmt == "blue": + to_blue(recording, filename=output_path.stem, path=output_path.parent, overwrite=True) + if not quiet: + click.echo(f" Created: {output_path}") + + +def determine_frequency_bounds(recording: Recording, freq_lower, freq_upper): + # Handle frequency bounds + if (freq_lower is None) != (freq_upper is None): + raise click.ClickException("Must specify both --freq-lower and --freq-upper, or neither") + + if freq_lower is None: + # Default to full bandwidth + sample_rate = recording.metadata.get("sample_rate", 1) + center_freq = recording.metadata.get("center_frequency", 0) + freq_lower = center_freq - (sample_rate / 2) + freq_upper = center_freq + (sample_rate / 2) + freq_default = True + else: + freq_default = False + if freq_lower >= freq_upper: + raise click.ClickException( + f"Invalid frequency range: lower ({format_frequency(freq_lower)}) " + f"must be < upper ({format_frequency(freq_upper)})" + ) + + return freq_lower, freq_upper, freq_default + + +def get_indices_list(indices, recording: Recording): + if indices: + try: + indices_list = [int(idx.strip()) for idx in indices.split(",")] + # Validate indices + for idx in indices_list: + if idx < 0 or idx >= len(recording.annotations): + raise click.ClickException( + f"Invalid index {idx}. Recording has {len(recording.annotations)} annotation(s)" + ) + except ValueError as e: + raise click.ClickException(f"Invalid indices format. Expected comma-separated integers: {e}") + + return indices_list + else: + return None + + +# ============================================================================ +# Main command group +# ============================================================================ + + +@click.group() +def annotate(): + """Manage and auto-detect annotations on RF recordings. + + \b + MANUAL MANAGEMENT: + list - List all current annotations + add - Manually add a specific annotation + remove - Delete an annotation by its index + clear - Remove all annotations from the recording + + \b + DETECTION & SEPARATION: + energy - Auto-detect using energy-based thresholding + cusum - Auto-detect segments using signal state changes + threshold - Auto-detect samples above magnitude percentage + separate - Auto-detect parallel frequency-offset signals, split into sub-bands + + \b + File Path Handling: + - SigMF files: Pass .sigmf-data, .sigmf-meta, or base name + - Other formats: .npy, .wav, .blue files + + \b + Output Behavior: + - SigMF: Updates .sigmf-meta only (data unchanged), in-place + - Other: Creates _annotated copy unless --overwrite specified + """ + pass + + +# ============================================================================ +# List subcommand +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--verbose", is_flag=True, help="Show detailed annotation info") +def list(input, verbose): + """List all annotations in a recording. + + \b + Examples: + utils annotate list recording.sigmf-data + utils annotate list signal.npy --verbose + """ + try: + recording = load_recording(input) + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if len(recording.annotations) == 0: + click.echo(f"No annotations in {Path(input).name}") + return + + click.echo(f"\nAnnotations in {Path(input).name}:") + for i, ann in enumerate(recording.annotations): + # Parse type from comment JSON + try: + comment_data = json.loads(ann.comment) + ann_type = comment_data.get("type", "unknown") + user_comment = comment_data.get("user_comment", "") + except (json.JSONDecodeError, TypeError): + ann_type = "unknown" + user_comment = ann.comment or "" + + # Basic info + freq_range = f"{format_frequency(ann.freq_lower_edge)} - {format_frequency(ann.freq_upper_edge)}" + click.echo( + f" [{i}] Samples {format_sample_count(ann.sample_start)}-" + f"{format_sample_count(ann.sample_start + ann.sample_count)}: {ann.label}" + ) + click.echo(f" Type: {ann_type}") + + if verbose: + if user_comment: + click.echo(f" Comment: {user_comment}") + click.echo(f" Frequency: {freq_range}") + if ann.detail: + click.echo(f" Detail: {ann.detail}") + + click.echo(f"\nTotal: {len(recording.annotations)} annotation(s)") + + +# ============================================================================ +# Add subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 200}) +@click.argument("input", type=click.Path(exists=True)) +@click.option("--start", type=int, required=True, help="Start sample index") +@click.option("--count", type=int, required=True, help="Sample count") +@click.option("--label", type=str, required=True, help="Annotation label") +@click.option("--freq-lower", type=float, help="Lower frequency edge (Hz)") +@click.option("--freq-upper", type=float, help="Upper frequency edge (Hz)") +@click.option("--comment", type=str, help="Human-readable comment") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def add(input, start, count, label, freq_lower, freq_upper, comment, annotation_type, output, overwrite, quiet): + """Add a manual annotation. + + \b + Examples: + utils annotate add file.npy --start 1000 --count 500 --label wifi + utils annotate add signal.sigmf-data --start 0 --count 1000 --label burst --comment "Strong signal" + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + # Validate sample range + n_samples = len(recording.data[0]) + if start < 0: + raise click.ClickException(f"--start must be >= 0, got {start}") + if count <= 0: + raise click.ClickException(f"--count must be > 0, got {count}") + if start + count > n_samples: + raise click.ClickException( + f"Invalid annotation range:\n" + f" Start: {start:,}\n" + f" Count: {count:,}\n" + f" End: {start + count:,}\n" + f"Recording only has {n_samples:,} samples" + ) + + # Handle frequency bounds + freq_lower, freq_upper, freq_default = determine_frequency_bounds( + recording=recording, freq_lower=freq_lower, freq_upper=freq_upper + ) + + # Build comment JSON + comment_data = {"type": annotation_type} + if comment: + comment_data["user_comment"] = comment + + # Create annotation + ann = Annotation( + sample_start=start, + sample_count=count, + freq_lower_edge=freq_lower, + freq_upper_edge=freq_upper, + label=label, + comment=json.dumps(comment_data), + detail={}, + ) + + recording._annotations.append(ann) + + if not quiet: + click.echo("\nAdding annotation:") + click.echo(f" Start: {format_sample_count(start)}") + click.echo(f" Count: {format_sample_count(count)} samples") + freq_str = ( + "full bandwidth" if freq_default else f"{format_frequency(freq_lower)} - {format_frequency(freq_upper)}" + ) + click.echo(f" Frequency: {freq_str}") + click.echo(f" Label: {label}") + click.echo(f" Type: {annotation_type}") + if comment: + click.echo(f" Comment: {comment}") + + try: + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Failed to save: {e}") + + +# ============================================================================ +# Remove subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 200}) +@click.argument("input", type=click.Path(exists=True)) +@click.argument("index", type=int) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def remove(input, index, output, overwrite, quiet): + """Remove annotation by index. + + Use 'utils annotate list' to see annotation indices. + + \b + Examples: + utils annotate remove signal.sigmf-data 2 + utils annotate remove file.npy 0 + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if index < 0 or index >= len(recording.annotations): + raise click.ClickException( + f"Cannot remove annotation at index {index}\n" + f"Recording has {len(recording.annotations)} annotation(s) (indices 0-{len(recording.annotations)-1})" + ) + + removed_ann = recording.annotations[index] + recording._annotations.pop(index) + + if not quiet: + click.echo(f"\nRemoving annotation [{index}]:") + click.echo( + f" Removed: samples {format_sample_count(removed_ann.sample_start)}-" + f"{format_sample_count(removed_ann.sample_start + removed_ann.sample_count)} ({removed_ann.label})" + ) + + try: + save_recording_auto(recording, output_path=input, input_path=input, quiet=quiet, overwrite=True) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Failed to save: {e}") + + +# ============================================================================ +# Clear subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 175}) +@click.argument("input", type=click.Path(exists=True)) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--force", is_flag=True, help="Skip confirmation") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def clear(input, output, overwrite, force, quiet): + """Clear all annotations. + + \b + Examples: + utils annotate clear signal.sigmf-data + utils annotate clear file.npy --force + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + count_before = len(recording.annotations) + + if count_before == 0: + if not quiet: + click.echo("No annotations to clear") + return + + # Confirm unless --force + if not force and not quiet: + click.echo(f"\nWarning: This will remove all {count_before} annotation(s)") + click.confirm("Continue?", abort=True) + + recording._annotations = [] + + if not quiet: + click.echo(f"\nCleared {count_before} annotation(s)") + + recording._annotations = [] + + try: + save_recording_auto(recording, output_path=input, input_path=input, quiet=quiet, overwrite=True) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Failed to save: {e}") + + +# ============================================================================ +# Energy detection subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 200}) +@click.argument("input", type=click.Path(exists=True)) +@click.option("--label", type=str, default="signal", help="Annotation label") +@click.option("--threshold", type=float, default=1.2, help="Threshold multiplier above noise floor") +@click.option("--segments", type=int, default=10, help="Number of segments for noise estimation") +@click.option("--window-size", type=int, default=200, help="Smoothing window size") +@click.option("--min-distance", type=int, default=5000, help="Min distance between detections") +@click.option( + "--freq-method", + type=click.Choice(["nbw", "obw", "full-detected", "full-bandwidth"]), + default="nbw", + help="Frequency bounding method", +) +@click.option("--nfft", type=int, default=None, help="FFT size for frequency calculation") +@click.option("--obw-power", type=float, default=0.99, help="Power percentage for OBW/NBW (0.98-0.9999)") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def energy( + input, + label, + threshold, + segments, + window_size, + min_distance, + freq_method, + nfft, + obw_power, + annotation_type, + output, + overwrite, + quiet, +): + """Auto-detect signals using energy-based method. + + Detects bursts based on energy above noise floor. Best for bursty signals + and intermittent transmissions. + + \b + Frequency Bounding Methods: + nbw - Nominal bandwidth (default, best for real signals) + obw - Occupied bandwidth (more conservative, includes sidelobes) + full-detected - Lowest to highest spectral component + full-bandwidth - Entire Nyquist span + + \b + Examples: + utils annotate energy capture.sigmf-data --label burst + utils annotate energy signal.npy --threshold 1.5 --min-distance 10000 + utils annotate energy signal.sigmf-data --freq-method obw + utils annotate energy signal.sigmf-data --freq-method full-detected + + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if not quiet: + click.echo("\nDetecting signals using energy-based method...") + click.echo(" Time detection:") + click.echo(f" Segments: {segments}") + click.echo(f" Threshold: {threshold}x noise floor") + click.echo(f" Window size: {window_size} samples") + click.echo(f" Min distance: {min_distance} samples") + click.echo(f" Frequency bounds: {freq_method}") + + try: + initial_count = len(recording.annotations) + recording = detect_signals_energy( + recording, + k=segments, + threshold_factor=threshold, + window_size=window_size, + min_distance=min_distance, + label=label, + annotation_type=annotation_type, + freq_method=freq_method, + nfft=nfft, + obw_power=obw_power, + ) + added = len(recording.annotations) - initial_count + + if not quiet: + click.echo(f" ✓ Added {added} annotation(s)") + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Energy detection failed: {e}") + + +# ============================================================================ +# CUSUM detection subcommand +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--label", type=str, default="segment", help="Annotation label") +@click.option("--min-duration", type=float, default=5.0, help="Min duration in ms (prevents over-segmentation)") +@click.option("--window-size", type=int, default=1, help="Smoothing window size") +@click.option("--tolerance", type=int, default=-1, help="Sample tolerance for merging") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def cusum(input, label, min_duration, window_size, tolerance, annotation_type, output, overwrite, quiet): + """Auto-detect segments using CUSUM method. + + Detects signal state changes (on/off, amplitude transitions). Best for + segmenting continuous signals. + + IMPORTANT: Always specify --min-duration to prevent excessive segmentation. + + \b + Examples: + utils annotate cusum signal.sigmf-data --min-duration 5.0 + utils annotate cusum data.npy --min-duration 10.0 --label state + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if not quiet: + click.echo("\nDetecting segments using CUSUM...") + click.echo(f" Min duration: {min_duration} ms") + if window_size != 1: + click.echo(f" Window size: {window_size} samples") + + try: + initial_count = len(recording.annotations) + recording = annotate_with_cusum( + recording, + label=label, + window_size=window_size, + min_duration=min_duration, + tolerance=tolerance, + annotation_type=annotation_type, + ) + added = len(recording.annotations) - initial_count + + if not quiet: + click.echo(f" ✓ Added {added} annotation(s)") + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"CUSUM detection failed: {e}") + + +# ============================================================================ +# Threshold detection subcommand +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--threshold", type=float, required=True, help="Threshold (0.0-1.0, fraction of max magnitude)") +@click.option("--label", type=str, default=None, help="Annotation label") +@click.option("--window-size", type=int, default=1024, help="Smoothing window size") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def threshold(input, threshold, label, window_size, annotation_type, output, overwrite, quiet): + """Auto-detect signals using threshold method. + + Detects samples above a percentage of maximum magnitude. Best for simple + power-based detection. + + \b + Examples: + utils annotate threshold signal.sigmf-data --threshold 0.7 --label wifi + utils annotate threshold data.npy --threshold 0.5 --window-size 2048 + """ + if not (0.0 <= threshold <= 1.0): + raise click.ClickException(f"--threshold must be between 0.0 and 1.0, got {threshold}") + + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if not quiet: + click.echo("\nDetecting signals using threshold qualifier...") + click.echo(f" Threshold: {threshold * 100:.1f}% of max magnitude") + click.echo(f" Window size: {window_size} samples") + + try: + initial_count = len(recording.annotations) + recording = threshold_qualifier( + recording, + threshold=threshold, + window_size=window_size, + label=label, + annotation_type=annotation_type, + ) + added = len(recording.annotations) - initial_count + + if not quiet: + click.echo(f" ✓ Added {added} annotation(s)") + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Threshold detection failed: {e}") + + +# ============================================================================ +# Separate subcommand (Phase 2: Parallel signal separation) +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--indices", type=str, help="Comma-separated annotation indices to split (default: all)") +@click.option("--nfft", type=int, default=65536, help="FFT size for spectral analysis") +@click.option("--noise-threshold-db", type=float, help="Noise floor threshold in dB (auto-estimated if not specified)") +@click.option("--min-component-bw", type=float, default=50e3, help="Min component bandwidth in Hz") +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +@click.option("--verbose", is_flag=True, help="Verbose output (show detected components)") +def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, overwrite, quiet, verbose): + """ + Auto-detect parallel frequency-offset signals and split into sub-bands. + + Provides methods to detect and separate overlapping frequency-domain signals + that occupy the same time window but different frequency bands. + + Detects multiple frequency components within single annotations and splits + them into separate annotations. Uses spectral peak detection with dual + bandwidth estimation. + + \b + Key Features: + - Spectral peak detection for frequency components + - Auto noise floor estimation (or user-specified) + - Dual bandwidth estimation: -3dB primary, cumulative power fallback + - Handles narrowband and wide signals (OFDM) + + \b + Examples: + utils annotate separate capture.sigmf-data + utils annotate separate signal.npy --indices 0,1,2 + utils annotate separate data.sigmf-data --noise-threshold-db -70 + utils annotate separate signal.npy --min-component-bw 100000 + + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + # Parse indices if specified + indices_list = get_indices_list(indices=indices, recording=recording) + + if len(recording.annotations) == 0: + if not quiet: + click.echo("No annotations to split") + return + + if not quiet: + click.echo("\nSplitting annotations by frequency components...") + click.echo(f" Input annotations: {len(recording.annotations)}") + if indices_list: + click.echo(f" Splitting indices: {indices_list}") + click.echo(f" FFT size: {nfft}") + if noise_threshold_db is not None: + click.echo(f" Noise threshold: {noise_threshold_db} dB") + else: + click.echo(" Noise threshold: auto-estimated") + click.echo(f" Min component BW: {format_frequency(min_component_bw)}") + + try: + initial_count = len(recording.annotations) + + recording = split_recording_annotations( + recording, + indices=indices_list, + nfft=nfft, + noise_threshold_db=noise_threshold_db, + min_component_bw=min_component_bw, + ) + + final_count = len(recording.annotations) + added = final_count - initial_count + + if not quiet: + click.echo(f" ✓ Output annotations: {final_count} ({'+' if added >= 0 else ''}{added} change)") + if verbose and added > 0: + click.echo("\n Details:") + for i in range(initial_count, final_count): + ann = recording.annotations[i] + freq_range = f"{format_frequency(ann.freq_lower_edge)} - {format_frequency(ann.freq_upper_edge)}" + click.echo( + f" [{i}] samples {format_sample_count(ann.sample_start)}-" + f"{format_sample_count(ann.sample_start + ann.sample_count)}: {freq_range}" + ) + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Spectral separation failed: {e}") From 4ee8ee5fe0ff06dc0cca03b0268016715a126170 Mon Sep 17 00:00:00 2001 From: gillian Date: Mon, 23 Feb 2026 14:00:06 -0500 Subject: [PATCH 03/18] Moving from utils --- src/ria_toolkit_oss/annotations/__init__.py | 55 +++++++++ src/ria_toolkit_oss/data/annotation.py | 128 ++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 src/ria_toolkit_oss/annotations/__init__.py create mode 100644 src/ria_toolkit_oss/data/annotation.py diff --git a/src/ria_toolkit_oss/annotations/__init__.py b/src/ria_toolkit_oss/annotations/__init__.py new file mode 100644 index 0000000..1d0a6df --- /dev/null +++ b/src/ria_toolkit_oss/annotations/__init__.py @@ -0,0 +1,55 @@ + +""" +The annotations package contains tools and utilities for creating, managing, and processing annotations. + +Provides automatic annotation generation using various signal detection algorithms: +- Energy-based detection (detect_signals_energy) +- CUSUM-based segmentation (annotate_with_cusum) +- Threshold-based qualification (threshold_qualifier) +- Signal isolation and extraction (isolate_signal) +- Occupied bandwidth analysis (calculate_occupied_bandwidth, calculate_nominal_bandwidth) + +All detection functions return Recording objects with added annotations. +""" + +__all__ = [ + # Energy-based detection + "detect_signals_energy", + "calculate_occupied_bandwidth", + "calculate_nominal_bandwidth", + "calculate_full_detected_bandwidth", + "annotate_with_obw", + # CUSUM detection + "annotate_with_cusum", + # Threshold detection + "threshold_qualifier", + # Parallel signal separation (Phase 2) + "find_spectral_components", + "split_annotation_by_components", + "split_recording_annotations", + # Signal isolation + "isolate_signal", + # Annotation transforms + "remove_contained_boxes", + "is_annotation_contained", + # Dataset creation + "qualify_slice_from_annotations", +] + +from .annotation_transforms import is_annotation_contained, remove_contained_boxes +from .cusum_annotator import annotate_with_cusum +from .energy_detector import ( + annotate_with_obw, + calculate_full_detected_bandwidth, + calculate_nominal_bandwidth, + calculate_occupied_bandwidth, + detect_signals_energy, +) +from .parallel_signal_separator import ( + find_spectral_components, + split_annotation_by_components, + split_recording_annotations, +) +from .qualify_slice import qualify_slice_from_annotations +from .signal_isolation import isolate_signal +from .threshold_qualifier import threshold_qualifier \ No newline at end of file diff --git a/src/ria_toolkit_oss/data/annotation.py b/src/ria_toolkit_oss/data/annotation.py new file mode 100644 index 0000000..1182480 --- /dev/null +++ b/src/ria_toolkit_oss/data/annotation.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import json +from typing import Any, Optional + +from sigmf import SigMFFile + + +class Annotation: + """Signal annotations are labels or additional information associated with specific data points or segments within + a signal. These annotations could be used for tasks like supervised learning, where the goal is to train a model + to recognize patterns or characteristics in the signal associated with these annotations. + + Annotations can be used to label interesting points in your recording. + + :param sample_start: The index of the starting sample of the annotation. + :type sample_start: int + :param sample_count: The index of the ending sample of the annotation, inclusive. + :type sample_count: int + :param freq_lower_edge: The lower frequency of the annotation. + :type freq_lower_edge: float + :param freq_upper_edge: The upper frequency of the annotation. + :type freq_upper_edge: float + :param label: The label that will be displayed with the bounding box in compatible viewers including IQEngine. + Defaults to an emtpy string. + :type label: str, optional + :param comment: A human-readable comment. Defaults to an empty string. + :type comment: str, optional + :param detail: A dictionary of user defined annotation-specific metadata. Defaults to None. + :type detail: dict, optional + """ + + def __init__( + self, + sample_start: int, + sample_count: int, + freq_lower_edge: float, + freq_upper_edge: float, + label: Optional[str] = "", + comment: Optional[str] = "", + detail: Optional[dict] = None, + ): + """Initialize a new Annotation instance.""" + self.sample_start = int(sample_start) + self.sample_count = int(sample_count) + self.freq_lower_edge = float(freq_lower_edge) + self.freq_upper_edge = float(freq_upper_edge) + self.label = str(label) + self.comment = str(comment) + + if detail is None: + self.detail = {} + elif not _is_jsonable(detail): + raise ValueError(f"Detail object is not json serializable: {detail}") + else: + self.detail = detail + + def is_valid(self) -> bool: + """ + Check that the annotation sample count is > 0 and the freq_lower_edge 0 and self.freq_lower_edge < self.freq_upper_edge + + def overlap(self, other): + """ + Quantify how much the bounding box in this annotation overlaps with another annotation. + + :param other: The other annotation. + :type other: Annotation + + :returns: The area of the overlap in samples*frequency, or 0 if they do not overlap.""" + + sample_overlap_start = max(self.sample_start, other.sample_start) + sample_overlap_end = min(self.sample_start + self.sample_count, other.sample_start + other.sample_count) + + freq_overlap_start = max(self.freq_lower_edge, other.freq_lower_edge) + freq_overlap_end = min(self.freq_upper_edge, other.freq_upper_edge) + + if freq_overlap_start >= freq_overlap_end or sample_overlap_start >= sample_overlap_end: + return 0 + else: + return (sample_overlap_end - sample_overlap_start) * (freq_overlap_end - freq_overlap_start) + + def area(self): + """ + The 'area' of the bounding box, samples*frequency. + Useful to quantify annotation size. + + :returns: sample length multiplied by bandwidth.""" + + return self.sample_count * (self.freq_upper_edge - self.freq_lower_edge) + + def __eq__(self, other: Annotation) -> bool: + return self.__dict__ == other.__dict__ + + def to_sigmf_format(self): + """ + Returns a JSON dictionary representing this annotation formatted to be saved in a .sigmf-meta file. + """ + + annotation_dict = {SigMFFile.START_INDEX_KEY: self.sample_start, SigMFFile.LENGTH_INDEX_KEY: self.sample_count} + + annotation_dict["metadata"] = { + SigMFFile.LABEL_KEY: self.label, + SigMFFile.COMMENT_KEY: self.comment, + SigMFFile.FHI_KEY: self.freq_upper_edge, + SigMFFile.FLO_KEY: self.freq_lower_edge, + "ria:detail": self.detail, + } + + if _is_jsonable(annotation_dict): + return annotation_dict + else: + raise ValueError("Annotation dictionary was not json serializable.") + + +def _is_jsonable(x: Any) -> bool: + """ + :return: True if x is JSON serializable, False otherwise. + """ + try: + json.dumps(x) + return True + except (TypeError, OverflowError): + return False From 5c0c20619fcc008234a5b070c5c3bcb789811247 Mon Sep 17 00:00:00 2001 From: gillian Date: Mon, 23 Feb 2026 14:00:59 -0500 Subject: [PATCH 04/18] Moving over from utils --- src/ria_toolkit_oss/data/__init__.py | 8 + src/ria_toolkit_oss/data/recording.py | 853 ++++++++++++++++++++++++++ 2 files changed, 861 insertions(+) create mode 100644 src/ria_toolkit_oss/data/__init__.py create mode 100644 src/ria_toolkit_oss/data/recording.py diff --git a/src/ria_toolkit_oss/data/__init__.py b/src/ria_toolkit_oss/data/__init__.py new file mode 100644 index 0000000..b72f469 --- /dev/null +++ b/src/ria_toolkit_oss/data/__init__.py @@ -0,0 +1,8 @@ +""" +The Data package contains abstract data types tailored for radio machine learning, such as ``Recording``, as well +as the abstract interfaces for the radio dataset and radio dataset builder framework. +""" + +__all__ = ["Annotation", "Recording"] +from .annotation import Annotation +from .recording import Recording diff --git a/src/ria_toolkit_oss/data/recording.py b/src/ria_toolkit_oss/data/recording.py new file mode 100644 index 0000000..50e03ee --- /dev/null +++ b/src/ria_toolkit_oss/data/recording.py @@ -0,0 +1,853 @@ +from __future__ import annotations + +import copy +import hashlib +import json +import os +import re +import time +import warnings +from typing import Any, Iterator, Optional + +import numpy as np +from numpy.typing import ArrayLike + +from utils.data.annotation import Annotation + +PROTECTED_KEYS = ["rec_id", "timestamp"] + + +class Recording: + """Tape of complex IQ (in-phase and quadrature) samples with associated metadata and annotations. + + Recording data is a complex array of shape C x N, where C is the number of channels + and N is the number of samples in each channel. + + Metadata is stored in a dictionary of key value pairs, + to include information such as sample_rate and center_frequency. + + Annotations are a list of :ref:`Annotation `, + defining bounding boxes in time and frequency with labels and metadata. + + Here, signal data is represented as a NumPy array. This class is then extended in the RIA Backends to provide + support for different data structures, such as Tensors. + + Recordings are long-form tapes can be obtained either from a software-defined radio (SDR) or generated + synthetically. Then, machine learning datasets are curated from collection of recordings by segmenting these + longer-form tapes into shorter units called slices. + + All recordings are assigned a unique 64-character recording ID, ``rec_id``. If this field is missing from the + provided metadata, a new ID will be generated upon object instantiation. + + :param data: Signal data as a tape IQ samples, either C x N complex, where C is the number of + channels and N is number of samples in the signal. If data is a one-dimensional array of complex samples with + length N, it will be reshaped to a two-dimensional array with dimensions 1 x N. + :type data: array_like + + :param metadata: Additional information associated with the recording. + :type metadata: dict, optional + :param annotations: A collection of ``Annotation`` objects defining bounding boxes. + :type annotations: list of Annotations, optional + + :param dtype: Explicitly specify the data-type of the complex samples. Must be a complex NumPy type, such as + ``np.complex64`` or ``np.complex128``. Default is None, in which case the type is determined implicitly. If + ``data`` is a NumPy array, the Recording will use the dtype of ``data`` directly without any conversion. + :type dtype: numpy dtype object, optional + :param timestamp: The timestamp when the recording data was generated. If provided, it should be a float or integer + representing the time in seconds since epoch (e.g., ``time.time()``). Only used if the `timestamp` field is not + present in the provided metadata. + :type dtype: float or int, optional + + :raises ValueError: If data is not complex 1xN or CxN. + :raises ValueError: If metadata is not a python dict. + :raises ValueError: If metadata is not json serializable. + :raises ValueError: If annotations is not a list of valid annotation objects. + + **Examples:** + + >>> import numpy + >>> from utils.data import Recording, Annotation + + >>> # Create an array of complex samples, just 1s in this case. + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + + >>> # Create a dictionary of relevant metadata. + >>> sample_rate = 1e6 + >>> center_frequency = 2.44e9 + >>> metadata = { + ... "sample_rate": sample_rate, + ... "center_frequency": center_frequency, + ... "author": "me", + ... } + + >>> # Create an annotation for the annotations list. + >>> annotations = [ + ... Annotation( + ... sample_start=0, + ... sample_count=1000, + ... freq_lower_edge=center_frequency - (sample_rate / 2), + ... freq_upper_edge=center_frequency + (sample_rate / 2), + ... label="example", + ... ) + ... ] + + >>> # Store samples, metadata, and annotations together in a convenient object. + >>> recording = Recording(data=samples, metadata=metadata, annotations=annotations) + >>> print(recording.metadata) + {'sample_rate': 1000000.0, 'center_frequency': 2440000000.0, 'author': 'me'} + >>> print(recording.annotations[0].label) + 'example' + """ + + def __init__( # noqa C901 + self, + data: ArrayLike | list[list], + metadata: Optional[dict[str, any]] = None, + dtype: Optional[np.dtype] = None, + timestamp: Optional[float | int] = None, + annotations: Optional[list[Annotation]] = None, + ): + + data_arr = np.asarray(data) + + if np.iscomplexobj(data_arr): + # Expect C x N + if data_arr.ndim == 1: + self._data = np.expand_dims(data_arr, axis=0) # N -> 1 x N + elif data_arr.ndim == 2: + self._data = data_arr + else: + raise ValueError("Complex data must be C x N.") + + else: + raise ValueError("Input data must be complex.") + + if dtype is not None: + self._data = self._data.astype(dtype) + + assert np.iscomplexobj(self._data) + + if metadata is None: + self._metadata = {} + elif isinstance(metadata, dict): + self._metadata = metadata + else: + raise ValueError(f"Metadata must be a python dict, but was {type(metadata)}.") + + if not _is_jsonable(metadata): + raise ValueError("Value must be JSON serializable.") + + if "timestamp" not in self.metadata: + if timestamp is not None: + if not isinstance(timestamp, (int, float)): + raise ValueError(f"timestamp must be int or float, not {type(timestamp)}") + self._metadata["timestamp"] = timestamp + else: + self._metadata["timestamp"] = time.time() + else: + if not isinstance(self._metadata["timestamp"], (int, float)): + raise ValueError("timestamp must be int or float, not ", type(self._metadata["timestamp"])) + + if "rec_id" not in self.metadata: + self._metadata["rec_id"] = generate_recording_id(data=self.data, timestamp=self._metadata["timestamp"]) + + if annotations is None: + self._annotations = [] + elif isinstance(annotations, list): + self._annotations = annotations + else: + raise ValueError("Annotations must be a list or None.") + + if not all(isinstance(annotation, Annotation) for annotation in self._annotations): + raise ValueError("All elements in self._annotations must be of type Annotation.") + + self._index = 0 + + @property + def data(self) -> np.ndarray: + """ + :return: Recording data, as a complex array. + :type: np.ndarray + + .. note:: + + For recordings with more than 1,024 samples, this property returns a read-only view of the data. + + .. note:: + + To access specific samples, consider indexing the object directly with ``rec[c, n]``. + """ + if self._data.size > 1024: + # Returning a read-only view prevents mutation at a distance while maintaining performance. + v = self._data.view() + v.setflags(write=False) + return v + else: + return self._data.copy() + + @property + def metadata(self) -> dict: + """ + :return: Dictionary of recording metadata. + :type: dict + """ + return self._metadata.copy() + + @property + def annotations(self) -> list[Annotation]: + """ + :return: List of recording annotations + :type: list of Annotation objects + """ + return self._annotations.copy() + + @property + def shape(self) -> tuple[int]: + """ + :return: The shape of the data array. + :type: tuple of ints + """ + return np.shape(self.data) + + @property + def n_chan(self) -> int: + """ + :return: The number of channels in the recording. + :type: int + """ + return self.shape[0] + + @property + def rec_id(self) -> str: + """ + :return: Recording ID. + :type: str + """ + return self.metadata["rec_id"] + + @property + def dtype(self) -> str: + """ + :return: Data-type of the data array's elements. + :type: numpy dtype object + """ + return self.data.dtype + + @property + def timestamp(self) -> float | int: + """ + :return: Recording timestamp (time in seconds since epoch). + :type: float or int + """ + return self.metadata["timestamp"] + + @property + def sample_rate(self) -> float | None: + """ + :return: Sample rate of the recording, or None if 'sample_rate' is not in metadata. + :type: str + """ + return self.metadata.get("sample_rate") + + @sample_rate.setter + def sample_rate(self, sample_rate: float | int) -> None: + """Set the sample rate of the recording. + + :param sample_rate: The sample rate of the recording. + :type sample_rate: float or int + + :return: None + """ + self.add_to_metadata(key="sample_rate", value=sample_rate) + + def astype(self, dtype: np.dtype) -> Recording: + """Copy of the recording, data cast to a specified type. + + .. todo: This method is not yet implemented. + + :param dtype: Data-type to which the array is cast. Must be a complex scalar type, such as ``np.complex64`` or + ``np.complex128``. + :type dtype: NumPy data type, optional + + .. note: Casting to a data type with less precision can risk losing data by truncating or rounding values, + potentially resulting in a loss of accuracy and significant information. + + :return: A new recording with the same metadata and data, with dtype. + + TODO: Add example usage. + """ + # Rather than check for a valid datatype, let's cast and check the result. This makes it easier to provide + # cross-platform support where the types are aliased across platforms. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") # Casting may generate user warnings. E.g., complex -> real + data = self.data.astype(dtype) + + if np.iscomplexobj(data): + return Recording(data=data, metadata=self.metadata, annotations=self.annotations) + else: + raise ValueError("dtype must be a complex number scalar type.") + + def add_to_metadata(self, key: str, value: Any) -> None: + """Add a new key-value pair to the recording metadata. + + :param key: New metadata key, must be snake_case. + :type key: str + :param value: Corresponding metadata value. + :type value: any + + :raises ValueError: If key is already in metadata or if key is not a valid metadata key. + :raises ValueError: If value is not JSON serializable. + + :return: None. + + **Examples:** + + Create a recording and add metadata: + + >>> import numpy + >>> from utils.data import Recording + >>> + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + >>> "sample_rate": 1e6, + >>> "center_frequency": 2.44e9, + >>> } + >>> + >>> recording = Recording(data=samples, metadata=metadata) + >>> print(recording.metadata) + {'sample_rate': 1000000.0, + 'center_frequency': 2440000000.0, + 'timestamp': 17369..., + 'rec_id': 'fda0f41...'} + >>> + >>> recording.add_to_metadata(key="author", value="me") + >>> print(recording.metadata) + {'sample_rate': 1000000.0, + 'center_frequency': 2440000000.0, + 'author': 'me', + 'timestamp': 17369..., + 'rec_id': 'fda0f41...'} + """ + if key in self.metadata: + raise ValueError( + f"Key {key} already in metadata. Use Recording.update_metadata() to modify existing fields." + ) + + if not _is_valid_metadata_key(key): + raise ValueError(f"Invalid metadata key: {key}.") + + if not _is_jsonable(value): + raise ValueError("Value must be JSON serializable.") + + self._metadata[key] = value + + def update_metadata(self, key: str, value: Any) -> None: + """Update the value of an existing metadata key, + or add the key value pair if it does not already exist. + + :param key: Existing metadata key. + :type key: str + :param value: New value to enter at key. + :type value: any + + :raises ValueError: If value is not JSON serializable + :raises ValueError: If key is protected. + + :return: None. + + **Examples:** + + Create a recording and update metadata: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + >>> "sample_rate": 1e6, + >>> "center_frequency": 2.44e9, + >>> "author": "me" + >>> } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> print(recording.metadata) + {'sample_rate': 1000000.0, + 'center_frequency': 2440000000.0, + 'author': "me", + 'timestamp': 17369... + 'rec_id': 'fda0f41...'} + + >>> recording.update_metadata(key="author", value=you") + >>> print(recording.metadata) + {'sample_rate': 1000000.0, + 'center_frequency': 2440000000.0, + 'author': "you", + 'timestamp': 17369... + 'rec_id': 'fda0f41...'} + """ + if key not in self.metadata: + self.add_to_metadata(key=key, value=value) + + if not _is_jsonable(value): + raise ValueError("Value must be JSON serializable.") + + if key in PROTECTED_KEYS: # Check protected keys. + raise ValueError(f"Key {key} is protected and cannot be modified or removed.") + + else: + self._metadata[key] = value + + def remove_from_metadata(self, key: str): + """ + Remove a key from the recording metadata. + Does not remove key if it is protected. + + :param key: The key to remove. + :type key: str + + :raises ValueError: If key is protected. + + :return: None. + + **Examples:** + + Create a recording and add metadata: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + ... "sample_rate": 1e6, + ... "center_frequency": 2.44e9, + ... } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> print(recording.metadata) + {'sample_rate': 1000000.0, + 'center_frequency': 2440000000.0, + 'timestamp': 17369..., # Example value + 'rec_id': 'fda0f41...'} # Example value + + >>> recording.add_to_metadata(key="author", value="me") + >>> print(recording.metadata) + {'sample_rate': 1000000.0, + 'center_frequency': 2440000000.0, + 'author': 'me', + 'timestamp': 17369..., # Example value + 'rec_id': 'fda0f41...'} # Example value + """ + if key not in PROTECTED_KEYS: + self._metadata.pop(key) + else: + raise ValueError(f"Key {key} is protected and cannot be modified or removed.") + + def view(self, output_path: Optional[str] = "images/signal.png", **kwargs) -> None: + """Create a plot of various signal visualizations as a PNG image. + + :param output_path: The output image path. Defaults to "images/signal.png". + :type output_path: str, optional + :param kwargs: Keyword arguments passed on to utils.view.view_sig. + :type: dict of keyword arguments + + **Examples:** + + Create a recording and view it as a plot in a .png image: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + >>> "sample_rate": 1e6, + >>> "center_frequency": 2.44e9, + >>> } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> recording.view() + """ + from utils.view import view_sig + + view_sig(recording=self, output_path=output_path, **kwargs) + + def simple_view(self, **kwargs) -> None: + """Create a plot of various signal visualizations as a PNG or SVG image. + + :param kwargs: Keyword arguments passed on to utils.view.view_signal_simple.create_plots. + :type: dict of keyword arguments + + **Examples:** + + Create a recording and view it as a plot in a .png image: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + >>> "sample_rate": 1e6, + >>> "center_frequency": 2.44e9, + >>> } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> recording.simple_view() + """ + from utils.view.view_signal_simple import view_simple_sig + + view_simple_sig(recording=self, **kwargs) + + def to_sigmf( + self, filename: Optional[str] = None, path: Optional[os.PathLike | str] = None, overwrite: bool = False + ) -> None: + """Write recording to a set of SigMF files. + + The SigMF io format is defined by the `SigMF Specification Project `_ + + :param recording: The recording to be written to file. + :type recording: utils.data.Recording + :param filename: The name of the file where the recording is to be saved. Defaults to auto generated filename. + :type filename: os.PathLike or str, optional + :param path: The directory path to where the recording is to be saved. Defaults to recordings/. + :type path: os.PathLike or str, optional + + :raises IOError: If there is an issue encountered during the file writing process. + + :return: None + + **Examples:** + + Create a recording and view it as a plot in a `.png` image: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + ... "sample_rate": 1e6, + ... "center_frequency": 2.44e9, + ... } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> recording.view() + """ + from utils.io.recording import to_sigmf + + to_sigmf(filename=filename, path=path, recording=self, overwrite=overwrite) + + def to_npy( + self, filename: Optional[str] = None, path: Optional[os.PathLike | str] = None, overwrite: bool = False + ) -> str: + """Write recording to ``.npy`` binary file. + + :param filename: The name of the file where the recording is to be saved. Defaults to auto generated filename. + :type filename: os.PathLike or str, optional + :param path: The directory path to where the recording is to be saved. Defaults to recordings/. + :type path: os.PathLike or str, optional + + :raises IOError: If there is an issue encountered during the file writing process. + + :return: Path where the file was saved. + :rtype: str + + **Examples:** + + Create a recording and save it to a .npy file: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + >>> "sample_rate": 1e6, + >>> "center_frequency": 2.44e9, + >>> } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> recording.to_npy() + """ + from utils.io.recording import to_npy + + to_npy(recording=self, filename=filename, path=path, overwrite=overwrite) + + def to_wav( + self, + filename: Optional[str] = None, + path: Optional[os.PathLike | str] = None, + target_sample_rate: Optional[int] = 48000, + bits_per_sample: int = 32, + overwrite: bool = False, + ) -> str: + """Write recording to WAV file with embedded YAML metadata. + + WAV format uses stereo audio with I (in-phase) in left channel and Q (quadrature) in right channel. + Metadata is stored in standard LIST INFO chunks with RF-specific metadata encoded as YAML + in the ICMT (comment) field for human readability. + + :param filename: The name of the file where the recording is to be saved. Defaults to auto generated filename. + :type filename: os.PathLike or str, optional + :param path: The directory path to where the recording is to be saved. Defaults to recordings/. + :type path: os.PathLike or str, optional + :param target_sample_rate: Sample rate stored in the WAV header when no sample_rate metadata + is present. IQ samples are written without decimation or interpolation. Default is 48000 Hz. + :type target_sample_rate: int, optional + :param bits_per_sample: Bits per sample (32 for float32, 16 for int16). Default is 32. + :type bits_per_sample: int, optional + :param overwrite: Whether to overwrite existing files. Default is False. + :type overwrite: bool, optional + + :raises IOError: If there is an issue encountered during the file writing process. + + :return: Path where the file was saved. + :rtype: str + + **Examples:** + + Create a recording and save it to a .wav file: + + >>> import numpy + >>> from utils.data import Recording + >>> samples = numpy.exp(1j * 2 * numpy.pi * 0.1 * numpy.arange(10000)) + >>> metadata = {"sample_rate": 1e6, "center_frequency": 915e6} + >>> recording = Recording(data=samples, metadata=metadata) + >>> recording.to_wav() + """ + from utils.io.recording import to_wav + + return to_wav( + recording=self, + filename=filename, + path=path, + target_sample_rate=target_sample_rate, + bits_per_sample=bits_per_sample, + overwrite=overwrite, + ) + + def to_blue( + self, + filename: Optional[str] = None, + path: Optional[os.PathLike | str] = None, + data_format: str = "CI", + overwrite: bool = False, + ) -> str: + """Write recording to MIDAS Blue file format. + + MIDAS Blue is a legacy RF file format with a 512-byte binary header. + Commonly used with X-Midas and other RF/radar signal processing tools. + + :param filename: The name of the file where the recording is to be saved. Defaults to auto generated filename. + :type filename: os.PathLike or str, optional + :param path: The directory path to where the recording is to be saved. Defaults to recordings/. + :type path: os.PathLike or str, optional + :param data_format: Format code (default 'CI' = complex int16). + Common formats: 'CI' (complex int16), 'CF' (complex float32), 'CD' (complex float64). + Integer formats require the IQ samples to already be scaled within [-1, 1). + :type data_format: str, optional + :param overwrite: Whether to overwrite existing files. Default is False. + :type overwrite: bool, optional + + :raises IOError: If there is an issue encountered during the file writing process. + + :return: Path where the file was saved. + :rtype: str + + **Examples:** + + Create a recording and save it to a .blue file: + + >>> import numpy + >>> from utils.data import Recording + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = {"sample_rate": 1e6, "center_frequency": 2.44e9} + >>> recording = Recording(data=samples, metadata=metadata) + >>> recording.to_blue() + """ + from utils.io.recording import to_blue + + return to_blue(recording=self, filename=filename, path=path, data_format=data_format, overwrite=overwrite) + + def trim(self, num_samples: int, start_sample: Optional[int] = 0) -> Recording: + """Trim Recording samples to a desired length, shifting annotations to maintain alignment. + + :param start_sample: The start index of the desired trimmed recording. Defaults to 0. + :type start_sample: int, optional + :param num_samples: The number of samples that the output trimmed recording will have. + :type num_samples: int + :raises IndexError: If start_sample + num_samples is greater than the length of the recording. + :raises IndexError: If sample_start < 0 or num_samples < 0. + + :return: The trimmed Recording. + :rtype: Recording + + **Examples:** + + Create a recording and trim it: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) + >>> metadata = { + ... "sample_rate": 1e6, + ... "center_frequency": 2.44e9, + ... } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> print(len(recording)) + 10000 + + >>> trimmed_recording = recording.trim(start_sample=1000, num_samples=1000) + >>> print(len(trimmed_recording)) + 1000 + """ + + if start_sample < 0: + raise IndexError("start_sample cannot be < 0.") + elif start_sample + num_samples > len(self): + raise IndexError( + f"start_sample {start_sample} + num_samples {num_samples} > recording length {len(self)}." + ) + + end_sample = start_sample + num_samples + + data = self.data[:, start_sample:end_sample] + + new_annotations = copy.deepcopy(self.annotations) + for annotation in new_annotations: + # trim annotation if it goes outside the trim boundaries + if annotation.sample_start < start_sample: + annotation.sample_count = annotation.sample_count - (start_sample - annotation.sample_start) + annotation.sample_start = start_sample + + if annotation.sample_start + annotation.sample_count > end_sample: + annotation.sample_count = end_sample - annotation.sample_start + + # shift annotation to align with the new start point + annotation.sample_start = annotation.sample_start - start_sample + + return Recording(data=data, metadata=self.metadata, annotations=new_annotations) + + def normalize(self) -> Recording: + """Scale the recording data, relative to its maximum value, so that the magnitude of the maximum sample is 1. + + :return: Recording where the maximum sample amplitude is 1. + :rtype: Recording + + **Examples:** + + Create a recording with maximum amplitude 0.5 and normalize to a maximum amplitude of 1: + + >>> import numpy + >>> from utils.data import Recording + + >>> samples = numpy.ones(10000, dtype=numpy.complex64) * 0.5 + >>> metadata = { + ... "sample_rate": 1e6, + ... "center_frequency": 2.44e9, + ... } + + >>> recording = Recording(data=samples, metadata=metadata) + >>> print(numpy.max(numpy.abs(recording.data))) + 0.5 + + >>> normalized_recording = recording.normalize() + >>> print(numpy.max(numpy.abs(normalized_recording.data))) + 1 + """ + scaled_data = self.data / np.max(abs(self.data)) + return Recording(data=scaled_data, metadata=self.metadata, annotations=self.annotations) + + def __len__(self) -> int: + """The length of a recording is defined by the number of complex samples in each channel of the recording.""" + return self.shape[1] + + def __eq__(self, other: Recording) -> bool: + """Two Recordings are equal if all data, metadata, and annotations are the same.""" + + # counter used to allow for differently ordered annotation lists + return ( + np.array_equal(self.data, other.data) + and self.metadata == other.metadata + and self.annotations == other.annotations + ) + + def __ne__(self, other: Recording) -> bool: + """Two Recordings are equal if all data, and metadata, and annotations are the same.""" + return not self.__eq__(other=other) + + def __iter__(self) -> Iterator: + self._index = 0 + return self + + def __next__(self) -> np.ndarray: + if self._index < self.n_chan: + to_ret = self.data[self._index] + self._index += 1 + return to_ret + else: + raise StopIteration + + def __getitem__(self, key: int | tuple[int] | slice) -> np.ndarray | np.complexfloating: + """If key is an integer, tuple of integers, or a slice, return the corresponding samples. + + For arrays with 1,024 or fewer samples, return a copy of the recording data. For larger arrays, return a + read-only view. This prevents mutation at a distance while maintaining performance. + """ + if isinstance(key, (int, tuple, slice)): + v = self._data[key] + if isinstance(v, np.complexfloating): + return v + elif v.size > 1024: + v.setflags(write=False) # Make view read-only. + return v + else: + return v.copy() + + else: + raise ValueError(f"Key must be an integer, tuple, or slice but was {type(key)}.") + + def __setitem__(self, *args, **kwargs) -> None: + """Raise an error if an attempt is made to assign to the recording.""" + raise ValueError("Assignment to Recording is not allowed.") + + +def generate_recording_id(data: np.ndarray, timestamp: Optional[float | int] = None) -> str: + """Generate unique 64-character recording ID. The recording ID is generated by hashing the recording data with + the datetime that the recording data was generated. If no datatime is provided, the current datatime is used. + + :param data: Tape of IQ samples, as a NumPy array. + :type data: np.ndarray + :param timestamp: Unix timestamp in seconds. Defaults to None. + :type timestamp: float or int, optional + + :return: 256-character hash, to be used as the recording ID. + :rtype: str + """ + if timestamp is None: + timestamp = time.time() + + byte_sequence = data.tobytes() + str(timestamp).encode("utf-8") + sha256_hash = hashlib.sha256(byte_sequence) + + return sha256_hash.hexdigest() + + +def _is_jsonable(x: Any) -> bool: + """ + :return: True if x is JSON serializable, False otherwise. + """ + try: + json.dumps(x) + return True + except (TypeError, OverflowError): + return False + + +def _is_valid_metadata_key(key: Any) -> bool: + """ + :return: True if key is a valid metadata key, False otherwise. + """ + if isinstance(key, str) and key.islower() and re.match(pattern=r"^[a-z_]+$", string=key) is not None: + return True + + else: + return False From af3ae03bafb810a031e76de5a7736b0de184d18d Mon Sep 17 00:00:00 2001 From: gillian Date: Mon, 23 Feb 2026 14:09:42 -0500 Subject: [PATCH 05/18] Moving annotate into CLI --- src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py | 2 +- src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py | 4 ++-- src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py index 74e1370..1ae1e32 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -14,7 +14,7 @@ from ria_toolkit_oss.annotations import ( from ria_toolkit_oss.data import Annotation from ria_toolkit_oss.data.recording import Recording from ria_toolkit_oss.io import load_recording, to_blue, to_npy, to_sigmf, to_wav -from ria_toolkit_oss_cli.common import format_frequency, format_sample_count +from ria_toolkit_oss_cli.ria_toolkit_oss.common import format_frequency, format_sample_count def normalize_sigmf_path(filepath): diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py index 60ddba9..77cac92 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py @@ -2,7 +2,7 @@ """ This module contains all the CLI bindings for the ria package. """ - +from .annotate import annotate from .capture import capture from .combine import combine from .convert import convert @@ -16,7 +16,7 @@ from .init import init from .split import split from .transform import transform from .transmit import transmit -from .view import view +from .view import viewe # Aliases synth = generate diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py index 8e0b51f..9c67e80 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py @@ -7,7 +7,7 @@ from typing import Optional import click from ria_toolkit_oss.io.recording import from_npy, load_recording -from ria_toolkit_oss.view.view_signal import view_channels, view_sig +from ria_toolkit_oss.view.view_signal import view_annotations, view_channels, view_sig from ria_toolkit_oss.view.view_signal_simple import view_simple_sig from .common import echo_progress, echo_verbose, load_yaml_config @@ -33,6 +33,11 @@ VISUALIZATION_TYPES = { "dark", "spines", ], + }, + "annotations": { + "function": view_annotations, + "description": "Annotation-focused spectrogram view", + "options": ["channel", "dark"], }, "channels": {"function": view_channels, "description": "Multi-channel IQ and spectrogram view", "options": []}, } From 16ac8dbfb6034701c34017f27c42a5b2691bfde4 Mon Sep 17 00:00:00 2001 From: muq Date: Mon, 23 Feb 2026 14:12:34 -0500 Subject: [PATCH 06/18] updated annotations from utils to oss --- .../annotations/annotation_transforms.py | 55 +++ .../annotations/cusum_annotator.py | 203 ++++++++ .../annotations/energy_detector.py | 438 ++++++++++++++++++ .../annotations/parallel_signal_separator.py | 435 +++++++++++++++++ .../annotations/qualify_slice.py | 35 ++ .../annotations/signal_isolation.py | 97 ++++ .../annotations/threshold_qualifier.py | 212 +++++++++ src/ria_toolkit_oss/view/view_signal.py | 95 +++- .../view/view_signal_simple.py | 68 ++- 9 files changed, 1618 insertions(+), 20 deletions(-) create mode 100644 src/ria_toolkit_oss/annotations/annotation_transforms.py create mode 100644 src/ria_toolkit_oss/annotations/cusum_annotator.py create mode 100644 src/ria_toolkit_oss/annotations/energy_detector.py create mode 100644 src/ria_toolkit_oss/annotations/parallel_signal_separator.py create mode 100644 src/ria_toolkit_oss/annotations/qualify_slice.py create mode 100644 src/ria_toolkit_oss/annotations/signal_isolation.py create mode 100644 src/ria_toolkit_oss/annotations/threshold_qualifier.py diff --git a/src/ria_toolkit_oss/annotations/annotation_transforms.py b/src/ria_toolkit_oss/annotations/annotation_transforms.py new file mode 100644 index 0000000..af48465 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/annotation_transforms.py @@ -0,0 +1,55 @@ +from utils.data.annotation import Annotation + +# TODO figure out how to transfer labels in the merge case + + +def remove_contained_boxes(annotations: list[Annotation]): + """ + Remove all annotations (bounding boxes) that are entirely contained within other boxes in the list. + + :param annotations: A list of Annotation objects. + :type annotations: list[Annotation] + + :returns: A new list of Annotation objects. + :rtype: list[Annotation]""" + + output_boxes = [] + + for i in range(len(annotations)): + contained = False + for j in range(len(annotations)): + if i != j and is_annotation_contained(annotations[i], annotations[j]): + contained = True + break + + if not contained: + output_boxes.append(annotations[i]) + + return output_boxes + + +def is_annotation_contained(inner: Annotation, outer: Annotation) -> bool: + """ + Check if an annotation box is entirely contained within another annotation bounding box. + + :param inner: The inner box. + :type inner: Annotation. + :param outer: The outer box. + :type outer: Annotation. + + :returns: True if inner is within outer, false otherwise. + :rtype: bool + """ + + inner_sample_stop = inner.sample_start + inner.sample_count + outer_sample_stop = outer.sample_start + outer.sample_count + + if inner.sample_start > outer.sample_start and inner_sample_stop < outer_sample_stop: + if inner.freq_lower_edge > outer.freq_lower_edge and inner.freq_upper_edge < outer.freq_upper_edge: + return True + + return False + + +def merge_annotations(annotations: list[Annotation], overlap_threshold) -> list[Annotation]: + raise NotImplementedError diff --git a/src/ria_toolkit_oss/annotations/cusum_annotator.py b/src/ria_toolkit_oss/annotations/cusum_annotator.py new file mode 100644 index 0000000..a32162b --- /dev/null +++ b/src/ria_toolkit_oss/annotations/cusum_annotator.py @@ -0,0 +1,203 @@ +import json +from typing import Optional + +import numpy as np + +from utils.data import Annotation, Recording + + +def annotate_with_cusum( + recording: Recording, + label: Optional[str] = "segment", + window_size: Optional[int] = 1, + min_duration: Optional[float] = None, + tolerance: Optional[int] = None, + annotation_type: Optional[str] = "standalone", +): + """ + Add annotations that divide the recording into distinct time segments. + + This algorithm computes the cumulative sum of the sample magnitudes and + determines break points in the signal. + + This tool can be used to find points where a signal turns on or off, or + changes between a low and high amplitude. + + :param recording: A ``Recording`` object to annotate. + :type recording: ``utils.data.Recording`` + :param label: Label for the detected segments. + :type label: str + :param window_size: The length (in samples) of the moving average window. + :type window_size: int + :param min_duration: The minimum duration (in ms) of a segment. + The algorithm will not produce annotations shorter than this length. + :type min_duration: float + :param tolerance: The minimum length (in samples) of a segment. + :type tolerance: int + :param annotation_type: Annotation type (standalone, parallel, intersection). + :type annotation_type: str + """ + + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0) + + # Create an object of the time segmenter + time_segmenter = TimeSegmenter(sample_rate, min_duration, window_size, tolerance) + + change_points = time_segmenter.apply(recording.data[0]) + + time_segments_indices = np.append(np.insert(change_points, 0, 0), len(recording.data[0])) + annotations = [] + for i in range(len(time_segments_indices) - 1): + # Build comment JSON with type metadata + comment_data = { + "type": annotation_type, + "generator": "cusum_annotator", + "params": { + "window_size": window_size, + "min_duration": min_duration, + "tolerance": tolerance, + }, + } + f_min, f_max = detect_frequency( + signal=recording.data[0], + start=time_segments_indices[i], + stop=time_segments_indices[i + 1], + sample_rate=sample_rate, + ) + + annotations.append( + Annotation( + sample_start=time_segments_indices[i], + sample_count=time_segments_indices[i + 1] - time_segments_indices[i], + freq_lower_edge=center_frequency + f_min, + freq_upper_edge=center_frequency + f_max, + label=label, + comment=json.dumps(comment_data), + detail={"generator": "cusum_annotator"}, + ) + ) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + annotations) + + +def _compute_cusum(_signal, sample_rate: int, tolerance: int = None, min_duration: float = -1): + """ + This function efficiently computes the cumulative sum of a give list (_signal), with an optional tolerance. + + Args: + - _signal: array of iq samples. + - Tolerance: the least acceptable length of a block, Defaults to None. + + Returns: + - cusum (array): Array of the cumulative sum of the given list + - sample_rate (int): __description_ + - change_points (array): Array of the indices at which a change in the CUSUM direction happens. + - min_duration (float): The least acceptable time width of each segment (in ms). Defaults to -1. + """ + + # efficiently calculate the running sum of the signal + # cusum = list(itertools.accumulate((_signal - np.mean(_signal)))) + x = _signal - np.mean(_signal) + cusum = np.cumsum(x) + + # 'diff' computes the differences between the consecutive values, + # then 'sign' determines if it is +ve or -ve. + change_indicators = np.sign(np.diff(cusum)) + change_points = np.where(np.diff(change_indicators))[0] + 1 + + # Limit the change_points + # Reject those whose number of samples < minimum accepted #n of samples in (min duration) ms. + if min_duration is not None and min_duration > 0: + min_samples_wide = int(min_duration * sample_rate / 1000) + segments_lengths = np.diff(change_points) + segments_lengths = np.insert(segments_lengths, 0, change_points[0]) + change_points = change_points[np.where(segments_lengths > min_samples_wide)[0]] + return cusum, change_points + + +def detect_frequency(signal, start, stop, sample_rate): + signal_segment = signal[start:stop] + if len(signal_segment) > 0: + fft_data = np.abs(np.fft.fftshift(np.fft.fft(signal_segment))) + fft_freqs = np.fft.fftshift(np.fft.fftfreq(len(signal_segment), 1 / sample_rate)) + + # Use a spectral threshold to find the 'height' of the orange block + spectral_thresh = np.max(fft_data) * 0.15 + sig_indices = np.where(fft_data > spectral_thresh)[0] + + if len(sig_indices) > 4: + return fft_freqs[sig_indices[0]], fft_freqs[sig_indices[-1]] + else: + return -sample_rate / 4, sample_rate / 4 + else: + return -sample_rate / 4, sample_rate / 4 + + +class TimeSegmenter: + """Time Segmenter class, it creates a segmenter object with certain\ + characteristics to easily split an input signal to segments based on\ + the cumulative sum of deviations (of the signal mean) + """ + + def __init__( + self, sample_rate: int, min_duration: float = 1, moving_average_window: int = 3, tolerance: int = None + ): + """_summary_ + + Args: + sample_rate (int): _description_ + min_duration (float, optional): _description_. Defaults to 1. + moving_average_window (int, optional): _description_. Defaults to 3. + tolerance (int, optional): _description_. Defaults to None. + """ + self.sample_rate = sample_rate + self.min_duration = min_duration + self.moving_average_window = moving_average_window + self._moving_avg_filter = self._init_filter() + self.tolerance = tolerance + + def _init_filter(self): + """_summary_ + + Returns: + _type_: _description_ + """ + return np.ones(self.moving_average_window) / self.moving_average_window + + def _apply_filter(self, iqsignal: np.array): + """_summary_ + + Args: + iqsignal (np.array): _description_ + + Returns: + _type_: _description_ + """ + return np.convolve(abs(iqsignal), self._moving_avg_filter, mode="same") + + def _create_segments(self, iq_signal: np.array, change_points: np.array): + """_summary_ + + Args: + iq_signal (np.array): _description_ + change_points (np.array): _description_ + + Returns: + _type_: _description_ + """ + return np.split(iq_signal, change_points) + + def apply(self, iq_signal: np.array): + """_summary_ + + Args: + iq_signal (np.array): _description_ + + Returns: + _type_: _description_ + """ + smoothed_signal = self._apply_filter(iq_signal) + _, change_points = _compute_cusum(smoothed_signal, self.sample_rate, self.tolerance, self.min_duration) + # segments = self._create_segments(iq_signal, change_points) + return change_points diff --git a/src/ria_toolkit_oss/annotations/energy_detector.py b/src/ria_toolkit_oss/annotations/energy_detector.py new file mode 100644 index 0000000..6cc2466 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/energy_detector.py @@ -0,0 +1,438 @@ +""" +Energy-based signal detection and bandwidth analysis. + +Provides automatic annotation generation using energy-based signal detection +and occupied bandwidth calculation following ITU-R SM.328 standard. +""" + +import json +from typing import Tuple + +import numpy as np +from scipy.signal import filtfilt + +from utils.data import Annotation, Recording + + +def detect_signals_energy( + recording: Recording, + k: int = 10, + threshold_factor: float = 1.2, + window_size: int = 200, + min_distance: int = 5000, + label: str = "signal", + annotation_type: str = "standalone", + freq_method: str = "nbw", + nfft: int = None, + obw_power: float = 0.99, +) -> Recording: + """ + Detect signal bursts using energy-based method with adaptive noise floor estimation. + + This algorithm smooths the signal with a moving average filter, estimates the noise + floor from k segments, applies a threshold to detect regions above noise, and merges + nearby detections. Detected time boundaries are then assigned frequency bounds based + on the selected frequency method. + + Time Detection Algorithm: + 1. Smooth signal using moving average (envelope detection) + 2. Divide smoothed signal into k segments + 3. Estimate noise floor as median of segment mean powers + 4. Detect regions where power exceeds threshold_factor * noise_floor + 5. Merge regions closer than min_distance samples + + Frequency Bounding (freq_method): + - 'nbw': Nominal bandwidth (OBW + center frequency) - DEFAULT + - 'obw': Occupied bandwidth (99.99% power, includes siedelobes) + - 'full-detected': Lowest to highest spectral component + - 'full-bandwidth': Entire Nyquist span (center_freq ± sample_rate/2) + + :param recording: Recording to analyze + :type recording: Recording + :param k: Number of segments for noise floor estimation (default: 10) + :type k: int + :param threshold_factor: Threshold multiplier above noise floor (typical: 1.2-2.0, default: 1.2) + :type threshold_factor: float + :param window_size: Moving average window size in samples (default: 200) + :type window_size: int + :param min_distance: Minimum distance between separate signals in samples (default: 5000) + :type min_distance: int + :param label: Label for detected annotations (default: "signal") + :type label: str + :param annotation_type: Annotation type (standalone, parallel, intersection, default: standalone) + :type annotation_type: str + :param freq_method: How to calculate frequency bounds (default: 'nbw') + :type freq_method: str + :param nfft: FFT size for frequency calculations (default: None) + :type nfft: int + :param obw_power: Power percentage for OBW (0.9999 = 99.99%, default: 0.99) + :type obw_power: float + + :returns: New Recording with added annotations + :rtype: Recording + + **Example**:: + + >>> from utils.io import load_recording + >>> from utils.annotations import detect_signals_energy + >>> recording = load_recording("capture.sigmf") + + >>> # Detect with NBW frequency bounds (default, best for real signals) + >>> annotated = detect_signals_energy(recording, label="burst") + + >>> # Detect with OBW (more conservative, includes siedelobes) + >>> annotated = detect_signals_energy( + ... recording, label="burst", freq_method="obw" + ... ) + + >>> # Detect with full detected range (captures all spectral components) + >>> annotated = detect_signals_energy( + ... recording, label="burst", freq_method="full-detected" + ... ) + """ + # Extract signal data (use first channel only) + signal = recording.data[0] + + # Calculate smoothed signal power + kernel = np.ones(window_size) / window_size + smoothed_power = filtfilt(kernel, [1], np.abs(signal) ** 2) + + # Estimate noise floor using segment-based median (robust to signal presence) + segments = np.array_split(smoothed_power, k) + noise_floor = np.median([np.mean(s) for s in segments]) + + # Detect signal boundaries (regions above threshold) + enter = noise_floor * threshold_factor + exit = enter * 0.8 + boundaries = [] + start = None + active = False + + for i, p in enumerate(smoothed_power): + if not active and p > enter: + start = i + active = True + elif active and p < exit: + boundaries.append((start, i - start)) + active = False + + if active: + boundaries.append((start, len(smoothed_power) - start)) + + # Merge boundaries that are closer than min_distance + merged_boundaries = [] + if boundaries: + start, length = boundaries[0] + for next_start, next_length in boundaries[1:]: + if next_start - (start + length) < min_distance: + # Merge with current boundary + length = next_start + next_length - start + else: + # Save current and start new boundary + merged_boundaries.append((start, length)) + start, length = next_start, next_length + # Add final boundary + merged_boundaries.append((start, length)) + + # Create annotations from detected boundaries + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0) + + # Validate frequency method + valid_freq_methods = ["nbw", "obw", "full-detected", "full-bandwidth"] + if freq_method not in valid_freq_methods: + raise ValueError(f"Invalid freq_method '{freq_method}'. " f"Must be one of: {', '.join(valid_freq_methods)}") + + annotations = [] + for start_sample, sample_count in merged_boundaries: + # Calculate frequency bounds based on method + freq_lower, freq_upper = calculate_frequency_bounds( + freq_method, center_frequency, sample_rate, nfft, signal, start_sample, sample_count, obw_power + ) + # Build comment JSON with type metadata + comment_data = { + "type": annotation_type, + "generator": "energy_detector", + "freq_method": freq_method, + "params": { + "threshold_factor": threshold_factor, + "window_size": window_size, + "noise_floor": float(noise_floor), + "threshold": float(enter), + }, + } + + anno = Annotation( + sample_start=start_sample, + sample_count=sample_count, + freq_lower_edge=freq_lower, + freq_upper_edge=freq_upper, + label=label, + comment=json.dumps(comment_data), + detail={"generator": "energy_detector", "freq_method": freq_method}, + ) + annotations.append(anno) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + annotations) + + +def calculate_occupied_bandwidth( + signal: np.ndarray, + sampling_rate: float, + nfft: int = None, + power_percentage: float = 0.99, +): + if nfft is None: + nfft = max(65536, 2 ** int(np.floor(np.log2(len(signal))))) + + window = np.blackman(len(signal)) + spec = np.fft.fftshift(np.fft.fft(signal * window, n=nfft)) + + psd = np.abs(spec) ** 2 + psd = psd / psd.sum() # normalize + + freqs = np.fft.fftshift(np.fft.fftfreq(nfft, 1 / sampling_rate)) + + cdf = np.cumsum(psd) + + tail = (1 - power_percentage) / 2 + + lower_idx = np.searchsorted(cdf, tail) + upper_idx = np.searchsorted(cdf, 1 - tail) + + return freqs[upper_idx] - freqs[lower_idx], freqs[lower_idx], freqs[upper_idx] + + +def calculate_nominal_bandwidth( + signal: np.ndarray, + sampling_rate: float, + nfft: int = None, + power_percentage: float = 0.99, +) -> Tuple[float, float]: + """ + Calculate nominal bandwidth and center frequency. + + Nominal bandwidth (NBW) is the occupied bandwidth along with the center + frequency of the signal's spectral occupancy. Useful for characterizing + signals with unknown or drifting center frequencies. + + :param signal: Complex IQ signal samples + :type signal: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param nfft: FFT size + :type nfft: int + :param power_percentage: Fraction of power to contain + :type power_percentage: float + + :returns: Tuple of (nominal_bandwidth_hz, center_frequency_hz) + :rtype: Tuple[float, float] + + **Example**:: + + >>> from utils.annotations import calculate_nominal_bandwidth + >>> nbw, center = calculate_nominal_bandwidth(signal, sampling_rate=10e6) + >>> print(f"NBW: {nbw/1e6:.3f} MHz, Center: {center/1e6:.3f} MHz") + """ + bw, lower_freq, upper_freq = calculate_occupied_bandwidth(signal, sampling_rate, nfft, power_percentage) + + # Center frequency is midpoint of occupied band + center_freq = (lower_freq + upper_freq) / 2 + + return lower_freq, upper_freq, center_freq + + +def calculate_full_detected_bandwidth( + signal: np.ndarray, + sampling_rate: float, + nfft: int = None, + start_offset: int = 1000, +) -> Tuple[float, float, float]: + """ + Calculate frequency range from lowest to highest spectral component. + + Unlike OBW/NBW which define a power-based bandwidth, this calculates + the absolute frequency span from the lowest non-zero spectral component + to the highest non-zero component. + + Useful for: + - Signals with spectral gaps + - Multiple parallel signals (captures all of them) + - Understanding total occupied spectrum vs. actual bandwidth + + :param signal: Complex IQ signal samples + :type signal: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param nfft: FFT size + :type nfft: int + :param start_offset: Skip samples at start + :type start_offset: int + + :returns: Tuple of (bandwidth_hz, lower_freq_hz, upper_freq_hz) + :rtype: Tuple[float, float, float] + + **Example**:: + + >>> # Signal with two components at different frequencies + >>> bw, f_low, f_high = calculate_full_detected_bandwidth( + ... signal, sampling_rate=10e6, nfft=65536 + ... ) + >>> print(f"Full span: {f_low/1e6:.3f} to {f_high/1e6:.3f} MHz") + """ + # Validate input + if len(signal) < nfft + start_offset: + raise ValueError( + f"Signal too short: need {nfft + start_offset} samples, " + f"got {len(signal)}. Reduce nfft or start_offset." + ) + + # Extract segment + signal_segment = signal[start_offset : nfft + start_offset] + + # Compute FFT and power spectral density + freq_spectrum = np.fft.fft(signal_segment, n=nfft) + psd = np.abs(freq_spectrum) ** 2 + + # Shift to center DC + psd_shifted = np.fft.fftshift(psd) + freq_bins = np.fft.fftshift(np.fft.fftfreq(nfft, 1 / sampling_rate)) + + # Find noise floor (mean of lowest 10% of bins) and all bins above noise floor + noise_floor = np.mean(np.sort(psd_shifted)[: int(len(psd_shifted) * 0.1)]) + above_noise = np.where(psd_shifted > noise_floor * 1.5)[0] + + if len(above_noise) == 0: + # No signal above noise, return zero bandwidth + return 0.0, 0.0, 0.0 + + # Get frequency range of signal components + lower_idx = above_noise[0] + upper_idx = above_noise[-1] + + lower_freq = freq_bins[lower_idx] + upper_freq = freq_bins[upper_idx] + + bandwidth = upper_freq - lower_freq + + return bandwidth, lower_freq, upper_freq + + +def annotate_with_obw( + recording: Recording, + label: str = "signal", + annotation_type: str = "standalone", + nfft: int = None, + power_percentage: float = 0.99, +) -> Recording: + """ + Create a single annotation spanning the occupied bandwidth of the entire recording. + + Analyzes the full recording to find its occupied bandwidth and creates an annotation + covering that frequency range for the entire time duration. + + :param recording: Recording to analyze + :type recording: Recording + :param label: Annotation label + :type label: str + :param annotation_type: Annotation type + :type annotation_type: str + :param nfft: FFT size + :type nfft: int + :param power_percentage: Power percentage for OBW calculation + :type power_percentage: float + + :returns: Recording with OBW annotation added + :rtype: Recording + + **Example**:: + + >>> from utils.annotations import annotate_with_obw + >>> annotated = annotate_with_obw(recording, label="signal_obw") + """ + signal = recording.data[0] + sample_rate = recording.metadata["sample_rate"] + center_freq = recording.metadata.get("center_frequency", 0) + + # Calculate OBW + obw, lower_offset, upper_offset = calculate_occupied_bandwidth(signal, sample_rate, nfft, power_percentage) + + # Convert baseband offsets to absolute frequencies + freq_lower = center_freq + lower_offset + freq_upper = center_freq + upper_offset + + # Create comment JSON + comment_data = { + "type": annotation_type, + "generator": "obw_annotator", + "obw_hz": float(obw), + "power_percentage": power_percentage, + "params": {"nfft": nfft}, + } + + # Create annotation spanning entire recording + anno = Annotation( + sample_start=0, + sample_count=len(signal), + freq_lower_edge=freq_lower, + freq_upper_edge=freq_upper, + label=label, + comment=json.dumps(comment_data), + detail={"generator": "obw_annotator", "obw_hz": float(obw)}, + ) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + [anno]) + + +def calculate_frequency_bounds( + freq_method, center_frequency, sample_rate, nfft, signal, start_sample, sample_count, obw_power +): + if freq_method == "full-bandwidth": + # Full Nyquist span + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + else: + # Extract segment for frequency analysis + segment_start = start_sample + segment_end = min(start_sample + sample_count, len(signal)) + segment = signal[segment_start:segment_end] + + if nfft is None or len(segment) >= nfft: + if freq_method == "nbw": + # Nominal bandwidth (OBW + center frequency) + try: + lower_freq, upper_freq, _ = calculate_nominal_bandwidth(segment, sample_rate, nfft, obw_power) + freq_lower = center_frequency + lower_freq + freq_upper = center_frequency + upper_freq + except (ValueError, IndexError): + # Fallback if calculation fails + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + + elif freq_method == "obw": + # Occupied bandwidth + try: + _, f_lower, f_upper = calculate_occupied_bandwidth(segment, sample_rate, nfft, obw_power) + freq_lower = center_frequency + f_lower + freq_upper = center_frequency + f_upper + except (ValueError, IndexError): + # Fallback if calculation fails + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + + elif freq_method == "full-detected": + # Full detected range (lowest to highest component) + try: + _, f_lower, f_upper = calculate_full_detected_bandwidth(segment, sample_rate, nfft) + freq_lower = center_frequency + f_lower + freq_upper = center_frequency + f_upper + except (ValueError, IndexError): + # Fallback if calculation fails + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + else: + # Segment too short for FFT, use full bandwidth + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + + return freq_lower, freq_upper diff --git a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py new file mode 100644 index 0000000..b75a28f --- /dev/null +++ b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py @@ -0,0 +1,435 @@ +""" +Parallel signal separation for multi-component frequency-offset signals. + +Provides methods to detect and separate overlapping frequency-domain signals +that occupy the same time window but different frequency bands. + +This module implements **spectral peak detection** to identify distinct frequency +components and split single time-domain annotations into frequency-specific +sub-annotations. + +**Key Design Decisions** (per Codex review): + +1. **Complex IQ Support**: Uses `scipy.signal.welch` with `return_onesided=False` + for proper complex signal handling. Window length automatically adapts to + signal length via `nperseg=min(nfft, len(signal))` to handle bursts >> from utils.annotations import find_spectral_components + >>> # Detect the two distinct channels (returns relative frequencies) + >>> components = find_spectral_components(signal, sampling_rate=20e6) + >>> print(f"Found {len(components)} components") + Found 2 components + +The module is designed to work with detected time-domain annotations, +allowing splitting of overlapping signals into separate training samples. +""" + +import json +from typing import List, Optional, Tuple + +import numpy as np +from scipy import ndimage +from scipy import signal as scipy_signal + +from utils.data import Annotation, Recording + + +def find_spectral_components( + signal_data: np.ndarray, + sampling_rate: float, + nfft: int = 65536, + noise_threshold_db: Optional[float] = None, + min_component_bw: float = 50e3, + time_percentile: float = 70.0, +) -> List[Tuple[float, float, float]]: + """ + Find distinct frequency components using spectral peak detection. + + Identifies separate frequency components in a signal by analyzing the power + spectral density and finding peaks corresponding to distinct signals. This is + useful for separating parallel signals that occupy different frequency bands. + + **Frequency Representation**: Returns frequencies in **baseband/relative** Hz + (centered at 0). To get absolute RF frequencies, add center_frequency_hz from + recording metadata to all returned values. + + Algorithm: + 1. Compute power spectral density using Welch (properly handles complex IQ) + 2. Auto-estimate noise floor from data if not specified + 3. Smooth PSD to reduce spurious peaks + 4. Find local maxima above noise floor + 5. Estimate bandwidth per peak using -3dB (fallback: cumulative power) + 6. Filter components below minimum bandwidth threshold + + :param signal_data: Complex IQ signal samples (np.complex64/128) + :type signal_data: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param nfft: FFT size / window length for Welch. Automatically capped at + signal length to handle bursts (default: 65536) + :type nfft: int + :param noise_threshold_db: Minimum SNR threshold in dB. If None (default), + auto-estimates as np.percentile(psd_db, 10). + Adapt this across hardware (Pluto: ~-100, ThinkRF: ~-60). + :type noise_threshold_db: Optional[float] + :param min_component_bw: Minimum component bandwidth in Hz (default: 50 kHz) + :type min_component_bw: float + :param power_threshold: Cumulative power threshold for fallback bandwidth + estimation (default: 0.99 = 99% power, like OBW) + :type power_threshold: float + + :returns: List of (center_freq_hz, lower_freq_hz, upper_freq_hz) tuples. + **All frequencies are relative (baseband, 0-centered).** + Add recording metadata['center_frequency'] to get absolute RF frequencies. + :rtype: List[Tuple[float, float, float]] + + :raises ValueError: If signal has fewer than 256 samples + + **Example**:: + + >>> from utils.io import load_recording + >>> from utils.annotations import find_spectral_components + >>> recording = load_recording("capture.sigmf") + >>> segment = recording.data[0][start:end] + >>> # Components in relative (baseband) frequency + >>> components = find_spectral_components(segment, sampling_rate=20e6) + >>> for center_rel, lower_rel, upper_rel in components: + ... # Convert to absolute RF frequency + ... center_abs = recording.metadata['center_frequency'] + center_rel + ... print(f"Component @ {center_abs/1e9:.3f} GHz") + """ + # Validate input + min_samples = 256 + if len(signal_data) < min_samples: + raise ValueError(f"Signal too short: need at least {min_samples} samples, " f"got {len(signal_data)}.") + + # Compute PSD using Welch method for complex IQ signals + # CRITICAL: return_onesided=False for proper complex signal handling + nperseg = min(nfft, len(signal_data)) + noverlap = nperseg // 2 + + # --- STFT --- + freqs, times, Zxx = scipy_signal.stft( + signal_data, + fs=sampling_rate, + window="blackman", + nperseg=nperseg, + noverlap=noverlap, + return_onesided=False, + boundary=None, + ) + + # Shift zero freq to center + Zxx = np.fft.fftshift(Zxx, axes=0) + freqs = np.fft.fftshift(freqs) + + # Power spectrogram + power = np.abs(Zxx) ** 2 + power_db = 10 * np.log10(power + 1e-12) + + # --- Aggregate across time robustly --- + # Using percentile instead of mean prevents short signals from being diluted + freq_profile_db = np.percentile(power_db, time_percentile, axis=1) + + # --- Noise floor estimation --- + if noise_threshold_db is None: + noise_threshold_db = np.percentile(freq_profile_db, 20) + + threshold = noise_threshold_db + 3 # 3 dB above noise floor + + # --- Smooth lightly (avoid merging nearby signals) --- + freq_profile_db = ndimage.gaussian_filter1d(freq_profile_db, sigma=1.5) + + # --- Binary mask of significant frequencies --- + mask = freq_profile_db > threshold + + # --- Find contiguous frequency regions --- + labeled, num_features = ndimage.label(mask) + + components = [] + + for region_label in range(1, num_features + 1): + region_indices = np.where(labeled == region_label)[0] + + if len(region_indices) == 0: + continue + + lower_idx = region_indices[0] + upper_idx = region_indices[-1] + + lower_freq = freqs[lower_idx] + upper_freq = freqs[upper_idx] + bw = upper_freq - lower_freq + + if bw < min_component_bw: + continue + + center_freq = (lower_freq + upper_freq) / 2 + components.append((center_freq, lower_freq, upper_freq)) + + return components + + +def split_annotation_by_components( + annotation: Annotation, + signal: np.ndarray, + sampling_rate: float, + center_frequency_hz: float = 0.0, + nfft: int = 65536, + noise_threshold_db: Optional[float] = None, + min_component_bw: float = 50e3, +) -> List[Annotation]: + """ + Split an annotation into multiple annotations by detected frequency components. + + Takes an existing annotation spanning multiple frequency components and + analyzes the frequency content to create separate sub-annotations for + each distinct frequency component. + + **Use case**: Energy detection found a time window with 2-3 parallel WiFi + channels. This function splits it into separate annotations per channel. + + **Frequency Handling**: `find_spectral_components` returns relative (baseband) + frequencies. This function adds `center_frequency_hz` to convert to absolute + RF frequencies for SigMF annotation bounds. This ensures correct frequency + context across baseband and RF domains. + + :param annotation: Original annotation to split + :type annotation: Annotation + :param signal: Full signal array (complex IQ) + :type signal: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param center_frequency_hz: RF center frequency to add to relative frequencies + from peak detection (default: 0.0 = baseband) + :type center_frequency_hz: float + :param nfft: FFT size for analysis (default: 65536, auto-capped at signal length) + :type nfft: int + :param noise_threshold_db: Noise floor threshold in dB. If None (default), + auto-estimates from data. + :type noise_threshold_db: Optional[float] + :param min_component_bw: Minimum component bandwidth in Hz (default: 50 kHz) + :type min_component_bw: float + + :returns: List of new annotations (one per detected component). + Returns empty list if no components found or segment too short. + :rtype: List[Annotation] + + **Example**:: + + >>> from utils.io import load_recording + >>> from utils.annotations import split_annotation_by_components + >>> recording = load_recording("capture.sigmf") + >>> # Original annotation spans multiple channels + >>> original = recording.annotations[0] + >>> # Split using RF center frequency from metadata + >>> components = split_annotation_by_components( + ... original, + ... recording.data[0], + ... recording.metadata['sample_rate'], + ... center_frequency_hz=recording.metadata.get('center_frequency', 0.0) + ... ) + >>> print(f"Split into {len(components)} components") + Split into 2 components + + **Algorithm**: + 1. Extract segment corresponding to annotation time bounds + 2. Find frequency components in that segment (returns relative frequencies) + 3. Add center_frequency_hz to get absolute RF frequencies + 4. Create new annotation for each component + 5. Preserve original metadata (label, type, etc.) + 6. Add component info to comment JSON + + **Notes**: + - Original annotation is not modified + - Returns empty list if segment too short (<256 samples) + - Segments Recording: + """ + Split multiple annotations in a recording by frequency components. + + Processes specified annotations (or all if indices=None), replacing each + with its frequency-separated components. Uses RF center_frequency from + recording metadata for proper absolute frequency conversion. + + :param recording: Recording to process + :type recording: Recording + :param indices: Annotation indices to split (None = all, default: None). + Use indices=[] to skip splitting (returns unchanged recording). + :type indices: Optional[List[int]] + :param nfft: FFT size for spectral analysis (default: 65536, + auto-capped at signal segment length) + :type nfft: int + :param noise_threshold_db: Noise floor threshold in dB. If None (default), + auto-estimates from each segment. + :type noise_threshold_db: Optional[float] + :param min_component_bw: Minimum component bandwidth in Hz (default: 50 kHz). + Components narrower than this are filtered out. + :type min_component_bw: float + + :returns: New Recording with split annotations + :rtype: Recording + + **Example**:: + + >>> from utils.io import load_recording + >>> from utils.annotations import split_recording_annotations + >>> recording = load_recording("capture.sigmf") + >>> # Split all annotations + >>> split_rec = split_recording_annotations(recording) + >>> print(f"Original: {len(recording.annotations)} annotations") + >>> print(f"Split: {len(split_rec.annotations)} annotations") + Original: 5 annotations + Split: 9 annotations + + **Algorithm**: + 1. For each annotation in indices (or all if None): + 2. Call split_annotation_by_components with RF center_frequency + 3. If components found, replace annotation with components + 4. If no components found, keep original annotation + 5. Annotations not in indices are kept unchanged + + **Notes**: + - Original recording is not modified + - Returns empty Recording.annotations if recording has no annotations + - RF center_frequency from metadata ensures correct absolute frequencies + - If an annotation can't be split (too short, wrong format), original kept + """ + if indices is None: + # Split all annotations + indices = list(range(len(recording.annotations))) + + if not recording.annotations: + # No annotations to split + return recording + + signal = recording.data[0] + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0.0) + + # Build new annotation list + new_annotations = [] + for i, anno in enumerate(recording.annotations): + if i in indices: + # Attempt to split this annotation + try: + components = split_annotation_by_components( + anno, + signal, + sample_rate, + center_frequency_hz=center_frequency, + nfft=nfft, + noise_threshold_db=noise_threshold_db, + min_component_bw=min_component_bw, + ) + if components: + # Split successful, use components + new_annotations.extend(components) + else: + # No components found, keep original + new_annotations.append(anno) + except Exception: + # Split failed for any reason, keep original + new_annotations.append(anno) + else: + # Not in split list, keep as-is + new_annotations.append(anno) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=new_annotations) diff --git a/src/ria_toolkit_oss/annotations/qualify_slice.py b/src/ria_toolkit_oss/annotations/qualify_slice.py new file mode 100644 index 0000000..10ff369 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/qualify_slice.py @@ -0,0 +1,35 @@ +import numpy as np + +from utils.data import Recording + + +def qualify_slice_from_annotations(recording: Recording, slice_length: int): + """ + Slice a recording into many smaller recordings, + discarding any slices which do not have annotations that apply to those samples. + Used together with an annotation based qualifier. + + :param recording: The recording to slice. + :type recording: Recording + :param slice_length: The length in samples of a slice. + :type slice_length: int""" + + if len(recording.annotations) == 0: + print("Warning, no annotations.") + + annotation_mask = np.zeros(len(recording.data[0])) + + for annotation in recording.annotations: + annotation_mask[annotation.sample_start : annotation.sample_start + annotation.sample_count] = 1 + + output_recordings = [] + + for i in range((len(recording.data[0]) // slice_length) - 1): + start_index = slice_length * i + end_index = slice_length * (i + 1) + + if 1 in annotation_mask[start_index:end_index]: + sl = recording.data[:, start_index:end_index] + output_recordings.append(Recording(data=sl, metadata=recording.metadata)) + + return output_recordings diff --git a/src/ria_toolkit_oss/annotations/signal_isolation.py b/src/ria_toolkit_oss/annotations/signal_isolation.py new file mode 100644 index 0000000..8d6c9ac --- /dev/null +++ b/src/ria_toolkit_oss/annotations/signal_isolation.py @@ -0,0 +1,97 @@ +import numpy as np +from scipy.signal import butter, lfilter + +from utils.data.annotation import Annotation +from utils.data.recording import Recording + + +def isolate_signal(recording: Recording, annotation: Annotation) -> Recording: + """ + Slice, filter and frequency shift the input recording according to the bounding box defined by the annotation. + + :param recording: The input Recording to be sliced. + :type recording: Recording + :param annotation: The Annotation object defining the area of the recording to isolate. + :type annotation: Annotation + :param decimate: Decimate the input signal after filtering to reduce the sample rate. + :type decimate: bool + + :returns: The subsection of the original recording defined by the annotation. + :rtype: Recording""" + + sample_start = max(0, annotation.sample_start) + sample_stop = min(len(recording), annotation.sample_start + annotation.sample_count) + + anno_base_center_freq = (annotation.freq_lower_edge + annotation.freq_upper_edge) / 2 - recording.metadata.get( + "center_frequency", 0 + ) + + anno_bw = annotation.freq_upper_edge - annotation.freq_lower_edge + + signal_slice = recording.data[0, sample_start:sample_stop] + + # normalize + signal_slice = signal_slice / np.max(np.abs(signal_slice)) + + isolation_bw = anno_bw + + # frequency shift the center of the box about zero + shifted_signal_slice = frequency_shift_iq_samples( + iq_samples=signal_slice, + sample_rate=recording.metadata["sample_rate"], + shift_frequency=-1 * anno_base_center_freq, + ) + + # filter + if isolation_bw < recording.metadata["sample_rate"] - 1: + filtered_signal = apply_complex_lowpass_filter( + signal=shifted_signal_slice, cutoff_frequency=isolation_bw, sample_rate=recording.metadata["sample_rate"] + ) + + else: + filtered_signal = shifted_signal_slice + + output = Recording(data=[filtered_signal], metadata=recording.metadata) + return output + + +def frequency_shift_iq_samples(iq_samples, sample_rate, shift_frequency): + # Number of samples + num_samples = len(iq_samples) + + # Create a time vector from 0 to the total duration in seconds + time_vector = np.arange(num_samples) / sample_rate + + # Generate the complex exponential for the frequency shift + complex_exponential = np.exp(1j * 2 * np.pi * shift_frequency * time_vector) + + # Apply the frequency shift to the IQ samples + shifted_samples = iq_samples * complex_exponential + + return shifted_samples + + +# Function to apply a lowpass Butterworth filter to a complex signal +def apply_complex_lowpass_filter(signal, cutoff_frequency, sample_rate, order=5): + # Design the lowpass filter + b, a = design_complex_lowpass_filter(cutoff_frequency, sample_rate, order) + + # Apply the lowpass filter + filtered_signal = lfilter(b, a, signal) + return filtered_signal + + +def design_complex_lowpass_filter(cutoff_frequency, sample_rate, order=5): + # Nyquist frequency for complex signals is the sample rate + nyquist = sample_rate + + # Ensure the cutoff frequency is positive and within the Nyquist limit + if cutoff_frequency <= 0 or cutoff_frequency > nyquist: + raise ValueError("Cutoff frequency must be between 0 and the Nyquist frequency.") + + # Normalize the cutoff frequency to the Nyquist frequency + cutoff_normalized = cutoff_frequency / nyquist + + # Create a Butterworth lowpass filter + b, a = butter(order, cutoff_normalized, btype="low") + return b, a diff --git a/src/ria_toolkit_oss/annotations/threshold_qualifier.py b/src/ria_toolkit_oss/annotations/threshold_qualifier.py new file mode 100644 index 0000000..200c9e8 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/threshold_qualifier.py @@ -0,0 +1,212 @@ +""" +Temporal signal detection and boundary refinement via Hysteresis Thresholding. + +Provides methods to detect signal bursts in the time domain by triggering on +smoothed power peaks and expanding boundaries to capture the full energy envelope. + +This module implements a **dual-threshold trigger** to solve the 'chatter' +problem in noisy environments, ensuring that signal annotations encapsulate +the entire rise and fall of a burst rather than just the peak. + +**Key Design Decisions**: + +1. **Hysteresis Logic (Dual-Threshold)**: + - **Trigger**: High threshold (`threshold * max_power`) ensures high confidence + in signal presence. + - **Boundary**: Low threshold (`0.5 * trigger`) allows the annotation to + "crawl" outward, capturing the lower-energy start and end of the burst + often missed by simple single-threshold detectors. + +2. **Temporal Smoothing**: Uses a moving average window (`window_size`) prior + - to thresholding. This prevents high-frequency noise spikes from causing + fragmented annotations and provides a more stable estimate of the + signal's power envelope. + +3. **Spectral Profiling**: Once a temporal segment is isolated, the module + - performs an automated FFT analysis. It identifies the **90% spectral + occupancy** to define the frequency boundaries (`f_min`, `f_max`), + allowing the detector to work on narrowband and wideband signals without + manual frequency tuning. + +4. **Baseband/RF Mapping**: Automatically handles the conversion from + - relative FFT bin frequencies to absolute RF frequencies by referencing + `recording.metadata["center_frequency"]`. + +5. **False Positive Mitigation**: Implements a hard minimum duration check + - (10ms) to ignore transient hardware spikes or noise floor fluctuations + that do not constitute a valid signal burst. + +The module is designed to be the primary "first-pass" detector for pulsed +waveforms (like ADS-B, Lora, or bursty FSK) before passing them to +classification or demodulation stages. +""" + +import json +from typing import Optional + +import numpy as np + +from utils.data import Annotation, Recording + + +def _find_ranges(indices, window_size): + """ + Groups individual indices into continuous temporal ranges. + + Args: + indices: Array of indices where the signal exceeded a threshold. + window_size: Maximum gap allowed between indices to consider them part + of the same range. + + Returns: + A list of (start, stop) tuples representing detected signal segments. + """ + + if len(indices) == 0: + return [] + + ranges = [] + + start = indices[0] + in_range = False + + for i in range(1, len(indices)): + # If the gap between current and previous index is within window_size, + # keep the range alive. + if indices[i] - indices[i - 1] <= window_size: + if not in_range: + # Start a new range + start = indices[i - 1] + in_range = True + else: + # Gap is too large; close the current range if one was active. + if in_range: + ranges.append((start, indices[i - 1])) + in_range = False + + # Ensure the final segment is captured if the loop ends while in_range. + if in_range: + ranges.append((start, indices[-1])) + + return ranges + + +def threshold_qualifier( + recording: Recording, + threshold: float, + window_size: Optional[int] = 1024, + label: Optional[str] = None, + annotation_type: Optional[str] = "standalone", +) -> Recording: + """ + Annotate a recording with bounding boxes for regions above a threshold. + Threshold is defined as a fraction of the maximum sample magnitude. + This algorithm searches for samples above the threshold and combines them into ranges if they + are within window_size of each other. + Detects and annotates signals using energy thresholding and spectral analysis. + + The algorithm follows these steps: + 1. Smooths power data using a moving average. + 2. Identifies 'peak' regions exceeding a high trigger threshold. + 3. Uses hysteresis to expand boundaries until power drops below a lower threshold. + 4. Performs an FFT on each segment to determine frequency occupancy. + + Args: + recording: The Recording object containing IQ or real signal data. + threshold: Sensitivity multiplier (0.0 to 1.0) applied to max power. + window_size: Size of the smoothing filter and max gap for merging hits. + label: Custom string label for annotations. + annotation_type: Metadata string for the 'type' field in the annotation. + + Returns: + A new Recording object populated with detected Annotations. + """ + # Extract signal and metadata + sample_data = recording.data[0] + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0) + + # --- 1. SIGNAL CONDITIONING --- + # Convert to power (Magnitude squared) + power_data = np.abs(sample_data) ** 2 + smoothing_window = np.ones(window_size) / window_size + smoothed_power = np.convolve(power_data, smoothing_window, mode="same") + + # Define thresholds based on the global peak of the smoothed signal + max_power = np.max(smoothed_power) + trigger_val = threshold * max_power # High threshold to trigger detection + boundary_val = (threshold / 2) * max_power # Low threshold to define signal edges + + # --- 2. INITIAL DETECTION --- + # Identify indices that strictly exceed the high trigger + indices = np.where(smoothed_power > trigger_val)[0] + initial_ranges = _find_ranges(indices=indices, window_size=window_size) + + annotations = [] + + threshold_base = min(sample_rate, len(sample_data)) + + for start, stop in initial_ranges: + if (stop - start) < (threshold_base * 0.01): + continue + + # --- 3. HYSTERESIS (Boundary Expansion) --- + # Search backward from 'start' until power drops below the low boundary_val + true_start = start + while true_start > 0 and smoothed_power[true_start] > boundary_val: + true_start -= 1 + + # Search forward from 'stop' until power drops below the low boundary_val + true_stop = stop + while true_stop < len(smoothed_power) - 1 and smoothed_power[true_stop] > boundary_val: + true_stop += 1 + + # --- 4. SPECTRAL ANALYSIS (Frequency Detection) --- + signal_segment = sample_data[true_start:true_stop] + if len(signal_segment) > 0: + fft_data = np.abs(np.fft.fftshift(np.fft.fft(signal_segment))) + fft_freqs = np.fft.fftshift(np.fft.fftfreq(len(signal_segment), 1 / sample_rate)) + + # Determine frequency bounds where spectral energy is > 15% of segment peak + spectral_thresh = np.max(fft_data) * 0.15 + sig_indices = np.where(fft_data > spectral_thresh)[0] + + # Ensure the signal has some spectral width before annotating + if len(sig_indices) < 5: + continue + + if len(sig_indices) > 0: + f_min, f_max = fft_freqs[sig_indices[0]], fft_freqs[sig_indices[-1]] + else: + # Default to middle half of bandwidth if no clear peaks found + f_min, f_max = -sample_rate / 4, sample_rate / 4 + else: + f_min, f_max = -sample_rate / 4, sample_rate / 4 + + # --- 5. ANNOTATION GENERATION --- + if label is None: + label = f"{int(threshold*100)}%" + + # Pack metadata for the UI/Downstream processing + comment_data = { + "type": annotation_type, + "generator": "threshold_qualifier", + "params": { + "threshold": threshold, + "window_size": window_size, + }, + } + + anno = Annotation( + sample_start=true_start, + sample_count=true_stop - true_start, + freq_lower_edge=center_frequency + f_min, + freq_upper_edge=center_frequency + f_max, + label=label, + comment=json.dumps(comment_data), + detail={"generator": "hysteresis_qualifier"}, + ) + annotations.append(anno) + + # Return a new Recording object including the new annotations + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + annotations) diff --git a/src/ria_toolkit_oss/view/view_signal.py b/src/ria_toolkit_oss/view/view_signal.py index f8d5731..0f2ed33 100644 --- a/src/ria_toolkit_oss/view/view_signal.py +++ b/src/ria_toolkit_oss/view/view_signal.py @@ -6,18 +6,14 @@ from typing import Optional import matplotlib.pyplot as plt import numpy as np from matplotlib import gridspec +from matplotlib.patches import Patch from PIL import Image from scipy.fft import fft, fftshift from scipy.signal import spectrogram from scipy.signal.windows import hann -from ria_toolkit_oss.datatypes.recording import Recording -from ria_toolkit_oss.view.tools import ( - COLORS, - decimate, - extract_metadata_fields, - set_path, -) +from utils.data.recording import Recording +from utils.view.tools import COLORS, decimate, extract_metadata_fields, set_path def get_fft_size(plot_length): @@ -39,6 +35,80 @@ def set_spines(ax, spines): ax.spines["left"].set_visible(False) +def view_annotations( + recording: Recording, + channel: Optional[int] = 0, + output_path: Optional[str] = "images/annotations.png", + title: Optional[str] = "Annotated Spectrogram", + dpi: Optional[int] = 300, + title_fontsize: Optional[int] = 15, + dark: Optional[bool] = True, +) -> None: + # 1. Setup Plotting Environment + plt.close("all") + if dark: + plt.style.use("dark_background") + else: + plt.style.use("default") + + fig, ax = plt.subplots(figsize=(12, 8)) + + complex_signal = recording.data[channel] + sample_rate, center_frequency, _ = extract_metadata_fields(recording.metadata) + annotations = recording.annotations + + # 2. Setup Color Mapping (No more hardcoded yellow fallback!) + # available_colors = [ + # COLORS.get("magenta", "magenta"), + # COLORS.get("accent", "cyan"), + # COLORS.get("light", "white"), + # "lime", + # ] + + palette = ["#FF00FF", "#00FF00", "#00FFFF", "#FFFF00", "#FF8000"] + unique_labels = sorted(list(set(ann.label for ann in annotations if ann.label))) + label_to_color = {label: palette[i % len(palette)] for i, label in enumerate(unique_labels)} + + # 3. Generate Spectrogram + Pxx, freqs, times, im = ax.specgram( + complex_signal, NFFT=256, Fs=sample_rate, Fc=center_frequency, noverlap=128, cmap="twilight" + ) + + # 4. Draw Annotations + for annotation in annotations: + # --- DEFINING VARIABLES FIRST --- + t_start = annotation.sample_start / sample_rate + t_width = annotation.sample_count / sample_rate + f_start = annotation.freq_lower_edge + f_height = annotation.freq_upper_edge - annotation.freq_lower_edge + + # Look up the color for this specific label + ann_color = label_to_color.get(annotation.label, "gray") + + # Draw the Rectangle + rect = plt.Rectangle( + (t_start, f_start), t_width, f_height, linewidth=1.5, edgecolor=ann_color, facecolor="none", alpha=0.8 + ) + ax.add_patch(rect) + + if unique_labels: + legend_elements = [ + Patch(facecolor=label_to_color[label], alpha=0.3, edgecolor=label_to_color[label], label=label) + for label in unique_labels + ] + ax.legend(handles=legend_elements, loc="upper right", framealpha=0.2) + + ax.set_title(title, fontsize=title_fontsize, pad=20) + ax.set_xlabel("Time (s)", fontsize=12) + ax.set_ylabel("Frequency (MHz)", fontsize=12) + ax.grid(alpha=0.1) # Add faint grid + + output_path, _ = set_path(output_path=output_path) + plt.savefig(output_path, dpi=dpi, bbox_inches="tight") + plt.close(fig) + print(f"Professional annotation plot saved to {output_path}") + + def view_channels( recording: Recording, output_path: Optional[str] = "images/signal.png", @@ -209,9 +279,7 @@ def view_sig( ) set_spines(spec_ax, spines) - spec_ax.set_title("Spectrogram", fontsize=subtitle_fontsize) - spec_ax.set_ylabel("Frequency (Hz)") - spec_ax.set_xlabel("Time (s)") + spec_ax.set_title("Spectrogram", loc="center", fontsize=subtitle_fontsize) if iq: iq_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :]) @@ -295,7 +363,11 @@ def view_sig( set_spines(meta_ax, spines) if logo and os.path.isfile(logo_path): - logo_ax = plt.subplot(gs[plot_y_indx + 2 :, 2]) + # logo_ax = plt.subplot(gs[plot_y_indx:, 2]) + logo_pos = [0.75, 0.05, 0.2, 0.08] + logo_ax = fig.add_axes(logo_pos, anchor="SE", zorder=10) + plot_x_indx = plot_x_indx + 1 + logo_ax.axis("off") try: @@ -314,7 +386,6 @@ def view_sig( hspace=2.5, # Vertical space between subplots ) - # save path handling output_path, _ = set_path(output_path=output_path) plt.savefig(output_path, dpi=dpi) print(f"Saved signal plot to {output_path}") diff --git a/src/ria_toolkit_oss/view/view_signal_simple.py b/src/ria_toolkit_oss/view/view_signal_simple.py index ab56f7d..248486f 100644 --- a/src/ria_toolkit_oss/view/view_signal_simple.py +++ b/src/ria_toolkit_oss/view/view_signal_simple.py @@ -3,6 +3,7 @@ from __future__ import annotations import gc +import json from typing import Optional import matplotlib @@ -11,13 +12,54 @@ import numpy as np from scipy.fft import fft, fftshift from scipy.signal.windows import hann -from ria_toolkit_oss.datatypes.recording import Recording -from ria_toolkit_oss.view.tools import ( - COLORS, - decimate, - extract_metadata_fields, - set_path, -) +from utils.data.recording import Recording +from utils.view.tools import COLORS, decimate, extract_metadata_fields, set_path + + +def _add_annotations(annotations, compact_mode, show_labels, sample_rate_hz, center_freq_hz, ax2): + if annotations and not compact_mode: + for annotation in annotations: + start_idx = annotation.get("core:sample_start", 0) + length = annotation.get("core:sample_count", 0) + start_time = start_idx / sample_rate_hz + end_time = (start_idx + length) / sample_rate_hz + freq_low = annotation.get("core:freq_lower_edge", center_freq_hz - sample_rate_hz / 4) + freq_high = annotation.get("core:freq_upper_edge", center_freq_hz + sample_rate_hz / 4) + comment = annotation.get("core:comment", "{}") + + try: + comment_data = json.loads(comment) if isinstance(comment, str) else comment + ann_type = comment_data.get("type", "unknown") + if ann_type == "intersection": + color = COLORS["success"] + elif ann_type == "parallel": + color = COLORS["primary"] + elif ann_type == "standalone": + color = COLORS["warning"] + else: + color = COLORS["error"] + except Exception: + color = COLORS["error"] + + rect = plt.Rectangle( + (start_time, freq_low), + end_time - start_time, + freq_high - freq_low, + color=color, + alpha=0.4, + linewidth=2, + ) + ax2.add_patch(rect) + if show_labels: + label = annotation.get("core:label", "Signal") + ax2.text( + start_time, + freq_high, + label, + color=COLORS["light"], + fontsize=10, + bbox=dict(boxstyle="round,pad=0.2", facecolor=color, alpha=0.7), + ) def _get_nfft_size(signal, fast_mode): @@ -138,6 +180,7 @@ def detect_constellation_symbols(signal: np.ndarray, method: str = "differential def view_simple_sig( recording: Recording, + annotations: Optional[list] = None, output_path: Optional[str] = "images/signal.png", saveplot: Optional[bool] = True, fast_mode: Optional[bool] = False, @@ -261,6 +304,15 @@ def view_simple_sig( ax2.set_title("Spectrogram", loc="left", pad=10) + _add_annotations( + annotations=annotations, + compact_mode=compact_mode, + show_labels=show_labels, + sample_rate_hz=sample_rate_hz, + center_freq_hz=center_freq_hz, + ax2=ax2, + ) + if ax_constellation is not None: constellation_samples = _get_plot_samples(signal=signal, fast_mode=fast_mode, slow_max=50_000, fast_max=20_000) method = "differential" if fast_mode else "combined" @@ -310,7 +362,7 @@ def view_simple_sig( else: plt.tight_layout() if show_title: - plt.subplots_adjust(top=0.90) + plt.subplots_adjust(top=0.92) if saveplot: output_path, extension = set_path(output_path=output_path) From 11d9532b5c7bb6b90df4907739c2620677d3b16d Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 31 Mar 2026 13:34:00 -0400 Subject: [PATCH 07/18] Port annotation system from utils and fix ria package imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Annotations package (new): - Add threshold_qualifier with 3-pass hysteresis detector (Pass 1: strong bursts, Pass 2: weak residual bursts, Pass 3: macro-window faint burst detection), auto window_size scaled to 1ms, channel selection, and stable noise_floor baseline throughout - Add energy_detector, cusum_annotator, parallel_signal_separator, qualify_slice, signal_isolation, annotation_transforms - Add __init__.py exporting the four functions used by the CLI - Fix all imports from utils.data → ria_toolkit_oss.datatypes CLI annotate command (new): - Port full annotate CLI from utils including list, add, remove, clear, energy, cusum, threshold, and separate subcommands - Fix imports from utils.* → ria_toolkit_oss.* and utils_cli.* → ria_toolkit_oss_cli.* - Safe overwrite logic: _annotated files always writable, originals protected; --overwrite writes in-place only on _annotated inputs CLI view command: - Add 'annotations' as a valid --type, wiring view_annotations from view_signal view_signal.py: - Add view_annotations function with blue/purple alternating palette and threshold %-sorted drawing order (lower % renders on top) recording.py (datatypes): - Fix lazy imports in to_wav() and to_blue() from utils.io → ria_toolkit_oss.io io/recording.py: - Add compatibility shim in from_npy to remap utils.data.annotation.Annotation to ria_toolkit_oss.datatypes.annotation.Annotation when loading .npy files pickled by the utils package --- .../datatypes/radio_datasets.rst | 8 +- src/ria_toolkit_oss/annotations/__init__.py | 4 + .../annotations/annotation_transforms.py | 55 ++ .../annotations/cusum_annotator.py | 203 +++++ .../annotations/energy_detector.py | 438 ++++++++++ .../annotations/parallel_signal_separator.py | 435 ++++++++++ .../annotations/qualify_slice.py | 35 + .../annotations/signal_isolation.py | 97 +++ .../annotations/threshold_qualifier.py | 352 ++++++++ src/ria_toolkit_oss/datatypes/recording.py | 4 +- src/ria_toolkit_oss/io/recording.py | 21 + .../Qoherent-logo-black-transparent.png | Bin 92294 -> 130 bytes .../Qoherent-logo-white-transparent.png | Bin 19826 -> 130 bytes src/ria_toolkit_oss/view/view_signal.py | 78 ++ .../ria_toolkit_oss/annotate.py | 820 ++++++++++++++++++ .../ria_toolkit_oss/commands.py | 1 + .../ria_toolkit_oss/generate.py | 4 +- .../ria_toolkit_oss/transform.py | 6 +- .../ria_toolkit_oss/view.py | 3 +- tests/ria_toolkit_oss_cli/README.md | 2 +- tests/ria_toolkit_oss_cli/__init__.py | 2 +- 21 files changed, 2554 insertions(+), 14 deletions(-) create mode 100644 src/ria_toolkit_oss/annotations/__init__.py create mode 100644 src/ria_toolkit_oss/annotations/annotation_transforms.py create mode 100644 src/ria_toolkit_oss/annotations/cusum_annotator.py create mode 100644 src/ria_toolkit_oss/annotations/energy_detector.py create mode 100644 src/ria_toolkit_oss/annotations/parallel_signal_separator.py create mode 100644 src/ria_toolkit_oss/annotations/qualify_slice.py create mode 100644 src/ria_toolkit_oss/annotations/signal_isolation.py create mode 100644 src/ria_toolkit_oss/annotations/threshold_qualifier.py create mode 100644 src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py diff --git a/docs/source/ria_toolkit_oss/datatypes/radio_datasets.rst b/docs/source/ria_toolkit_oss/datatypes/radio_datasets.rst index 149fbaf..95d47e2 100644 --- a/docs/source/ria_toolkit_oss/datatypes/radio_datasets.rst +++ b/docs/source/ria_toolkit_oss/datatypes/radio_datasets.rst @@ -11,15 +11,15 @@ The Radio Dataset Framework provides a software interface to access and manipula the need for users to interface with the source files directly. Instead, users initialize and interact with a Python object, while the complexities of efficient data retrieval and source file manipulation are managed behind the scenes. -Utils includes an abstract class called :py:obj:`ria_toolkit_oss.datatypes.datasets.RadioDataset`, which defines common properties and +Ria Toolkit OSS includes an abstract class called :py:obj:`ria_toolkit_oss.datatypes.datasets.RadioDataset`, which defines common properties and behaviors for all radio datasets. :py:obj:`ria_toolkit_oss.datatypes.datasets.RadioDataset` can be considered a blueprint for all other radio dataset classes. This class is then subclassed to define more specific blueprints for different types of radio datasets. For example, :py:obj:`ria_toolkit_oss.datatypes.datasets.IQDataset`, which is tailored for machine learning tasks involving the processing of signals represented as IQ (In-phase and Quadrature) samples. -Then, in the various project backends, there are concrete dataset classes, which inherit from both Utils and the base +Then, in the various project backends, there are concrete dataset classes, which inherit from both Ria Toolkit OSS and the base dataset class from the respective backend. For example, the :py:obj:`TorchIQDataset` class extends both -:py:obj:`ria_toolkit_oss.datatypes.datasets.IQDataset` from Utils and :py:obj:`torch.ria_toolkit_oss.datatypes.IterableDataset` from +:py:obj:`ria_toolkit_oss.datatypes.datasets.IQDataset` from Ria Toolkit OSS and :py:obj:`torch.ria_toolkit_oss.datatypes.IterableDataset` from PyTorch, providing a concrete dataset class tailored for IQ datasets and optimized for the PyTorch backend. Dataset initialization @@ -130,7 +130,7 @@ Dataset processing and manipulation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ All radio datasets support methods tailored specifically for radio processing. These methods are backend-independent, -inherited from the blueprints in Utils like :py:obj:`ria_toolkit_oss.datatypes.datasets.RadioDataset`. +inherited from the blueprints in Ria Toolkit OSS like :py:obj:`ria_toolkit_oss.datatypes.datasets.RadioDataset`. For example, we can trim down the length of the examples from 1,024 to 512 samples, and then augment the dataset: diff --git a/src/ria_toolkit_oss/annotations/__init__.py b/src/ria_toolkit_oss/annotations/__init__.py new file mode 100644 index 0000000..e64c37f --- /dev/null +++ b/src/ria_toolkit_oss/annotations/__init__.py @@ -0,0 +1,4 @@ +from .cusum_annotator import annotate_with_cusum +from .energy_detector import detect_signals_energy +from .parallel_signal_separator import split_recording_annotations +from .threshold_qualifier import threshold_qualifier diff --git a/src/ria_toolkit_oss/annotations/annotation_transforms.py b/src/ria_toolkit_oss/annotations/annotation_transforms.py new file mode 100644 index 0000000..47300c1 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/annotation_transforms.py @@ -0,0 +1,55 @@ +from ria_toolkit_oss.datatypes.annotation import Annotation + +# TODO figure out how to transfer labels in the merge case + + +def remove_contained_boxes(annotations: list[Annotation]): + """ + Remove all annotations (bounding boxes) that are entirely contained within other boxes in the list. + + :param annotations: A list of Annotation objects. + :type annotations: list[Annotation] + + :returns: A new list of Annotation objects. + :rtype: list[Annotation]""" + + output_boxes = [] + + for i in range(len(annotations)): + contained = False + for j in range(len(annotations)): + if i != j and is_annotation_contained(annotations[i], annotations[j]): + contained = True + break + + if not contained: + output_boxes.append(annotations[i]) + + return output_boxes + + +def is_annotation_contained(inner: Annotation, outer: Annotation) -> bool: + """ + Check if an annotation box is entirely contained within another annotation bounding box. + + :param inner: The inner box. + :type inner: Annotation. + :param outer: The outer box. + :type outer: Annotation. + + :returns: True if inner is within outer, false otherwise. + :rtype: bool + """ + + inner_sample_stop = inner.sample_start + inner.sample_count + outer_sample_stop = outer.sample_start + outer.sample_count + + if inner.sample_start > outer.sample_start and inner_sample_stop < outer_sample_stop: + if inner.freq_lower_edge > outer.freq_lower_edge and inner.freq_upper_edge < outer.freq_upper_edge: + return True + + return False + + +def merge_annotations(annotations: list[Annotation], overlap_threshold) -> list[Annotation]: + raise NotImplementedError diff --git a/src/ria_toolkit_oss/annotations/cusum_annotator.py b/src/ria_toolkit_oss/annotations/cusum_annotator.py new file mode 100644 index 0000000..d37186c --- /dev/null +++ b/src/ria_toolkit_oss/annotations/cusum_annotator.py @@ -0,0 +1,203 @@ +import json +from typing import Optional + +import numpy as np + +from ria_toolkit_oss.datatypes import Annotation, Recording + + +def annotate_with_cusum( + recording: Recording, + label: Optional[str] = "segment", + window_size: Optional[int] = 1, + min_duration: Optional[float] = None, + tolerance: Optional[int] = None, + annotation_type: Optional[str] = "standalone", +): + """ + Add annotations that divide the recording into distinct time segments. + + This algorithm computes the cumulative sum of the sample magnitudes and + determines break points in the signal. + + This tool can be used to find points where a signal turns on or off, or + changes between a low and high amplitude. + + :param recording: A ``Recording`` object to annotate. + :type recording: ``ria_toolkit_oss.datatypes.Recording`` + :param label: Label for the detected segments. + :type label: str + :param window_size: The length (in samples) of the moving average window. + :type window_size: int + :param min_duration: The minimum duration (in ms) of a segment. + The algorithm will not produce annotations shorter than this length. + :type min_duration: float + :param tolerance: The minimum length (in samples) of a segment. + :type tolerance: int + :param annotation_type: Annotation type (standalone, parallel, intersection). + :type annotation_type: str + """ + + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0) + + # Create an object of the time segmenter + time_segmenter = TimeSegmenter(sample_rate, min_duration, window_size, tolerance) + + change_points = time_segmenter.apply(recording.data[0]) + + time_segments_indices = np.append(np.insert(change_points, 0, 0), len(recording.data[0])) + annotations = [] + for i in range(len(time_segments_indices) - 1): + # Build comment JSON with type metadata + comment_data = { + "type": annotation_type, + "generator": "cusum_annotator", + "params": { + "window_size": window_size, + "min_duration": min_duration, + "tolerance": tolerance, + }, + } + f_min, f_max = detect_frequency( + signal=recording.data[0], + start=time_segments_indices[i], + stop=time_segments_indices[i + 1], + sample_rate=sample_rate, + ) + + annotations.append( + Annotation( + sample_start=time_segments_indices[i], + sample_count=time_segments_indices[i + 1] - time_segments_indices[i], + freq_lower_edge=center_frequency + f_min, + freq_upper_edge=center_frequency + f_max, + label=label, + comment=json.dumps(comment_data), + detail={"generator": "cusum_annotator"}, + ) + ) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + annotations) + + +def _compute_cusum(_signal, sample_rate: int, tolerance: int = None, min_duration: float = -1): + """ + This function efficiently computes the cumulative sum of a give list (_signal), with an optional tolerance. + + Args: + - _signal: array of iq samples. + - Tolerance: the least acceptable length of a block, Defaults to None. + + Returns: + - cusum (array): Array of the cumulative sum of the given list + - sample_rate (int): __description_ + - change_points (array): Array of the indices at which a change in the CUSUM direction happens. + - min_duration (float): The least acceptable time width of each segment (in ms). Defaults to -1. + """ + + # efficiently calculate the running sum of the signal + # cusum = list(itertools.accumulate((_signal - np.mean(_signal)))) + x = _signal - np.mean(_signal) + cusum = np.cumsum(x) + + # 'diff' computes the differences between the consecutive values, + # then 'sign' determines if it is +ve or -ve. + change_indicators = np.sign(np.diff(cusum)) + change_points = np.where(np.diff(change_indicators))[0] + 1 + + # Limit the change_points + # Reject those whose number of samples < minimum accepted #n of samples in (min duration) ms. + if min_duration is not None and min_duration > 0: + min_samples_wide = int(min_duration * sample_rate / 1000) + segments_lengths = np.diff(change_points) + segments_lengths = np.insert(segments_lengths, 0, change_points[0]) + change_points = change_points[np.where(segments_lengths > min_samples_wide)[0]] + return cusum, change_points + + +def detect_frequency(signal, start, stop, sample_rate): + signal_segment = signal[start:stop] + if len(signal_segment) > 0: + fft_data = np.abs(np.fft.fftshift(np.fft.fft(signal_segment))) + fft_freqs = np.fft.fftshift(np.fft.fftfreq(len(signal_segment), 1 / sample_rate)) + + # Use a spectral threshold to find the 'height' of the orange block + spectral_thresh = np.max(fft_data) * 0.15 + sig_indices = np.where(fft_data > spectral_thresh)[0] + + if len(sig_indices) > 4: + return fft_freqs[sig_indices[0]], fft_freqs[sig_indices[-1]] + else: + return -sample_rate / 4, sample_rate / 4 + else: + return -sample_rate / 4, sample_rate / 4 + + +class TimeSegmenter: + """Time Segmenter class, it creates a segmenter object with certain\ + characteristics to easily split an input signal to segments based on\ + the cumulative sum of deviations (of the signal mean) + """ + + def __init__( + self, sample_rate: int, min_duration: float = 1, moving_average_window: int = 3, tolerance: int = None + ): + """_summary_ + + Args: + sample_rate (int): _description_ + min_duration (float, optional): _description_. Defaults to 1. + moving_average_window (int, optional): _description_. Defaults to 3. + tolerance (int, optional): _description_. Defaults to None. + """ + self.sample_rate = sample_rate + self.min_duration = min_duration + self.moving_average_window = moving_average_window + self._moving_avg_filter = self._init_filter() + self.tolerance = tolerance + + def _init_filter(self): + """_summary_ + + Returns: + _type_: _description_ + """ + return np.ones(self.moving_average_window) / self.moving_average_window + + def _apply_filter(self, iqsignal: np.array): + """_summary_ + + Args: + iqsignal (np.array): _description_ + + Returns: + _type_: _description_ + """ + return np.convolve(abs(iqsignal), self._moving_avg_filter, mode="same") + + def _create_segments(self, iq_signal: np.array, change_points: np.array): + """_summary_ + + Args: + iq_signal (np.array): _description_ + change_points (np.array): _description_ + + Returns: + _type_: _description_ + """ + return np.split(iq_signal, change_points) + + def apply(self, iq_signal: np.array): + """_summary_ + + Args: + iq_signal (np.array): _description_ + + Returns: + _type_: _description_ + """ + smoothed_signal = self._apply_filter(iq_signal) + _, change_points = _compute_cusum(smoothed_signal, self.sample_rate, self.tolerance, self.min_duration) + # segments = self._create_segments(iq_signal, change_points) + return change_points diff --git a/src/ria_toolkit_oss/annotations/energy_detector.py b/src/ria_toolkit_oss/annotations/energy_detector.py new file mode 100644 index 0000000..98329d5 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/energy_detector.py @@ -0,0 +1,438 @@ +""" +Energy-based signal detection and bandwidth analysis. + +Provides automatic annotation generation using energy-based signal detection +and occupied bandwidth calculation following ITU-R SM.328 standard. +""" + +import json +from typing import Tuple + +import numpy as np +from scipy.signal import filtfilt + +from ria_toolkit_oss.datatypes import Annotation, Recording + + +def detect_signals_energy( + recording: Recording, + k: int = 10, + threshold_factor: float = 1.2, + window_size: int = 200, + min_distance: int = 5000, + label: str = "signal", + annotation_type: str = "standalone", + freq_method: str = "nbw", + nfft: int = None, + obw_power: float = 0.99, +) -> Recording: + """ + Detect signal bursts using energy-based method with adaptive noise floor estimation. + + This algorithm smooths the signal with a moving average filter, estimates the noise + floor from k segments, applies a threshold to detect regions above noise, and merges + nearby detections. Detected time boundaries are then assigned frequency bounds based + on the selected frequency method. + + Time Detection Algorithm: + 1. Smooth signal using moving average (envelope detection) + 2. Divide smoothed signal into k segments + 3. Estimate noise floor as median of segment mean powers + 4. Detect regions where power exceeds threshold_factor * noise_floor + 5. Merge regions closer than min_distance samples + + Frequency Bounding (freq_method): + - 'nbw': Nominal bandwidth (OBW + center frequency) - DEFAULT + - 'obw': Occupied bandwidth (99.99% power, includes siedelobes) + - 'full-detected': Lowest to highest spectral component + - 'full-bandwidth': Entire Nyquist span (center_freq ± sample_rate/2) + + :param recording: Recording to analyze + :type recording: Recording + :param k: Number of segments for noise floor estimation (default: 10) + :type k: int + :param threshold_factor: Threshold multiplier above noise floor (typical: 1.2-2.0, default: 1.2) + :type threshold_factor: float + :param window_size: Moving average window size in samples (default: 200) + :type window_size: int + :param min_distance: Minimum distance between separate signals in samples (default: 5000) + :type min_distance: int + :param label: Label for detected annotations (default: "signal") + :type label: str + :param annotation_type: Annotation type (standalone, parallel, intersection, default: standalone) + :type annotation_type: str + :param freq_method: How to calculate frequency bounds (default: 'nbw') + :type freq_method: str + :param nfft: FFT size for frequency calculations (default: None) + :type nfft: int + :param obw_power: Power percentage for OBW (0.9999 = 99.99%, default: 0.99) + :type obw_power: float + + :returns: New Recording with added annotations + :rtype: Recording + + **Example**:: + + >>> from ria.io import load_recording + >>> from ria_toolkit_oss.annotations import detect_signals_energy + >>> recording = load_recording("capture.sigmf") + + >>> # Detect with NBW frequency bounds (default, best for real signals) + >>> annotated = detect_signals_energy(recording, label="burst") + + >>> # Detect with OBW (more conservative, includes siedelobes) + >>> annotated = detect_signals_energy( + ... recording, label="burst", freq_method="obw" + ... ) + + >>> # Detect with full detected range (captures all spectral components) + >>> annotated = detect_signals_energy( + ... recording, label="burst", freq_method="full-detected" + ... ) + """ + # Extract signal data (use first channel only) + signal = recording.data[0] + + # Calculate smoothed signal power + kernel = np.ones(window_size) / window_size + smoothed_power = filtfilt(kernel, [1], np.abs(signal) ** 2) + + # Estimate noise floor using segment-based median (robust to signal presence) + segments = np.array_split(smoothed_power, k) + noise_floor = np.median([np.mean(s) for s in segments]) + + # Detect signal boundaries (regions above threshold) + enter = noise_floor * threshold_factor + exit = enter * 0.8 + boundaries = [] + start = None + active = False + + for i, p in enumerate(smoothed_power): + if not active and p > enter: + start = i + active = True + elif active and p < exit: + boundaries.append((start, i - start)) + active = False + + if active: + boundaries.append((start, len(smoothed_power) - start)) + + # Merge boundaries that are closer than min_distance + merged_boundaries = [] + if boundaries: + start, length = boundaries[0] + for next_start, next_length in boundaries[1:]: + if next_start - (start + length) < min_distance: + # Merge with current boundary + length = next_start + next_length - start + else: + # Save current and start new boundary + merged_boundaries.append((start, length)) + start, length = next_start, next_length + # Add final boundary + merged_boundaries.append((start, length)) + + # Create annotations from detected boundaries + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0) + + # Validate frequency method + valid_freq_methods = ["nbw", "obw", "full-detected", "full-bandwidth"] + if freq_method not in valid_freq_methods: + raise ValueError(f"Invalid freq_method '{freq_method}'. " f"Must be one of: {', '.join(valid_freq_methods)}") + + annotations = [] + for start_sample, sample_count in merged_boundaries: + # Calculate frequency bounds based on method + freq_lower, freq_upper = calculate_frequency_bounds( + freq_method, center_frequency, sample_rate, nfft, signal, start_sample, sample_count, obw_power + ) + # Build comment JSON with type metadata + comment_data = { + "type": annotation_type, + "generator": "energy_detector", + "freq_method": freq_method, + "params": { + "threshold_factor": threshold_factor, + "window_size": window_size, + "noise_floor": float(noise_floor), + "threshold": float(enter), + }, + } + + anno = Annotation( + sample_start=start_sample, + sample_count=sample_count, + freq_lower_edge=freq_lower, + freq_upper_edge=freq_upper, + label=label, + comment=json.dumps(comment_data), + detail={"generator": "energy_detector", "freq_method": freq_method}, + ) + annotations.append(anno) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + annotations) + + +def calculate_occupied_bandwidth( + signal: np.ndarray, + sampling_rate: float, + nfft: int = None, + power_percentage: float = 0.99, +): + if nfft is None: + nfft = max(65536, 2 ** int(np.floor(np.log2(len(signal))))) + + window = np.blackman(len(signal)) + spec = np.fft.fftshift(np.fft.fft(signal * window, n=nfft)) + + psd = np.abs(spec) ** 2 + psd = psd / psd.sum() # normalize + + freqs = np.fft.fftshift(np.fft.fftfreq(nfft, 1 / sampling_rate)) + + cdf = np.cumsum(psd) + + tail = (1 - power_percentage) / 2 + + lower_idx = np.searchsorted(cdf, tail) + upper_idx = np.searchsorted(cdf, 1 - tail) + + return freqs[upper_idx] - freqs[lower_idx], freqs[lower_idx], freqs[upper_idx] + + +def calculate_nominal_bandwidth( + signal: np.ndarray, + sampling_rate: float, + nfft: int = None, + power_percentage: float = 0.99, +) -> Tuple[float, float]: + """ + Calculate nominal bandwidth and center frequency. + + Nominal bandwidth (NBW) is the occupied bandwidth along with the center + frequency of the signal's spectral occupancy. Useful for characterizing + signals with unknown or drifting center frequencies. + + :param signal: Complex IQ signal samples + :type signal: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param nfft: FFT size + :type nfft: int + :param power_percentage: Fraction of power to contain + :type power_percentage: float + + :returns: Tuple of (nominal_bandwidth_hz, center_frequency_hz) + :rtype: Tuple[float, float] + + **Example**:: + + >>> from utils.annotations import calculate_nominal_bandwidth + >>> nbw, center = calculate_nominal_bandwidth(signal, sampling_rate=10e6) + >>> print(f"NBW: {nbw/1e6:.3f} MHz, Center: {center/1e6:.3f} MHz") + """ + bw, lower_freq, upper_freq = calculate_occupied_bandwidth(signal, sampling_rate, nfft, power_percentage) + + # Center frequency is midpoint of occupied band + center_freq = (lower_freq + upper_freq) / 2 + + return lower_freq, upper_freq, center_freq + + +def calculate_full_detected_bandwidth( + signal: np.ndarray, + sampling_rate: float, + nfft: int = None, + start_offset: int = 1000, +) -> Tuple[float, float, float]: + """ + Calculate frequency range from lowest to highest spectral component. + + Unlike OBW/NBW which define a power-based bandwidth, this calculates + the absolute frequency span from the lowest non-zero spectral component + to the highest non-zero component. + + Useful for: + - Signals with spectral gaps + - Multiple parallel signals (captures all of them) + - Understanding total occupied spectrum vs. actual bandwidth + + :param signal: Complex IQ signal samples + :type signal: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param nfft: FFT size + :type nfft: int + :param start_offset: Skip samples at start + :type start_offset: int + + :returns: Tuple of (bandwidth_hz, lower_freq_hz, upper_freq_hz) + :rtype: Tuple[float, float, float] + + **Example**:: + + >>> # Signal with two components at different frequencies + >>> bw, f_low, f_high = calculate_full_detected_bandwidth( + ... signal, sampling_rate=10e6, nfft=65536 + ... ) + >>> print(f"Full span: {f_low/1e6:.3f} to {f_high/1e6:.3f} MHz") + """ + # Validate input + if len(signal) < nfft + start_offset: + raise ValueError( + f"Signal too short: need {nfft + start_offset} samples, " + f"got {len(signal)}. Reduce nfft or start_offset." + ) + + # Extract segment + signal_segment = signal[start_offset : nfft + start_offset] + + # Compute FFT and power spectral density + freq_spectrum = np.fft.fft(signal_segment, n=nfft) + psd = np.abs(freq_spectrum) ** 2 + + # Shift to center DC + psd_shifted = np.fft.fftshift(psd) + freq_bins = np.fft.fftshift(np.fft.fftfreq(nfft, 1 / sampling_rate)) + + # Find noise floor (mean of lowest 10% of bins) and all bins above noise floor + noise_floor = np.mean(np.sort(psd_shifted)[: int(len(psd_shifted) * 0.1)]) + above_noise = np.where(psd_shifted > noise_floor * 1.5)[0] + + if len(above_noise) == 0: + # No signal above noise, return zero bandwidth + return 0.0, 0.0, 0.0 + + # Get frequency range of signal components + lower_idx = above_noise[0] + upper_idx = above_noise[-1] + + lower_freq = freq_bins[lower_idx] + upper_freq = freq_bins[upper_idx] + + bandwidth = upper_freq - lower_freq + + return bandwidth, lower_freq, upper_freq + + +def annotate_with_obw( + recording: Recording, + label: str = "signal", + annotation_type: str = "standalone", + nfft: int = None, + power_percentage: float = 0.99, +) -> Recording: + """ + Create a single annotation spanning the occupied bandwidth of the entire recording. + + Analyzes the full recording to find its occupied bandwidth and creates an annotation + covering that frequency range for the entire time duration. + + :param recording: Recording to analyze + :type recording: Recording + :param label: Annotation label + :type label: str + :param annotation_type: Annotation type + :type annotation_type: str + :param nfft: FFT size + :type nfft: int + :param power_percentage: Power percentage for OBW calculation + :type power_percentage: float + + :returns: Recording with OBW annotation added + :rtype: Recording + + **Example**:: + + >>> from ria_toolkit_oss.annotations import annotate_with_obw + >>> annotated = annotate_with_obw(recording, label="signal_obw") + """ + signal = recording.data[0] + sample_rate = recording.metadata["sample_rate"] + center_freq = recording.metadata.get("center_frequency", 0) + + # Calculate OBW + obw, lower_offset, upper_offset = calculate_occupied_bandwidth(signal, sample_rate, nfft, power_percentage) + + # Convert baseband offsets to absolute frequencies + freq_lower = center_freq + lower_offset + freq_upper = center_freq + upper_offset + + # Create comment JSON + comment_data = { + "type": annotation_type, + "generator": "obw_annotator", + "obw_hz": float(obw), + "power_percentage": power_percentage, + "params": {"nfft": nfft}, + } + + # Create annotation spanning entire recording + anno = Annotation( + sample_start=0, + sample_count=len(signal), + freq_lower_edge=freq_lower, + freq_upper_edge=freq_upper, + label=label, + comment=json.dumps(comment_data), + detail={"generator": "obw_annotator", "obw_hz": float(obw)}, + ) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + [anno]) + + +def calculate_frequency_bounds( + freq_method, center_frequency, sample_rate, nfft, signal, start_sample, sample_count, obw_power +): + if freq_method == "full-bandwidth": + # Full Nyquist span + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + else: + # Extract segment for frequency analysis + segment_start = start_sample + segment_end = min(start_sample + sample_count, len(signal)) + segment = signal[segment_start:segment_end] + + if nfft is None or len(segment) >= nfft: + if freq_method == "nbw": + # Nominal bandwidth (OBW + center frequency) + try: + lower_freq, upper_freq, _ = calculate_nominal_bandwidth(segment, sample_rate, nfft, obw_power) + freq_lower = center_frequency + lower_freq + freq_upper = center_frequency + upper_freq + except (ValueError, IndexError): + # Fallback if calculation fails + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + + elif freq_method == "obw": + # Occupied bandwidth + try: + _, f_lower, f_upper = calculate_occupied_bandwidth(segment, sample_rate, nfft, obw_power) + freq_lower = center_frequency + f_lower + freq_upper = center_frequency + f_upper + except (ValueError, IndexError): + # Fallback if calculation fails + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + + elif freq_method == "full-detected": + # Full detected range (lowest to highest component) + try: + _, f_lower, f_upper = calculate_full_detected_bandwidth(segment, sample_rate, nfft) + freq_lower = center_frequency + f_lower + freq_upper = center_frequency + f_upper + except (ValueError, IndexError): + # Fallback if calculation fails + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + else: + # Segment too short for FFT, use full bandwidth + freq_lower = center_frequency - (sample_rate / 2) + freq_upper = center_frequency + (sample_rate / 2) + + return freq_lower, freq_upper diff --git a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py new file mode 100644 index 0000000..957cf58 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py @@ -0,0 +1,435 @@ +""" +Parallel signal separation for multi-component frequency-offset signals. + +Provides methods to detect and separate overlapping frequency-domain signals +that occupy the same time window but different frequency bands. + +This module implements **spectral peak detection** to identify distinct frequency +components and split single time-domain annotations into frequency-specific +sub-annotations. + +**Key Design Decisions** (per Codex review): + +1. **Complex IQ Support**: Uses `scipy.signal.welch` with `return_onesided=False` + for proper complex signal handling. Window length automatically adapts to + signal length via `nperseg=min(nfft, len(signal))` to handle bursts >> from ria_toolkit_oss.annotations import find_spectral_components + >>> # Detect the two distinct channels (returns relative frequencies) + >>> components = find_spectral_components(signal, sampling_rate=20e6) + >>> print(f"Found {len(components)} components") + Found 2 components + +The module is designed to work with detected time-domain annotations, +allowing splitting of overlapping signals into separate training samples. +""" + +import json +from typing import List, Optional, Tuple + +import numpy as np +from scipy import ndimage +from scipy import signal as scipy_signal + +from ria_toolkit_oss.datatypes import Annotation, Recording + + +def find_spectral_components( + signal_data: np.ndarray, + sampling_rate: float, + nfft: int = 65536, + noise_threshold_db: Optional[float] = None, + min_component_bw: float = 50e3, + time_percentile: float = 70.0, +) -> List[Tuple[float, float, float]]: + """ + Find distinct frequency components using spectral peak detection. + + Identifies separate frequency components in a signal by analyzing the power + spectral density and finding peaks corresponding to distinct signals. This is + useful for separating parallel signals that occupy different frequency bands. + + **Frequency Representation**: Returns frequencies in **baseband/relative** Hz + (centered at 0). To get absolute RF frequencies, add center_frequency_hz from + recording metadata to all returned values. + + Algorithm: + 1. Compute power spectral density using Welch (properly handles complex IQ) + 2. Auto-estimate noise floor from data if not specified + 3. Smooth PSD to reduce spurious peaks + 4. Find local maxima above noise floor + 5. Estimate bandwidth per peak using -3dB (fallback: cumulative power) + 6. Filter components below minimum bandwidth threshold + + :param signal_data: Complex IQ signal samples (np.complex64/128) + :type signal_data: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param nfft: FFT size / window length for Welch. Automatically capped at + signal length to handle bursts (default: 65536) + :type nfft: int + :param noise_threshold_db: Minimum SNR threshold in dB. If None (default), + auto-estimates as np.percentile(psd_db, 10). + Adapt this across hardware (Pluto: ~-100, ThinkRF: ~-60). + :type noise_threshold_db: Optional[float] + :param min_component_bw: Minimum component bandwidth in Hz (default: 50 kHz) + :type min_component_bw: float + :param power_threshold: Cumulative power threshold for fallback bandwidth + estimation (default: 0.99 = 99% power, like OBW) + :type power_threshold: float + + :returns: List of (center_freq_hz, lower_freq_hz, upper_freq_hz) tuples. + **All frequencies are relative (baseband, 0-centered).** + Add recording metadata['center_frequency'] to get absolute RF frequencies. + :rtype: List[Tuple[float, float, float]] + + :raises ValueError: If signal has fewer than 256 samples + + **Example**:: + + >>> from ria.io import load_recording + >>> from ria_toolkit_oss.annotations import find_spectral_components + >>> recording = load_recording("capture.sigmf") + >>> segment = recording.data[0][start:end] + >>> # Components in relative (baseband) frequency + >>> components = find_spectral_components(segment, sampling_rate=20e6) + >>> for center_rel, lower_rel, upper_rel in components: + ... # Convert to absolute RF frequency + ... center_abs = recording.metadata['center_frequency'] + center_rel + ... print(f"Component @ {center_abs/1e9:.3f} GHz") + """ + # Validate input + min_samples = 256 + if len(signal_data) < min_samples: + raise ValueError(f"Signal too short: need at least {min_samples} samples, " f"got {len(signal_data)}.") + + # Compute PSD using Welch method for complex IQ signals + # CRITICAL: return_onesided=False for proper complex signal handling + nperseg = min(nfft, len(signal_data)) + noverlap = nperseg // 2 + + # --- STFT --- + freqs, times, Zxx = scipy_signal.stft( + signal_data, + fs=sampling_rate, + window="blackman", + nperseg=nperseg, + noverlap=noverlap, + return_onesided=False, + boundary=None, + ) + + # Shift zero freq to center + Zxx = np.fft.fftshift(Zxx, axes=0) + freqs = np.fft.fftshift(freqs) + + # Power spectrogram + power = np.abs(Zxx) ** 2 + power_db = 10 * np.log10(power + 1e-12) + + # --- Aggregate across time robustly --- + # Using percentile instead of mean prevents short signals from being diluted + freq_profile_db = np.percentile(power_db, time_percentile, axis=1) + + # --- Noise floor estimation --- + if noise_threshold_db is None: + noise_threshold_db = np.percentile(freq_profile_db, 20) + + threshold = noise_threshold_db + 3 # 3 dB above noise floor + + # --- Smooth lightly (avoid merging nearby signals) --- + freq_profile_db = ndimage.gaussian_filter1d(freq_profile_db, sigma=1.5) + + # --- Binary mask of significant frequencies --- + mask = freq_profile_db > threshold + + # --- Find contiguous frequency regions --- + labeled, num_features = ndimage.label(mask) + + components = [] + + for region_label in range(1, num_features + 1): + region_indices = np.where(labeled == region_label)[0] + + if len(region_indices) == 0: + continue + + lower_idx = region_indices[0] + upper_idx = region_indices[-1] + + lower_freq = freqs[lower_idx] + upper_freq = freqs[upper_idx] + bw = upper_freq - lower_freq + + if bw < min_component_bw: + continue + + center_freq = (lower_freq + upper_freq) / 2 + components.append((center_freq, lower_freq, upper_freq)) + + return components + + +def split_annotation_by_components( + annotation: Annotation, + signal: np.ndarray, + sampling_rate: float, + center_frequency_hz: float = 0.0, + nfft: int = 65536, + noise_threshold_db: Optional[float] = None, + min_component_bw: float = 50e3, +) -> List[Annotation]: + """ + Split an annotation into multiple annotations by detected frequency components. + + Takes an existing annotation spanning multiple frequency components and + analyzes the frequency content to create separate sub-annotations for + each distinct frequency component. + + **Use case**: Energy detection found a time window with 2-3 parallel WiFi + channels. This function splits it into separate annotations per channel. + + **Frequency Handling**: `find_spectral_components` returns relative (baseband) + frequencies. This function adds `center_frequency_hz` to convert to absolute + RF frequencies for SigMF annotation bounds. This ensures correct frequency + context across baseband and RF domains. + + :param annotation: Original annotation to split + :type annotation: Annotation + :param signal: Full signal array (complex IQ) + :type signal: np.ndarray + :param sampling_rate: Sample rate in Hz + :type sampling_rate: float + :param center_frequency_hz: RF center frequency to add to relative frequencies + from peak detection (default: 0.0 = baseband) + :type center_frequency_hz: float + :param nfft: FFT size for analysis (default: 65536, auto-capped at signal length) + :type nfft: int + :param noise_threshold_db: Noise floor threshold in dB. If None (default), + auto-estimates from data. + :type noise_threshold_db: Optional[float] + :param min_component_bw: Minimum component bandwidth in Hz (default: 50 kHz) + :type min_component_bw: float + + :returns: List of new annotations (one per detected component). + Returns empty list if no components found or segment too short. + :rtype: List[Annotation] + + **Example**:: + + >>> from ria.io import load_recording + >>> from ria_toolkit_oss.annotations import split_annotation_by_components + >>> recording = load_recording("capture.sigmf") + >>> # Original annotation spans multiple channels + >>> original = recording.annotations[0] + >>> # Split using RF center frequency from metadata + >>> components = split_annotation_by_components( + ... original, + ... recording.data[0], + ... recording.metadata['sample_rate'], + ... center_frequency_hz=recording.metadata.get('center_frequency', 0.0) + ... ) + >>> print(f"Split into {len(components)} components") + Split into 2 components + + **Algorithm**: + 1. Extract segment corresponding to annotation time bounds + 2. Find frequency components in that segment (returns relative frequencies) + 3. Add center_frequency_hz to get absolute RF frequencies + 4. Create new annotation for each component + 5. Preserve original metadata (label, type, etc.) + 6. Add component info to comment JSON + + **Notes**: + - Original annotation is not modified + - Returns empty list if segment too short (<256 samples) + - Segments Recording: + """ + Split multiple annotations in a recording by frequency components. + + Processes specified annotations (or all if indices=None), replacing each + with its frequency-separated components. Uses RF center_frequency from + recording metadata for proper absolute frequency conversion. + + :param recording: Recording to process + :type recording: Recording + :param indices: Annotation indices to split (None = all, default: None). + Use indices=[] to skip splitting (returns unchanged recording). + :type indices: Optional[List[int]] + :param nfft: FFT size for spectral analysis (default: 65536, + auto-capped at signal segment length) + :type nfft: int + :param noise_threshold_db: Noise floor threshold in dB. If None (default), + auto-estimates from each segment. + :type noise_threshold_db: Optional[float] + :param min_component_bw: Minimum component bandwidth in Hz (default: 50 kHz). + Components narrower than this are filtered out. + :type min_component_bw: float + + :returns: New Recording with split annotations + :rtype: Recording + + **Example**:: + + >>> from ria.io import load_recording + >>> from ria_toolkit_oss.annotations import split_recording_annotations + >>> recording = load_recording("capture.sigmf") + >>> # Split all annotations + >>> split_rec = split_recording_annotations(recording) + >>> print(f"Original: {len(recording.annotations)} annotations") + >>> print(f"Split: {len(split_rec.annotations)} annotations") + Original: 5 annotations + Split: 9 annotations + + **Algorithm**: + 1. For each annotation in indices (or all if None): + 2. Call split_annotation_by_components with RF center_frequency + 3. If components found, replace annotation with components + 4. If no components found, keep original annotation + 5. Annotations not in indices are kept unchanged + + **Notes**: + - Original recording is not modified + - Returns empty Recording.annotations if recording has no annotations + - RF center_frequency from metadata ensures correct absolute frequencies + - If an annotation can't be split (too short, wrong format), original kept + """ + if indices is None: + # Split all annotations + indices = list(range(len(recording.annotations))) + + if not recording.annotations: + # No annotations to split + return recording + + signal = recording.data[0] + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0.0) + + # Build new annotation list + new_annotations = [] + for i, anno in enumerate(recording.annotations): + if i in indices: + # Attempt to split this annotation + try: + components = split_annotation_by_components( + anno, + signal, + sample_rate, + center_frequency_hz=center_frequency, + nfft=nfft, + noise_threshold_db=noise_threshold_db, + min_component_bw=min_component_bw, + ) + if components: + # Split successful, use components + new_annotations.extend(components) + else: + # No components found, keep original + new_annotations.append(anno) + except Exception: + # Split failed for any reason, keep original + new_annotations.append(anno) + else: + # Not in split list, keep as-is + new_annotations.append(anno) + + return Recording(data=recording.data, metadata=recording.metadata, annotations=new_annotations) diff --git a/src/ria_toolkit_oss/annotations/qualify_slice.py b/src/ria_toolkit_oss/annotations/qualify_slice.py new file mode 100644 index 0000000..2336fe5 --- /dev/null +++ b/src/ria_toolkit_oss/annotations/qualify_slice.py @@ -0,0 +1,35 @@ +import numpy as np + +from ria_toolkit_oss.datatypes import Recording + + +def qualify_slice_from_annotations(recording: Recording, slice_length: int): + """ + Slice a recording into many smaller recordings, + discarding any slices which do not have annotations that apply to those samples. + Used together with an annotation based qualifier. + + :param recording: The recording to slice. + :type recording: Recording + :param slice_length: The length in samples of a slice. + :type slice_length: int""" + + if len(recording.annotations) == 0: + print("Warning, no annotations.") + + annotation_mask = np.zeros(len(recording.data[0])) + + for annotation in recording.annotations: + annotation_mask[annotation.sample_start : annotation.sample_start + annotation.sample_count] = 1 + + output_recordings = [] + + for i in range((len(recording.data[0]) // slice_length) - 1): + start_index = slice_length * i + end_index = slice_length * (i + 1) + + if 1 in annotation_mask[start_index:end_index]: + sl = recording.data[:, start_index:end_index] + output_recordings.append(Recording(data=sl, metadata=recording.metadata)) + + return output_recordings diff --git a/src/ria_toolkit_oss/annotations/signal_isolation.py b/src/ria_toolkit_oss/annotations/signal_isolation.py new file mode 100644 index 0000000..47852ae --- /dev/null +++ b/src/ria_toolkit_oss/annotations/signal_isolation.py @@ -0,0 +1,97 @@ +import numpy as np +from scipy.signal import butter, lfilter + +from ria_toolkit_oss.datatypes.annotation import Annotation +from ria_toolkit_oss.datatypes.recording import Recording + + +def isolate_signal(recording: Recording, annotation: Annotation) -> Recording: + """ + Slice, filter and frequency shift the input recording according to the bounding box defined by the annotation. + + :param recording: The input Recording to be sliced. + :type recording: Recording + :param annotation: The Annotation object defining the area of the recording to isolate. + :type annotation: Annotation + :param decimate: Decimate the input signal after filtering to reduce the sample rate. + :type decimate: bool + + :returns: The subsection of the original recording defined by the annotation. + :rtype: Recording""" + + sample_start = max(0, annotation.sample_start) + sample_stop = min(len(recording), annotation.sample_start + annotation.sample_count) + + anno_base_center_freq = (annotation.freq_lower_edge + annotation.freq_upper_edge) / 2 - recording.metadata.get( + "center_frequency", 0 + ) + + anno_bw = annotation.freq_upper_edge - annotation.freq_lower_edge + + signal_slice = recording.data[0, sample_start:sample_stop] + + # normalize + signal_slice = signal_slice / np.max(np.abs(signal_slice)) + + isolation_bw = anno_bw + + # frequency shift the center of the box about zero + shifted_signal_slice = frequency_shift_iq_samples( + iq_samples=signal_slice, + sample_rate=recording.metadata["sample_rate"], + shift_frequency=-1 * anno_base_center_freq, + ) + + # filter + if isolation_bw < recording.metadata["sample_rate"] - 1: + filtered_signal = apply_complex_lowpass_filter( + signal=shifted_signal_slice, cutoff_frequency=isolation_bw, sample_rate=recording.metadata["sample_rate"] + ) + + else: + filtered_signal = shifted_signal_slice + + output = Recording(data=[filtered_signal], metadata=recording.metadata) + return output + + +def frequency_shift_iq_samples(iq_samples, sample_rate, shift_frequency): + # Number of samples + num_samples = len(iq_samples) + + # Create a time vector from 0 to the total duration in seconds + time_vector = np.arange(num_samples) / sample_rate + + # Generate the complex exponential for the frequency shift + complex_exponential = np.exp(1j * 2 * np.pi * shift_frequency * time_vector) + + # Apply the frequency shift to the IQ samples + shifted_samples = iq_samples * complex_exponential + + return shifted_samples + + +# Function to apply a lowpass Butterworth filter to a complex signal +def apply_complex_lowpass_filter(signal, cutoff_frequency, sample_rate, order=5): + # Design the lowpass filter + b, a = design_complex_lowpass_filter(cutoff_frequency, sample_rate, order) + + # Apply the lowpass filter + filtered_signal = lfilter(b, a, signal) + return filtered_signal + + +def design_complex_lowpass_filter(cutoff_frequency, sample_rate, order=5): + # Nyquist frequency for complex signals is the sample rate + nyquist = sample_rate + + # Ensure the cutoff frequency is positive and within the Nyquist limit + if cutoff_frequency <= 0 or cutoff_frequency > nyquist: + raise ValueError("Cutoff frequency must be between 0 and the Nyquist frequency.") + + # Normalize the cutoff frequency to the Nyquist frequency + cutoff_normalized = cutoff_frequency / nyquist + + # Create a Butterworth lowpass filter + b, a = butter(order, cutoff_normalized, btype="low") + return b, a diff --git a/src/ria_toolkit_oss/annotations/threshold_qualifier.py b/src/ria_toolkit_oss/annotations/threshold_qualifier.py new file mode 100644 index 0000000..338f13c --- /dev/null +++ b/src/ria_toolkit_oss/annotations/threshold_qualifier.py @@ -0,0 +1,352 @@ +""" +Temporal signal detection and boundary refinement via Hysteresis Thresholding. + +Provides methods to detect signal bursts in the time domain by triggering on +smoothed power peaks and expanding boundaries to capture the full energy envelope. + +This module implements a **dual-threshold trigger** to solve the 'chatter' +problem in noisy environments, ensuring that signal annotations encapsulate +the entire rise and fall of a burst rather than just the peak. + +**Key Design Decisions**: + +1. **Hysteresis Logic (Dual-Threshold)**: + - **Trigger**: High threshold (`threshold * max_power`) ensures high confidence + in signal presence. + - **Boundary**: Low threshold (`0.5 * trigger`) allows the annotation to + "crawl" outward, capturing the lower-energy start and end of the burst + often missed by simple single-threshold detectors. + +2. **Temporal Smoothing**: Uses a moving average window (`window_size`) prior + - to thresholding. This prevents high-frequency noise spikes from causing + fragmented annotations and provides a more stable estimate of the + signal's power envelope. + +3. **Spectral Profiling**: Once a temporal segment is isolated, the module + - performs an automated FFT analysis. It identifies the **90% spectral + occupancy** to define the frequency boundaries (`f_min`, `f_max`), + allowing the detector to work on narrowband and wideband signals without + manual frequency tuning. + +4. **Baseband/RF Mapping**: Automatically handles the conversion from + - relative FFT bin frequencies to absolute RF frequencies by referencing + `recording.metadata["center_frequency"]`. + +5. **False Positive Mitigation**: Implements a hard minimum duration check + - (10ms) to ignore transient hardware spikes or noise floor fluctuations + that do not constitute a valid signal burst. + +The module is designed to be the primary "first-pass" detector for pulsed +waveforms (like ADS-B, Lora, or bursty FSK) before passing them to +classification or demodulation stages. +""" + +import json +from typing import Optional + +import numpy as np + +from ria_toolkit_oss.datatypes import Annotation, Recording + + +def _find_ranges(indices, max_gap): + """ + Groups individual indices into continuous temporal ranges. + + Args: + indices: Array of indices where the signal exceeded a threshold. + max_gap: Maximum gap allowed between indices to consider them part + of the same range. + + Returns: + A list of (start, stop) tuples representing detected signal segments. + """ + + if len(indices) == 0: + return [] + + start = indices[0] + prev = indices[0] + ranges = [] + + for i in range(1, len(indices)): + if indices[i] - prev > max_gap: + ranges.append((start, prev)) + start = indices[i] + prev = indices[i] + + ranges.append((start, prev)) + + return ranges + + +def _expand_and_filter_ranges( + smoothed_power: np.ndarray, + initial_ranges: list[tuple[int, int]], + boundary_val: float, + min_duration_samples: int, +) -> list[tuple[int, int]]: + """Apply hysteresis expansion and minimum-duration filtering.""" + out: list[tuple[int, int]] = [] + n = len(smoothed_power) + for start, stop in initial_ranges: + if (stop - start) < min_duration_samples: + continue + + true_start = start + while true_start > 0 and smoothed_power[true_start] > boundary_val: + true_start -= 1 + + true_stop = stop + while true_stop < n - 1 and smoothed_power[true_stop] > boundary_val: + true_stop += 1 + + if (true_stop - true_start) >= min_duration_samples: + out.append((true_start, true_stop)) + return out + + +def _merge_ranges(ranges: list[tuple[int, int]], max_gap: int) -> list[tuple[int, int]]: + """Merge overlapping or near-adjacent ranges.""" + if not ranges: + return [] + ranges = sorted(ranges, key=lambda r: r[0]) + merged = [ranges[0]] + for s, e in ranges[1:]: + last_s, last_e = merged[-1] + if s <= last_e + max_gap: + merged[-1] = (last_s, max(last_e, e)) + else: + merged.append((s, e)) + return merged + + +def _estimate_noise_floor(power: np.ndarray, quantile: float = 20.0) -> float: + """Estimate baseline from the quieter portion of the envelope.""" + return float(np.percentile(power, quantile)) + + +def _estimate_group_gap(sample_rate: float) -> int: + """Use a fixed temporal grouping gap instead of reusing the smoothing window.""" + return max(1, int(0.001 * sample_rate)) + + +def _estimate_spectral_bounds(signal_segment: np.ndarray, sample_rate: float) -> tuple[float, float]: + """Estimate occupied bandwidth from a smoothed magnitude spectrum.""" + if len(signal_segment) == 0: + return -sample_rate / 4, sample_rate / 4 + + window = np.hanning(len(signal_segment)) + windowed = signal_segment * window + + fft_data = np.abs(np.fft.fftshift(np.fft.fft(windowed))) + fft_freqs = np.fft.fftshift(np.fft.fftfreq(len(signal_segment), 1 / sample_rate)) + + # Smooth the spectrum so noise-like wideband bursts form a contiguous mask + # instead of thousands of tiny isolated runs. + spectral_smooth_bins = max(5, min(257, (len(signal_segment) // 512) | 1)) + spectral_kernel = np.ones(spectral_smooth_bins, dtype=np.float64) / spectral_smooth_bins + smoothed_fft = np.convolve(fft_data, spectral_kernel, mode="same") + + spectral_floor = float(np.percentile(smoothed_fft, 20)) + spectral_peak = float(np.max(smoothed_fft)) + spectral_ratio = spectral_peak / max(spectral_floor, 1e-12) + + if spectral_ratio < 1.2: + return -sample_rate / 4, sample_rate / 4 + + spectral_thresh = spectral_floor + 0.1 * (spectral_peak - spectral_floor) + sig_indices = np.where(smoothed_fft > spectral_thresh)[0] + + if len(sig_indices) == 0: + peak_idx = int(np.argmax(smoothed_fft)) + bin_hz = sample_rate / len(signal_segment) + half_bins = max(1, int(np.ceil(10_000.0 / bin_hz))) + lo_idx = max(0, peak_idx - half_bins) + hi_idx = min(len(smoothed_fft) - 1, peak_idx + half_bins) + else: + runs = _find_ranges(sig_indices, max_gap=max(1, spectral_smooth_bins // 2)) + peak_idx = int(np.argmax(smoothed_fft)) + lo_idx, hi_idx = min(runs, key=lambda run: 0 if run[0] <= peak_idx <= run[1] else min(abs(run[0] - peak_idx), abs(run[1] - peak_idx))) + + # Prevent extremely narrow tone boxes from collapsing to just a few bins. + min_total_bw_hz = 20_000.0 + min_half_bins = max(1, int(np.ceil((min_total_bw_hz / 2) / (sample_rate / len(signal_segment))))) + center_idx = int(round((lo_idx + hi_idx) / 2)) + lo_idx = max(0, min(lo_idx, center_idx - min_half_bins)) + hi_idx = min(len(smoothed_fft) - 1, max(hi_idx, center_idx + min_half_bins)) + + return float(fft_freqs[lo_idx]), float(fft_freqs[hi_idx]) + + +def threshold_qualifier( + recording: Recording, + threshold: float, + window_size: Optional[int] = None, + label: Optional[str] = None, + annotation_type: Optional[str] = "standalone", + channel: int = 0, +) -> Recording: + """ + Annotate a recording with bounding boxes for regions above a threshold. + Threshold is defined as a fraction of the maximum sample magnitude. + This algorithm searches for samples above the threshold and combines them into ranges if they + are within window_size of each other. + Detects and annotates signals using energy thresholding and spectral analysis. + + The algorithm follows these steps: + 1. Smooths power data using a moving average. + 2. Identifies 'peak' regions exceeding a high trigger threshold. + 3. Uses hysteresis to expand boundaries until power drops below a lower threshold. + 4. Performs an FFT on each segment to determine frequency occupancy. + + Args: + recording: The Recording object containing IQ or real signal data. + threshold: Sensitivity multiplier (0.0 to 1.0) applied to max power. + window_size: Size of the smoothing filter in samples. Defaults to 1ms worth of samples. + label: Custom string label for annotations. + annotation_type: Metadata string for the 'type' field in the annotation. + channel: Index of the channel to annotate. Defaults to 0. + + Returns: + A new Recording object populated with detected Annotations. + """ + # Extract signal and metadata + sample_data = recording.data[channel] + sample_rate = recording.metadata["sample_rate"] + center_frequency = recording.metadata.get("center_frequency", 0) + + if window_size is None: + window_size = max(64, int(sample_rate * 0.001)) + + # --- 1. SIGNAL CONDITIONING --- + # Convert to power (Magnitude squared) + power_data = np.abs(sample_data) ** 2 + smoothing_window = np.ones(window_size) / window_size + smoothed_power = np.convolve(power_data, smoothing_window, mode="same") + group_gap_samples = _estimate_group_gap(sample_rate) + + # Define thresholds using peak relative to baseline. + max_power = np.max(smoothed_power) + noise_floor = _estimate_noise_floor(smoothed_power) + dynamic_range_ratio = max_power / max(noise_floor, 1e-12) + + # Soft early exit: keep a guard for low-contrast noise, but compute it from + # the quieter tail of the envelope so burst-heavy captures are not rejected. + if dynamic_range_ratio < 1.5: + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations) + + trigger_val = noise_floor + threshold * (max_power - noise_floor) + boundary_val = noise_floor + 0.5 * threshold * (max_power - noise_floor) + + # --- 2. INITIAL DETECTION --- + # Enforce an explicit minimum duration in seconds; this is stable across + # varying capture lengths and avoids over-fitting to recording length. + min_duration_samples = max(1, int(0.005 * sample_rate)) + annotations = [] + + # Pass 1: Detect stronger bursts. + indices = np.where(smoothed_power > trigger_val)[0] + pass1_initial = _find_ranges(indices=indices, max_gap=group_gap_samples) + pass1_ranges = _expand_and_filter_ranges( + smoothed_power=smoothed_power, + initial_ranges=pass1_initial, + boundary_val=boundary_val, + min_duration_samples=min_duration_samples, + ) + + # Pass 2: Recover weaker bursts on residual power not already covered. + # This improves recall in mixed-amplitude captures. + mask = np.ones_like(smoothed_power, dtype=np.float32) + for s, e in pass1_ranges: + mask[max(0, s) : min(len(mask), e)] = 0.0 + residual_power = smoothed_power * mask + + residual_max = float(np.max(residual_power)) + residual_ratio = residual_max / max(noise_floor, 1e-12) + + pass2_ranges: list[tuple[int, int]] = [] + if residual_ratio >= 2.0: + weak_threshold = max(0.3, threshold * 0.7) + weak_trigger = noise_floor + weak_threshold * (residual_max - noise_floor) + weak_boundary = noise_floor + 0.5 * weak_threshold * (residual_max - noise_floor) + weak_indices = np.where(residual_power > weak_trigger)[0] + pass2_initial = _find_ranges(indices=weak_indices, max_gap=group_gap_samples) + pass2_ranges = _expand_and_filter_ranges( + smoothed_power=smoothed_power, + initial_ranges=pass2_initial, + boundary_val=weak_boundary, + min_duration_samples=min_duration_samples, + ) + + # Pass 3: Detect sustained faint bursts via macro-window averaging. + # Targets bursts whose peak power is near the trigger level but whose + # *average* power is consistently elevated above the noise floor — these + # are missed by peak-based detection because only a few short spikes exceed + # the trigger, all too brief to pass the minimum-duration filter. + # + # The mask is applied to power_data *before* convolving so that bright + # burst energy does not bleed through the long window into adjacent regions, + # which would inflate macro_residual_max and push the trigger above the + # faint burst's average power. + macro_window_size = max(window_size * 16, int(sample_rate * 0.02)) + macro_kernel = np.ones(macro_window_size, dtype=np.float64) / macro_window_size + # Expand each annotated range by half the macro window on both sides so that + # the long convolution cannot "see" the leading/trailing edges of already- + # annotated bursts, which would produce spurious short fragments in Pass 3. + macro_expand = macro_window_size * 2 + masked_power_for_macro = power_data.copy() + n = len(masked_power_for_macro) + for s, e in pass1_ranges + pass2_ranges: + masked_power_for_macro[max(0, s - macro_expand) : min(n, e + macro_expand)] = 0.0 + macro_residual = np.convolve(masked_power_for_macro, macro_kernel, mode="same") + + macro_residual_max = float(np.max(macro_residual)) + + pass3_ranges: list[tuple[int, int]] = [] + if macro_residual_max / max(noise_floor, 1e-12) >= 1.3: + macro_trigger = noise_floor + threshold * (macro_residual_max - noise_floor) + macro_boundary = noise_floor + 0.5 * threshold * (macro_residual_max - noise_floor) + macro_indices = np.where(macro_residual > macro_trigger)[0] + macro_initial = _find_ranges(indices=macro_indices, max_gap=group_gap_samples) + pass3_ranges = _expand_and_filter_ranges( + smoothed_power=macro_residual, + initial_ranges=macro_initial, + boundary_val=macro_boundary, + min_duration_samples=min_duration_samples, + ) + + all_ranges = _merge_ranges(pass1_ranges + pass2_ranges + pass3_ranges, max_gap=group_gap_samples) + + for true_start, true_stop in all_ranges: + + # --- 4. SPECTRAL ANALYSIS (Frequency Detection) --- + signal_segment = sample_data[true_start:true_stop] + f_min, f_max = _estimate_spectral_bounds(signal_segment, sample_rate) + + # --- 5. ANNOTATION GENERATION --- + ann_label = label if label is not None else f"{int(threshold*100)}%" + + # Pack metadata for the UI/Downstream processing + comment_data = { + "type": annotation_type, + "generator": "threshold_qualifier", + "params": { + "threshold": threshold, + "window_size": window_size, + }, + } + + anno = Annotation( + sample_start=true_start, + sample_count=true_stop - true_start, + freq_lower_edge=center_frequency + f_min, + freq_upper_edge=center_frequency + f_max, + label=ann_label, + comment=json.dumps(comment_data), + detail={"generator": "hysteresis_qualifier"}, + ) + annotations.append(anno) + + # Return a new Recording object including the new annotations + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations + annotations) diff --git a/src/ria_toolkit_oss/datatypes/recording.py b/src/ria_toolkit_oss/datatypes/recording.py index 1faec21..b282d9d 100644 --- a/src/ria_toolkit_oss/datatypes/recording.py +++ b/src/ria_toolkit_oss/datatypes/recording.py @@ -601,7 +601,7 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.to_wav() """ - from utils.io.recording import to_wav + from ria_toolkit_oss.io.recording import to_wav return to_wav( recording=self, @@ -651,7 +651,7 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.to_blue() """ - from utils.io.recording import to_blue + from ria_toolkit_oss.io.recording import to_blue return to_blue(recording=self, filename=filename, path=path, data_format=data_format, overwrite=overwrite) diff --git a/src/ria_toolkit_oss/io/recording.py b/src/ria_toolkit_oss/io/recording.py index f162be6..0234e11 100644 --- a/src/ria_toolkit_oss/io/recording.py +++ b/src/ria_toolkit_oss/io/recording.py @@ -134,6 +134,27 @@ def from_npy(file: os.PathLike | str, legacy: bool = False) -> Recording: annotations = list(np.load(f, allow_pickle=True)) except EOFError: annotations = [] + except ModuleNotFoundError: + # File was pickled with utils.data.Annotation — remap to ria_toolkit_oss + import pickle + import sys + import types + import ria_toolkit_oss.datatypes.annotation as _ann_mod + + utils_shim = types.ModuleType("utils") + utils_data = types.ModuleType("utils.data") + utils_data_annotation = types.ModuleType("utils.data.annotation") + utils_data_annotation.Annotation = _ann_mod.Annotation + utils_shim.data = utils_data + utils_data.annotation = utils_data_annotation + sys.modules.setdefault("utils", utils_shim) + sys.modules.setdefault("utils.data", utils_data) + sys.modules.setdefault("utils.data.annotation", utils_data_annotation) + + f.seek(0) + np.load(f, allow_pickle=True) # skip data + np.load(f, allow_pickle=True) # skip metadata + annotations = list(np.load(f, allow_pickle=True)) recording = Recording(data=data, metadata=metadata, annotations=annotations) return recording diff --git a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png b/src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png index 8da49e8a8d35a0383b86b5d7001463a78eb42178..807e4efe1fb6ae956178725218544c91b14d93e7 100644 GIT binary patch literal 130 zcmWN_!41P83;@7CQ?Nh-7%*|R2^b1eTcSep==9C&q`UHWwEmHG&SPv!J==V|%2;me z8JE=GY8*MK%ZT1sj=F=#jd#k(4y-rPg|#FsR$D+rxKgmrYg>qdu}(XnvSZ}TkqZ;v NkwA?0%aQ=di9f)bCcgjx literal 92294 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 z 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 >x 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 diff --git a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png b/src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png index a18426e743c09368b889c16eb3b72dd30ea8056f..185600c26a5c15b58d9479110ab44ef9308e4c93 100644 GIT binary patch literal 130 zcmWN_K@Ni;5CFhCuiyg}SYbtfv%qd4X^Sc9p|7u}N#^43X#Jz>aqMF==Gl(NOU86t z&ot%!He=^(E~k1cdQ^gd8|26|y9Ing6fWSigh-ZxC3}~*5YyrySQLyi1-8Cm@NQ)R N5mKZ5Si3a>sz2I3CoKQ~ literal 19826 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 V7Gv3e865xs002ovPDHLkV1hF_$2b50 diff --git a/src/ria_toolkit_oss/view/view_signal.py b/src/ria_toolkit_oss/view/view_signal.py index f8d5731..3632ea1 100644 --- a/src/ria_toolkit_oss/view/view_signal.py +++ b/src/ria_toolkit_oss/view/view_signal.py @@ -4,6 +4,7 @@ import textwrap from typing import Optional import matplotlib.pyplot as plt +from matplotlib.patches import Patch import numpy as np from matplotlib import gridspec from PIL import Image @@ -39,6 +40,83 @@ def set_spines(ax, spines): ax.spines["left"].set_visible(False) +def view_annotations( + recording: Recording, + channel: Optional[int] = 0, + output_path: Optional[str] = "images/annotations.png", + title: Optional[str] = "Annotated Spectrogram", + dpi: Optional[int] = 300, + title_fontsize: Optional[int] = 15, + dark: Optional[bool] = True, +) -> None: + # 1. Setup Plotting Environment + plt.close("all") + if dark: + plt.style.use("dark_background") + else: + plt.style.use("default") + + fig, ax = plt.subplots(figsize=(12, 8)) + + complex_signal = recording.data[channel] + sample_rate, center_frequency, _ = extract_metadata_fields(recording.metadata) + annotations = recording.annotations + + # 2. Setup Color Mapping + available_colors = [ + COLORS.get("magenta", "magenta"), + COLORS.get("accent", "cyan"), + COLORS.get("light", "white"), + "lime", + ] + + palette = ["#2196F3", "#9C27B0", "#64B5F6", "#7B1FA2", "#5C6BC0", "#CE93D8", "#1565C0", "#7C4DFF"] + unique_labels = sorted(list(set(ann.label for ann in annotations if ann.label))) + label_to_color = {label: palette[i % len(palette)] for i, label in enumerate(unique_labels)} + + # 3. Generate Spectrogram + Pxx, freqs, times, im = ax.specgram( + complex_signal, NFFT=256, Fs=sample_rate, Fc=center_frequency, noverlap=128, cmap="twilight" + ) + + # 4. Draw Annotations (highest threshold % first so lower % renders on top) + def _threshold_sort_key(ann): + try: + return int(ann.label.rstrip("%")) + except (ValueError, AttributeError): + return 0 + + for annotation in sorted(annotations, key=_threshold_sort_key, reverse=True): + t_start = annotation.sample_start / sample_rate + t_width = annotation.sample_count / sample_rate + f_start = annotation.freq_lower_edge + f_height = annotation.freq_upper_edge - annotation.freq_lower_edge + + ann_color = label_to_color.get(annotation.label, "gray") + + rect = plt.Rectangle( + (t_start, f_start), t_width, f_height, linewidth=1.5, edgecolor=ann_color, facecolor="none", alpha=0.8 + ) + ax.add_patch(rect) + + if unique_labels: + legend_elements = [ + Patch(facecolor=label_to_color[label], alpha=0.3, edgecolor=label_to_color[label], label=label) + for label in unique_labels + ] + ax.legend(handles=legend_elements, loc="upper right", framealpha=0.2) + + ax.set_title(title, fontsize=title_fontsize, pad=20) + ax.set_xlabel("Time (s)", fontsize=12) + ax.set_ylabel("Frequency (MHz)", fontsize=12) + ax.grid(alpha=0.1) + + output_path, _ = set_path(output_path=output_path) + plt.savefig(output_path, dpi=dpi, bbox_inches="tight") + plt.close(fig) + print(f"Professional annotation plot saved to {output_path}") + + def view_channels( recording: Recording, output_path: Optional[str] = "images/signal.png", diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py new file mode 100644 index 0000000..cdfabb5 --- /dev/null +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -0,0 +1,820 @@ +"""Annotate command - Automatic detection and manual annotation management.""" + +import json +from pathlib import Path + +import click + +from ria_toolkit_oss.annotations import ( + annotate_with_cusum, + detect_signals_energy, + split_recording_annotations, + threshold_qualifier, +) +from ria_toolkit_oss.datatypes import Annotation +from ria_toolkit_oss.datatypes.recording import Recording +from ria_toolkit_oss.io import load_recording, to_blue, to_npy, to_sigmf, to_wav +from ria_toolkit_oss_cli.ria_toolkit_oss.common import format_frequency, format_sample_count + + +def normalize_sigmf_path(filepath): + """Normalize SigMF path to base name without extension.""" + path = Path(filepath) + + # Handle .sigmf-data, .sigmf-meta, or .sigmf + if ".sigmf" in path.suffix: + # Remove the suffix to get base name + return path.with_suffix("") + else: + return path + + +def detect_input_format(filepath): + """Detect file format from extension.""" + path = Path(filepath) + ext = path.suffix.lower() + + if ext in [".sigmf-data", ".sigmf-meta"]: + return "sigmf" + elif path.name.endswith(".sigmf"): + return "sigmf" + elif ext == ".npy": + return "npy" + elif ext == ".wav": + return "wav" + elif ext == ".blue": + return "blue" + else: + raise click.ClickException(f"Unknown format for '{filepath}'. Supported: .sigmf, .npy, .wav, .blue") + + +def determine_output_path(input_path, output_path, fmt, quiet, overwrite): + input_path = Path(input_path) + input_is_annotated = input_path.stem.endswith("_annotated") + + if output_path: + target = Path(output_path) + elif overwrite and input_is_annotated: + # Write back in-place only when the input is already an _annotated file + target = input_path + else: + target = input_path.with_name(f"{input_path.stem}_annotated{input_path.suffix}") + + if fmt == "sigmf": + final_path = normalize_sigmf_path(target) + if not quiet: + click.echo(f"Saving SigMF metadata to: {final_path}") + else: + final_path = target + if not quiet: + click.echo(f"Saving to: {final_path}") + + # Always allow writing to _annotated files; guard against overwriting originals + target_is_annotated = final_path.stem.endswith("_annotated") + if final_path.exists() and not target_is_annotated and final_path != input_path: + click.echo(f"Error: {final_path} is not an annotated file and cannot be overwritten.", err=True) + return None + + return final_path + + +def save_recording_auto(recording, output_path, input_path, quiet=False, overwrite=False): + """Save recording, auto-detecting format from extension. + + For SigMF: Only overwrites metadata file, data file is unchanged + For other formats: Creates _annotated copy by default, unless overwrite=True + """ + input_path = Path(input_path) + fmt = detect_input_format(input_path) + + # Determine output path + output_path = determine_output_path( + input_path=input_path, output_path=output_path, fmt=fmt, quiet=quiet, overwrite=overwrite + ) + + if fmt == "sigmf": + # Normalize path for SigMF + base_path = output_path + stem = base_path.name + parent = base_path.parent + + # For SigMF: only save metadata, copy data if needed + meta_path = parent / f"{stem}.sigmf-meta" + data_path = parent / f"{stem}.sigmf-data" + + # If output is different from input, copy data file + input_base = normalize_sigmf_path(input_path) + if input_base != base_path: + import shutil + + # Construct input data path correctly + # input_base is like /path/to/recording or /path/to/recording.sigmf + # We need /path/to/recording.sigmf-data + if str(input_base).endswith(".sigmf"): + input_data = Path(str(input_base).replace(".sigmf", ".sigmf-data")) + else: + input_data = input_base.parent / f"{input_base.name}.sigmf-data" + if not quiet: + click.echo(f" Copying: {data_path}") + shutil.copy2(input_data, data_path) + + # Always save metadata (this is the whole point) + to_sigmf(recording, filename=stem, path=parent, overwrite=True) + + if not quiet: + click.echo(f" Updated: {meta_path}") + if input_base != base_path: + click.echo(f" Created: {data_path}") + + elif fmt == "npy": + to_npy(recording, filename=output_path.stem, path=output_path.parent, overwrite=True) + if not quiet: + click.echo(f" Created: {output_path}") + elif fmt == "wav": + to_wav(recording, filename=output_path.stem, path=output_path.parent, overwrite=True) + if not quiet: + click.echo(f" Created: {output_path}") + elif fmt == "blue": + to_blue(recording, filename=output_path.stem, path=output_path.parent, overwrite=True) + if not quiet: + click.echo(f" Created: {output_path}") + + +def determine_frequency_bounds(recording: Recording, freq_lower, freq_upper): + # Handle frequency bounds + if (freq_lower is None) != (freq_upper is None): + raise click.ClickException("Must specify both --freq-lower and --freq-upper, or neither") + + if freq_lower is None: + # Default to full bandwidth + sample_rate = recording.metadata.get("sample_rate", 1) + center_freq = recording.metadata.get("center_frequency", 0) + freq_lower = center_freq - (sample_rate / 2) + freq_upper = center_freq + (sample_rate / 2) + freq_default = True + else: + freq_default = False + if freq_lower >= freq_upper: + raise click.ClickException( + f"Invalid frequency range: lower ({format_frequency(freq_lower)}) " + f"must be < upper ({format_frequency(freq_upper)})" + ) + + return freq_lower, freq_upper, freq_default + + +def get_indices_list(indices, recording: Recording): + if indices: + try: + indices_list = [int(idx.strip()) for idx in indices.split(",")] + # Validate indices + for idx in indices_list: + if idx < 0 or idx >= len(recording.annotations): + raise click.ClickException( + f"Invalid index {idx}. Recording has {len(recording.annotations)} annotation(s)" + ) + except ValueError as e: + raise click.ClickException(f"Invalid indices format. Expected comma-separated integers: {e}") + + return indices_list + else: + return None + + +# ============================================================================ +# Main command group +# ============================================================================ + + +@click.group() +def annotate(): + """Manage and auto-detect annotations on RF recordings. + + \b + MANUAL MANAGEMENT: + list - List all current annotations + add - Manually add a specific annotation + remove - Delete an annotation by its index + clear - Remove all annotations from the recording + + \b + DETECTION & SEPARATION: + energy - Auto-detect using energy-based thresholding + cusum - Auto-detect segments using signal state changes + threshold - Auto-detect samples above magnitude percentage + separate - Auto-detect parallel frequency-offset signals, split into sub-bands + + \b + File Path Handling: + - SigMF files: Pass .sigmf-data, .sigmf-meta, or base name + - Other formats: .npy, .wav, .blue files + + \b + Output Behavior: + - SigMF: Updates .sigmf-meta only (data unchanged), in-place + - Other: Creates _annotated copy unless --overwrite specified + """ + pass + + +# ============================================================================ +# List subcommand +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--verbose", is_flag=True, help="Show detailed annotation info") +def list(input, verbose): + """List all annotations in a recording. + + \b + Examples: + ria annotate list recording.sigmf-data + ria annotate list signal.npy --verbose + """ + try: + recording = load_recording(input) + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if len(recording.annotations) == 0: + click.echo(f"No annotations in {Path(input).name}") + return + + click.echo(f"\nAnnotations in {Path(input).name}:") + for i, ann in enumerate(recording.annotations): + # Parse type from comment JSON + try: + comment_data = json.loads(ann.comment) + ann_type = comment_data.get("type", "unknown") + user_comment = comment_data.get("user_comment", "") + except (json.JSONDecodeError, TypeError): + ann_type = "unknown" + user_comment = ann.comment or "" + + # Basic info + freq_range = f"{format_frequency(ann.freq_lower_edge)} - {format_frequency(ann.freq_upper_edge)}" + click.echo( + f" [{i}] Samples {format_sample_count(ann.sample_start)}-" + f"{format_sample_count(ann.sample_start + ann.sample_count)}: {ann.label}" + ) + click.echo(f" Type: {ann_type}") + + if verbose: + if user_comment: + click.echo(f" Comment: {user_comment}") + click.echo(f" Frequency: {freq_range}") + if ann.detail: + click.echo(f" Detail: {ann.detail}") + + click.echo(f"\nTotal: {len(recording.annotations)} annotation(s)") + + +# ============================================================================ +# Add subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 200}) +@click.argument("input", type=click.Path(exists=True)) +@click.option("--start", type=int, required=True, help="Start sample index") +@click.option("--count", type=int, required=True, help="Sample count") +@click.option("--label", type=str, required=True, help="Annotation label") +@click.option("--freq-lower", type=float, help="Lower frequency edge (Hz)") +@click.option("--freq-upper", type=float, help="Upper frequency edge (Hz)") +@click.option("--comment", type=str, help="Human-readable comment") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def add(input, start, count, label, freq_lower, freq_upper, comment, annotation_type, output, overwrite, quiet): + """Add a manual annotation. + + \b + Examples: + ria annotate add file.npy --start 1000 --count 500 --label wifi + ria annotate add signal.sigmf-data --start 0 --count 1000 --label burst --comment "Strong signal" + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + # Validate sample range + n_samples = len(recording.data[0]) + if start < 0: + raise click.ClickException(f"--start must be >= 0, got {start}") + if count <= 0: + raise click.ClickException(f"--count must be > 0, got {count}") + if start + count > n_samples: + raise click.ClickException( + f"Invalid annotation range:\n" + f" Start: {start:,}\n" + f" Count: {count:,}\n" + f" End: {start + count:,}\n" + f"Recording only has {n_samples:,} samples" + ) + + # Handle frequency bounds + freq_lower, freq_upper, freq_default = determine_frequency_bounds( + recording=recording, freq_lower=freq_lower, freq_upper=freq_upper + ) + + # Build comment JSON + comment_data = {"type": annotation_type} + if comment: + comment_data["user_comment"] = comment + + # Create annotation + ann = Annotation( + sample_start=start, + sample_count=count, + freq_lower_edge=freq_lower, + freq_upper_edge=freq_upper, + label=label, + comment=json.dumps(comment_data), + detail={}, + ) + + recording._annotations.append(ann) + + if not quiet: + click.echo("\nAdding annotation:") + click.echo(f" Start: {format_sample_count(start)}") + click.echo(f" Count: {format_sample_count(count)} samples") + freq_str = ( + "full bandwidth" if freq_default else f"{format_frequency(freq_lower)} - {format_frequency(freq_upper)}" + ) + click.echo(f" Frequency: {freq_str}") + click.echo(f" Label: {label}") + click.echo(f" Type: {annotation_type}") + if comment: + click.echo(f" Comment: {comment}") + + try: + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Failed to save: {e}") + + +# ============================================================================ +# Remove subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 200}) +@click.argument("input", type=click.Path(exists=True)) +@click.argument("index", type=int) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def remove(input, index, output, overwrite, quiet): + """Remove annotation by index. + + Use 'ria annotate list' to see annotation indices. + + \b + Examples: + ria annotate remove signal.sigmf-data 2 + ria annotate remove file.npy 0 + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if index < 0 or index >= len(recording.annotations): + raise click.ClickException( + f"Cannot remove annotation at index {index}\n" + f"Recording has {len(recording.annotations)} annotation(s) (indices 0-{len(recording.annotations)-1})" + ) + + removed_ann = recording.annotations[index] + recording._annotations.pop(index) + + if not quiet: + click.echo(f"\nRemoving annotation [{index}]:") + click.echo( + f" Removed: samples {format_sample_count(removed_ann.sample_start)}-" + f"{format_sample_count(removed_ann.sample_start + removed_ann.sample_count)} ({removed_ann.label})" + ) + + try: + save_recording_auto(recording, output_path=input, input_path=input, quiet=quiet, overwrite=True) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Failed to save: {e}") + + +# ============================================================================ +# Clear subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 175}) +@click.argument("input", type=click.Path(exists=True)) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--force", is_flag=True, help="Skip confirmation") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def clear(input, output, overwrite, force, quiet): + """Clear all annotations. + + \b + Examples: + ria annotate clear signal.sigmf-data + ria annotate clear file.npy --force + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + count_before = len(recording.annotations) + + if count_before == 0: + if not quiet: + click.echo("No annotations to clear") + return + + # Confirm unless --force + if not force and not quiet: + click.echo(f"\nWarning: This will remove all {count_before} annotation(s)") + click.confirm("Continue?", abort=True) + + recording._annotations = [] + + if not quiet: + click.echo(f"\nCleared {count_before} annotation(s)") + + recording._annotations = [] + + try: + save_recording_auto(recording, output_path=input, input_path=input, quiet=quiet, overwrite=True) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Failed to save: {e}") + + +# ============================================================================ +# Energy detection subcommand +# ============================================================================ + + +@annotate.command(context_settings={"max_content_width": 200}) +@click.argument("input", type=click.Path(exists=True)) +@click.option("--label", type=str, default="signal", help="Annotation label") +@click.option("--threshold", type=float, default=1.2, help="Threshold multiplier above noise floor") +@click.option("--segments", type=int, default=10, help="Number of segments for noise estimation") +@click.option("--window-size", type=int, default=200, help="Smoothing window size") +@click.option("--min-distance", type=int, default=5000, help="Min distance between detections") +@click.option( + "--freq-method", + type=click.Choice(["nbw", "obw", "full-detected", "full-bandwidth"]), + default="nbw", + help="Frequency bounding method", +) +@click.option("--nfft", type=int, default=None, help="FFT size for frequency calculation") +@click.option("--obw-power", type=float, default=0.99, help="Power percentage for OBW/NBW (0.98-0.9999)") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def energy( + input, + label, + threshold, + segments, + window_size, + min_distance, + freq_method, + nfft, + obw_power, + annotation_type, + output, + overwrite, + quiet, +): + """Auto-detect signals using energy-based method. + + Detects bursts based on energy above noise floor. Best for bursty signals + and intermittent transmissions. + + \b + Frequency Bounding Methods: + nbw - Nominal bandwidth (default, best for real signals) + obw - Occupied bandwidth (more conservative, includes sidelobes) + full-detected - Lowest to highest spectral component + full-bandwidth - Entire Nyquist span + + \b + Examples: + ria annotate energy capture.sigmf-data --label burst + ria annotate energy signal.npy --threshold 1.5 --min-distance 10000 + ria annotate energy signal.sigmf-data --freq-method obw + ria annotate energy signal.sigmf-data --freq-method full-detected + + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if not quiet: + click.echo("\nDetecting signals using energy-based method...") + click.echo(" Time detection:") + click.echo(f" Segments: {segments}") + click.echo(f" Threshold: {threshold}x noise floor") + click.echo(f" Window size: {window_size} samples") + click.echo(f" Min distance: {min_distance} samples") + click.echo(f" Frequency bounds: {freq_method}") + + try: + initial_count = len(recording.annotations) + recording = detect_signals_energy( + recording, + k=segments, + threshold_factor=threshold, + window_size=window_size, + min_distance=min_distance, + label=label, + annotation_type=annotation_type, + freq_method=freq_method, + nfft=nfft, + obw_power=obw_power, + ) + added = len(recording.annotations) - initial_count + + if not quiet: + click.echo(f" ✓ Added {added} annotation(s)") + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Energy detection failed: {e}") + + +# ============================================================================ +# CUSUM detection subcommand +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--label", type=str, default="segment", help="Annotation label") +@click.option("--min-duration", type=float, default=5.0, help="Min duration in ms (prevents over-segmentation)") +@click.option("--window-size", type=int, default=1, help="Smoothing window size") +@click.option("--tolerance", type=int, default=-1, help="Sample tolerance for merging") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def cusum(input, label, min_duration, window_size, tolerance, annotation_type, output, overwrite, quiet): + """Auto-detect segments using CUSUM method. + + Detects signal state changes (on/off, amplitude transitions). Best for + segmenting continuous signals. + + IMPORTANT: Always specify --min-duration to prevent excessive segmentation. + + \b + Examples: + ria annotate cusum signal.sigmf-data --min-duration 5.0 + ria annotate cusum data.npy --min-duration 10.0 --label state + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if not quiet: + click.echo("\nDetecting segments using CUSUM...") + click.echo(f" Min duration: {min_duration} ms") + if window_size != 1: + click.echo(f" Window size: {window_size} samples") + + try: + initial_count = len(recording.annotations) + recording = annotate_with_cusum( + recording, + label=label, + window_size=window_size, + min_duration=min_duration, + tolerance=tolerance, + annotation_type=annotation_type, + ) + added = len(recording.annotations) - initial_count + + if not quiet: + click.echo(f" ✓ Added {added} annotation(s)") + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"CUSUM detection failed: {e}") + + +# ============================================================================ +# Threshold detection subcommand +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--threshold", type=float, required=True, help="Threshold (0.0-1.0, fraction of max magnitude)") +@click.option("--label", type=str, default=None, help="Annotation label") +@click.option("--window-size", type=int, default=None, help="Smoothing window size in samples (default: 1ms at recording sample rate)") +@click.option( + "--type", + "annotation_type", + type=click.Choice(["standalone", "parallel", "intersection"]), + default="standalone", + help="Annotation type", +) +@click.option("--channel", type=int, default=0, help="Channel index to annotate (default: 0)") +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +def threshold(input, threshold, label, window_size, annotation_type, channel, output, overwrite, quiet): + """Auto-detect signals using threshold method. + + Detects samples above a percentage of maximum magnitude. Best for simple + power-based detection. + + \b + Examples: + ria annotate threshold signal.sigmf-data --threshold 0.7 --label wifi + ria annotate threshold data.npy --threshold 0.5 --window-size 2048 + """ + if not (0.0 <= threshold <= 1.0): + raise click.ClickException(f"--threshold must be between 0.0 and 1.0, got {threshold}") + + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + if not quiet: + click.echo("\nDetecting signals using threshold qualifier...") + click.echo(f" Threshold: {threshold * 100:.1f}% of max magnitude") + click.echo(f" Window size: {'auto (1ms)' if window_size is None else f'{window_size} samples'}") + click.echo(f" Channel: {channel}") + + try: + initial_count = len(recording.annotations) + recording = threshold_qualifier( + recording, + threshold=threshold, + window_size=window_size, + label=label, + annotation_type=annotation_type, + channel=channel, + ) + added = len(recording.annotations) - initial_count + + if not quiet: + click.echo(f" ✓ Added {added} annotation(s)") + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Threshold detection failed: {e}") + + +# ============================================================================ +# Separate subcommand (Phase 2: Parallel signal separation) +# ============================================================================ + + +@annotate.command() +@click.argument("input", type=click.Path(exists=True)) +@click.option("--indices", type=str, help="Comma-separated annotation indices to split (default: all)") +@click.option("--nfft", type=int, default=65536, help="FFT size for spectral analysis") +@click.option("--noise-threshold-db", type=float, help="Noise floor threshold in dB (auto-estimated if not specified)") +@click.option("--min-component-bw", type=float, default=50e3, help="Min component bandwidth in Hz") +@click.option("--output", "-o", type=click.Path(), help="Output file path") +@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") +@click.option("--quiet", is_flag=True, help="Quiet mode") +@click.option("--verbose", is_flag=True, help="Verbose output (show detected components)") +def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, overwrite, quiet, verbose): + """ + Auto-detect parallel frequency-offset signals and split into sub-bands. + + Provides methods to detect and separate overlapping frequency-domain signals + that occupy the same time window but different frequency bands. + + Detects multiple frequency components within single annotations and splits + them into separate annotations. Uses spectral peak detection with dual + bandwidth estimation. + + \b + Key Features: + - Spectral peak detection for frequency components + - Auto noise floor estimation (or user-specified) + - Dual bandwidth estimation: -3dB primary, cumulative power fallback + - Handles narrowband and wide signals (OFDM) + + \b + Examples: + ria annotate separate capture.sigmf-data + ria annotate separate signal.npy --indices 0,1,2 + ria annotate separate data.sigmf-data --noise-threshold-db -70 + ria annotate separate signal.npy --min-component-bw 100000 + + """ + try: + recording = load_recording(input) + if not quiet: + click.echo(f"Loaded: {input}") + except Exception as e: + raise click.ClickException(f"Failed to load recording: {e}") + + # Parse indices if specified + indices_list = get_indices_list(indices=indices, recording=recording) + + if len(recording.annotations) == 0: + if not quiet: + click.echo("No annotations to split") + return + + if not quiet: + click.echo("\nSplitting annotations by frequency components...") + click.echo(f" Input annotations: {len(recording.annotations)}") + if indices_list: + click.echo(f" Splitting indices: {indices_list}") + click.echo(f" FFT size: {nfft}") + if noise_threshold_db is not None: + click.echo(f" Noise threshold: {noise_threshold_db} dB") + else: + click.echo(" Noise threshold: auto-estimated") + click.echo(f" Min component BW: {format_frequency(min_component_bw)}") + + try: + initial_count = len(recording.annotations) + + recording = split_recording_annotations( + recording, + indices=indices_list, + nfft=nfft, + noise_threshold_db=noise_threshold_db, + min_component_bw=min_component_bw, + ) + + final_count = len(recording.annotations) + added = final_count - initial_count + + if not quiet: + click.echo(f" ✓ Output annotations: {final_count} ({'+' if added >= 0 else ''}{added} change)") + if verbose and added > 0: + click.echo("\n Details:") + for i in range(initial_count, final_count): + ann = recording.annotations[i] + freq_range = f"{format_frequency(ann.freq_lower_edge)} - {format_frequency(ann.freq_upper_edge)}" + click.echo( + f" [{i}] samples {format_sample_count(ann.sample_start)}-" + f"{format_sample_count(ann.sample_start + ann.sample_count)}: {freq_range}" + ) + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + except Exception as e: + raise click.ClickException(f"Spectral separation failed: {e}") diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py index 60ddba9..e942386 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py @@ -3,6 +3,7 @@ This module contains all the CLI bindings for the ria package. """ +from .annotate import annotate from .capture import capture from .combine import combine from .convert import convert diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/generate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/generate.py index f2e14ba..1026b4a 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/generate.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/generate.py @@ -232,8 +232,8 @@ def generate(): \b Examples: - utils synth chirp -b 1e6 -p 0.01 -s 10e6 -o chirp_basic.sigmf - utils synth fsk -M 2 -r 100e3 -s 2e6 -o fsk2_basic.sigmf + ria synth chirp -b 1e6 -p 0.01 -s 10e6 -o chirp_basic.sigmf + ria synth fsk -M 2 -r 100e3 -s 2e6 -o fsk2_basic.sigmf """ pass diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/transform.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/transform.py index 94524f2..5d6e724 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/transform.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/transform.py @@ -264,13 +264,13 @@ def transform(): Examples:\n \b # List available augmentations - utils transform augment --list + ria transform augment --list \b # Apply channel swap - utils transform augment channel_swap input.npy + ria transform augment channel_swap input.npy \b # Apply AWGN impairment - utils transform impair awgn input.npy --snr-db 15 + ria transform impair awgn input.npy --snr-db 15 """ pass diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py index 8e0b51f..9fecec3 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py @@ -7,7 +7,7 @@ from typing import Optional import click from ria_toolkit_oss.io.recording import from_npy, load_recording -from ria_toolkit_oss.view.view_signal import view_channels, view_sig +from ria_toolkit_oss.view.view_signal import view_annotations, view_channels, view_sig from ria_toolkit_oss.view.view_signal_simple import view_simple_sig from .common import echo_progress, echo_verbose, load_yaml_config @@ -35,6 +35,7 @@ VISUALIZATION_TYPES = { ], }, "channels": {"function": view_channels, "description": "Multi-channel IQ and spectrogram view", "options": []}, + "annotations": {"function": view_annotations, "description": "Annotated spectrogram view", "options": ["channel", "dark"]}, } diff --git a/tests/ria_toolkit_oss_cli/README.md b/tests/ria_toolkit_oss_cli/README.md index 1c4cc8e..3e78415 100644 --- a/tests/ria_toolkit_oss_cli/README.md +++ b/tests/ria_toolkit_oss_cli/README.md @@ -1,6 +1,6 @@ # CLI Tests -Comprehensive test suite for the utils CLI commands. +Comprehensive test suite for the ria CLI commands. ## Test Structure diff --git a/tests/ria_toolkit_oss_cli/__init__.py b/tests/ria_toolkit_oss_cli/__init__.py index 77c8a64..26d94ee 100644 --- a/tests/ria_toolkit_oss_cli/__init__.py +++ b/tests/ria_toolkit_oss_cli/__init__.py @@ -1 +1 @@ -"""Tests for utils CLI commands.""" +"""Tests for ria CLI commands.""" From 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 31 Mar 2026 13:48:38 -0400 Subject: [PATCH 08/18] Removing logos --- .../view/graphics/Qoherent-logo-black-transparent.png | 3 --- .../view/graphics/Qoherent-logo-white-transparent.png | 3 --- 2 files changed, 6 deletions(-) delete mode 100644 src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png delete mode 100644 src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png diff --git a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png b/src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png deleted file mode 100644 index 807e4ef..0000000 --- a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da9e6d42351b478fc6f3032619689734503f9971340429fa6bdedbdfd96042c2 -size 92294 diff --git a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png b/src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png deleted file mode 100644 index 185600c..0000000 --- a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c8ea31724b66fed24cf58a0a50a0b664028b712506f683a5e04d65419ef2ffa -size 19826 From 5b1c51797ba0602bb9ca16f3132779ca9cf61335 Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 31 Mar 2026 14:54:27 -0400 Subject: [PATCH 09/18] logos --- .../Qoherent-logo-black-transparent.png | Bin 92294 -> 130 bytes .../Qoherent-logo-white-transparent.png | Bin 19826 -> 130 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png b/src/ria_toolkit_oss/view/graphics/Qoherent-logo-black-transparent.png index 8da49e8a8d35a0383b86b5d7001463a78eb42178..807e4efe1fb6ae956178725218544c91b14d93e7 100644 GIT binary patch literal 130 zcmWN_!41P83;@7CQ?Nh-7%*|R2^b1eTcSep==9C&q`UHWwEmHG&SPv!J==V|%2;me z8JE=GY8*MK%ZT1sj=F=#jd#k(4y-rPg|#FsR$D+rxKgmrYg>qdu}(XnvSZ}TkqZ;v NkwA?0%aQ=di9f)bCcgjx literal 92294 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 z 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 >x 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 diff --git a/src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png b/src/ria_toolkit_oss/view/graphics/Qoherent-logo-white-transparent.png index a18426e743c09368b889c16eb3b72dd30ea8056f..185600c26a5c15b58d9479110ab44ef9308e4c93 100644 GIT binary patch literal 130 zcmWN_K@Ni;5CFhCuiyg}SYbtfv%qd4X^Sc9p|7u}N#^43X#Jz>aqMF==Gl(NOU86t z&ot%!He=^(E~k1cdQ^gd8|26|y9Ing6fWSigh-ZxC3}~*5YyrySQLyi1-8Cm@NQ)R N5mKZ5Si3a>sz2I3CoKQ~ literal 19826 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 V7Gv3e865xs002ovPDHLkV1hF_$2b50 From 5cfced8855414e54d9e2d10733dce1bb71d04d29 Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 31 Mar 2026 15:16:32 -0400 Subject: [PATCH 10/18] Fix merge conflicts and port all imports from utils to ria_toolkit_oss Resolves unresolved merge conflict markers left in committed files across the annotations, view, data, and CLI packages. Updates all remaining imports from the old utils.* namespace to ria_toolkit_oss.datatypes, ria_toolkit_oss.io, and ria_toolkit_oss.view equivalents. --- src/ria_toolkit_oss/annotations/__init__.py | 7 - .../annotations/annotation_transforms.py | 4 - .../annotations/cusum_annotator.py | 8 -- .../annotations/energy_detector.py | 15 +-- .../annotations/parallel_signal_separator.py | 23 ---- .../annotations/qualify_slice.py | 4 - .../annotations/signal_isolation.py | 5 - .../annotations/threshold_qualifier.py | 121 ------------------ src/ria_toolkit_oss/data/recording.py | 28 ++-- .../frequency_translation/upconversion.py | 4 +- src/ria_toolkit_oss/view/recording.py | 8 +- src/ria_toolkit_oss/view/view_signal.py | 33 +---- .../view/view_signal_simple.py | 4 +- .../ria_toolkit_oss/annotate.py | 86 ------------- .../ria_toolkit_oss/commands.py | 4 +- 15 files changed, 26 insertions(+), 328 deletions(-) diff --git a/src/ria_toolkit_oss/annotations/__init__.py b/src/ria_toolkit_oss/annotations/__init__.py index d2d5806..ebb3d10 100644 --- a/src/ria_toolkit_oss/annotations/__init__.py +++ b/src/ria_toolkit_oss/annotations/__init__.py @@ -1,4 +1,3 @@ -<<<<<<< HEAD """ The annotations package contains tools and utilities for creating, managing, and processing annotations. @@ -54,9 +53,3 @@ from .parallel_signal_separator import ( from .qualify_slice import qualify_slice_from_annotations from .signal_isolation import isolate_signal from .threshold_qualifier import threshold_qualifier -======= -from .cusum_annotator import annotate_with_cusum -from .energy_detector import detect_signals_energy -from .parallel_signal_separator import split_recording_annotations -from .threshold_qualifier import threshold_qualifier ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 diff --git a/src/ria_toolkit_oss/annotations/annotation_transforms.py b/src/ria_toolkit_oss/annotations/annotation_transforms.py index d91e13d..47300c1 100644 --- a/src/ria_toolkit_oss/annotations/annotation_transforms.py +++ b/src/ria_toolkit_oss/annotations/annotation_transforms.py @@ -1,8 +1,4 @@ -<<<<<<< HEAD -from utils.data.annotation import Annotation -======= from ria_toolkit_oss.datatypes.annotation import Annotation ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 # TODO figure out how to transfer labels in the merge case diff --git a/src/ria_toolkit_oss/annotations/cusum_annotator.py b/src/ria_toolkit_oss/annotations/cusum_annotator.py index 9837e07..d37186c 100644 --- a/src/ria_toolkit_oss/annotations/cusum_annotator.py +++ b/src/ria_toolkit_oss/annotations/cusum_annotator.py @@ -3,11 +3,7 @@ from typing import Optional import numpy as np -<<<<<<< HEAD -from utils.data import Annotation, Recording -======= from ria_toolkit_oss.datatypes import Annotation, Recording ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 def annotate_with_cusum( @@ -28,11 +24,7 @@ def annotate_with_cusum( changes between a low and high amplitude. :param recording: A ``Recording`` object to annotate. -<<<<<<< HEAD - :type recording: ``utils.data.Recording`` -======= :type recording: ``ria_toolkit_oss.datatypes.Recording`` ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 :param label: Label for the detected segments. :type label: str :param window_size: The length (in samples) of the moving average window. diff --git a/src/ria_toolkit_oss/annotations/energy_detector.py b/src/ria_toolkit_oss/annotations/energy_detector.py index 9706532..109fe6e 100644 --- a/src/ria_toolkit_oss/annotations/energy_detector.py +++ b/src/ria_toolkit_oss/annotations/energy_detector.py @@ -11,11 +11,7 @@ from typing import Tuple import numpy as np from scipy.signal import filtfilt -<<<<<<< HEAD -from utils.data import Annotation, Recording -======= from ria_toolkit_oss.datatypes import Annotation, Recording ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 def detect_signals_energy( @@ -77,13 +73,8 @@ def detect_signals_energy( **Example**:: -<<<<<<< HEAD - >>> from utils.io import load_recording - >>> from utils.annotations import detect_signals_energy -======= >>> from ria.io import load_recording >>> from ria_toolkit_oss.annotations import detect_signals_energy ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 >>> recording = load_recording("capture.sigmf") >>> # Detect with NBW frequency bounds (default, best for real signals) @@ -239,7 +230,7 @@ def calculate_nominal_bandwidth( **Example**:: - >>> from utils.annotations import calculate_nominal_bandwidth + >>> from ria_toolkit_oss.annotations import calculate_nominal_bandwidth >>> nbw, center = calculate_nominal_bandwidth(signal, sampling_rate=10e6) >>> print(f"NBW: {nbw/1e6:.3f} MHz, Center: {center/1e6:.3f} MHz") """ @@ -356,11 +347,7 @@ def annotate_with_obw( **Example**:: -<<<<<<< HEAD - >>> from utils.annotations import annotate_with_obw -======= >>> from ria_toolkit_oss.annotations import annotate_with_obw ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 >>> annotated = annotate_with_obw(recording, label="signal_obw") """ signal = recording.data[0] diff --git a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py index 6a412ed..957cf58 100644 --- a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py +++ b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py @@ -38,11 +38,7 @@ sub-annotations. Example: Two WiFi channels captured simultaneously: -<<<<<<< HEAD - >>> from utils.annotations import find_spectral_components -======= >>> from ria_toolkit_oss.annotations import find_spectral_components ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 >>> # Detect the two distinct channels (returns relative frequencies) >>> components = find_spectral_components(signal, sampling_rate=20e6) >>> print(f"Found {len(components)} components") @@ -59,11 +55,7 @@ import numpy as np from scipy import ndimage from scipy import signal as scipy_signal -<<<<<<< HEAD -from utils.data import Annotation, Recording -======= from ria_toolkit_oss.datatypes import Annotation, Recording ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 def find_spectral_components( @@ -119,13 +111,8 @@ def find_spectral_components( **Example**:: -<<<<<<< HEAD - >>> from utils.io import load_recording - >>> from utils.annotations import find_spectral_components -======= >>> from ria.io import load_recording >>> from ria_toolkit_oss.annotations import find_spectral_components ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 >>> recording = load_recording("capture.sigmf") >>> segment = recording.data[0][start:end] >>> # Components in relative (baseband) frequency @@ -254,13 +241,8 @@ def split_annotation_by_components( **Example**:: -<<<<<<< HEAD - >>> from utils.io import load_recording - >>> from utils.annotations import split_annotation_by_components -======= >>> from ria.io import load_recording >>> from ria_toolkit_oss.annotations import split_annotation_by_components ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 >>> recording = load_recording("capture.sigmf") >>> # Original annotation spans multiple channels >>> original = recording.annotations[0] @@ -387,13 +369,8 @@ def split_recording_annotations( **Example**:: -<<<<<<< HEAD - >>> from utils.io import load_recording - >>> from utils.annotations import split_recording_annotations -======= >>> from ria.io import load_recording >>> from ria_toolkit_oss.annotations import split_recording_annotations ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 >>> recording = load_recording("capture.sigmf") >>> # Split all annotations >>> split_rec = split_recording_annotations(recording) diff --git a/src/ria_toolkit_oss/annotations/qualify_slice.py b/src/ria_toolkit_oss/annotations/qualify_slice.py index 08d590b..2336fe5 100644 --- a/src/ria_toolkit_oss/annotations/qualify_slice.py +++ b/src/ria_toolkit_oss/annotations/qualify_slice.py @@ -1,10 +1,6 @@ import numpy as np -<<<<<<< HEAD -from utils.data import Recording -======= from ria_toolkit_oss.datatypes import Recording ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 def qualify_slice_from_annotations(recording: Recording, slice_length: int): diff --git a/src/ria_toolkit_oss/annotations/signal_isolation.py b/src/ria_toolkit_oss/annotations/signal_isolation.py index 255b95b..47852ae 100644 --- a/src/ria_toolkit_oss/annotations/signal_isolation.py +++ b/src/ria_toolkit_oss/annotations/signal_isolation.py @@ -1,13 +1,8 @@ import numpy as np from scipy.signal import butter, lfilter -<<<<<<< HEAD -from utils.data.annotation import Annotation -from utils.data.recording import Recording -======= from ria_toolkit_oss.datatypes.annotation import Annotation from ria_toolkit_oss.datatypes.recording import Recording ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 def isolate_signal(recording: Recording, annotation: Annotation) -> Recording: diff --git a/src/ria_toolkit_oss/annotations/threshold_qualifier.py b/src/ria_toolkit_oss/annotations/threshold_qualifier.py index 7efb541..338f13c 100644 --- a/src/ria_toolkit_oss/annotations/threshold_qualifier.py +++ b/src/ria_toolkit_oss/annotations/threshold_qualifier.py @@ -46,29 +46,17 @@ from typing import Optional import numpy as np -<<<<<<< HEAD -from utils.data import Annotation, Recording - - -def _find_ranges(indices, window_size): -======= from ria_toolkit_oss.datatypes import Annotation, Recording def _find_ranges(indices, max_gap): ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ Groups individual indices into continuous temporal ranges. Args: indices: Array of indices where the signal exceeded a threshold. -<<<<<<< HEAD - window_size: Maximum gap allowed between indices to consider them part - of the same range. -======= max_gap: Maximum gap allowed between indices to consider them part of the same range. ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 Returns: A list of (start, stop) tuples representing detected signal segments. @@ -77,30 +65,6 @@ def _find_ranges(indices, max_gap): if len(indices) == 0: return [] -<<<<<<< HEAD - ranges = [] - - start = indices[0] - in_range = False - - for i in range(1, len(indices)): - # If the gap between current and previous index is within window_size, - # keep the range alive. - if indices[i] - indices[i - 1] <= window_size: - if not in_range: - # Start a new range - start = indices[i - 1] - in_range = True - else: - # Gap is too large; close the current range if one was active. - if in_range: - ranges.append((start, indices[i - 1])) - in_range = False - - # Ensure the final segment is captured if the loop ends while in_range. - if in_range: - ranges.append((start, indices[-1])) -======= start = indices[0] prev = indices[0] ranges = [] @@ -112,19 +76,10 @@ def _find_ranges(indices, max_gap): prev = indices[i] ranges.append((start, prev)) ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 return ranges -<<<<<<< HEAD -def threshold_qualifier( - recording: Recording, - threshold: float, - window_size: Optional[int] = 1024, - label: Optional[str] = None, - annotation_type: Optional[str] = "standalone", -======= def _expand_and_filter_ranges( smoothed_power: np.ndarray, initial_ranges: list[tuple[int, int]], @@ -231,7 +186,6 @@ def threshold_qualifier( label: Optional[str] = None, annotation_type: Optional[str] = "standalone", channel: int = 0, ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 ) -> Recording: """ Annotate a recording with bounding boxes for regions above a threshold. @@ -249,27 +203,15 @@ def threshold_qualifier( Args: recording: The Recording object containing IQ or real signal data. threshold: Sensitivity multiplier (0.0 to 1.0) applied to max power. -<<<<<<< HEAD - window_size: Size of the smoothing filter and max gap for merging hits. - label: Custom string label for annotations. - annotation_type: Metadata string for the 'type' field in the annotation. -======= window_size: Size of the smoothing filter in samples. Defaults to 1ms worth of samples. label: Custom string label for annotations. annotation_type: Metadata string for the 'type' field in the annotation. channel: Index of the channel to annotate. Defaults to 0. ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 Returns: A new Recording object populated with detected Annotations. """ # Extract signal and metadata -<<<<<<< HEAD - sample_data = recording.data[0] - sample_rate = recording.metadata["sample_rate"] - center_frequency = recording.metadata.get("center_frequency", 0) - -======= sample_data = recording.data[channel] sample_rate = recording.metadata["sample_rate"] center_frequency = recording.metadata.get("center_frequency", 0) @@ -277,69 +219,11 @@ def threshold_qualifier( if window_size is None: window_size = max(64, int(sample_rate * 0.001)) ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 # --- 1. SIGNAL CONDITIONING --- # Convert to power (Magnitude squared) power_data = np.abs(sample_data) ** 2 smoothing_window = np.ones(window_size) / window_size smoothed_power = np.convolve(power_data, smoothing_window, mode="same") -<<<<<<< HEAD - - # Define thresholds based on the global peak of the smoothed signal - max_power = np.max(smoothed_power) - trigger_val = threshold * max_power # High threshold to trigger detection - boundary_val = (threshold / 2) * max_power # Low threshold to define signal edges - - # --- 2. INITIAL DETECTION --- - # Identify indices that strictly exceed the high trigger - indices = np.where(smoothed_power > trigger_val)[0] - initial_ranges = _find_ranges(indices=indices, window_size=window_size) - - annotations = [] - - threshold_base = min(sample_rate, len(sample_data)) - - for start, stop in initial_ranges: - if (stop - start) < (threshold_base * 0.01): - continue - - # --- 3. HYSTERESIS (Boundary Expansion) --- - # Search backward from 'start' until power drops below the low boundary_val - true_start = start - while true_start > 0 and smoothed_power[true_start] > boundary_val: - true_start -= 1 - - # Search forward from 'stop' until power drops below the low boundary_val - true_stop = stop - while true_stop < len(smoothed_power) - 1 and smoothed_power[true_stop] > boundary_val: - true_stop += 1 - - # --- 4. SPECTRAL ANALYSIS (Frequency Detection) --- - signal_segment = sample_data[true_start:true_stop] - if len(signal_segment) > 0: - fft_data = np.abs(np.fft.fftshift(np.fft.fft(signal_segment))) - fft_freqs = np.fft.fftshift(np.fft.fftfreq(len(signal_segment), 1 / sample_rate)) - - # Determine frequency bounds where spectral energy is > 15% of segment peak - spectral_thresh = np.max(fft_data) * 0.15 - sig_indices = np.where(fft_data > spectral_thresh)[0] - - # Ensure the signal has some spectral width before annotating - if len(sig_indices) < 5: - continue - - if len(sig_indices) > 0: - f_min, f_max = fft_freqs[sig_indices[0]], fft_freqs[sig_indices[-1]] - else: - # Default to middle half of bandwidth if no clear peaks found - f_min, f_max = -sample_rate / 4, sample_rate / 4 - else: - f_min, f_max = -sample_rate / 4, sample_rate / 4 - - # --- 5. ANNOTATION GENERATION --- - if label is None: - label = f"{int(threshold*100)}%" -======= group_gap_samples = _estimate_group_gap(sample_rate) # Define thresholds using peak relative to baseline. @@ -442,7 +326,6 @@ def threshold_qualifier( # --- 5. ANNOTATION GENERATION --- ann_label = label if label is not None else f"{int(threshold*100)}%" ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 # Pack metadata for the UI/Downstream processing comment_data = { @@ -459,11 +342,7 @@ def threshold_qualifier( sample_count=true_stop - true_start, freq_lower_edge=center_frequency + f_min, freq_upper_edge=center_frequency + f_max, -<<<<<<< HEAD - label=label, -======= label=ann_label, ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 comment=json.dumps(comment_data), detail={"generator": "hysteresis_qualifier"}, ) diff --git a/src/ria_toolkit_oss/data/recording.py b/src/ria_toolkit_oss/data/recording.py index 50e03ee..20939bd 100644 --- a/src/ria_toolkit_oss/data/recording.py +++ b/src/ria_toolkit_oss/data/recording.py @@ -12,7 +12,7 @@ from typing import Any, Iterator, Optional import numpy as np from numpy.typing import ArrayLike -from utils.data.annotation import Annotation +from ria_toolkit_oss.datatypes.annotation import Annotation PROTECTED_KEYS = ["rec_id", "timestamp"] @@ -66,7 +66,7 @@ class Recording: **Examples:** >>> import numpy - >>> from utils.data import Recording, Annotation + >>> from ria_toolkit_oss.datatypes import Recording, Annotation >>> # Create an array of complex samples, just 1s in this case. >>> samples = numpy.ones(10000, dtype=numpy.complex64) @@ -305,7 +305,7 @@ class Recording: Create a recording and add metadata: >>> import numpy - >>> from utils.data import Recording + >>> from ria_toolkit_oss.datatypes import Recording >>> >>> samples = numpy.ones(10000, dtype=numpy.complex64) >>> metadata = { @@ -360,7 +360,7 @@ class Recording: Create a recording and update metadata: >>> import numpy - >>> from utils.data import Recording + >>> from ria_toolkit_oss.datatypes import Recording >>> samples = numpy.ones(10000, dtype=numpy.complex64) >>> metadata = { @@ -414,7 +414,7 @@ class Recording: Create a recording and add metadata: >>> import numpy - >>> from utils.data import Recording + >>> from ria_toolkit_oss.datatypes import Recording >>> samples = numpy.ones(10000, dtype=numpy.complex64) >>> metadata = { @@ -455,7 +455,7 @@ class Recording: Create a recording and view it as a plot in a .png image: >>> import numpy - >>> from utils.data import Recording + >>> from ria_toolkit_oss.datatypes import Recording >>> samples = numpy.ones(10000, dtype=numpy.complex64) >>> metadata = { @@ -466,14 +466,14 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.view() """ - from utils.view import view_sig + from ria_toolkit_oss.view import view_sig view_sig(recording=self, output_path=output_path, **kwargs) def simple_view(self, **kwargs) -> None: """Create a plot of various signal visualizations as a PNG or SVG image. - :param kwargs: Keyword arguments passed on to utils.view.view_signal_simple.create_plots. + :param kwargs: Keyword arguments passed on to ria_toolkit_oss.view.view_signal_simple.create_plots. :type: dict of keyword arguments **Examples:** @@ -481,7 +481,7 @@ class Recording: Create a recording and view it as a plot in a .png image: >>> import numpy - >>> from utils.data import Recording + >>> from ria_toolkit_oss.datatypes import Recording >>> samples = numpy.ones(10000, dtype=numpy.complex64) >>> metadata = { @@ -492,7 +492,7 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.simple_view() """ - from utils.view.view_signal_simple import view_simple_sig + from ria_toolkit_oss.view.view_signal_simple import view_simple_sig view_simple_sig(recording=self, **kwargs) @@ -530,7 +530,7 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.view() """ - from utils.io.recording import to_sigmf + from ria_toolkit_oss.io.recording import to_sigmf to_sigmf(filename=filename, path=path, recording=self, overwrite=overwrite) @@ -565,7 +565,7 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.to_npy() """ - from utils.io.recording import to_npy + from ria_toolkit_oss.io.recording import to_npy to_npy(recording=self, filename=filename, path=path, overwrite=overwrite) @@ -611,7 +611,7 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.to_wav() """ - from utils.io.recording import to_wav + from ria_toolkit_oss.io.recording import to_wav return to_wav( recording=self, @@ -661,7 +661,7 @@ class Recording: >>> recording = Recording(data=samples, metadata=metadata) >>> recording.to_blue() """ - from utils.io.recording import to_blue + from ria_toolkit_oss.io.recording import to_blue return to_blue(recording=self, filename=filename, path=path, data_format=data_format, overwrite=overwrite) diff --git a/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py b/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py index 18f5464..c0272c3 100644 --- a/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py +++ b/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py @@ -1,6 +1,6 @@ import numpy as np -from utils.signal.block_generator.block import Block -from utils.signal.block_generator.data_types import DataType +from ria_toolkit_oss.signal.block_generator.block import Block +from ria_toolkit_oss.signal.block_generator.data_types import DataType class FrequencyUpConversion(Block): diff --git a/src/ria_toolkit_oss/view/recording.py b/src/ria_toolkit_oss/view/recording.py index 381f07e..b9c413b 100644 --- a/src/ria_toolkit_oss/view/recording.py +++ b/src/ria_toolkit_oss/view/recording.py @@ -11,7 +11,7 @@ def spectrogram(rec: Recording, thumbnail: bool = False) -> Figure: """Create a spectrogram for the recording. :param rec: Signal to plot. - :type rec: utils.data.Recording + :type rec: ria_toolkit_oss.datatypes.Recording :param thumbnail: Whether to return a small thumbnail version or full plot. :type thumbnail: bool @@ -95,7 +95,7 @@ def iq_time_series(rec: Recording) -> Figure: """Create a time series plot of the real and imaginary parts of signal. :param rec: Signal to plot. - :type rec: utils.data.Recording + :type rec: ria_toolkit_oss.datatypes.Recording :return: Time series plot as a Plotly figure. """ @@ -125,7 +125,7 @@ def frequency_spectrum(rec: Recording) -> Figure: """Create a frequency spectrum plot from the recording. :param rec: Input signal to plot. - :type rec: utils.data.Recording + :type rec: ria_toolkit_oss.datatypes.Recording :return: Frequency spectrum as a Plotly figure. """ @@ -160,7 +160,7 @@ def constellation(rec: Recording) -> Figure: """Create a constellation plot from the recording. :param rec: Input signal to plot. - :type rec: utils.data.Recording + :type rec: ria_toolkit_oss.datatypes.Recording :return: Constellation as a Plotly figure. """ diff --git a/src/ria_toolkit_oss/view/view_signal.py b/src/ria_toolkit_oss/view/view_signal.py index 38b6056..86cfbe7 100644 --- a/src/ria_toolkit_oss/view/view_signal.py +++ b/src/ria_toolkit_oss/view/view_signal.py @@ -13,8 +13,8 @@ from scipy.fft import fft, fftshift from scipy.signal import spectrogram from scipy.signal.windows import hann -from utils.data.recording import Recording -from utils.view.tools import COLORS, decimate, extract_metadata_fields, set_path +from ria_toolkit_oss.datatypes.recording import Recording +from ria_toolkit_oss.view.tools import COLORS, decimate, extract_metadata_fields, set_path def get_fft_size(plot_length): @@ -58,17 +58,6 @@ def view_annotations( sample_rate, center_frequency, _ = extract_metadata_fields(recording.metadata) annotations = recording.annotations -<<<<<<< HEAD - # 2. Setup Color Mapping (No more hardcoded yellow fallback!) - # available_colors = [ - # COLORS.get("magenta", "magenta"), - # COLORS.get("accent", "cyan"), - # COLORS.get("light", "white"), - # "lime", - # ] - - palette = ["#FF00FF", "#00FF00", "#00FFFF", "#FFFF00", "#FF8000"] -======= # 2. Setup Color Mapping available_colors = [ COLORS.get("magenta", "magenta"), @@ -78,7 +67,6 @@ def view_annotations( ] palette = ["#2196F3", "#9C27B0", "#64B5F6", "#7B1FA2", "#5C6BC0", "#CE93D8", "#1565C0", "#7C4DFF"] ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 unique_labels = sorted(list(set(ann.label for ann in annotations if ann.label))) label_to_color = {label: palette[i % len(palette)] for i, label in enumerate(unique_labels)} @@ -87,11 +75,6 @@ def view_annotations( complex_signal, NFFT=256, Fs=sample_rate, Fc=center_frequency, noverlap=128, cmap="twilight" ) -<<<<<<< HEAD - # 4. Draw Annotations - for annotation in annotations: - # --- DEFINING VARIABLES FIRST --- -======= # 4. Draw Annotations (highest threshold % first so lower % renders on top) def _threshold_sort_key(ann): try: @@ -100,21 +83,13 @@ def view_annotations( return 0 for annotation in sorted(annotations, key=_threshold_sort_key, reverse=True): ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 t_start = annotation.sample_start / sample_rate t_width = annotation.sample_count / sample_rate f_start = annotation.freq_lower_edge f_height = annotation.freq_upper_edge - annotation.freq_lower_edge -<<<<<<< HEAD - # Look up the color for this specific label ann_color = label_to_color.get(annotation.label, "gray") - # Draw the Rectangle -======= - ann_color = label_to_color.get(annotation.label, "gray") - ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 rect = plt.Rectangle( (t_start, f_start), t_width, f_height, linewidth=1.5, edgecolor=ann_color, facecolor="none", alpha=0.8 ) @@ -130,11 +105,7 @@ def view_annotations( ax.set_title(title, fontsize=title_fontsize, pad=20) ax.set_xlabel("Time (s)", fontsize=12) ax.set_ylabel("Frequency (MHz)", fontsize=12) -<<<<<<< HEAD - ax.grid(alpha=0.1) # Add faint grid -======= ax.grid(alpha=0.1) ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 output_path, _ = set_path(output_path=output_path) plt.savefig(output_path, dpi=dpi, bbox_inches="tight") diff --git a/src/ria_toolkit_oss/view/view_signal_simple.py b/src/ria_toolkit_oss/view/view_signal_simple.py index 248486f..d97452e 100644 --- a/src/ria_toolkit_oss/view/view_signal_simple.py +++ b/src/ria_toolkit_oss/view/view_signal_simple.py @@ -12,8 +12,8 @@ import numpy as np from scipy.fft import fft, fftshift from scipy.signal.windows import hann -from utils.data.recording import Recording -from utils.view.tools import COLORS, decimate, extract_metadata_fields, set_path +from ria_toolkit_oss.datatypes.recording import Recording +from ria_toolkit_oss.view.tools import COLORS, decimate, extract_metadata_fields, set_path def _add_annotations(annotations, compact_mode, show_labels, sample_rate_hz, center_freq_hz, ax2): diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py index 1bbad66..cdfabb5 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -11,13 +11,8 @@ from ria_toolkit_oss.annotations import ( split_recording_annotations, threshold_qualifier, ) -<<<<<<< HEAD -from ria_toolkit_oss.data import Annotation -from ria_toolkit_oss.data.recording import Recording -======= from ria_toolkit_oss.datatypes import Annotation from ria_toolkit_oss.datatypes.recording import Recording ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 from ria_toolkit_oss.io import load_recording, to_blue, to_npy, to_sigmf, to_wav from ria_toolkit_oss_cli.ria_toolkit_oss.common import format_frequency, format_sample_count @@ -55,15 +50,6 @@ def detect_input_format(filepath): def determine_output_path(input_path, output_path, fmt, quiet, overwrite): input_path = Path(input_path) -<<<<<<< HEAD - - if output_path: - target = Path(output_path) - final_path = target - else: - annotated_name = f"{input_path.stem}_annotated" - target = input_path.with_name(f"{annotated_name}{input_path.suffix}") -======= input_is_annotated = input_path.stem.endswith("_annotated") if output_path: @@ -73,7 +59,6 @@ def determine_output_path(input_path, output_path, fmt, quiet, overwrite): target = input_path else: target = input_path.with_name(f"{input_path.stem}_annotated{input_path.suffix}") ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 if fmt == "sigmf": final_path = normalize_sigmf_path(target) @@ -84,15 +69,10 @@ def determine_output_path(input_path, output_path, fmt, quiet, overwrite): if not quiet: click.echo(f"Saving to: {final_path}") -<<<<<<< HEAD - if final_path.exists() and not overwrite and final_path != input_path: - click.echo(f"Error: {final_path} already exists. Use --overwrite to replace it.", err=True) -======= # Always allow writing to _annotated files; guard against overwriting originals target_is_annotated = final_path.stem.endswith("_annotated") if final_path.exists() and not target_is_annotated and final_path != input_path: click.echo(f"Error: {final_path} is not an annotated file and cannot be overwritten.", err=True) ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 return None return final_path @@ -250,13 +230,8 @@ def list(input, verbose): \b Examples: -<<<<<<< HEAD - utils annotate list recording.sigmf-data - utils annotate list signal.npy --verbose -======= ria annotate list recording.sigmf-data ria annotate list signal.npy --verbose ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ try: recording = load_recording(input) @@ -324,13 +299,8 @@ def add(input, start, count, label, freq_lower, freq_upper, comment, annotation_ \b Examples: -<<<<<<< HEAD - utils annotate add file.npy --start 1000 --count 500 --label wifi - utils annotate add signal.sigmf-data --start 0 --count 1000 --label burst --comment "Strong signal" -======= ria annotate add file.npy --start 1000 --count 500 --label wifi ria annotate add signal.sigmf-data --start 0 --count 1000 --label burst --comment "Strong signal" ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ try: recording = load_recording(input) @@ -412,21 +382,12 @@ def add(input, start, count, label, freq_lower, freq_upper, comment, annotation_ def remove(input, index, output, overwrite, quiet): """Remove annotation by index. -<<<<<<< HEAD - Use 'utils annotate list' to see annotation indices. - - \b - Examples: - utils annotate remove signal.sigmf-data 2 - utils annotate remove file.npy 0 -======= Use 'ria annotate list' to see annotation indices. \b Examples: ria annotate remove signal.sigmf-data 2 ria annotate remove file.npy 0 ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ try: recording = load_recording(input) @@ -475,13 +436,8 @@ def clear(input, output, overwrite, force, quiet): \b Examples: -<<<<<<< HEAD - utils annotate clear signal.sigmf-data - utils annotate clear file.npy --force -======= ria annotate clear signal.sigmf-data ria annotate clear file.npy --force ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ try: recording = load_recording(input) @@ -576,17 +532,10 @@ def energy( \b Examples: -<<<<<<< HEAD - utils annotate energy capture.sigmf-data --label burst - utils annotate energy signal.npy --threshold 1.5 --min-distance 10000 - utils annotate energy signal.sigmf-data --freq-method obw - utils annotate energy signal.sigmf-data --freq-method full-detected -======= ria annotate energy capture.sigmf-data --label burst ria annotate energy signal.npy --threshold 1.5 --min-distance 10000 ria annotate energy signal.sigmf-data --freq-method obw ria annotate energy signal.sigmf-data --freq-method full-detected ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ try: @@ -662,13 +611,8 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o \b Examples: -<<<<<<< HEAD - utils annotate cusum signal.sigmf-data --min-duration 5.0 - utils annotate cusum data.npy --min-duration 10.0 --label state -======= ria annotate cusum signal.sigmf-data --min-duration 5.0 ria annotate cusum data.npy --min-duration 10.0 --label state ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ try: recording = load_recording(input) @@ -714,11 +658,7 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o @click.argument("input", type=click.Path(exists=True)) @click.option("--threshold", type=float, required=True, help="Threshold (0.0-1.0, fraction of max magnitude)") @click.option("--label", type=str, default=None, help="Annotation label") -<<<<<<< HEAD -@click.option("--window-size", type=int, default=1024, help="Smoothing window size") -======= @click.option("--window-size", type=int, default=None, help="Smoothing window size in samples (default: 1ms at recording sample rate)") ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 @click.option( "--type", "annotation_type", @@ -726,18 +666,11 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o default="standalone", help="Annotation type", ) -<<<<<<< HEAD -@click.option("--output", "-o", type=click.Path(), help="Output file path") -@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") -@click.option("--quiet", is_flag=True, help="Quiet mode") -def threshold(input, threshold, label, window_size, annotation_type, output, overwrite, quiet): -======= @click.option("--channel", type=int, default=0, help="Channel index to annotate (default: 0)") @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") def threshold(input, threshold, label, window_size, annotation_type, channel, output, overwrite, quiet): ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """Auto-detect signals using threshold method. Detects samples above a percentage of maximum magnitude. Best for simple @@ -745,13 +678,8 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, ou \b Examples: -<<<<<<< HEAD - utils annotate threshold signal.sigmf-data --threshold 0.7 --label wifi - utils annotate threshold data.npy --threshold 0.5 --window-size 2048 -======= ria annotate threshold signal.sigmf-data --threshold 0.7 --label wifi ria annotate threshold data.npy --threshold 0.5 --window-size 2048 ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ if not (0.0 <= threshold <= 1.0): raise click.ClickException(f"--threshold must be between 0.0 and 1.0, got {threshold}") @@ -766,12 +694,8 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, ou if not quiet: click.echo("\nDetecting signals using threshold qualifier...") click.echo(f" Threshold: {threshold * 100:.1f}% of max magnitude") -<<<<<<< HEAD - click.echo(f" Window size: {window_size} samples") -======= click.echo(f" Window size: {'auto (1ms)' if window_size is None else f'{window_size} samples'}") click.echo(f" Channel: {channel}") ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 try: initial_count = len(recording.annotations) @@ -781,10 +705,7 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, ou window_size=window_size, label=label, annotation_type=annotation_type, -<<<<<<< HEAD -======= channel=channel, ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 ) added = len(recording.annotations) - initial_count @@ -833,17 +754,10 @@ def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, \b Examples: -<<<<<<< HEAD - utils annotate separate capture.sigmf-data - utils annotate separate signal.npy --indices 0,1,2 - utils annotate separate data.sigmf-data --noise-threshold-db -70 - utils annotate separate signal.npy --min-component-bw 100000 -======= ria annotate separate capture.sigmf-data ria annotate separate signal.npy --indices 0,1,2 ria annotate separate data.sigmf-data --noise-threshold-db -70 ria annotate separate signal.npy --min-component-bw 100000 ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 """ try: diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py index 1377ac6..53cf37f 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py @@ -2,10 +2,8 @@ """ This module contains all the CLI bindings for the ria package. """ -<<<<<<< HEAD -======= ->>>>>>> 2bb2d9d5a780dbc17172135a5a1f10eba14b1af4 + from .annotate import annotate from .capture import capture from .combine import combine From 5d909c4a22ab1787f9b559ce35a530dc3f8040da Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 31 Mar 2026 15:27:45 -0400 Subject: [PATCH 11/18] Update to be accurate to ria toolkit --- tests/ria_toolkit_oss_cli/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/ria_toolkit_oss_cli/README.md b/tests/ria_toolkit_oss_cli/README.md index 3e78415..06a5258 100644 --- a/tests/ria_toolkit_oss_cli/README.md +++ b/tests/ria_toolkit_oss_cli/README.md @@ -13,25 +13,25 @@ Comprehensive test suite for the ria CLI commands. ### Run all CLI tests: ```bash -poetry run pytest tests/utils_cli/ -v +poetry run pytest tests/ria_toolkit_oss_cli/ -v ``` ### Run specific test file: ```bash -poetry run pytest tests/utils_cli/test_common.py -v -poetry run pytest tests/utils_cli/test_discover.py -v -poetry run pytest tests/utils_cli/test_capture.py -v +poetry run pytest tests/ria_toolkit_oss_cli/test_common.py -v +poetry run pytest tests/ria_toolkit_oss_cli/test_discover.py -v +poetry run pytest tests/ria_toolkit_oss_cli/test_capture.py -v ``` ### Run specific test class or function: ```bash -poetry run pytest tests/utils_cli/test_capture.py::TestCaptureCommand::test_capture_basic -v -poetry run pytest tests/utils_cli/test_common.py::test_parse_frequency -v +poetry run pytest tests/ria_toolkit_oss_cli/test_capture.py::TestCaptureCommand::test_capture_basic -v +poetry run pytest tests/ria_toolkit_oss_cli/test_common.py::test_parse_frequency -v ``` ### Run with coverage: ```bash -poetry run pytest tests/utils_cli/ --cov=utils_cli --cov-report=html +poetry run pytest tests/ria_toolkit_oss_cli/ --cov=utils_cli --cov-report=html ``` ## Test Coverage From 0e3e0220846582e15b09993a37347132ee336faf Mon Sep 17 00:00:00 2001 From: madrigal Date: Thu, 2 Apr 2026 10:35:21 -0400 Subject: [PATCH 12/18] Updated poetry.lock --- poetry.lock | 2186 +++++++++++++++++++++++++++------------------------ 1 file changed, 1150 insertions(+), 1036 deletions(-) diff --git a/poetry.lock b/poetry.lock index 86d581f..db83521 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand. [[package]] name = "alabaster" @@ -14,24 +14,23 @@ files = [ [[package]] name = "anyio" -version = "4.10.0" +version = "4.13.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, - {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, + {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"}, + {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -sniffio = ">=1.1" typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -trio = ["trio (>=0.26.1)"] +trio = ["trio (>=0.32.0)"] [[package]] name = "astroid" @@ -50,34 +49,26 @@ typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "25.3.0" +version = "26.1.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, + {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, + {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, ] -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] @@ -132,26 +123,26 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "cachetools" -version = "6.2.0" +version = "7.0.5" description = "Extensible memoizing collections and decorators" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6"}, - {file = "cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32"}, + {file = "cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114"}, + {file = "cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990"}, ] [[package]] name = "certifi" -version = "2025.8.3" +version = "2026.2.25" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["docs"] files = [ - {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, - {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, ] [[package]] @@ -252,117 +243,155 @@ files = [ [package.dependencies] pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} -[[package]] -name = "chardet" -version = "5.2.0" -description = "Universal encoding detector for Python 3" -optional = false -python-versions = ">=3.7" -groups = ["test"] -files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, -] - [[package]] name = "charset-normalizer" -version = "3.4.3" +version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" groups = ["docs"] files = [ - {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, - {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, - {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943"}, + {file = "charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00"}, + {file = "charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6"}, + {file = "charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110"}, + {file = "charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f"}, + {file = "charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c"}, + {file = "charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207"}, + {file = "charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444"}, + {file = "charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c"}, + {file = "charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d"}, + {file = "charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5"}, ] [[package]] name = "click" -version = "8.2.1" +version = "8.3.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs"] files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, ] [package.dependencies] @@ -570,14 +599,14 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "dill" -version = "0.4.0" +version = "0.4.1" description = "serialize all of Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, + {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, + {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, ] [package.extras] @@ -610,15 +639,15 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.3.0" +version = "1.3.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" groups = ["docs", "test"] markers = "python_version == \"3.10\"" files = [ - {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, - {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, ] [package.dependencies] @@ -629,14 +658,14 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.19.1" +version = "3.25.2" description = "A platform independent file lock." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, - {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, + {file = "filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70"}, + {file = "filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694"}, ] [[package]] @@ -658,62 +687,62 @@ pyflakes = ">=3.4.0,<3.5.0" [[package]] name = "fonttools" -version = "4.61.1" +version = "4.62.1" description = "Tools to manipulate font files" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24"}, - {file = "fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958"}, - {file = "fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da"}, - {file = "fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6"}, - {file = "fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1"}, - {file = "fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881"}, - {file = "fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47"}, - {file = "fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6"}, - {file = "fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09"}, - {file = "fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37"}, - {file = "fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb"}, - {file = "fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9"}, - {file = "fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87"}, - {file = "fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56"}, - {file = "fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a"}, - {file = "fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7"}, - {file = "fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e"}, - {file = "fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2"}, - {file = "fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796"}, - {file = "fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d"}, - {file = "fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8"}, - {file = "fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0"}, - {file = "fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261"}, - {file = "fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9"}, - {file = "fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c"}, - {file = "fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e"}, - {file = "fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5"}, - {file = "fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd"}, - {file = "fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3"}, - {file = "fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d"}, - {file = "fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c"}, - {file = "fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b"}, - {file = "fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd"}, - {file = "fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e"}, - {file = "fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c"}, - {file = "fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75"}, - {file = "fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063"}, - {file = "fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2"}, - {file = "fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c"}, - {file = "fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c"}, - {file = "fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa"}, - {file = "fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91"}, - {file = "fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19"}, - {file = "fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba"}, - {file = "fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7"}, - {file = "fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118"}, - {file = "fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5"}, - {file = "fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b"}, - {file = "fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371"}, - {file = "fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69"}, + {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ad5cca75776cd453b1b035b530e943334957ae152a36a88a320e779d61fc980c"}, + {file = "fonttools-4.62.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b3ae47e8636156a9accff64c02c0924cbebad62854c4a6dbdc110cd5b4b341a"}, + {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b9e288b4da2f64fd6180644221749de651703e8d0c16bd4b719533a3a7d6e3"}, + {file = "fonttools-4.62.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bca7a1c1faf235ffe25d4f2e555246b4750220b38de8261d94ebc5ce8a23c23"}, + {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4e0fcf265ad26e487c56cb12a42dffe7162de708762db951e1b3f755319507d"}, + {file = "fonttools-4.62.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d850f66830a27b0d498ee05adb13a3781637b1826982cd7e2b3789ef0cc71ae"}, + {file = "fonttools-4.62.1-cp310-cp310-win32.whl", hash = "sha256:486f32c8047ccd05652aba17e4a8819a3a9d78570eb8a0e3b4503142947880ed"}, + {file = "fonttools-4.62.1-cp310-cp310-win_amd64.whl", hash = "sha256:5a648bde915fba9da05ae98856987ca91ba832949a9e2888b48c47ef8b96c5a9"}, + {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:40975849bac44fb0b9253d77420c6d8b523ac4dcdcefeff6e4d706838a5b80f7"}, + {file = "fonttools-4.62.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9dde91633f77fa576879a0c76b1d89de373cae751a98ddf0109d54e173b40f14"}, + {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6acb4109f8bee00fec985c8c7afb02299e35e9c94b57287f3ea542f28bd0b0a7"}, + {file = "fonttools-4.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c5c25671ce8805e0d080e2ffdeca7f1e86778c5cbfbeae86d7f866d8830517b"}, + {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a5d8825e1140f04e6c99bb7d37a9e31c172f3bc208afbe02175339e699c710e1"}, + {file = "fonttools-4.62.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:268abb1cb221e66c014acc234e872b7870d8b5d4657a83a8f4205094c32d2416"}, + {file = "fonttools-4.62.1-cp311-cp311-win32.whl", hash = "sha256:942b03094d7edbb99bdf1ae7e9090898cad7bf9030b3d21f33d7072dbcb51a53"}, + {file = "fonttools-4.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:e8514f4924375f77084e81467e63238b095abda5107620f49421c368a6017ed2"}, + {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974"}, + {file = "fonttools-4.62.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12859ff0b47dd20f110804c3e0d0970f7b832f561630cd879969011541a464a9"}, + {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c125ffa00c3d9003cdaaf7f2c79e6e535628093e14b5de1dccb08859b680936"}, + {file = "fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392"}, + {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0aa72c43a601cfa9273bb1ae0518f1acadc01ee181a6fc60cd758d7fdadffc04"}, + {file = "fonttools-4.62.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:19177c8d96c7c36359266e571c5173bcee9157b59cfc8cb0153c5673dc5a3a7d"}, + {file = "fonttools-4.62.1-cp312-cp312-win32.whl", hash = "sha256:a24decd24d60744ee8b4679d38e88b8303d86772053afc29b19d23bb8207803c"}, + {file = "fonttools-4.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42"}, + {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c22b1014017111c401469e3acc5433e6acf6ebcc6aa9efb538a533c800971c79"}, + {file = "fonttools-4.62.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68959f5fc58ed4599b44aad161c2837477d7f35f5f79402d97439974faebfebe"}, + {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef46db46c9447103b8f3ff91e8ba009d5fe181b1920a83757a5762551e32bb68"}, + {file = "fonttools-4.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6706d1cb1d5e6251a97ad3c1b9347505c5615c112e66047abbef0f8545fa30d1"}, + {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e7abd2b1e11736f58c1de27819e1955a53267c21732e78243fa2fa2e5c1e069"}, + {file = "fonttools-4.62.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:403d28ce06ebfc547fbcb0cb8b7f7cc2f7a2d3e1a67ba9a34b14632df9e080f9"}, + {file = "fonttools-4.62.1-cp313-cp313-win32.whl", hash = "sha256:93c316e0f5301b2adbe6a5f658634307c096fd5aae60a5b3412e4f3e1728ab24"}, + {file = "fonttools-4.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:7aa21ff53e28a9c2157acbc44e5b401149d3c9178107130e82d74ceb500e5056"}, + {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca"}, + {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca"}, + {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782"}, + {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae"}, + {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7"}, + {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a"}, + {file = "fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800"}, + {file = "fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e"}, + {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82"}, + {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260"}, + {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4"}, + {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b"}, + {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87"}, + {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c"}, + {file = "fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a"}, + {file = "fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e"}, + {file = "fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd"}, + {file = "fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d"}, ] [package.extras] @@ -743,53 +772,75 @@ files = [ [[package]] name = "h5py" -version = "3.14.0" +version = "3.16.0" description = "Read and write HDF5 files from Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "h5py-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24df6b2622f426857bda88683b16630014588a0e4155cba44e872eb011c4eaed"}, - {file = "h5py-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ff2389961ee5872de697054dd5a033b04284afc3fb52dc51d94561ece2c10c6"}, - {file = "h5py-3.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:016e89d3be4c44f8d5e115fab60548e518ecd9efe9fa5c5324505a90773e6f03"}, - {file = "h5py-3.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1223b902ef0b5d90bcc8a4778218d6d6cd0f5561861611eda59fa6c52b922f4d"}, - {file = "h5py-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:852b81f71df4bb9e27d407b43071d1da330d6a7094a588efa50ef02553fa7ce4"}, - {file = "h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088"}, - {file = "h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8"}, - {file = "h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad"}, - {file = "h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b"}, - {file = "h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713"}, - {file = "h5py-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e0045115d83272090b0717c555a31398c2c089b87d212ceba800d3dc5d952e23"}, - {file = "h5py-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6da62509b7e1d71a7d110478aa25d245dd32c8d9a1daee9d2a42dba8717b047a"}, - {file = "h5py-3.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554ef0ced3571366d4d383427c00c966c360e178b5fb5ee5bb31a435c424db0c"}, - {file = "h5py-3.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cbd41f4e3761f150aa5b662df991868ca533872c95467216f2bec5fcad84882"}, - {file = "h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f"}, - {file = "h5py-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aa4b7bbce683379b7bf80aaba68e17e23396100336a8d500206520052be2f812"}, - {file = "h5py-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9603a501a04fcd0ba28dd8f0995303d26a77a980a1f9474b3417543d4c6174"}, - {file = "h5py-3.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8cbaf6910fa3983c46172666b0b8da7b7bd90d764399ca983236f2400436eeb"}, - {file = "h5py-3.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d90e6445ab7c146d7f7981b11895d70bc1dd91278a4f9f9028bc0c95e4a53f13"}, - {file = "h5py-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae18e3de237a7a830adb76aaa68ad438d85fe6e19e0d99944a3ce46b772c69b3"}, - {file = "h5py-3.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5cc1601e78027cedfec6dd50efb4802f018551754191aeb58d948bd3ec3bd7a"}, - {file = "h5py-3.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e59d2136a8b302afd25acdf7a89b634e0eb7c66b1a211ef2d0457853768a2ef"}, - {file = "h5py-3.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:573c33ad056ac7c1ab6d567b6db9df3ffc401045e3f605736218f96c1e0490c6"}, - {file = "h5py-3.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccbe17dc187c0c64178f1a10aa274ed3a57d055117588942b8a08793cc448216"}, - {file = "h5py-3.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f025cf30ae738c4c4e38c7439a761a71ccfcce04c2b87b2a2ac64e8c5171d43"}, - {file = "h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4"}, + {file = "h5py-3.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e06f864bedb2c8e7c1358e6c73af48519e317457c444d6f3d332bb4e8fa6d7d9"}, + {file = "h5py-3.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec86d4fffd87a0f4cb3d5796ceb5a50123a2a6d99b43e616e5504e66a953eca3"}, + {file = "h5py-3.16.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:86385ea895508220b8a7e45efa428aeafaa586bd737c7af9ee04661d8d84a10d"}, + {file = "h5py-3.16.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8975273c2c5921c25700193b408e28d6bdd0111c37468b2d4e25dcec4cd1d84d"}, + {file = "h5py-3.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1677ad48b703f44efc9ea0c3ab284527f81bc4f318386aaaebc5fede6bbae56f"}, + {file = "h5py-3.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c4dd4cf5f0a4e36083f73172f6cfc25a5710789269547f132a20975bfe2434c"}, + {file = "h5py-3.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:bdef06507725b455fccba9c16529121a5e1fbf56aa375f7d9713d9e8ff42454d"}, + {file = "h5py-3.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:719439d14b83f74eeb080e9650a6c7aa6d0d9ea0ca7f804347b05fac6fbf18af"}, + {file = "h5py-3.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3f0a0e136f2e95dd0b67146abb6668af4f1a69c81ef8651a2d316e8e01de447"}, + {file = "h5py-3.16.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a6fbc5367d4046801f9b7db9191b31895f22f1c6df1f9987d667854cac493538"}, + {file = "h5py-3.16.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:fb1720028d99040792bb2fb31facb8da44a6f29df7697e0b84f0d79aff2e9bd3"}, + {file = "h5py-3.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:314b6054fe0b1051c2b0cb2df5cbdab15622fb05e80f202e3b6a5eee0d6fe365"}, + {file = "h5py-3.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ffbab2fedd6581f6aa31cf1639ca2cb86e02779de525667892ebf4cc9fd26434"}, + {file = "h5py-3.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:17d1f1630f92ad74494a9a7392ab25982ce2b469fc62da6074c0ce48366a2999"}, + {file = "h5py-3.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b9c49dd58dc44cf70af944784e2c2038b6f799665d0dcbbc812a26e0faa859"}, + {file = "h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d"}, + {file = "h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d"}, + {file = "h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527"}, + {file = "h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e"}, + {file = "h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794"}, + {file = "h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074"}, + {file = "h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6"}, + {file = "h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db"}, + {file = "h5py-3.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:370a845f432c2c9619db8eed334d1e610c6015796122b0e57aa46312c22617d9"}, + {file = "h5py-3.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42108e93326c50c2810025aade9eac9d6827524cdccc7d4b75a546e5ab308edb"}, + {file = "h5py-3.16.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:099f2525c9dcf28de366970a5fb34879aab20491589fa89ce2863a84218bb524"}, + {file = "h5py-3.16.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9300ad32dea9dfc5171f94d5f6948e159ed93e4701280b0f508773b3f582f402"}, + {file = "h5py-3.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:171038f23bccddfc23f344cadabdfc9917ff554db6a0d417180d2747fe4c75a7"}, + {file = "h5py-3.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e420b539fb6023a259a1b14d4c9f6df8cf50d7268f48e161169987a57b737ff"}, + {file = "h5py-3.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:18f2bbcd545e6991412253b98727374c356d67caa920e68dc79eab36bf5fedad"}, + {file = "h5py-3.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:656f00e4d903199a1d58df06b711cf3ca632b874b4207b7dbec86185b5c8c7d4"}, + {file = "h5py-3.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9c9d307c0ef862d1cd5714f72ecfafe0a5d7529c44845afa8de9f46e5ba8bd65"}, + {file = "h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8c1eff849cdd53cbc73c214c30ebdb6f1bb8b64790b4b4fc36acdb5e43570210"}, + {file = "h5py-3.16.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e2c04d129f180019e216ee5f9c40b78a418634091c8782e1f723a6ca3658b965"}, + {file = "h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4360f15875a532bc7b98196c7592ed4fc92672a57c0a621355961cafb17a6dd"}, + {file = "h5py-3.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3fae9197390c325e62e0a1aa977f2f62d994aa87aab182abbea85479b791197c"}, + {file = "h5py-3.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43259303989ac8adacc9986695b31e35dba6fd1e297ff9c6a04b7da5542139cc"}, + {file = "h5py-3.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:fa48993a0b799737ba7fd21e2350fa0a60701e58180fae9f2de834bc39a147ab"}, + {file = "h5py-3.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:1897a771a7f40d05c262fc8f37376ec37873218544b70216872876c627640f63"}, + {file = "h5py-3.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15922e485844f77c0b9d275396d435db3baa58292a9c2176a386e072e0cf2491"}, + {file = "h5py-3.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:df02dd29bd247f98674634dfe41f89fd7c16ba3d7de8695ec958f58404a4e618"}, + {file = "h5py-3.16.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0f456f556e4e2cebeebd9d66adf8dc321770a42593494a0b6f0af54a7567b242"}, + {file = "h5py-3.16.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3e6cb3387c756de6a9492d601553dffea3fe11b5f22b443aac708c69f3f55e16"}, + {file = "h5py-3.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8389e13a1fd745ad2856873e8187fd10268b2d9677877bb667b41aebd771d8b7"}, + {file = "h5py-3.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:346df559a0f7dcb31cf8e44805319e2ab24b8957c45e7708ce503b2ec79ba725"}, + {file = "h5py-3.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4c6ab014ab704b4feaa719ae783b86522ed0bf1f82184704ed3c9e4e3228796e"}, + {file = "h5py-3.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:faca8fb4e4319c09d83337adc80b2ca7d5c5a343c2d6f1b6388f32cfecca13c1"}, + {file = "h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738"}, ] [package.dependencies] -numpy = ">=1.19.3" +numpy = ">=1.21.2" [[package]] name = "idna" -version = "3.10" +version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" groups = ["docs"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] [package.extras] @@ -797,26 +848,40 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "imagesize" -version = "1.4.1" +version = "1.5.0" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" groups = ["docs"] +markers = "python_version >= \"3.12\"" files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, + {file = "imagesize-1.5.0-py2.py3-none-any.whl", hash = "sha256:32677681b3f434c2cb496f00e89c5a291247b35b1f527589909e008057da5899"}, + {file = "imagesize-1.5.0.tar.gz", hash = "sha256:8bfc5363a7f2133a89f0098451e0bcb1cd71aba4dc02bbcecb39d99d40e1b94f"}, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +description = "Get image size from headers (BMP/PNG/JPEG/JPEG2000/GIF/TIFF/SVG/Netpbm/WebP/AVIF/HEIC/HEIF)" +optional = false +python-versions = "<3.15,>=3.10" +groups = ["docs"] +markers = "python_version < \"3.12\"" +files = [ + {file = "imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96"}, + {file = "imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3"}, ] [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] @@ -854,21 +919,21 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63"}, - {file = "jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85"}, + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, ] [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" -rpds-py = ">=0.7.1" +rpds-py = ">=0.25.0" [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] @@ -876,14 +941,14 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2025.4.1" +version = "2025.9.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, - {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, ] [package.dependencies] @@ -891,184 +956,228 @@ referencing = ">=0.31.0" [[package]] name = "kiwisolver" -version = "1.4.9" +version = "1.5.0" description = "A fast implementation of the Cassowary constraint solver" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1"}, - {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32cc0a5365239a6ea0c6ed461e8838d053b57e397443c0ca894dcc8e388d4374"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc0b66c1eec9021353a4b4483afb12dfd50e3669ffbb9152d6842eb34c7e29fd"}, + {file = "kiwisolver-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86e0287879f75621ae85197b0877ed2f8b7aa57b511c7331dce2eb6f4de7d476"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:62f59da443c4f4849f73a51a193b1d9d258dcad0c41bc4d1b8fb2bcc04bfeb22"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9190426b7aa26c5229501fa297b8d0653cfd3f5a36f7990c264e157cbf886b3b"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c8277104ded0a51e699c8c3aff63ce2c56d4ed5519a5f73e0fd7057f959a2b9e"}, + {file = "kiwisolver-1.5.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8f9baf6f0a6e7571c45c8863010b45e837c3ee1c2c77fcd6ef423be91b21fedb"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cff8e5383db4989311f99e814feeb90c4723eb4edca425b9d5d9c3fefcdd9537"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ebae99ed6764f2b5771c522477b311be313e8841d2e0376db2b10922daebbba4"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d5cd5189fc2b6a538b75ae45433140c4823463918f7b1617c31e68b085c0022c"}, + {file = "kiwisolver-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f42c23db5d1521218a3276bb08666dcb662896a0be7347cba864eca45ff64ede"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:94eff26096eb5395136634622515b234ecb6c9979824c1f5004c6e3c3c85ccd2"}, + {file = "kiwisolver-1.5.0-cp310-cp310-win_arm64.whl", hash = "sha256:dd952e03bfbb096cfe2dd35cd9e00f269969b67536cb4370994afc20ff2d0875"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9eed0f7edbb274413b6ee781cca50541c8c0facd3d6fd289779e494340a2b85c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c4923e404d6bcd91b6779c009542e5647fef32e4a5d75e115e3bbac6f2335eb"}, + {file = "kiwisolver-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df54df7e686afa55e6f21fb86195224a6d9beb71d637e8d7920c95cf0f89aac"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2517e24d7315eb51c10664cdb865195df38ab74456c677df67bb47f12d088a27"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff710414307fefa903e0d9bdf300972f892c23477829f49504e59834f4195398"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6176c1811d9d5a04fa391c490cc44f451e240697a16977f11c6f722efb9041db"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50847dca5d197fcbd389c805aa1a1cf32f25d2e7273dc47ab181a517666b68cc"}, + {file = "kiwisolver-1.5.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:01808c6d15f4c3e8559595d6d1fe6411c68e4a3822b4b9972b44473b24f4e679"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f9f4121ec58628c96baa3de1a55a4e3a333c5102c8e94b64e23bf7b2083309"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b7d335370ae48a780c6e6a6bbfa97342f563744c39c35562f3f367665f5c1de2"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:800ee55980c18545af444d93fdd60c56b580db5cc54867d8cbf8a1dc0829938c"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c438f6ca858697c9ab67eb28246c92508af972e114cac34e57a6d4ba17a3ac08"}, + {file = "kiwisolver-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c63c91f95173f9c2a67c7c526b2cea976828a0e7fced9cdcead2802dc10f8a4"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:beb7f344487cdcb9e1efe4b7a29681b74d34c08f0043a327a74da852a6749e7b"}, + {file = "kiwisolver-1.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad4ae4ffd1ee9cd11357b4c66b612da9888f4f4daf2f36995eda64bd45370cac"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588"}, + {file = "kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083"}, + {file = "kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314"}, + {file = "kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384"}, + {file = "kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8"}, + {file = "kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167"}, + {file = "kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276"}, + {file = "kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2"}, + {file = "kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7"}, + {file = "kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:295d9ffe712caa9f8a3081de8d32fc60191b4b51c76f02f951fd8407253528f4"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:51e8c4084897de9f05898c2c2a39af6318044ae969d46ff7a34ed3f96274adca"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b83af57bdddef03c01a9138034c6ff03181a3028d9a1003b301eb1a55e161a3f"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf4679a3d71012a7c2bf360e5cd878fbd5e4fcac0896b56393dec239d81529ed"}, + {file = "kiwisolver-1.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:41024ed50e44ab1a60d3fe0a9d15a4ccc9f5f2b1d814ff283c8d01134d5b81bc"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ec4c85dc4b687c7f7f15f553ff26a98bfe8c58f5f7f0ac8905f0ba4c7be60232"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:12e91c215a96e39f57989c8912ae761286ac5a9584d04030ceb3368a357f017a"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be4a51a55833dc29ab5d7503e7bcb3b3af3402d266018137127450005cdfe737"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daae526907e262de627d8f70058a0f64acc9e2641c164c99c8f594b34a799a16"}, + {file = "kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1"}, + {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"}, ] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] [[package]] @@ -1176,20 +1285,20 @@ files = [ [[package]] name = "narwhals" -version = "2.3.0" +version = "2.18.1" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "narwhals-2.3.0-py3-none-any.whl", hash = "sha256:5507b1a9a9c2b1c55a627fdf6cf722fef2e23498bd14362a332c8848a311c321"}, - {file = "narwhals-2.3.0.tar.gz", hash = "sha256:b66bc4ab7b6746354f60c4b3941e3ce60c066588c35360e2dc6c063489000a16"}, + {file = "narwhals-2.18.1-py3-none-any.whl", hash = "sha256:a0a8bb80205323851338888ba3a12b4f65d352362c8a94be591244faf36504ad"}, + {file = "narwhals-2.18.1.tar.gz", hash = "sha256:652a1fcc9d432bbf114846688884c215f17eb118aa640b7419295d2f910d2a8b"}, ] [package.extras] -cudf = ["cudf (>=24.10.0)"] +cudf = ["cudf-cu12 (>=24.10.0)"] dask = ["dask[dataframe] (>=2024.8)"] -duckdb = ["duckdb (>=1.0)"] +duckdb = ["duckdb (>=1.1)"] ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] modin = ["modin"] pandas = ["pandas (>=1.1.3)"] @@ -1197,6 +1306,7 @@ polars = ["polars (>=0.20.4)"] pyarrow = ["pyarrow (>=13.0.0)"] pyspark = ["pyspark (>=3.5.0)"] pyspark-connect = ["pyspark[connect] (>=3.5.0)"] +sql = ["duckdb (>=1.1)", "sqlparse"] sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"] [[package]] @@ -1247,66 +1357,79 @@ files = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "docs", "test"] files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] name = "pandas" -version = "2.3.2" +version = "2.3.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pandas-2.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52bc29a946304c360561974c6542d1dd628ddafa69134a7131fdfd6a5d7a1a35"}, - {file = "pandas-2.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:220cc5c35ffaa764dd5bb17cf42df283b5cb7fdf49e10a7b053a06c9cb48ee2b"}, - {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c05e15111221384019897df20c6fe893b2f697d03c811ee67ec9e0bb5a3424"}, - {file = "pandas-2.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc03acc273c5515ab69f898df99d9d4f12c4d70dbfc24c3acc6203751d0804cf"}, - {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d25c20a03e8870f6339bcf67281b946bd20b86f1a544ebbebb87e66a8d642cba"}, - {file = "pandas-2.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21bb612d148bb5860b7eb2c10faacf1a810799245afd342cf297d7551513fbb6"}, - {file = "pandas-2.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:b62d586eb25cb8cb70a5746a378fc3194cb7f11ea77170d59f889f5dfe3cec7a"}, - {file = "pandas-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1333e9c299adcbb68ee89a9bb568fc3f20f9cbb419f1dd5225071e6cddb2a743"}, - {file = "pandas-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76972bcbd7de8e91ad5f0ca884a9f2c477a2125354af624e022c49e5bd0dfff4"}, - {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b98bdd7c456a05eef7cd21fd6b29e3ca243591fe531c62be94a2cc987efb5ac2"}, - {file = "pandas-2.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d81573b3f7db40d020983f78721e9bfc425f411e616ef019a10ebf597aedb2e"}, - {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e190b738675a73b581736cc8ec71ae113d6c3768d0bd18bffa5b9a0927b0b6ea"}, - {file = "pandas-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c253828cb08f47488d60f43c5fc95114c771bbfff085da54bfc79cb4f9e3a372"}, - {file = "pandas-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:9467697b8083f9667b212633ad6aa4ab32436dcbaf4cd57325debb0ddef2012f"}, - {file = "pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9"}, - {file = "pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b"}, - {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175"}, - {file = "pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9"}, - {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4"}, - {file = "pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811"}, - {file = "pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae"}, - {file = "pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e"}, - {file = "pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9"}, - {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a"}, - {file = "pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b"}, - {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6"}, - {file = "pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a"}, - {file = "pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b"}, - {file = "pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57"}, - {file = "pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2"}, - {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9"}, - {file = "pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2"}, - {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012"}, - {file = "pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370"}, - {file = "pandas-2.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:88080a0ff8a55eac9c84e3ff3c7665b3b5476c6fbc484775ca1910ce1c3e0b87"}, - {file = "pandas-2.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4a558c7620340a0931828d8065688b3cc5b4c8eb674bcaf33d18ff4a6870b4a"}, - {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45178cf09d1858a1509dc73ec261bf5b25a625a389b65be2e47b559905f0ab6a"}, - {file = "pandas-2.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77cefe00e1b210f9c76c697fedd8fdb8d3dd86563e9c8adc9fa72b90f5e9e4c2"}, - {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:13bd629c653856f00c53dc495191baa59bcafbbf54860a46ecc50d3a88421a96"}, - {file = "pandas-2.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:36d627906fd44b5fd63c943264e11e96e923f8de77d6016dc2f667b9ad193438"}, - {file = "pandas-2.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:a9d7ec92d71a420185dec44909c32e9a362248c4ae2238234b76d5be37f208cc"}, - {file = "pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, ] [package.dependencies] @@ -1346,115 +1469,121 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.4" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, ] +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + [[package]] name = "pillow" -version = "12.0.0" +version = "12.2.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"}, - {file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"}, - {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"}, - {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"}, - {file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"}, - {file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"}, - {file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"}, - {file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"}, - {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"}, - {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"}, - {file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"}, - {file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"}, - {file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"}, - {file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"}, - {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"}, - {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"}, - {file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"}, - {file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"}, - {file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"}, - {file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"}, - {file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"}, - {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"}, - {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"}, - {file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"}, - {file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"}, - {file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"}, - {file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"}, - {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"}, - {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"}, - {file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"}, - {file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"}, - {file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"}, - {file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"}, - {file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"}, - {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"}, - {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"}, - {file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"}, - {file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"}, - {file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"}, - {file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"}, - {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"}, - {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"}, - {file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"}, - {file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"}, - {file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"}, - {file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"}, - {file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"}, + {file = "pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f"}, + {file = "pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136"}, + {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c"}, + {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3"}, + {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa"}, + {file = "pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032"}, + {file = "pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5"}, + {file = "pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024"}, + {file = "pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab"}, + {file = "pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705"}, + {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176"}, + {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b"}, + {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909"}, + {file = "pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808"}, + {file = "pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60"}, + {file = "pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe"}, + {file = "pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5"}, + {file = "pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005"}, + {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780"}, + {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5"}, + {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5"}, + {file = "pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940"}, + {file = "pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5"}, + {file = "pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414"}, + {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c"}, + {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2"}, + {file = "pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c"}, + {file = "pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795"}, + {file = "pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed"}, + {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3"}, + {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9"}, + {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795"}, + {file = "pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e"}, + {file = "pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b"}, + {file = "pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06"}, + {file = "pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b"}, + {file = "pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea"}, + {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4"}, + {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4"}, + {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea"}, + {file = "pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24"}, + {file = "pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98"}, + {file = "pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"}, + {file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"}, + {file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"}, + {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"}, + {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"}, + {file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"}, + {file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"}, + {file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"}, + {file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"}, + {file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"}, + {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"}, + {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"}, + {file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"}, + {file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"}, + {file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1"}, + {file = "pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e"}, + {file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"}, ] [package.extras] @@ -1467,31 +1596,26 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.9.4" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev", "test"] files = [ - {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, - {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, ] -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - [[package]] name = "plotly" -version = "6.3.0" +version = "6.6.0" description = "An open-source interactive data visualization library for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "plotly-6.3.0-py3-none-any.whl", hash = "sha256:7ad806edce9d3cdd882eaebaf97c0c9e252043ed1ed3d382c3e3520ec07806d4"}, - {file = "plotly-6.3.0.tar.gz", hash = "sha256:8840a184d18ccae0f9189c2b9a2943923fd5cae7717b723f36eef78f444e5a73"}, + {file = "plotly-6.6.0-py3-none-any.whl", hash = "sha256:8d6daf0f87412e0c0bfe72e809d615217ab57cc715899a1e5145135a7800d1d0"}, + {file = "plotly-6.6.0.tar.gz", hash = "sha256:b897f15f3b02028d69f755f236be890ba950d0a42d7dfc619b44e2d8cea8748c"}, ] [package.dependencies] @@ -1504,7 +1628,7 @@ dev-build = ["build", "jupyter", "plotly[dev-core]"] 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"] -kaleido = ["kaleido (>=1.0.0)"] +kaleido = ["kaleido (>=1.1.0)"] [[package]] name = "pluggy" @@ -1536,15 +1660,15 @@ files = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] markers = "implementation_name == \"pypy\"" files = [ - {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, - {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] @@ -1561,14 +1685,14 @@ files = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["docs", "test"] files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, ] [package.extras] @@ -1576,14 +1700,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.3.8" +version = "3.3.9" description = "python code static checker" optional = false python-versions = ">=3.9.0" groups = ["dev"] files = [ - {file = "pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83"}, - {file = "pylint-3.3.8.tar.gz", hash = "sha256:26698de19941363037e2937d3db9ed94fb3303fdadf7d98847875345a8bb6b05"}, + {file = "pylint-3.3.9-py3-none-any.whl", hash = "sha256:01f9b0462c7730f94786c283f3e52a1fbdf0494bbe0971a78d7277ef46a751e7"}, + {file = "pylint-3.3.9.tar.gz", hash = "sha256:d312737d7b25ccf6b01cc4ac629b5dcd14a0fcf3ec392735ac70f137a9d5f83a"}, ] [package.dependencies] @@ -1606,14 +1730,14 @@ testutils = ["gitpython (>3)"] [[package]] name = "pyparsing" -version = "3.2.5" +version = "3.3.2" description = "pyparsing - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e"}, - {file = "pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6"}, + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, ] [package.extras] @@ -1621,34 +1745,34 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyproject-api" -version = "1.9.1" +version = "1.10.0" description = "API to interact with the python pyproject.toml based projects" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "pyproject_api-1.9.1-py3-none-any.whl", hash = "sha256:7d6238d92f8962773dd75b5f0c4a6a27cce092a14b623b811dba656f3b628948"}, - {file = "pyproject_api-1.9.1.tar.gz", hash = "sha256:43c9918f49daab37e302038fc1aed54a8c7a91a9fa935d00b9a485f37e0f5335"}, + {file = "pyproject_api-1.10.0-py3-none-any.whl", hash = "sha256:8757c41a79c0f4ab71b99abed52b97ecf66bd20b04fa59da43b5840bac105a09"}, + {file = "pyproject_api-1.10.0.tar.gz", hash = "sha256:40c6f2d82eebdc4afee61c773ed208c04c19db4c4a60d97f8d7be3ebc0bbb330"}, ] [package.dependencies] packaging = ">=25" -tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} +tomli = {version = ">=2.3", markers = "python_version < \"3.11\""} [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=3.2)"] -testing = ["covdefaults (>=2.3)", "pytest (>=8.3.5)", "pytest-cov (>=6.1.1)", "pytest-mock (>=3.14)", "setuptools (>=80.3.1)"] +docs = ["furo (>=2025.9.25)", "sphinx-autodoc-typehints (>=3.5.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)", "setuptools (>=80.9)"] [[package]] name = "pytest" -version = "8.4.1" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" groups = ["test"] files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] @@ -1678,16 +1802,36 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-discovery" +version = "1.2.1" +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"}, +] + +[package.dependencies] +filelock = ">=3.15.4" +platformdirs = ">=4.3.6,<5" + +[package.extras] +docs = ["furo (>=2025.12.19)", "sphinx (>=9.1)", "sphinx-autodoc-typehints (>=3.6.3)", "sphinxcontrib-mermaid (>=2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.5.4)", "pytest (>=8.3.5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"] + [[package]] name = "pytz" -version = "2025.2" +version = "2026.1.post1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" groups = ["main"] files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, + {file = "pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a"}, + {file = "pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1"}, ] [[package]] @@ -1880,26 +2024,26 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "quantiphy" -version = "2.20" +version = "2.21" description = "physical quantities (numbers with units)" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "quantiphy-2.20-py3-none-any.whl", hash = "sha256:afdf5f9d1cc87359bd7daf19dc1cb23808eb2e264d9395b36ca6527fb4d71b3a"}, - {file = "quantiphy-2.20.tar.gz", hash = "sha256:ba5375ac55c3b90077a793588dd5a88aaf81b2c3b0fc9c9359513ac39f6ed84d"}, + {file = "quantiphy-2.21-py3-none-any.whl", hash = "sha256:7d145d89a908de38d63c8dc0a8883bc668c85d84ed37fe6535d26cf7c077f69f"}, + {file = "quantiphy-2.21.tar.gz", hash = "sha256:bf8d06ffa7150f69a5c7e3fb4a7a0a535109df85e7c0ab0f39fb317c5c9cafe0"}, ] [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" description = "JSON Referencing + Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, ] [package.dependencies] @@ -1909,189 +2053,149 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, + {file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"}, + {file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"}, ] [package.dependencies] -certifi = ">=2017.4.17" +certifi = ">=2023.5.7" charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +urllib3 = ">=1.26,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] name = "rpds-py" -version = "0.27.1" +version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef"}, - {file = "rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1"}, - {file = "rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8"}, - {file = "rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9"}, - {file = "rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4"}, - {file = "rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1"}, - {file = "rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881"}, - {file = "rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a"}, - {file = "rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9"}, - {file = "rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948"}, - {file = "rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39"}, - {file = "rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15"}, - {file = "rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746"}, - {file = "rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90"}, - {file = "rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a"}, - {file = "rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1"}, - {file = "rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998"}, - {file = "rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39"}, - {file = "rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594"}, - {file = "rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502"}, - {file = "rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b"}, - {file = "rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d"}, - {file = "rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2"}, - {file = "rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002"}, - {file = "rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3"}, - {file = "rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83"}, - {file = "rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d"}, - {file = "rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228"}, - {file = "rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21"}, - {file = "rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd"}, - {file = "rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7"}, - {file = "rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688"}, - {file = "rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797"}, - {file = "rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334"}, - {file = "rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9"}, - {file = "rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212"}, - {file = "rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675"}, - {file = "rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3"}, - {file = "rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456"}, - {file = "rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3"}, - {file = "rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2"}, - {file = "rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48"}, - {file = "rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb"}, - {file = "rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0"}, - {file = "rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a"}, - {file = "rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772"}, - {file = "rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527"}, - {file = "rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e"}, - {file = "rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec"}, - {file = "rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b"}, - {file = "rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52"}, - {file = "rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b"}, - {file = "rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6"}, - {file = "rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c"}, - {file = "rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859"}, - {file = "rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] [[package]] @@ -2160,14 +2264,14 @@ test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis [[package]] name = "sigmf" -version = "1.2.10" +version = "1.7.2" description = "Easily interact with Signal Metadata Format (SigMF) recordings." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "sigmf-1.2.10-py3-none-any.whl", hash = "sha256:a9a7f7d0d48c350a78fc48ab14baa0fb7fa2441855467f3593d4c591d2018c11"}, - {file = "sigmf-1.2.10.tar.gz", hash = "sha256:0301a312abb9d34e090798a67886ea0aa2bf796242d856e49c8707fd3d3c9255"}, + {file = "sigmf-1.7.2-py3-none-any.whl", hash = "sha256:6599b95e8bd3ac2c568b8ec46c312a77b80868cbda79d729234f396d2194d3d8"}, + {file = "sigmf-1.7.2.tar.gz", hash = "sha256:5f80f7127539358c7528ccf26e0ac5b3c268ecaeb69a921542e8ff71d0c85346"}, ] [package.dependencies] @@ -2175,7 +2279,6 @@ jsonschema = "*" numpy = "*" [package.extras] -apps = ["scipy"] test = ["hypothesis", "pylint", "pytest", "pytest-cov"] [[package]] @@ -2190,18 +2293,6 @@ files = [ {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["docs"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - [[package]] name = "snowballstemmer" version = "3.0.1" @@ -2410,14 +2501,14 @@ test = ["pytest"] [[package]] name = "starlette" -version = "0.47.3" +version = "1.0.0" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51"}, - {file = "starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9"}, + {file = "starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b"}, + {file = "starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149"}, ] [package.dependencies] @@ -2429,83 +2520,114 @@ full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart [[package]] name = "tomli" -version = "2.2.1" +version = "2.4.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev", "docs", "test"] markers = "python_version == \"3.10\"" files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, + {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, + {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, + {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, + {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, + {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, + {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, + {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, + {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, + {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, + {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, + {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, + {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, + {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, + {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, + {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, + {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, + {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, + {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, + {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, + {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, + {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, + {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, + {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, + {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, + {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, + {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, + {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, + {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, + {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, + {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, + {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, + {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, ] [[package]] -name = "tomlkit" -version = "0.13.3" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, -] - -[[package]] -name = "tox" -version = "4.28.4" -description = "tox is a generic virtualenv management and test command line tool" +name = "tomli-w" +version = "1.2.0" +description = "A lil' TOML writer" optional = false python-versions = ">=3.9" groups = ["test"] files = [ - {file = "tox-4.28.4-py3-none-any.whl", hash = "sha256:8d4ad9ee916ebbb59272bb045e154a10fa12e3bbdcf94cc5185cbdaf9b241f99"}, - {file = "tox-4.28.4.tar.gz", hash = "sha256:b5b14c6307bd8994ff1eba5074275826620325ee1a4f61316959d562bfd70b9d"}, + {file = "tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90"}, + {file = "tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021"}, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, +] + +[[package]] +name = "tox" +version = "4.52.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"}, ] [package.dependencies] -cachetools = ">=6.1" -chardet = ">=5.2" +cachetools = ">=7.0.3" colorama = ">=0.4.6" -filelock = ">=3.18" -packaging = ">=25" -platformdirs = ">=4.3.8" +filelock = ">=3.25" +packaging = ">=26" +platformdirs = ">=4.9.4" pluggy = ">=1.6" -pyproject-api = ">=1.9.1" -tomli = {version = ">=2.2.1", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.14.1", markers = "python_version < \"3.11\""} -virtualenv = ">=20.31.2" +pyproject-api = ">=1.10" +python-discovery = ">=1.2.1" +tomli = {version = ">=2.4", markers = "python_version < \"3.11\""} +tomli-w = ">=1.2" +typing-extensions = {version = ">=4.15", markers = "python_version < \"3.11\""} +virtualenv = ">=21.1" + +[package.extras] +completion = ["argcomplete (>=3.6.3)"] [[package]] name = "typing-extensions" @@ -2522,44 +2644,44 @@ markers = {main = "python_version <= \"3.12\"", dev = "python_version == \"3.10\ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, ] [[package]] name = "urllib3" -version = "2.5.0" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] -brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" -version = "0.35.0" +version = "0.42.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, - {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, + {file = "uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359"}, + {file = "uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775"}, ] [package.dependencies] @@ -2568,144 +2690,144 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=10.4)"] [[package]] name = "virtualenv" -version = "20.34.0" +version = "21.2.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, - {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, + {file = "virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f"}, + {file = "virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098"}, ] [package.dependencies] distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" +filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" +python-discovery = ">=1" typing-extensions = {version = ">=4.13.2", markers = "python_version < \"3.11\""} -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] - [[package]] name = "watchfiles" -version = "1.1.0" +version = "1.1.1" description = "Simple, modern and high performance file watching and code reload in python." optional = false python-versions = ">=3.9" groups = ["docs"] files = [ - {file = "watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc"}, - {file = "watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5"}, - {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9"}, - {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72"}, - {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc"}, - {file = "watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587"}, - {file = "watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82"}, - {file = "watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2"}, - {file = "watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8"}, - {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"}, - {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4"}, - {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d"}, - {file = "watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2"}, - {file = "watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12"}, - {file = "watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a"}, - {file = "watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179"}, - {file = "watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd"}, - {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f"}, - {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4"}, - {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f"}, - {file = "watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd"}, - {file = "watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47"}, - {file = "watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6"}, - {file = "watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30"}, - {file = "watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b"}, - {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c"}, - {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b"}, - {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb"}, - {file = "watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9"}, - {file = "watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7"}, - {file = "watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5"}, - {file = "watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1"}, - {file = "watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4"}, - {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20"}, - {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef"}, - {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb"}, - {file = "watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297"}, - {file = "watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92"}, - {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e"}, - {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b"}, - {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259"}, - {file = "watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f"}, - {file = "watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb"}, - {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147"}, - {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8"}, - {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db"}, - {file = "watchfiles-1.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa"}, - {file = "watchfiles-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29"}, - {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e"}, - {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86"}, - {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f"}, - {file = "watchfiles-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267"}, - {file = "watchfiles-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea"}, - {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432"}, - {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866"}, - {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277"}, - {file = "watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575"}, + {file = "watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c"}, + {file = "watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab"}, + {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82"}, + {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4"}, + {file = "watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844"}, + {file = "watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e"}, + {file = "watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5"}, + {file = "watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606"}, + {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701"}, + {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10"}, + {file = "watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849"}, + {file = "watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4"}, + {file = "watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e"}, + {file = "watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d"}, + {file = "watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803"}, + {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94"}, + {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43"}, + {file = "watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9"}, + {file = "watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9"}, + {file = "watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404"}, + {file = "watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18"}, + {file = "watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d"}, + {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b"}, + {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374"}, + {file = "watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0"}, + {file = "watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42"}, + {file = "watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18"}, + {file = "watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da"}, + {file = "watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77"}, + {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef"}, + {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"}, + {file = "watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5"}, + {file = "watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05"}, + {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6"}, + {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81"}, + {file = "watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b"}, + {file = "watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a"}, + {file = "watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02"}, + {file = "watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21"}, + {file = "watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c"}, + {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099"}, + {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01"}, + {file = "watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70"}, + {file = "watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02"}, + {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be"}, + {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f"}, + {file = "watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b"}, + {file = "watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e"}, + {file = "watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2"}, ] [package.dependencies] @@ -2713,81 +2835,73 @@ anyio = ">=3.0.0" [[package]] name = "websockets" -version = "15.0.1" +version = "16.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["docs"] files = [ - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, - {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, - {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, - {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, - {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, - {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, - {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, - {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, - {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, - {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, - {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, - {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, - {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, - {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, - {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, - {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, - {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, - {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, - {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, - {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, - {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, - {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, - {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, - {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, - {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, - {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, - {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, - {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, - {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, - {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, + {file = "websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a"}, + {file = "websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0"}, + {file = "websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957"}, + {file = "websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72"}, + {file = "websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde"}, + {file = "websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3"}, + {file = "websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3"}, + {file = "websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9"}, + {file = "websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35"}, + {file = "websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8"}, + {file = "websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad"}, + {file = "websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d"}, + {file = "websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe"}, + {file = "websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b"}, + {file = "websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5"}, + {file = "websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64"}, + {file = "websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6"}, + {file = "websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac"}, + {file = "websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00"}, + {file = "websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79"}, + {file = "websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39"}, + {file = "websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c"}, + {file = "websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f"}, + {file = "websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1"}, + {file = "websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2"}, + {file = "websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89"}, + {file = "websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea"}, + {file = "websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9"}, + {file = "websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230"}, + {file = "websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c"}, + {file = "websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5"}, + {file = "websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82"}, + {file = "websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8"}, + {file = "websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f"}, + {file = "websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a"}, + {file = "websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156"}, + {file = "websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0"}, + {file = "websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904"}, + {file = "websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"}, + {file = "websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e"}, + {file = "websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4"}, + {file = "websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1"}, + {file = "websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3"}, + {file = "websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8"}, + {file = "websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d"}, + {file = "websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244"}, + {file = "websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e"}, + {file = "websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641"}, + {file = "websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8"}, + {file = "websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e"}, + {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944"}, + {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206"}, + {file = "websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6"}, + {file = "websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd"}, + {file = "websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d"}, + {file = "websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03"}, + {file = "websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da"}, + {file = "websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c"}, + {file = "websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767"}, + {file = "websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec"}, + {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, ] [metadata] From cfa1da9f4da2bc6ac0dfcdfafcd59abd64e6e858 Mon Sep 17 00:00:00 2001 From: madrigal Date: Thu, 2 Apr 2026 10:37:42 -0400 Subject: [PATCH 13/18] Linting --- src/ria_toolkit_oss/annotations/__init__.py | 1 - .../annotations/threshold_qualifier.py | 5 +++- .../datatypes/datasets/radio_dataset.py | 6 +++-- src/ria_toolkit_oss/io/recording.py | 2 +- src/ria_toolkit_oss/sdr/blade.py | 12 ++++++---- src/ria_toolkit_oss/sdr/hackrf.py | 6 +++-- src/ria_toolkit_oss/sdr/pluto.py | 24 ++++++++++++------- src/ria_toolkit_oss/sdr/rtlsdr.py | 12 ++++++---- src/ria_toolkit_oss/sdr/thinkrf.py | 6 +++-- src/ria_toolkit_oss/sdr/usrp.py | 12 ++++++---- .../signal/block_generator/basic/add.py | 6 +++-- .../frequency_translation/upconversion.py | 1 + .../signal/block_generator/process_block.py | 6 +++-- .../block_generator/recordable_block.py | 6 +++-- .../source/recording_source.py | 6 +++-- .../transforms/iq_augmentations.py | 6 +++-- src/ria_toolkit_oss/view/view_signal.py | 15 +++++------- .../view/view_signal_simple.py | 7 +++++- .../ria_toolkit_oss/annotate.py | 12 ++++++++-- .../ria_toolkit_oss/commands.py | 2 +- .../ria_toolkit_oss/view.py | 3 +-- 21 files changed, 102 insertions(+), 54 deletions(-) diff --git a/src/ria_toolkit_oss/annotations/__init__.py b/src/ria_toolkit_oss/annotations/__init__.py index ebb3d10..1542dcf 100644 --- a/src/ria_toolkit_oss/annotations/__init__.py +++ b/src/ria_toolkit_oss/annotations/__init__.py @@ -1,4 +1,3 @@ - """ The annotations package contains tools and utilities for creating, managing, and processing annotations. diff --git a/src/ria_toolkit_oss/annotations/threshold_qualifier.py b/src/ria_toolkit_oss/annotations/threshold_qualifier.py index 338f13c..1d23ef6 100644 --- a/src/ria_toolkit_oss/annotations/threshold_qualifier.py +++ b/src/ria_toolkit_oss/annotations/threshold_qualifier.py @@ -167,7 +167,10 @@ def _estimate_spectral_bounds(signal_segment: np.ndarray, sample_rate: float) -> else: runs = _find_ranges(sig_indices, max_gap=max(1, spectral_smooth_bins // 2)) peak_idx = int(np.argmax(smoothed_fft)) - lo_idx, hi_idx = min(runs, key=lambda run: 0 if run[0] <= peak_idx <= run[1] else min(abs(run[0] - peak_idx), abs(run[1] - peak_idx))) + lo_idx, hi_idx = min( + runs, + key=lambda run: 0 if run[0] <= peak_idx <= run[1] else min(abs(run[0] - peak_idx), abs(run[1] - peak_idx)), + ) # Prevent extremely narrow tone boxes from collapsing to just a few bins. min_total_bw_hz = 20_000.0 diff --git a/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py b/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py index cf3fd23..7a70589 100644 --- a/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py +++ b/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py @@ -956,8 +956,10 @@ def get_result_sizes( # noqa: C901 # TODO: Simplify function # Check that each class that will be augmented does not already suffice target_size for cls_name, target_size_value in zip(classes_to_augment, target_size): if class_sizes[cls_name] >= target_size_value: - raise ValueError(f"""target_size of {target_size_value} is already sufficed for current size of - {class_sizes[cls_name]} for class: {cls_name}""") + raise ValueError( + f"""target_size of {target_size_value} is already sufficed for current size of + {class_sizes[cls_name]} for class: {cls_name}""" + ) for index, class_name in enumerate(classes_to_augment): result_sizes[class_name] = target_size[index] diff --git a/src/ria_toolkit_oss/io/recording.py b/src/ria_toolkit_oss/io/recording.py index 0234e11..f56012f 100644 --- a/src/ria_toolkit_oss/io/recording.py +++ b/src/ria_toolkit_oss/io/recording.py @@ -136,9 +136,9 @@ def from_npy(file: os.PathLike | str, legacy: bool = False) -> Recording: annotations = [] except ModuleNotFoundError: # File was pickled with utils.data.Annotation — remap to ria_toolkit_oss - import pickle import sys import types + import ria_toolkit_oss.datatypes.annotation as _ann_mod utils_shim = types.ModuleType("utils") diff --git a/src/ria_toolkit_oss/sdr/blade.py b/src/ria_toolkit_oss/sdr/blade.py index 576a91c..c8e6f3f 100644 --- a/src/ria_toolkit_oss/sdr/blade.py +++ b/src/ria_toolkit_oss/sdr/blade.py @@ -474,8 +474,10 @@ class Blade(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets \ - the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This sets \ + the gain relative to the maximum possible gain." + ) else: abs_gain = rx_gain_max + gain else: @@ -548,8 +550,10 @@ class Blade(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain." + ) else: abs_gain = tx_gain_max + gain else: diff --git a/src/ria_toolkit_oss/sdr/hackrf.py b/src/ria_toolkit_oss/sdr/hackrf.py index 79e00a7..06b49a7 100644 --- a/src/ria_toolkit_oss/sdr/hackrf.py +++ b/src/ria_toolkit_oss/sdr/hackrf.py @@ -172,8 +172,10 @@ class HackRF(SDR): tx_gain_max = 47 if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This \ - sets the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This \ + sets the gain relative to the maximum possible gain." + ) else: abs_gain = tx_gain_max + gain else: diff --git a/src/ria_toolkit_oss/sdr/pluto.py b/src/ria_toolkit_oss/sdr/pluto.py index 701198a..47af8df 100644 --- a/src/ria_toolkit_oss/sdr/pluto.py +++ b/src/ria_toolkit_oss/sdr/pluto.py @@ -274,16 +274,20 @@ class Pluto(SDR): data = [self._convert_tx_samples(samples), self._convert_tx_samples(samples)] else: if len(recording) > 2: - warnings.warn("More recordings were provided than channels in the Pluto. \ - Only the first two recordings will be used") + warnings.warn( + "More recordings were provided than channels in the Pluto. \ + Only the first two recordings will be used" + ) sample0 = self._convert_tx_samples(recording.data[0]) sample1 = self._convert_tx_samples(recording.data[1]) data = [sample0, sample1] elif isinstance(recording, list): if len(recording) > 2: - warnings.warn("More recordings were provided than channels in the Pluto. \ - Only the first two recordings will be used") + warnings.warn( + "More recordings were provided than channels in the Pluto. \ + Only the first two recordings will be used" + ) if isinstance(recording[0], np.ndarray): data = [self._convert_tx_samples(recording[0]), self._convert_tx_samples(recording[1])] @@ -423,8 +427,10 @@ class Pluto(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets \ - the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This sets \ + the gain relative to the maximum possible gain." + ) else: abs_gain = rx_gain_max + gain else: @@ -534,8 +540,10 @@ class Pluto(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain." + ) else: abs_gain = tx_gain_max + gain else: diff --git a/src/ria_toolkit_oss/sdr/rtlsdr.py b/src/ria_toolkit_oss/sdr/rtlsdr.py index bae677a..d49ab6e 100644 --- a/src/ria_toolkit_oss/sdr/rtlsdr.py +++ b/src/ria_toolkit_oss/sdr/rtlsdr.py @@ -131,15 +131,19 @@ class RTLSDR(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain." + ) target_gain = max_gain + gain else: target_gain = gain if target_gain < min_gain or target_gain > max_gain: - print(f"Requested gain {target_gain} dB out of range;\ - clamping to valid span {min_gain}-{max_gain} dB.") + print( + f"Requested gain {target_gain} dB out of range;\ + clamping to valid span {min_gain}-{max_gain} dB." + ) target_gain = min(max(target_gain, min_gain), max_gain) target_gain = min(available_gains, key=lambda g: abs(g - target_gain)) diff --git a/src/ria_toolkit_oss/sdr/thinkrf.py b/src/ria_toolkit_oss/sdr/thinkrf.py index adc25ff..7d108ab 100644 --- a/src/ria_toolkit_oss/sdr/thinkrf.py +++ b/src/ria_toolkit_oss/sdr/thinkrf.py @@ -392,8 +392,10 @@ class ThinkRF(SDR): actual_sample_rate = self.BASE_SAMPLE_RATE / decimation if abs(actual_sample_rate - requested_sample_rate) > 1e3: # More than 1 kHz difference - print(f"ThinkRF: Requested {requested_sample_rate/1e6:.2f} MS/s → \ - Using decimation={decimation} ({actual_sample_rate/1e6:.2f} MS/s)") + print( + f"ThinkRF: Requested {requested_sample_rate/1e6:.2f} MS/s → \ + Using decimation={decimation} ({actual_sample_rate/1e6:.2f} MS/s)" + ) return decimation, actual_sample_rate diff --git a/src/ria_toolkit_oss/sdr/usrp.py b/src/ria_toolkit_oss/sdr/usrp.py index 70bbc46..f458378 100644 --- a/src/ria_toolkit_oss/sdr/usrp.py +++ b/src/ria_toolkit_oss/sdr/usrp.py @@ -148,8 +148,10 @@ class USRP(SDR): gain_range = self.usrp.get_rx_gain_range() if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain." + ) else: # set gain relative to max abs_gain = gain_range.stop() + gain @@ -354,8 +356,10 @@ class USRP(SDR): gain_range = self.usrp.get_tx_gain_range() if gain_mode == "relative": if gain > 0: - raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain.") + raise SDRParameterError( + "When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain." + ) else: # set gain relative to max abs_gain = gain_range.stop() + gain diff --git a/src/ria_toolkit_oss/signal/block_generator/basic/add.py b/src/ria_toolkit_oss/signal/block_generator/basic/add.py index fe59f16..9830f7b 100644 --- a/src/ria_toolkit_oss/signal/block_generator/basic/add.py +++ b/src/ria_toolkit_oss/signal/block_generator/basic/add.py @@ -37,8 +37,10 @@ class Add(RecordableBlock, ProcessBlock): samples = block.get_samples(num_samples) if len(samples) != num_samples: - raise ValueError(f"Block {self.__class__.__name__} requested {num_samples} \ - from block {block.__class__.__name__} but got {len(samples)}.") + raise ValueError( + f"Block {self.__class__.__name__} requested {num_samples} \ + from block {block.__class__.__name__} but got {len(samples)}." + ) return samples diff --git a/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py b/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py index c0272c3..36b7117 100644 --- a/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py +++ b/src/ria_toolkit_oss/signal/block_generator/frequency_translation/upconversion.py @@ -1,4 +1,5 @@ import numpy as np + from ria_toolkit_oss.signal.block_generator.block import Block from ria_toolkit_oss.signal.block_generator.data_types import DataType diff --git a/src/ria_toolkit_oss/signal/block_generator/process_block.py b/src/ria_toolkit_oss/signal/block_generator/process_block.py index 98eccd1..4b618cd 100644 --- a/src/ria_toolkit_oss/signal/block_generator/process_block.py +++ b/src/ria_toolkit_oss/signal/block_generator/process_block.py @@ -23,9 +23,11 @@ class ProcessBlock(Block, ABC): ) elif not all(isinstance(item, Block) for item in input): - raise ValueError(f"Invalid input to block '{self.__class__.__name__}'. \ + raise ValueError( + f"Invalid input to block '{self.__class__.__name__}'. \ Expected a list of Block objects but got \ - {'[' + ',' .join(f'{item.__class__.__name__}({repr(item)})' for item in input) + ']'}") + {'[' + ',' .join(f'{item.__class__.__name__}({repr(item)})' for item in input) + ']'}" + ) elif len(input) != len(self.input_type): raise ValueError( diff --git a/src/ria_toolkit_oss/signal/block_generator/recordable_block.py b/src/ria_toolkit_oss/signal/block_generator/recordable_block.py index 28f2f2b..4b63648 100644 --- a/src/ria_toolkit_oss/signal/block_generator/recordable_block.py +++ b/src/ria_toolkit_oss/signal/block_generator/recordable_block.py @@ -20,8 +20,10 @@ class RecordableBlock(Block, Recordable): :raises ValueError: If the number of samples is incorrect.""" samples = self.get_samples(num_samples) if len(samples) != num_samples: - raise ValueError(f"Error in block {self.__class__.__name__} record(). \ - Requested {num_samples} samples but got {len(samples)}") + raise ValueError( + f"Error in block {self.__class__.__name__} record(). \ + Requested {num_samples} samples but got {len(samples)}" + ) metadata = self._get_metadata() return Recording(data=samples, metadata=metadata) diff --git a/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py b/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py index 8a1e642..1b7795a 100644 --- a/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py +++ b/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py @@ -39,7 +39,9 @@ class RecordingSource(SourceBlock, RecordableBlock): :raises ValueError: If num_samples is greater than the recording length. """ if num_samples - 1 >= self.recording.data.shape[1]: - raise ValueError(f"{num_samples} samples requested from recording source with \ - {self.recording.data.shape[1]} samples available.") + raise ValueError( + f"{num_samples} samples requested from recording source with \ + {self.recording.data.shape[1]} samples available." + ) return self.recording.data[0, 0:num_samples] diff --git a/src/ria_toolkit_oss/transforms/iq_augmentations.py b/src/ria_toolkit_oss/transforms/iq_augmentations.py index 106f151..ca859a0 100644 --- a/src/ria_toolkit_oss/transforms/iq_augmentations.py +++ b/src/ria_toolkit_oss/transforms/iq_augmentations.py @@ -610,8 +610,10 @@ def cut_out( # noqa: C901 # TODO: Simplify function raise ValueError("signal must be CxN complex.") if fill_type not in {"zeros", "ones", "low-snr", "avg-snr", "high-snr"}: - raise UserWarning("""fill_type must be "zeros", "ones", "low-snr", "avg-snr", or "high-snr", - "ones" has been selected by default""") + raise UserWarning( + """fill_type must be "zeros", "ones", "low-snr", "avg-snr", or "high-snr", + "ones" has been selected by default""" + ) if max_section_size < 1 or max_section_size >= n: raise ValueError("max_section_size must be at least 1 and must be less than the length of signal.") diff --git a/src/ria_toolkit_oss/view/view_signal.py b/src/ria_toolkit_oss/view/view_signal.py index 86cfbe7..ded3c8c 100644 --- a/src/ria_toolkit_oss/view/view_signal.py +++ b/src/ria_toolkit_oss/view/view_signal.py @@ -4,7 +4,6 @@ import textwrap from typing import Optional import matplotlib.pyplot as plt -from matplotlib.patches import Patch import numpy as np from matplotlib import gridspec from matplotlib.patches import Patch @@ -14,7 +13,12 @@ from scipy.signal import spectrogram from scipy.signal.windows import hann from ria_toolkit_oss.datatypes.recording import Recording -from ria_toolkit_oss.view.tools import COLORS, decimate, extract_metadata_fields, set_path +from ria_toolkit_oss.view.tools import ( + COLORS, + decimate, + extract_metadata_fields, + set_path, +) def get_fft_size(plot_length): @@ -59,13 +63,6 @@ def view_annotations( annotations = recording.annotations # 2. Setup Color Mapping - available_colors = [ - COLORS.get("magenta", "magenta"), - COLORS.get("accent", "cyan"), - COLORS.get("light", "white"), - "lime", - ] - palette = ["#2196F3", "#9C27B0", "#64B5F6", "#7B1FA2", "#5C6BC0", "#CE93D8", "#1565C0", "#7C4DFF"] unique_labels = sorted(list(set(ann.label for ann in annotations if ann.label))) label_to_color = {label: palette[i % len(palette)] for i, label in enumerate(unique_labels)} diff --git a/src/ria_toolkit_oss/view/view_signal_simple.py b/src/ria_toolkit_oss/view/view_signal_simple.py index d97452e..1b847ab 100644 --- a/src/ria_toolkit_oss/view/view_signal_simple.py +++ b/src/ria_toolkit_oss/view/view_signal_simple.py @@ -13,7 +13,12 @@ from scipy.fft import fft, fftshift from scipy.signal.windows import hann from ria_toolkit_oss.datatypes.recording import Recording -from ria_toolkit_oss.view.tools import COLORS, decimate, extract_metadata_fields, set_path +from ria_toolkit_oss.view.tools import ( + COLORS, + decimate, + extract_metadata_fields, + set_path, +) def _add_annotations(annotations, compact_mode, show_labels, sample_rate_hz, center_freq_hz, ax2): diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py index cdfabb5..4a8d6ac 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -14,7 +14,10 @@ from ria_toolkit_oss.annotations import ( from ria_toolkit_oss.datatypes import Annotation from ria_toolkit_oss.datatypes.recording import Recording from ria_toolkit_oss.io import load_recording, to_blue, to_npy, to_sigmf, to_wav -from ria_toolkit_oss_cli.ria_toolkit_oss.common import format_frequency, format_sample_count +from ria_toolkit_oss_cli.ria_toolkit_oss.common import ( + format_frequency, + format_sample_count, +) def normalize_sigmf_path(filepath): @@ -658,7 +661,12 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o @click.argument("input", type=click.Path(exists=True)) @click.option("--threshold", type=float, required=True, help="Threshold (0.0-1.0, fraction of max magnitude)") @click.option("--label", type=str, default=None, help="Annotation label") -@click.option("--window-size", type=int, default=None, help="Smoothing window size in samples (default: 1ms at recording sample rate)") +@click.option( + "--window-size", + type=int, + default=None, + help="Smoothing window size in samples (default: 1ms at recording sample rate)", +) @click.option( "--type", "annotation_type", diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py index 53cf37f..b545366 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py @@ -18,7 +18,7 @@ from .init import init from .split import split from .transform import transform from .transmit import transmit -from .view import viewe +from .view import viewe # Aliases synth = generate diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py index b1748f6..0a7fa62 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py @@ -34,13 +34,12 @@ VISUALIZATION_TYPES = { "spines", ], }, - "annotations": { + "annotations": { "function": view_annotations, "description": "Annotation-focused spectrogram view", "options": ["channel", "dark"], }, "channels": {"function": view_channels, "description": "Multi-channel IQ and spectrogram view", "options": []}, - "annotations": {"function": view_annotations, "description": "Annotated spectrogram view", "options": ["channel", "dark"]}, } From e1025794a889df056dceae5ed1440d7e96190766 Mon Sep 17 00:00:00 2001 From: madrigal Date: Thu, 2 Apr 2026 10:52:17 -0400 Subject: [PATCH 14/18] Fixed overwrite issue --- src/ria_toolkit_oss/io/recording.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ria_toolkit_oss/io/recording.py b/src/ria_toolkit_oss/io/recording.py index f56012f..3f0f403 100644 --- a/src/ria_toolkit_oss/io/recording.py +++ b/src/ria_toolkit_oss/io/recording.py @@ -337,7 +337,7 @@ def to_sigmf( meta_dict = sigMF_metafile.ordered_metadata() meta_dict["ria"] = metadata - sigMF_metafile.tofile(meta_file_path) + sigMF_metafile.tofile(meta_file_path, overwrite=overwrite) def from_sigmf(file: os.PathLike | str) -> Recording: From c7b88f1f1474d3786fd3c1681eee7a71859e36e2 Mon Sep 17 00:00:00 2001 From: madrigal Date: Thu, 2 Apr 2026 10:52:31 -0400 Subject: [PATCH 15/18] Fixed import typo --- src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py index b545366..bc82fc8 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py @@ -18,7 +18,7 @@ from .init import init from .split import split from .transform import transform from .transmit import transmit -from .view import viewe +from .view import view # Aliases synth = generate From 1005228d69594dcd342302cb985588748c700b31 Mon Sep 17 00:00:00 2001 From: madrigal Date: Thu, 2 Apr 2026 11:07:12 -0400 Subject: [PATCH 16/18] Linting and updated poetry.lock --- poetry.lock | 102 +++++++++++++++++- .../datatypes/datasets/radio_dataset.py | 6 +- src/ria_toolkit_oss/sdr/blade.py | 12 +-- src/ria_toolkit_oss/sdr/hackrf.py | 6 +- src/ria_toolkit_oss/sdr/pluto.py | 24 ++--- src/ria_toolkit_oss/sdr/rtlsdr.py | 12 +-- src/ria_toolkit_oss/sdr/thinkrf.py | 6 +- src/ria_toolkit_oss/sdr/usrp.py | 12 +-- .../signal/block_generator/basic/add.py | 6 +- .../signal/block_generator/process_block.py | 6 +- .../block_generator/recordable_block.py | 6 +- .../source/recording_source.py | 6 +- .../ria_toolkit_oss/commands.py | 1 - 13 files changed, 135 insertions(+), 70 deletions(-) diff --git a/poetry.lock b/poetry.lock index 43af2ce..6b691a5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -896,13 +896,113 @@ files = [ [package.dependencies] numpy = ">=1.21.2" +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httptools" +version = "0.7.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.9" +groups = ["server", "test"] +files = [ + {file = "httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78"}, + {file = "httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4"}, + {file = "httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05"}, + {file = "httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed"}, + {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a"}, + {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b"}, + {file = "httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568"}, + {file = "httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657"}, + {file = "httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70"}, + {file = "httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df"}, + {file = "httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e"}, + {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274"}, + {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec"}, + {file = "httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb"}, + {file = "httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5"}, + {file = "httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5"}, + {file = "httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03"}, + {file = "httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2"}, + {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362"}, + {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c"}, + {file = "httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321"}, + {file = "httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3"}, + {file = "httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca"}, + {file = "httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c"}, + {file = "httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66"}, + {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346"}, + {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650"}, + {file = "httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6"}, + {file = "httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270"}, + {file = "httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3"}, + {file = "httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1"}, + {file = "httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b"}, + {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60"}, + {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca"}, + {file = "httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96"}, + {file = "httptools-0.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4"}, + {file = "httptools-0.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a"}, + {file = "httptools-0.7.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf"}, + {file = "httptools-0.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28"}, + {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517"}, + {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad"}, + {file = "httptools-0.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023"}, + {file = "httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9"}, +] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "idna" version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" -groups = ["docs"] +groups = ["agent", "docs", "server", "test"] files = [ {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, diff --git a/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py b/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py index 1ee9646..eea94c5 100644 --- a/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py +++ b/src/ria_toolkit_oss/datatypes/datasets/radio_dataset.py @@ -958,10 +958,8 @@ def get_result_sizes( # noqa: C901 # TODO: Simplify function # Check that each class that will be augmented does not already suffice target_size for cls_name, target_size_value in zip(classes_to_augment, target_size): if class_sizes[cls_name] >= target_size_value: - raise ValueError( - f"""target_size of {target_size_value} is already sufficed for current size of - {class_sizes[cls_name]} for class: {cls_name}""" - ) + raise ValueError(f"""target_size of {target_size_value} is already sufficed for current size of + {class_sizes[cls_name]} for class: {cls_name}""") for index, class_name in enumerate(classes_to_augment): result_sizes[class_name] = target_size[index] diff --git a/src/ria_toolkit_oss/sdr/blade.py b/src/ria_toolkit_oss/sdr/blade.py index c8e6f3f..576a91c 100644 --- a/src/ria_toolkit_oss/sdr/blade.py +++ b/src/ria_toolkit_oss/sdr/blade.py @@ -474,10 +474,8 @@ class Blade(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This sets \ - the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets \ + the gain relative to the maximum possible gain.") else: abs_gain = rx_gain_max + gain else: @@ -550,10 +548,8 @@ class Blade(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain.") else: abs_gain = tx_gain_max + gain else: diff --git a/src/ria_toolkit_oss/sdr/hackrf.py b/src/ria_toolkit_oss/sdr/hackrf.py index 06b49a7..79e00a7 100644 --- a/src/ria_toolkit_oss/sdr/hackrf.py +++ b/src/ria_toolkit_oss/sdr/hackrf.py @@ -172,10 +172,8 @@ class HackRF(SDR): tx_gain_max = 47 if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This \ - sets the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This \ + sets the gain relative to the maximum possible gain.") else: abs_gain = tx_gain_max + gain else: diff --git a/src/ria_toolkit_oss/sdr/pluto.py b/src/ria_toolkit_oss/sdr/pluto.py index 52cd7e6..68b3973 100644 --- a/src/ria_toolkit_oss/sdr/pluto.py +++ b/src/ria_toolkit_oss/sdr/pluto.py @@ -274,20 +274,16 @@ class Pluto(SDR): data = [self._convert_tx_samples(samples), self._convert_tx_samples(samples)] else: if len(recording) > 2: - warnings.warn( - "More recordings were provided than channels in the Pluto. \ - Only the first two recordings will be used" - ) + warnings.warn("More recordings were provided than channels in the Pluto. \ + Only the first two recordings will be used") sample0 = self._convert_tx_samples(recording.data[0]) sample1 = self._convert_tx_samples(recording.data[1]) data = [sample0, sample1] elif isinstance(recording, list): if len(recording) > 2: - warnings.warn( - "More recordings were provided than channels in the Pluto. \ - Only the first two recordings will be used" - ) + warnings.warn("More recordings were provided than channels in the Pluto. \ + Only the first two recordings will be used") if isinstance(recording[0], np.ndarray): data = [self._convert_tx_samples(recording[0]), self._convert_tx_samples(recording[1])] @@ -432,10 +428,8 @@ class Pluto(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This sets \ - the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets \ + the gain relative to the maximum possible gain.") else: abs_gain = rx_gain_max + gain else: @@ -545,10 +539,8 @@ class Pluto(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain.") else: abs_gain = tx_gain_max + gain else: diff --git a/src/ria_toolkit_oss/sdr/rtlsdr.py b/src/ria_toolkit_oss/sdr/rtlsdr.py index d49ab6e..bae677a 100644 --- a/src/ria_toolkit_oss/sdr/rtlsdr.py +++ b/src/ria_toolkit_oss/sdr/rtlsdr.py @@ -131,19 +131,15 @@ class RTLSDR(SDR): if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain.") target_gain = max_gain + gain else: target_gain = gain if target_gain < min_gain or target_gain > max_gain: - print( - f"Requested gain {target_gain} dB out of range;\ - clamping to valid span {min_gain}-{max_gain} dB." - ) + print(f"Requested gain {target_gain} dB out of range;\ + clamping to valid span {min_gain}-{max_gain} dB.") target_gain = min(max(target_gain, min_gain), max_gain) target_gain = min(available_gains, key=lambda g: abs(g - target_gain)) diff --git a/src/ria_toolkit_oss/sdr/thinkrf.py b/src/ria_toolkit_oss/sdr/thinkrf.py index 7d108ab..adc25ff 100644 --- a/src/ria_toolkit_oss/sdr/thinkrf.py +++ b/src/ria_toolkit_oss/sdr/thinkrf.py @@ -392,10 +392,8 @@ class ThinkRF(SDR): actual_sample_rate = self.BASE_SAMPLE_RATE / decimation if abs(actual_sample_rate - requested_sample_rate) > 1e3: # More than 1 kHz difference - print( - f"ThinkRF: Requested {requested_sample_rate/1e6:.2f} MS/s → \ - Using decimation={decimation} ({actual_sample_rate/1e6:.2f} MS/s)" - ) + print(f"ThinkRF: Requested {requested_sample_rate/1e6:.2f} MS/s → \ + Using decimation={decimation} ({actual_sample_rate/1e6:.2f} MS/s)") return decimation, actual_sample_rate diff --git a/src/ria_toolkit_oss/sdr/usrp.py b/src/ria_toolkit_oss/sdr/usrp.py index f458378..70bbc46 100644 --- a/src/ria_toolkit_oss/sdr/usrp.py +++ b/src/ria_toolkit_oss/sdr/usrp.py @@ -148,10 +148,8 @@ class USRP(SDR): gain_range = self.usrp.get_rx_gain_range() if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain.") else: # set gain relative to max abs_gain = gain_range.stop() + gain @@ -356,10 +354,8 @@ class USRP(SDR): gain_range = self.usrp.get_tx_gain_range() if gain_mode == "relative": if gain > 0: - raise SDRParameterError( - "When gain_mode = 'relative', gain must be < 0. This sets\ - the gain relative to the maximum possible gain." - ) + raise SDRParameterError("When gain_mode = 'relative', gain must be < 0. This sets\ + the gain relative to the maximum possible gain.") else: # set gain relative to max abs_gain = gain_range.stop() + gain diff --git a/src/ria_toolkit_oss/signal/block_generator/basic/add.py b/src/ria_toolkit_oss/signal/block_generator/basic/add.py index 9830f7b..fe59f16 100644 --- a/src/ria_toolkit_oss/signal/block_generator/basic/add.py +++ b/src/ria_toolkit_oss/signal/block_generator/basic/add.py @@ -37,10 +37,8 @@ class Add(RecordableBlock, ProcessBlock): samples = block.get_samples(num_samples) if len(samples) != num_samples: - raise ValueError( - f"Block {self.__class__.__name__} requested {num_samples} \ - from block {block.__class__.__name__} but got {len(samples)}." - ) + raise ValueError(f"Block {self.__class__.__name__} requested {num_samples} \ + from block {block.__class__.__name__} but got {len(samples)}.") return samples diff --git a/src/ria_toolkit_oss/signal/block_generator/process_block.py b/src/ria_toolkit_oss/signal/block_generator/process_block.py index 4b618cd..98eccd1 100644 --- a/src/ria_toolkit_oss/signal/block_generator/process_block.py +++ b/src/ria_toolkit_oss/signal/block_generator/process_block.py @@ -23,11 +23,9 @@ class ProcessBlock(Block, ABC): ) elif not all(isinstance(item, Block) for item in input): - raise ValueError( - f"Invalid input to block '{self.__class__.__name__}'. \ + raise ValueError(f"Invalid input to block '{self.__class__.__name__}'. \ Expected a list of Block objects but got \ - {'[' + ',' .join(f'{item.__class__.__name__}({repr(item)})' for item in input) + ']'}" - ) + {'[' + ',' .join(f'{item.__class__.__name__}({repr(item)})' for item in input) + ']'}") elif len(input) != len(self.input_type): raise ValueError( diff --git a/src/ria_toolkit_oss/signal/block_generator/recordable_block.py b/src/ria_toolkit_oss/signal/block_generator/recordable_block.py index 4b63648..28f2f2b 100644 --- a/src/ria_toolkit_oss/signal/block_generator/recordable_block.py +++ b/src/ria_toolkit_oss/signal/block_generator/recordable_block.py @@ -20,10 +20,8 @@ class RecordableBlock(Block, Recordable): :raises ValueError: If the number of samples is incorrect.""" samples = self.get_samples(num_samples) if len(samples) != num_samples: - raise ValueError( - f"Error in block {self.__class__.__name__} record(). \ - Requested {num_samples} samples but got {len(samples)}" - ) + raise ValueError(f"Error in block {self.__class__.__name__} record(). \ + Requested {num_samples} samples but got {len(samples)}") metadata = self._get_metadata() return Recording(data=samples, metadata=metadata) diff --git a/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py b/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py index 1b7795a..8a1e642 100644 --- a/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py +++ b/src/ria_toolkit_oss/signal/block_generator/source/recording_source.py @@ -39,9 +39,7 @@ class RecordingSource(SourceBlock, RecordableBlock): :raises ValueError: If num_samples is greater than the recording length. """ if num_samples - 1 >= self.recording.data.shape[1]: - raise ValueError( - f"{num_samples} samples requested from recording source with \ - {self.recording.data.shape[1]} samples available." - ) + raise ValueError(f"{num_samples} samples requested from recording source with \ + {self.recording.data.shape[1]} samples available.") return self.recording.data[0, 0:num_samples] diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py index 56cb3a4..174a5f4 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py @@ -3,7 +3,6 @@ This module contains all the CLI bindings for the ria package. """ - from .annotate import annotate from .campaign import campaign from .capture import capture From 93ae08bc91402f8387583d2bd4a93b75657d2969 Mon Sep 17 00:00:00 2001 From: muq Date: Mon, 20 Apr 2026 12:08:43 -0400 Subject: [PATCH 17/18] Port threshold_qualifier improvements and Pass 2 spillover fix from utils The OSS threshold_qualifier was last synced from utils on Feb 23 2026, before the major robustness improvements landed in utils on Mar 19 2026. This commit brings it fully up to date. Changes ported from utils: - Multi-pass detection (Pass 1 strong burst, Pass 2 weak residual, Pass 3 sustained faint burst via macro-window averaging) - Noise floor estimation via percentile instead of simple max*threshold - Dynamic range ratio guard (early exit on low-contrast captures) - Improved _find_ranges, _expand_and_filter_ranges, _merge_ranges helpers - Spectral smoothing in _estimate_spectral_bounds for wideband bursts - Minimum duration filter expressed in absolute time (5ms) not sample count Also includes the Pass 2 hysteresis spillover fix: - Pass 2 expansion now runs against residual_power (masked) instead of smoothed_power, preventing it from walking into Pass 1 territory - Pass 2 mask now has a window_size guard band around Pass 1 ranges, matching the guard already used in Pass 3 Only change from utils: import swapped to ria_toolkit_oss.datatypes. --- .../annotations/annotation_transforms.py | 2 +- .../annotations/cusum_annotator.py | 2 +- .../annotations/energy_detector.py | 2 +- .../annotations/parallel_signal_separator.py | 2 +- .../annotations/qualify_slice.py | 2 +- .../annotations/signal_isolation.py | 4 +- .../annotations/threshold_qualifier.py | 281 +++++++++++++----- .../ria_toolkit_oss/view.py | 7 +- 8 files changed, 226 insertions(+), 76 deletions(-) diff --git a/src/ria_toolkit_oss/annotations/annotation_transforms.py b/src/ria_toolkit_oss/annotations/annotation_transforms.py index af48465..47300c1 100644 --- a/src/ria_toolkit_oss/annotations/annotation_transforms.py +++ b/src/ria_toolkit_oss/annotations/annotation_transforms.py @@ -1,4 +1,4 @@ -from utils.data.annotation import Annotation +from ria_toolkit_oss.datatypes.annotation import Annotation # TODO figure out how to transfer labels in the merge case diff --git a/src/ria_toolkit_oss/annotations/cusum_annotator.py b/src/ria_toolkit_oss/annotations/cusum_annotator.py index a32162b..afd92cf 100644 --- a/src/ria_toolkit_oss/annotations/cusum_annotator.py +++ b/src/ria_toolkit_oss/annotations/cusum_annotator.py @@ -3,7 +3,7 @@ from typing import Optional import numpy as np -from utils.data import Annotation, Recording +from ria_toolkit_oss.datatypes import Annotation, Recording def annotate_with_cusum( diff --git a/src/ria_toolkit_oss/annotations/energy_detector.py b/src/ria_toolkit_oss/annotations/energy_detector.py index 6cc2466..0f8cc3e 100644 --- a/src/ria_toolkit_oss/annotations/energy_detector.py +++ b/src/ria_toolkit_oss/annotations/energy_detector.py @@ -11,7 +11,7 @@ from typing import Tuple import numpy as np from scipy.signal import filtfilt -from utils.data import Annotation, Recording +from ria_toolkit_oss.datatypes import Annotation, Recording def detect_signals_energy( diff --git a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py index b75a28f..21b1956 100644 --- a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py +++ b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py @@ -55,7 +55,7 @@ import numpy as np from scipy import ndimage from scipy import signal as scipy_signal -from utils.data import Annotation, Recording +from ria_toolkit_oss.datatypes import Annotation, Recording def find_spectral_components( diff --git a/src/ria_toolkit_oss/annotations/qualify_slice.py b/src/ria_toolkit_oss/annotations/qualify_slice.py index 10ff369..2336fe5 100644 --- a/src/ria_toolkit_oss/annotations/qualify_slice.py +++ b/src/ria_toolkit_oss/annotations/qualify_slice.py @@ -1,6 +1,6 @@ import numpy as np -from utils.data import Recording +from ria_toolkit_oss.datatypes import Recording def qualify_slice_from_annotations(recording: Recording, slice_length: int): diff --git a/src/ria_toolkit_oss/annotations/signal_isolation.py b/src/ria_toolkit_oss/annotations/signal_isolation.py index 8d6c9ac..47852ae 100644 --- a/src/ria_toolkit_oss/annotations/signal_isolation.py +++ b/src/ria_toolkit_oss/annotations/signal_isolation.py @@ -1,8 +1,8 @@ import numpy as np from scipy.signal import butter, lfilter -from utils.data.annotation import Annotation -from utils.data.recording import Recording +from ria_toolkit_oss.datatypes.annotation import Annotation +from ria_toolkit_oss.datatypes.recording import Recording def isolate_signal(recording: Recording, annotation: Annotation) -> Recording: diff --git a/src/ria_toolkit_oss/annotations/threshold_qualifier.py b/src/ria_toolkit_oss/annotations/threshold_qualifier.py index 200c9e8..804c5e1 100644 --- a/src/ria_toolkit_oss/annotations/threshold_qualifier.py +++ b/src/ria_toolkit_oss/annotations/threshold_qualifier.py @@ -46,17 +46,17 @@ from typing import Optional import numpy as np -from utils.data import Annotation, Recording +from ria_toolkit_oss.datatypes import Annotation, Recording -def _find_ranges(indices, window_size): +def _find_ranges(indices, max_gap): """ Groups individual indices into continuous temporal ranges. Args: indices: Array of indices where the signal exceeded a threshold. - window_size: Maximum gap allowed between indices to consider them part - of the same range. + max_gap: Maximum gap allowed between indices to consider them part + of the same range. Returns: A list of (start, stop) tuples representing detected signal segments. @@ -65,38 +65,130 @@ def _find_ranges(indices, window_size): if len(indices) == 0: return [] + start = indices[0] + prev = indices[0] ranges = [] - start = indices[0] - in_range = False - for i in range(1, len(indices)): - # If the gap between current and previous index is within window_size, - # keep the range alive. - if indices[i] - indices[i - 1] <= window_size: - if not in_range: - # Start a new range - start = indices[i - 1] - in_range = True - else: - # Gap is too large; close the current range if one was active. - if in_range: - ranges.append((start, indices[i - 1])) - in_range = False + if indices[i] - prev > max_gap: + ranges.append((start, prev)) + start = indices[i] + prev = indices[i] - # Ensure the final segment is captured if the loop ends while in_range. - if in_range: - ranges.append((start, indices[-1])) + ranges.append((start, prev)) return ranges +def _expand_and_filter_ranges( + smoothed_power: np.ndarray, + initial_ranges: list[tuple[int, int]], + boundary_val: float, + min_duration_samples: int, +) -> list[tuple[int, int]]: + """Apply hysteresis expansion and minimum-duration filtering.""" + out: list[tuple[int, int]] = [] + n = len(smoothed_power) + for start, stop in initial_ranges: + if (stop - start) < min_duration_samples: + continue + + true_start = start + while true_start > 0 and smoothed_power[true_start] > boundary_val: + true_start -= 1 + + true_stop = stop + while true_stop < n - 1 and smoothed_power[true_stop] > boundary_val: + true_stop += 1 + + if (true_stop - true_start) >= min_duration_samples: + out.append((true_start, true_stop)) + return out + + +def _merge_ranges(ranges: list[tuple[int, int]], max_gap: int) -> list[tuple[int, int]]: + """Merge overlapping or near-adjacent ranges.""" + if not ranges: + return [] + ranges = sorted(ranges, key=lambda r: r[0]) + merged = [ranges[0]] + for s, e in ranges[1:]: + last_s, last_e = merged[-1] + if s <= last_e + max_gap: + merged[-1] = (last_s, max(last_e, e)) + else: + merged.append((s, e)) + return merged + + +def _estimate_noise_floor(power: np.ndarray, quantile: float = 20.0) -> float: + """Estimate baseline from the quieter portion of the envelope.""" + return float(np.percentile(power, quantile)) + + +def _estimate_group_gap(sample_rate: float) -> int: + """Use a fixed temporal grouping gap instead of reusing the smoothing window.""" + return max(1, int(0.001 * sample_rate)) + + +def _estimate_spectral_bounds(signal_segment: np.ndarray, sample_rate: float) -> tuple[float, float]: + """Estimate occupied bandwidth from a smoothed magnitude spectrum.""" + if len(signal_segment) == 0: + return -sample_rate / 4, sample_rate / 4 + + window = np.hanning(len(signal_segment)) + windowed = signal_segment * window + + fft_data = np.abs(np.fft.fftshift(np.fft.fft(windowed))) + fft_freqs = np.fft.fftshift(np.fft.fftfreq(len(signal_segment), 1 / sample_rate)) + + # Smooth the spectrum so noise-like wideband bursts form a contiguous mask + # instead of thousands of tiny isolated runs. + spectral_smooth_bins = max(5, min(257, (len(signal_segment) // 512) | 1)) + spectral_kernel = np.ones(spectral_smooth_bins, dtype=np.float64) / spectral_smooth_bins + smoothed_fft = np.convolve(fft_data, spectral_kernel, mode="same") + + spectral_floor = float(np.percentile(smoothed_fft, 20)) + spectral_peak = float(np.max(smoothed_fft)) + spectral_ratio = spectral_peak / max(spectral_floor, 1e-12) + + if spectral_ratio < 1.2: + return -sample_rate / 4, sample_rate / 4 + + spectral_thresh = spectral_floor + 0.1 * (spectral_peak - spectral_floor) + sig_indices = np.where(smoothed_fft > spectral_thresh)[0] + + if len(sig_indices) == 0: + peak_idx = int(np.argmax(smoothed_fft)) + bin_hz = sample_rate / len(signal_segment) + half_bins = max(1, int(np.ceil(10_000.0 / bin_hz))) + lo_idx = max(0, peak_idx - half_bins) + hi_idx = min(len(smoothed_fft) - 1, peak_idx + half_bins) + else: + runs = _find_ranges(sig_indices, max_gap=max(1, spectral_smooth_bins // 2)) + peak_idx = int(np.argmax(smoothed_fft)) + lo_idx, hi_idx = min( + runs, + key=lambda run: 0 if run[0] <= peak_idx <= run[1] else min(abs(run[0] - peak_idx), abs(run[1] - peak_idx)), + ) + + # Prevent extremely narrow tone boxes from collapsing to just a few bins. + min_total_bw_hz = 20_000.0 + min_half_bins = max(1, int(np.ceil((min_total_bw_hz / 2) / (sample_rate / len(signal_segment))))) + center_idx = int(round((lo_idx + hi_idx) / 2)) + lo_idx = max(0, min(lo_idx, center_idx - min_half_bins)) + hi_idx = min(len(smoothed_fft) - 1, max(hi_idx, center_idx + min_half_bins)) + + return float(fft_freqs[lo_idx]), float(fft_freqs[hi_idx]) + + def threshold_qualifier( recording: Recording, threshold: float, - window_size: Optional[int] = 1024, + window_size: Optional[int] = None, label: Optional[str] = None, annotation_type: Optional[str] = "standalone", + channel: int = 0, ) -> Recording: """ Annotate a recording with bounding boxes for regions above a threshold. @@ -114,78 +206,133 @@ def threshold_qualifier( Args: recording: The Recording object containing IQ or real signal data. threshold: Sensitivity multiplier (0.0 to 1.0) applied to max power. - window_size: Size of the smoothing filter and max gap for merging hits. + window_size: Size of the smoothing filter in samples. Defaults to 1ms worth of samples. label: Custom string label for annotations. annotation_type: Metadata string for the 'type' field in the annotation. + channel: Index of the channel to annotate. Defaults to 0. Returns: A new Recording object populated with detected Annotations. """ # Extract signal and metadata - sample_data = recording.data[0] + sample_data = recording.data[channel] sample_rate = recording.metadata["sample_rate"] center_frequency = recording.metadata.get("center_frequency", 0) + if window_size is None: + window_size = max(64, int(sample_rate * 0.001)) + # --- 1. SIGNAL CONDITIONING --- # Convert to power (Magnitude squared) power_data = np.abs(sample_data) ** 2 smoothing_window = np.ones(window_size) / window_size smoothed_power = np.convolve(power_data, smoothing_window, mode="same") + group_gap_samples = _estimate_group_gap(sample_rate) - # Define thresholds based on the global peak of the smoothed signal + # Define thresholds using peak relative to baseline. max_power = np.max(smoothed_power) - trigger_val = threshold * max_power # High threshold to trigger detection - boundary_val = (threshold / 2) * max_power # Low threshold to define signal edges + noise_floor = _estimate_noise_floor(smoothed_power) + dynamic_range_ratio = max_power / max(noise_floor, 1e-12) + + # Soft early exit: keep a guard for low-contrast noise, but compute it from + # the quieter tail of the envelope so burst-heavy captures are not rejected. + if dynamic_range_ratio < 1.5: + return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations) + + trigger_val = noise_floor + threshold * (max_power - noise_floor) + boundary_val = noise_floor + 0.5 * threshold * (max_power - noise_floor) # --- 2. INITIAL DETECTION --- - # Identify indices that strictly exceed the high trigger - indices = np.where(smoothed_power > trigger_val)[0] - initial_ranges = _find_ranges(indices=indices, window_size=window_size) - + # Enforce an explicit minimum duration in seconds; this is stable across + # varying capture lengths and avoids over-fitting to recording length. + min_duration_samples = max(1, int(0.005 * sample_rate)) annotations = [] - threshold_base = min(sample_rate, len(sample_data)) + # Pass 1: Detect stronger bursts. + indices = np.where(smoothed_power > trigger_val)[0] + pass1_initial = _find_ranges(indices=indices, max_gap=group_gap_samples) + pass1_ranges = _expand_and_filter_ranges( + smoothed_power=smoothed_power, + initial_ranges=pass1_initial, + boundary_val=boundary_val, + min_duration_samples=min_duration_samples, + ) - for start, stop in initial_ranges: - if (stop - start) < (threshold_base * 0.01): - continue + # Pass 2: Recover weaker bursts on residual power not already covered. + # This improves recall in mixed-amplitude captures. + # Expand each Pass-1 range by the smoothing window on both sides so the + # smoothing skirts of a strong burst are not re-detected as a weak burst + # immediately adjacent to it (mirrors the guard used in Pass 3). + mask = np.ones_like(smoothed_power, dtype=np.float32) + pass2_mask_expand = window_size + for s, e in pass1_ranges: + mask[max(0, s - pass2_mask_expand) : min(len(mask), e + pass2_mask_expand)] = 0.0 + residual_power = smoothed_power * mask - # --- 3. HYSTERESIS (Boundary Expansion) --- - # Search backward from 'start' until power drops below the low boundary_val - true_start = start - while true_start > 0 and smoothed_power[true_start] > boundary_val: - true_start -= 1 + residual_max = float(np.max(residual_power)) + residual_ratio = residual_max / max(noise_floor, 1e-12) - # Search forward from 'stop' until power drops below the low boundary_val - true_stop = stop - while true_stop < len(smoothed_power) - 1 and smoothed_power[true_stop] > boundary_val: - true_stop += 1 + pass2_ranges: list[tuple[int, int]] = [] + if residual_ratio >= 2.0: + weak_threshold = max(0.3, threshold * 0.7) + weak_trigger = noise_floor + weak_threshold * (residual_max - noise_floor) + weak_boundary = noise_floor + 0.5 * weak_threshold * (residual_max - noise_floor) + weak_indices = np.where(residual_power > weak_trigger)[0] + pass2_initial = _find_ranges(indices=weak_indices, max_gap=group_gap_samples) + pass2_ranges = _expand_and_filter_ranges( + smoothed_power=residual_power, + initial_ranges=pass2_initial, + boundary_val=weak_boundary, + min_duration_samples=min_duration_samples, + ) + + # Pass 3: Detect sustained faint bursts via macro-window averaging. + # Targets bursts whose peak power is near the trigger level but whose + # *average* power is consistently elevated above the noise floor — these + # are missed by peak-based detection because only a few short spikes exceed + # the trigger, all too brief to pass the minimum-duration filter. + # + # The mask is applied to power_data *before* convolving so that bright + # burst energy does not bleed through the long window into adjacent regions, + # which would inflate macro_residual_max and push the trigger above the + # faint burst's average power. + macro_window_size = max(window_size * 16, int(sample_rate * 0.02)) + macro_kernel = np.ones(macro_window_size, dtype=np.float64) / macro_window_size + # Expand each annotated range by half the macro window on both sides so that + # the long convolution cannot "see" the leading/trailing edges of already- + # annotated bursts, which would produce spurious short fragments in Pass 3. + macro_expand = macro_window_size * 2 + masked_power_for_macro = power_data.copy() + n = len(masked_power_for_macro) + for s, e in pass1_ranges + pass2_ranges: + masked_power_for_macro[max(0, s - macro_expand) : min(n, e + macro_expand)] = 0.0 + macro_residual = np.convolve(masked_power_for_macro, macro_kernel, mode="same") + + macro_residual_max = float(np.max(macro_residual)) + + pass3_ranges: list[tuple[int, int]] = [] + if macro_residual_max / max(noise_floor, 1e-12) >= 1.3: + macro_trigger = noise_floor + threshold * (macro_residual_max - noise_floor) + macro_boundary = noise_floor + 0.5 * threshold * (macro_residual_max - noise_floor) + macro_indices = np.where(macro_residual > macro_trigger)[0] + macro_initial = _find_ranges(indices=macro_indices, max_gap=group_gap_samples) + pass3_ranges = _expand_and_filter_ranges( + smoothed_power=macro_residual, + initial_ranges=macro_initial, + boundary_val=macro_boundary, + min_duration_samples=min_duration_samples, + ) + + all_ranges = _merge_ranges(pass1_ranges + pass2_ranges + pass3_ranges, max_gap=group_gap_samples) + + for true_start, true_stop in all_ranges: # --- 4. SPECTRAL ANALYSIS (Frequency Detection) --- signal_segment = sample_data[true_start:true_stop] - if len(signal_segment) > 0: - fft_data = np.abs(np.fft.fftshift(np.fft.fft(signal_segment))) - fft_freqs = np.fft.fftshift(np.fft.fftfreq(len(signal_segment), 1 / sample_rate)) - - # Determine frequency bounds where spectral energy is > 15% of segment peak - spectral_thresh = np.max(fft_data) * 0.15 - sig_indices = np.where(fft_data > spectral_thresh)[0] - - # Ensure the signal has some spectral width before annotating - if len(sig_indices) < 5: - continue - - if len(sig_indices) > 0: - f_min, f_max = fft_freqs[sig_indices[0]], fft_freqs[sig_indices[-1]] - else: - # Default to middle half of bandwidth if no clear peaks found - f_min, f_max = -sample_rate / 4, sample_rate / 4 - else: - f_min, f_max = -sample_rate / 4, sample_rate / 4 + f_min, f_max = _estimate_spectral_bounds(signal_segment, sample_rate) # --- 5. ANNOTATION GENERATION --- - if label is None: - label = f"{int(threshold*100)}%" + ann_label = label if label is not None else f"{int(threshold*100)}%" # Pack metadata for the UI/Downstream processing comment_data = { @@ -202,7 +349,7 @@ def threshold_qualifier( sample_count=true_stop - true_start, freq_lower_edge=center_frequency + f_min, freq_upper_edge=center_frequency + f_max, - label=label, + label=ann_label, comment=json.dumps(comment_data), detail={"generator": "hysteresis_qualifier"}, ) diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py index 9c67e80..438cc29 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py @@ -34,7 +34,7 @@ VISUALIZATION_TYPES = { "spines", ], }, - "annotations": { + "annotations": { "function": view_annotations, "description": "Annotation-focused spectrogram view", "options": ["channel", "dark"], @@ -199,7 +199,7 @@ def print_metadata(recording, quiet): @click.option( "--type", "viz_type", - type=click.Choice(list(VISUALIZATION_TYPES.keys())), + type=click.Choice(list(VISUALIZATION_TYPES.keys()) + ["annotate", "annotation"]), default="simple", show_default=True, help="Visualization type", @@ -302,6 +302,9 @@ def view( # Legacy NPY file ria view old_capture.npy --legacy --type simple """ + if viz_type in ["annotate", "annotation"]: + viz_type = "annotations" + # Load config file if specified if config: _ = load_yaml_config(config) From db000da5179225627df8333493dd0e8091a428ea Mon Sep 17 00:00:00 2001 From: ben Date: Mon, 20 Apr 2026 15:08:31 -0400 Subject: [PATCH 18/18] Add noqa C901 to view() to pass flake8 complexity check --- src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py index 438cc29..cac8ceb 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/view.py @@ -243,7 +243,7 @@ def print_metadata(recording, quiet): @click.option("--verbose", "-v", is_flag=True, help="Verbose output") @click.option("--quiet", "-q", is_flag=True, help="Suppress output") @click.option("--overwrite", is_flag=True, help="Overwrite existing output file") -def view( +def view( # noqa: C901 input, viz_type, output,