oscura 0.8.0__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +19 -19
- oscura/analyzers/__init__.py +2 -0
- oscura/analyzers/digital/extraction.py +2 -3
- oscura/analyzers/digital/quality.py +1 -1
- oscura/analyzers/digital/timing.py +1 -1
- oscura/analyzers/patterns/__init__.py +66 -0
- oscura/analyzers/power/basic.py +3 -3
- oscura/analyzers/power/soa.py +1 -1
- oscura/analyzers/power/switching.py +3 -3
- oscura/analyzers/signal_classification.py +529 -0
- oscura/analyzers/signal_integrity/sparams.py +3 -3
- oscura/analyzers/statistics/basic.py +10 -7
- oscura/analyzers/validation.py +1 -1
- oscura/analyzers/waveform/measurements.py +200 -156
- oscura/analyzers/waveform/measurements_with_uncertainty.py +91 -35
- oscura/analyzers/waveform/spectral.py +164 -73
- oscura/api/dsl/commands.py +15 -6
- oscura/api/server/templates/base.html +137 -146
- oscura/api/server/templates/export.html +84 -110
- oscura/api/server/templates/home.html +248 -267
- oscura/api/server/templates/protocols.html +44 -48
- oscura/api/server/templates/reports.html +27 -35
- oscura/api/server/templates/session_detail.html +68 -78
- oscura/api/server/templates/sessions.html +62 -72
- oscura/api/server/templates/waveforms.html +54 -64
- oscura/automotive/__init__.py +1 -1
- oscura/automotive/can/session.py +1 -1
- oscura/automotive/dbc/generator.py +638 -23
- oscura/automotive/uds/decoder.py +99 -6
- oscura/cli/analyze.py +8 -2
- oscura/cli/batch.py +36 -5
- oscura/cli/characterize.py +18 -4
- oscura/cli/export.py +47 -5
- oscura/cli/main.py +2 -0
- oscura/cli/onboarding/wizard.py +10 -6
- oscura/cli/pipeline.py +585 -0
- oscura/cli/visualize.py +6 -4
- oscura/convenience.py +400 -32
- oscura/core/measurement_result.py +286 -0
- oscura/core/progress.py +1 -1
- oscura/core/types.py +232 -239
- oscura/correlation/multi_protocol.py +1 -1
- oscura/export/legacy/__init__.py +11 -0
- oscura/export/legacy/wav.py +75 -0
- oscura/exporters/__init__.py +19 -0
- oscura/exporters/wireshark.py +809 -0
- oscura/hardware/acquisition/file.py +5 -19
- oscura/hardware/acquisition/saleae.py +10 -10
- oscura/hardware/acquisition/socketcan.py +4 -6
- oscura/hardware/acquisition/synthetic.py +1 -5
- oscura/hardware/acquisition/visa.py +6 -6
- oscura/hardware/security/side_channel_detector.py +5 -508
- oscura/inference/message_format.py +686 -1
- oscura/jupyter/display.py +2 -2
- oscura/jupyter/magic.py +3 -3
- oscura/loaders/__init__.py +17 -12
- oscura/loaders/binary.py +1 -1
- oscura/loaders/chipwhisperer.py +1 -2
- oscura/loaders/configurable.py +1 -1
- oscura/loaders/csv_loader.py +2 -2
- oscura/loaders/hdf5_loader.py +1 -1
- oscura/loaders/lazy.py +6 -1
- oscura/loaders/mmap_loader.py +0 -1
- oscura/loaders/numpy_loader.py +8 -7
- oscura/loaders/preprocessing.py +3 -5
- oscura/loaders/rigol.py +21 -7
- oscura/loaders/sigrok.py +2 -5
- oscura/loaders/tdms.py +3 -2
- oscura/loaders/tektronix.py +38 -32
- oscura/loaders/tss.py +20 -27
- oscura/loaders/vcd.py +13 -8
- oscura/loaders/wav.py +1 -6
- oscura/pipeline/__init__.py +76 -0
- oscura/pipeline/handlers/__init__.py +165 -0
- oscura/pipeline/handlers/analyzers.py +1045 -0
- oscura/pipeline/handlers/decoders.py +899 -0
- oscura/pipeline/handlers/exporters.py +1103 -0
- oscura/pipeline/handlers/filters.py +891 -0
- oscura/pipeline/handlers/loaders.py +640 -0
- oscura/pipeline/handlers/transforms.py +768 -0
- oscura/reporting/formatting/measurements.py +55 -14
- oscura/reporting/templates/enhanced/protocol_re.html +504 -503
- oscura/side_channel/__init__.py +38 -57
- oscura/utils/builders/signal_builder.py +5 -5
- oscura/utils/comparison/compare.py +7 -9
- oscura/utils/comparison/golden.py +1 -1
- oscura/utils/filtering/convenience.py +2 -2
- oscura/utils/math/arithmetic.py +38 -62
- oscura/utils/math/interpolation.py +20 -20
- oscura/utils/pipeline/__init__.py +4 -17
- oscura/utils/progressive.py +1 -4
- oscura/utils/triggering/edge.py +1 -1
- oscura/utils/triggering/pattern.py +2 -2
- oscura/utils/triggering/pulse.py +2 -2
- oscura/utils/triggering/window.py +3 -3
- oscura/validation/hil_testing.py +11 -11
- oscura/visualization/__init__.py +46 -284
- oscura/visualization/batch.py +72 -433
- oscura/visualization/plot.py +542 -53
- oscura/visualization/styles.py +184 -318
- oscura/workflows/batch/advanced.py +1 -1
- oscura/workflows/batch/aggregate.py +7 -8
- oscura/workflows/complete_re.py +251 -23
- oscura/workflows/digital.py +27 -4
- oscura/workflows/multi_trace.py +136 -17
- oscura/workflows/waveform.py +11 -6
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/METADATA +59 -79
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/RECORD +111 -136
- oscura/side_channel/dpa.py +0 -1025
- oscura/utils/optimization/__init__.py +0 -19
- oscura/utils/optimization/parallel.py +0 -443
- oscura/utils/optimization/search.py +0 -532
- oscura/utils/pipeline/base.py +0 -338
- oscura/utils/pipeline/composition.py +0 -248
- oscura/utils/pipeline/parallel.py +0 -449
- oscura/utils/pipeline/pipeline.py +0 -375
- oscura/utils/search/__init__.py +0 -16
- oscura/utils/search/anomaly.py +0 -424
- oscura/utils/search/context.py +0 -294
- oscura/utils/search/pattern.py +0 -288
- oscura/utils/storage/__init__.py +0 -61
- oscura/utils/storage/database.py +0 -1166
- oscura/visualization/accessibility.py +0 -526
- oscura/visualization/annotations.py +0 -371
- oscura/visualization/axis_scaling.py +0 -305
- oscura/visualization/colors.py +0 -451
- oscura/visualization/digital.py +0 -436
- oscura/visualization/eye.py +0 -571
- oscura/visualization/histogram.py +0 -281
- oscura/visualization/interactive.py +0 -1035
- oscura/visualization/jitter.py +0 -1042
- oscura/visualization/keyboard.py +0 -394
- oscura/visualization/layout.py +0 -400
- oscura/visualization/optimization.py +0 -1079
- oscura/visualization/palettes.py +0 -446
- oscura/visualization/power.py +0 -508
- oscura/visualization/power_extended.py +0 -955
- oscura/visualization/presets.py +0 -469
- oscura/visualization/protocols.py +0 -1246
- oscura/visualization/render.py +0 -223
- oscura/visualization/rendering.py +0 -444
- oscura/visualization/reverse_engineering.py +0 -838
- oscura/visualization/signal_integrity.py +0 -989
- oscura/visualization/specialized.py +0 -643
- oscura/visualization/spectral.py +0 -1226
- oscura/visualization/thumbnails.py +0 -340
- oscura/visualization/time_axis.py +0 -351
- oscura/visualization/waveform.py +0 -454
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.10.0.dist-info}/licenses/LICENSE +0 -0
oscura/cli/pipeline.py
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"""Pipeline CLI command group for Oscura.
|
|
2
|
+
|
|
3
|
+
Provides command-line interface for pipeline operations including execution,
|
|
4
|
+
validation, and handler inspection.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
$ oscura pipeline run analysis.yaml
|
|
8
|
+
$ oscura pipeline run analysis.yaml --var input_file=trace.wfm --var baud_rate=115200
|
|
9
|
+
$ oscura pipeline validate analysis.yaml
|
|
10
|
+
$ oscura pipeline handlers
|
|
11
|
+
$ oscura pipeline handler-info decoder.uart
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
from oscura.pipeline import (
|
|
24
|
+
Pipeline,
|
|
25
|
+
PipelineExecutionError,
|
|
26
|
+
PipelineValidationError,
|
|
27
|
+
register_all_handlers,
|
|
28
|
+
)
|
|
29
|
+
from oscura.pipeline.handlers import get_all_handlers, get_handler, list_handler_types
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from collections.abc import Callable
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("oscura.cli.pipeline")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_variables(var_tuples: tuple[str, ...]) -> dict[str, Any]:
|
|
38
|
+
"""Parse --var key=value arguments into dictionary.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
var_tuples: Tuple of "key=value" strings
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary of parsed variables
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
click.BadParameter: If variable format is invalid
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> _parse_variables(("input_file=trace.wfm", "baud_rate=115200"))
|
|
51
|
+
{"input_file": "trace.wfm", "baud_rate": 115200}
|
|
52
|
+
"""
|
|
53
|
+
variables: dict[str, Any] = {}
|
|
54
|
+
for var_str in var_tuples:
|
|
55
|
+
if "=" not in var_str:
|
|
56
|
+
raise click.BadParameter(f"Invalid variable format: '{var_str}'. Expected 'key=value'.")
|
|
57
|
+
|
|
58
|
+
key, value = var_str.split("=", 1)
|
|
59
|
+
key = key.strip()
|
|
60
|
+
value = value.strip()
|
|
61
|
+
|
|
62
|
+
# Try to parse as int, float, bool, or keep as string
|
|
63
|
+
parsed_value: Any
|
|
64
|
+
if value.lower() in ("true", "yes", "on"):
|
|
65
|
+
parsed_value = True
|
|
66
|
+
elif value.lower() in ("false", "no", "off"):
|
|
67
|
+
parsed_value = False
|
|
68
|
+
elif value.isdigit():
|
|
69
|
+
parsed_value = int(value)
|
|
70
|
+
else:
|
|
71
|
+
try:
|
|
72
|
+
parsed_value = float(value)
|
|
73
|
+
except ValueError:
|
|
74
|
+
parsed_value = value
|
|
75
|
+
|
|
76
|
+
variables[key] = parsed_value
|
|
77
|
+
|
|
78
|
+
return variables
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _create_progress_callback(verbose: bool) -> Callable[[str, int], None]:
|
|
82
|
+
"""Create progress callback for pipeline execution.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
verbose: Whether to show detailed progress
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Progress callback function
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def progress_callback(step_name: str, percent: int) -> None:
|
|
92
|
+
"""Report pipeline execution progress.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
step_name: Name of current step
|
|
96
|
+
percent: Completion percentage (0-100)
|
|
97
|
+
"""
|
|
98
|
+
if verbose:
|
|
99
|
+
click.echo(f"[{percent:3d}%] Executing step: {step_name}")
|
|
100
|
+
|
|
101
|
+
return progress_callback
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_handler_info(handler_type: str, handler_func: Callable[..., Any]) -> dict[str, Any]:
|
|
105
|
+
"""Extract handler information from function.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
handler_type: Handler type identifier (e.g., "input.file")
|
|
109
|
+
handler_func: Handler function
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Dictionary with handler metadata
|
|
113
|
+
"""
|
|
114
|
+
# Parse docstring to extract metadata
|
|
115
|
+
docstring = handler_func.__doc__ or ""
|
|
116
|
+
lines = docstring.strip().split("\n")
|
|
117
|
+
|
|
118
|
+
# Extract description (first line or paragraph)
|
|
119
|
+
description = ""
|
|
120
|
+
params_section = []
|
|
121
|
+
outputs_section = []
|
|
122
|
+
current_section = None
|
|
123
|
+
|
|
124
|
+
for line in lines:
|
|
125
|
+
line = line.strip()
|
|
126
|
+
if line.lower().startswith("parameters:"):
|
|
127
|
+
current_section = "params"
|
|
128
|
+
continue
|
|
129
|
+
elif line.lower().startswith("outputs:"):
|
|
130
|
+
current_section = "outputs"
|
|
131
|
+
continue
|
|
132
|
+
elif line.lower().startswith("inputs:"):
|
|
133
|
+
current_section = "inputs"
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if current_section == "params" and line:
|
|
137
|
+
params_section.append(line)
|
|
138
|
+
elif current_section == "outputs" and line:
|
|
139
|
+
outputs_section.append(line)
|
|
140
|
+
elif not description and line and current_section is None:
|
|
141
|
+
description = line
|
|
142
|
+
|
|
143
|
+
# Determine category from handler type
|
|
144
|
+
category = handler_type.split(".")[0] if "." in handler_type else "unknown"
|
|
145
|
+
|
|
146
|
+
result: dict[str, Any] = {
|
|
147
|
+
"type": handler_type,
|
|
148
|
+
"category": category,
|
|
149
|
+
"description": description,
|
|
150
|
+
"parameters": params_section,
|
|
151
|
+
"outputs": outputs_section,
|
|
152
|
+
}
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@click.group()
|
|
157
|
+
def pipeline() -> None:
|
|
158
|
+
"""Pipeline operations for declarative workflows.
|
|
159
|
+
|
|
160
|
+
Execute, validate, and inspect YAML-based analysis pipelines with
|
|
161
|
+
60+ built-in handlers for common operations.
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
oscura pipeline run analysis.yaml
|
|
165
|
+
oscura pipeline validate analysis.yaml
|
|
166
|
+
oscura pipeline handlers
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pipeline.command()
|
|
171
|
+
@click.argument("yaml_file", type=click.Path(exists=True))
|
|
172
|
+
@click.option(
|
|
173
|
+
"--var",
|
|
174
|
+
multiple=True,
|
|
175
|
+
help="Set template variable (format: key=value). Can be used multiple times.",
|
|
176
|
+
)
|
|
177
|
+
@click.option(
|
|
178
|
+
"--dry-run",
|
|
179
|
+
is_flag=True,
|
|
180
|
+
help="Validate pipeline without executing.",
|
|
181
|
+
)
|
|
182
|
+
@click.option(
|
|
183
|
+
"--json",
|
|
184
|
+
"json_output",
|
|
185
|
+
is_flag=True,
|
|
186
|
+
help="Output results as JSON.",
|
|
187
|
+
)
|
|
188
|
+
@click.option(
|
|
189
|
+
"--verbose",
|
|
190
|
+
"-v",
|
|
191
|
+
is_flag=True,
|
|
192
|
+
help="Enable verbose logging and progress reporting.",
|
|
193
|
+
)
|
|
194
|
+
@click.pass_context
|
|
195
|
+
def run(
|
|
196
|
+
ctx: click.Context,
|
|
197
|
+
yaml_file: str,
|
|
198
|
+
var: tuple[str, ...],
|
|
199
|
+
dry_run: bool,
|
|
200
|
+
json_output: bool,
|
|
201
|
+
verbose: bool,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Execute a pipeline from YAML file.
|
|
204
|
+
|
|
205
|
+
Loads and executes the specified pipeline with optional template variables.
|
|
206
|
+
Displays progress during execution and reports results.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
ctx: Click context object.
|
|
210
|
+
yaml_file: Path to pipeline YAML file.
|
|
211
|
+
var: Template variables (key=value pairs).
|
|
212
|
+
dry_run: If True, validate only without executing.
|
|
213
|
+
json_output: If True, output results as JSON.
|
|
214
|
+
verbose: If True, enable detailed progress reporting.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
oscura pipeline run analysis.yaml
|
|
218
|
+
oscura pipeline run analysis.yaml --var input_file=trace.wfm --var baud_rate=115200
|
|
219
|
+
oscura pipeline run analysis.yaml --dry-run
|
|
220
|
+
oscura pipeline run analysis.yaml --json
|
|
221
|
+
"""
|
|
222
|
+
# Set logging level
|
|
223
|
+
if verbose:
|
|
224
|
+
logging.basicConfig(level=logging.INFO)
|
|
225
|
+
logger.setLevel(logging.INFO)
|
|
226
|
+
|
|
227
|
+
# Parse variables
|
|
228
|
+
variables = _parse_variables(var)
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
# Load pipeline
|
|
232
|
+
if verbose:
|
|
233
|
+
click.echo(f"Loading pipeline: {yaml_file}")
|
|
234
|
+
pipeline_obj = Pipeline.load(yaml_file, variables=variables)
|
|
235
|
+
|
|
236
|
+
# Register all handlers
|
|
237
|
+
register_all_handlers(pipeline_obj)
|
|
238
|
+
|
|
239
|
+
# Dry-run mode: validate only
|
|
240
|
+
if dry_run:
|
|
241
|
+
if verbose:
|
|
242
|
+
click.echo("Validating pipeline (dry-run mode)...")
|
|
243
|
+
# Validation happens during load
|
|
244
|
+
if json_output:
|
|
245
|
+
result = {
|
|
246
|
+
"status": "valid",
|
|
247
|
+
"pipeline": pipeline_obj.definition.name,
|
|
248
|
+
"steps": len(pipeline_obj.definition.steps),
|
|
249
|
+
}
|
|
250
|
+
click.echo(json.dumps(result, indent=2))
|
|
251
|
+
else:
|
|
252
|
+
click.echo(f"Pipeline '{pipeline_obj.definition.name}' is valid.")
|
|
253
|
+
click.echo(f"Steps: {len(pipeline_obj.definition.steps)}")
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
# Execute pipeline
|
|
257
|
+
if verbose:
|
|
258
|
+
click.echo(f"Executing pipeline: {pipeline_obj.definition.name}")
|
|
259
|
+
pipeline_obj.on_progress(_create_progress_callback(verbose))
|
|
260
|
+
|
|
261
|
+
results = pipeline_obj.execute()
|
|
262
|
+
|
|
263
|
+
# Display results
|
|
264
|
+
if json_output:
|
|
265
|
+
output = {
|
|
266
|
+
"status": "success" if results.success else "error",
|
|
267
|
+
"pipeline": results.pipeline_name,
|
|
268
|
+
"steps_executed": len(results.step_results),
|
|
269
|
+
"outputs": {
|
|
270
|
+
key: str(value)[:100]
|
|
271
|
+
if not isinstance(value, (int, float, bool, str))
|
|
272
|
+
else value
|
|
273
|
+
for key, value in results.outputs.items()
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
if results.error:
|
|
277
|
+
output["error"] = results.error
|
|
278
|
+
click.echo(json.dumps(output, indent=2))
|
|
279
|
+
else:
|
|
280
|
+
click.echo(f"\nPipeline '{results.pipeline_name}' completed successfully.")
|
|
281
|
+
click.echo(f"Steps executed: {len(results.step_results)}")
|
|
282
|
+
if results.outputs:
|
|
283
|
+
click.echo("\nOutputs:")
|
|
284
|
+
for key, value in results.outputs.items():
|
|
285
|
+
value_str = str(value)
|
|
286
|
+
if len(value_str) > 80:
|
|
287
|
+
value_str = value_str[:77] + "..."
|
|
288
|
+
click.echo(f" {key}: {value_str}")
|
|
289
|
+
|
|
290
|
+
except PipelineValidationError as e:
|
|
291
|
+
if json_output:
|
|
292
|
+
error: dict[str, Any] = {
|
|
293
|
+
"status": "validation_error",
|
|
294
|
+
"message": str(e),
|
|
295
|
+
}
|
|
296
|
+
if e.step_name is not None:
|
|
297
|
+
error["step"] = e.step_name
|
|
298
|
+
if e.suggestion is not None:
|
|
299
|
+
error["suggestion"] = e.suggestion
|
|
300
|
+
click.echo(json.dumps(error, indent=2))
|
|
301
|
+
else:
|
|
302
|
+
click.echo(f"Validation error: {e}", err=True)
|
|
303
|
+
if e.step_name:
|
|
304
|
+
click.echo(f" Step: {e.step_name}", err=True)
|
|
305
|
+
if e.suggestion:
|
|
306
|
+
click.echo(f" Suggestion: {e.suggestion}", err=True)
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
|
|
309
|
+
except PipelineExecutionError as e:
|
|
310
|
+
if json_output:
|
|
311
|
+
error = {
|
|
312
|
+
"status": "execution_error",
|
|
313
|
+
"message": str(e),
|
|
314
|
+
"step": e.step_name,
|
|
315
|
+
}
|
|
316
|
+
if e.traceback_str:
|
|
317
|
+
error["traceback"] = e.traceback_str
|
|
318
|
+
click.echo(json.dumps(error, indent=2))
|
|
319
|
+
else:
|
|
320
|
+
click.echo(f"Execution error: {e}", err=True)
|
|
321
|
+
if e.step_name:
|
|
322
|
+
click.echo(f" Step: {e.step_name}", err=True)
|
|
323
|
+
if e.traceback_str and verbose:
|
|
324
|
+
click.echo(f"\nTraceback:\n{e.traceback_str}", err=True)
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
327
|
+
except FileNotFoundError as e:
|
|
328
|
+
if json_output:
|
|
329
|
+
error = {"status": "file_not_found", "message": str(e)}
|
|
330
|
+
click.echo(json.dumps(error, indent=2))
|
|
331
|
+
else:
|
|
332
|
+
click.echo(f"File not found: {e}", err=True)
|
|
333
|
+
sys.exit(1)
|
|
334
|
+
|
|
335
|
+
except Exception as e:
|
|
336
|
+
if json_output:
|
|
337
|
+
error = {"status": "error", "message": str(e), "type": type(e).__name__}
|
|
338
|
+
click.echo(json.dumps(error, indent=2))
|
|
339
|
+
else:
|
|
340
|
+
click.echo(f"Unexpected error: {e}", err=True)
|
|
341
|
+
if verbose:
|
|
342
|
+
import traceback
|
|
343
|
+
|
|
344
|
+
click.echo(traceback.format_exc(), err=True)
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@pipeline.command()
|
|
349
|
+
@click.argument("yaml_file", type=click.Path(exists=True))
|
|
350
|
+
@click.option(
|
|
351
|
+
"--var",
|
|
352
|
+
multiple=True,
|
|
353
|
+
help="Set template variable (format: key=value). Can be used multiple times.",
|
|
354
|
+
)
|
|
355
|
+
@click.option(
|
|
356
|
+
"--json",
|
|
357
|
+
"json_output",
|
|
358
|
+
is_flag=True,
|
|
359
|
+
help="Output results as JSON.",
|
|
360
|
+
)
|
|
361
|
+
def validate(
|
|
362
|
+
yaml_file: str,
|
|
363
|
+
var: tuple[str, ...],
|
|
364
|
+
json_output: bool,
|
|
365
|
+
) -> None:
|
|
366
|
+
"""Validate pipeline without executing (dry-run).
|
|
367
|
+
|
|
368
|
+
Checks pipeline syntax, step dependencies, and handler availability
|
|
369
|
+
without executing any steps.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
yaml_file: Path to pipeline YAML file.
|
|
373
|
+
var: Template variables (key=value pairs).
|
|
374
|
+
json_output: If True, output results as JSON.
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
oscura pipeline validate analysis.yaml
|
|
378
|
+
oscura pipeline validate analysis.yaml --var input_file=trace.wfm
|
|
379
|
+
oscura pipeline validate analysis.yaml --json
|
|
380
|
+
"""
|
|
381
|
+
# Parse variables
|
|
382
|
+
variables = _parse_variables(var)
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
# Load pipeline (validation happens during load)
|
|
386
|
+
pipeline_obj = Pipeline.load(yaml_file, variables=variables)
|
|
387
|
+
|
|
388
|
+
# Register handlers to check availability
|
|
389
|
+
register_all_handlers(pipeline_obj)
|
|
390
|
+
|
|
391
|
+
# Check all steps have handlers
|
|
392
|
+
missing_handlers = []
|
|
393
|
+
for step in pipeline_obj.definition.steps:
|
|
394
|
+
if get_handler(step.type) is None:
|
|
395
|
+
missing_handlers.append(step.type)
|
|
396
|
+
|
|
397
|
+
if missing_handlers:
|
|
398
|
+
if json_output:
|
|
399
|
+
error_data: dict[str, Any] = {
|
|
400
|
+
"status": "validation_error",
|
|
401
|
+
"message": f"Missing handlers for step types: {', '.join(missing_handlers)}",
|
|
402
|
+
"missing_handlers": missing_handlers,
|
|
403
|
+
}
|
|
404
|
+
click.echo(json.dumps(error_data, indent=2))
|
|
405
|
+
else:
|
|
406
|
+
click.echo("Validation error: Missing handlers", err=True)
|
|
407
|
+
click.echo(f" Missing: {', '.join(missing_handlers)}", err=True)
|
|
408
|
+
click.echo("\nRun 'oscura pipeline handlers' to see available handlers.", err=True)
|
|
409
|
+
sys.exit(1)
|
|
410
|
+
|
|
411
|
+
# Success
|
|
412
|
+
if json_output:
|
|
413
|
+
result = {
|
|
414
|
+
"status": "valid",
|
|
415
|
+
"pipeline": pipeline_obj.definition.name,
|
|
416
|
+
"version": pipeline_obj.definition.version,
|
|
417
|
+
"steps": len(pipeline_obj.definition.steps),
|
|
418
|
+
"description": pipeline_obj.definition.description,
|
|
419
|
+
}
|
|
420
|
+
click.echo(json.dumps(result, indent=2))
|
|
421
|
+
else:
|
|
422
|
+
click.echo(f"Pipeline '{pipeline_obj.definition.name}' is valid.")
|
|
423
|
+
click.echo(f" Version: {pipeline_obj.definition.version}")
|
|
424
|
+
click.echo(f" Steps: {len(pipeline_obj.definition.steps)}")
|
|
425
|
+
if pipeline_obj.definition.description:
|
|
426
|
+
click.echo(f" Description: {pipeline_obj.definition.description}")
|
|
427
|
+
|
|
428
|
+
except PipelineValidationError as e:
|
|
429
|
+
if json_output:
|
|
430
|
+
error_info: dict[str, Any] = {
|
|
431
|
+
"status": "validation_error",
|
|
432
|
+
"message": str(e),
|
|
433
|
+
}
|
|
434
|
+
if e.step_name is not None:
|
|
435
|
+
error_info["step"] = e.step_name
|
|
436
|
+
if e.suggestion is not None:
|
|
437
|
+
error_info["suggestion"] = e.suggestion
|
|
438
|
+
click.echo(json.dumps(error_info, indent=2))
|
|
439
|
+
else:
|
|
440
|
+
click.echo(f"Validation error: {e}", err=True)
|
|
441
|
+
if e.step_name:
|
|
442
|
+
click.echo(f" Step: {e.step_name}", err=True)
|
|
443
|
+
if e.suggestion:
|
|
444
|
+
click.echo(f" Suggestion: {e.suggestion}", err=True)
|
|
445
|
+
sys.exit(1)
|
|
446
|
+
|
|
447
|
+
except FileNotFoundError as e:
|
|
448
|
+
if json_output:
|
|
449
|
+
error_msg: dict[str, Any] = {"status": "file_not_found", "message": str(e)}
|
|
450
|
+
click.echo(json.dumps(error_msg, indent=2))
|
|
451
|
+
else:
|
|
452
|
+
click.echo(f"File not found: {e}", err=True)
|
|
453
|
+
sys.exit(1)
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
if json_output:
|
|
457
|
+
error_obj: dict[str, Any] = {
|
|
458
|
+
"status": "error",
|
|
459
|
+
"message": str(e),
|
|
460
|
+
"type": type(e).__name__,
|
|
461
|
+
}
|
|
462
|
+
click.echo(json.dumps(error_obj, indent=2))
|
|
463
|
+
else:
|
|
464
|
+
click.echo(f"Unexpected error: {e}", err=True)
|
|
465
|
+
sys.exit(1)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@pipeline.command()
|
|
469
|
+
@click.option(
|
|
470
|
+
"--json",
|
|
471
|
+
"json_output",
|
|
472
|
+
is_flag=True,
|
|
473
|
+
help="Output results as JSON.",
|
|
474
|
+
)
|
|
475
|
+
def handlers(json_output: bool) -> None:
|
|
476
|
+
"""List all registered pipeline handlers.
|
|
477
|
+
|
|
478
|
+
Displays all available handler types organized by category
|
|
479
|
+
(input, decoder, analysis, filter, transform, output).
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
json_output: If True, output results as JSON.
|
|
483
|
+
|
|
484
|
+
Example:
|
|
485
|
+
oscura pipeline handlers
|
|
486
|
+
oscura pipeline handlers --json
|
|
487
|
+
"""
|
|
488
|
+
# Get all handlers
|
|
489
|
+
all_handlers = get_all_handlers()
|
|
490
|
+
|
|
491
|
+
if not all_handlers:
|
|
492
|
+
if json_output:
|
|
493
|
+
click.echo(json.dumps({"handlers": [], "total": 0}))
|
|
494
|
+
else:
|
|
495
|
+
click.echo("No handlers registered.")
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
# Group by category
|
|
499
|
+
categories: dict[str, list[str]] = {}
|
|
500
|
+
for handler_type in sorted(all_handlers.keys()):
|
|
501
|
+
category = handler_type.split(".")[0] if "." in handler_type else "other"
|
|
502
|
+
if category not in categories:
|
|
503
|
+
categories[category] = []
|
|
504
|
+
categories[category].append(handler_type)
|
|
505
|
+
|
|
506
|
+
# Output results
|
|
507
|
+
if json_output:
|
|
508
|
+
result = {
|
|
509
|
+
"handlers": sorted(all_handlers.keys()),
|
|
510
|
+
"categories": categories,
|
|
511
|
+
"total": len(all_handlers),
|
|
512
|
+
"by_category": {cat: len(handlers) for cat, handlers in categories.items()},
|
|
513
|
+
}
|
|
514
|
+
click.echo(json.dumps(result, indent=2))
|
|
515
|
+
else:
|
|
516
|
+
click.echo(f"Registered Pipeline Handlers ({len(all_handlers)} total):\n")
|
|
517
|
+
for category in sorted(categories.keys()):
|
|
518
|
+
handlers = categories[category]
|
|
519
|
+
click.echo(f"{category.upper()} ({len(handlers)}):")
|
|
520
|
+
for handler_type in handlers:
|
|
521
|
+
click.echo(f" - {handler_type}")
|
|
522
|
+
click.echo()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
@pipeline.command()
|
|
526
|
+
@click.argument("handler_type")
|
|
527
|
+
@click.option(
|
|
528
|
+
"--json",
|
|
529
|
+
"json_output",
|
|
530
|
+
is_flag=True,
|
|
531
|
+
help="Output results as JSON.",
|
|
532
|
+
)
|
|
533
|
+
def handler_info(handler_type: str, json_output: bool) -> None:
|
|
534
|
+
"""Show detailed information about a handler.
|
|
535
|
+
|
|
536
|
+
Displays handler documentation, parameters, inputs, and outputs.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
handler_type: Handler type identifier (e.g., "decoder.uart").
|
|
540
|
+
json_output: If True, output results as JSON.
|
|
541
|
+
|
|
542
|
+
Example:
|
|
543
|
+
oscura pipeline handler-info decoder.uart
|
|
544
|
+
oscura pipeline handler-info input.file --json
|
|
545
|
+
"""
|
|
546
|
+
handler = get_handler(handler_type)
|
|
547
|
+
|
|
548
|
+
if handler is None:
|
|
549
|
+
if json_output:
|
|
550
|
+
error = {
|
|
551
|
+
"status": "not_found",
|
|
552
|
+
"message": f"Handler '{handler_type}' not found",
|
|
553
|
+
"available": list_handler_types(),
|
|
554
|
+
}
|
|
555
|
+
click.echo(json.dumps(error, indent=2))
|
|
556
|
+
else:
|
|
557
|
+
click.echo(f"Handler '{handler_type}' not found.", err=True)
|
|
558
|
+
click.echo("\nAvailable handlers:", err=True)
|
|
559
|
+
for h in list_handler_types()[:5]:
|
|
560
|
+
click.echo(f" - {h}", err=True)
|
|
561
|
+
if len(list_handler_types()) > 5:
|
|
562
|
+
click.echo(f" ... and {len(list_handler_types()) - 5} more", err=True)
|
|
563
|
+
click.echo("\nRun 'oscura pipeline handlers' for full list.", err=True)
|
|
564
|
+
sys.exit(1)
|
|
565
|
+
|
|
566
|
+
# Extract handler information
|
|
567
|
+
info = _format_handler_info(handler_type, handler)
|
|
568
|
+
|
|
569
|
+
if json_output:
|
|
570
|
+
click.echo(json.dumps(info, indent=2))
|
|
571
|
+
else:
|
|
572
|
+
click.echo(f"Handler: {info['type']}")
|
|
573
|
+
click.echo(f"Category: {info['category']}")
|
|
574
|
+
if info["description"]:
|
|
575
|
+
click.echo(f"\nDescription:\n {info['description']}")
|
|
576
|
+
|
|
577
|
+
if info["parameters"]:
|
|
578
|
+
click.echo("\nParameters:")
|
|
579
|
+
for param in info["parameters"]:
|
|
580
|
+
click.echo(f" {param}")
|
|
581
|
+
|
|
582
|
+
if info["outputs"]:
|
|
583
|
+
click.echo("\nOutputs:")
|
|
584
|
+
for output in info["outputs"]:
|
|
585
|
+
click.echo(f" {output}")
|
oscura/cli/visualize.py
CHANGED
|
@@ -90,10 +90,12 @@ def visualize(
|
|
|
90
90
|
|
|
91
91
|
# Plot waveform (handle IQTrace separately)
|
|
92
92
|
if isinstance(trace, IQTrace):
|
|
93
|
-
# Plot I/Q components
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
# Plot I/Q components (extract from complex data)
|
|
94
|
+
i_data = trace.data.real
|
|
95
|
+
q_data = trace.data.imag
|
|
96
|
+
time_axis = np.arange(len(trace.data)) / trace.metadata.sample_rate
|
|
97
|
+
ax.plot(time_axis * 1e3, i_data, linewidth=0.5, label="I")
|
|
98
|
+
ax.plot(time_axis * 1e3, q_data, linewidth=0.5, label="Q")
|
|
97
99
|
ax.legend()
|
|
98
100
|
else:
|
|
99
101
|
# Plot regular waveform or digital trace
|