oscura 0.8.0__py3-none-any.whl → 0.11.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/__main__.py +4 -0
- 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/ml/signal_classifier.py +6 -0
- 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 +182 -84
- 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/dtc/data.json +17 -102
- oscura/automotive/flexray/fibex.py +9 -1
- 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/schemas/device_mapping.json +2 -8
- oscura/core/schemas/packet_format.json +4 -24
- oscura/core/schemas/protocol_definition.json +2 -12
- 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/validation.py +17 -10
- 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/sessions/legacy.py +49 -1
- 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 +12 -9
- 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.11.0.dist-info/METADATA +460 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/RECORD +120 -145
- 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/METADATA +0 -661
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/WHEEL +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.8.0.dist-info → oscura-0.11.0.dist-info}/licenses/LICENSE +0 -0
oscura/loaders/tss.py
CHANGED
|
@@ -13,7 +13,7 @@ A .tss session file typically contains:
|
|
|
13
13
|
Example:
|
|
14
14
|
>>> import oscura as osc
|
|
15
15
|
>>> trace = osc.load("oscilloscope_session.tss")
|
|
16
|
-
>>> print(f"Channel: {trace.metadata.
|
|
16
|
+
>>> print(f"Channel: {trace.metadata.channel}")
|
|
17
17
|
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
18
18
|
|
|
19
19
|
>>> # Load specific channel
|
|
@@ -93,7 +93,7 @@ def load_tss(
|
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
# Select channel
|
|
96
|
-
trace,
|
|
96
|
+
trace, channel = _select_channel(waveforms, channel, path)
|
|
97
97
|
|
|
98
98
|
# Enrich metadata with session information
|
|
99
99
|
try:
|
|
@@ -317,18 +317,18 @@ def _load_all_waveforms(path: Path) -> dict[str, WaveformTrace | DigitalTrace |
|
|
|
317
317
|
|
|
318
318
|
for wfm_name in sorted(wfm_files): # Sort for consistent ordering
|
|
319
319
|
# Derive channel name from filename
|
|
320
|
-
|
|
320
|
+
channel = _derive_channel(wfm_name)
|
|
321
321
|
|
|
322
322
|
# Load waveform
|
|
323
323
|
trace = _load_wfm_from_archive(zf, wfm_name, path)
|
|
324
324
|
|
|
325
325
|
# Store with normalized channel name
|
|
326
|
-
waveforms[
|
|
326
|
+
waveforms[channel] = trace
|
|
327
327
|
|
|
328
328
|
return waveforms
|
|
329
329
|
|
|
330
330
|
|
|
331
|
-
def
|
|
331
|
+
def _derive_channel(wfm_filename: str) -> str:
|
|
332
332
|
"""Derive channel name from .wfm filename.
|
|
333
333
|
|
|
334
334
|
Args:
|
|
@@ -338,13 +338,13 @@ def _derive_channel_name(wfm_filename: str) -> str:
|
|
|
338
338
|
Normalized channel name (lowercase, e.g., "ch1", "ch2", "d0").
|
|
339
339
|
|
|
340
340
|
Examples:
|
|
341
|
-
>>>
|
|
341
|
+
>>> _derive_channel("CH1.wfm")
|
|
342
342
|
'ch1'
|
|
343
|
-
>>>
|
|
343
|
+
>>> _derive_channel("subdir/CH2_Voltage.wfm")
|
|
344
344
|
'ch2'
|
|
345
|
-
>>>
|
|
345
|
+
>>> _derive_channel("D0.wfm")
|
|
346
346
|
'd0'
|
|
347
|
-
>>>
|
|
347
|
+
>>> _derive_channel("MATH1.wfm")
|
|
348
348
|
'math1'
|
|
349
349
|
"""
|
|
350
350
|
# Get base filename without path
|
|
@@ -373,27 +373,27 @@ def _select_channel(
|
|
|
373
373
|
path: Path to session file (for error messages).
|
|
374
374
|
|
|
375
375
|
Returns:
|
|
376
|
-
Tuple of (selected_trace,
|
|
376
|
+
Tuple of (selected_trace, channel).
|
|
377
377
|
|
|
378
378
|
Raises:
|
|
379
379
|
LoaderError: If channel not found or index out of range.
|
|
380
380
|
"""
|
|
381
381
|
if channel is None:
|
|
382
382
|
# Default: first channel (alphabetically sorted)
|
|
383
|
-
|
|
384
|
-
return waveforms[
|
|
383
|
+
channel = sorted(waveforms.keys())[0]
|
|
384
|
+
return waveforms[channel], channel
|
|
385
385
|
|
|
386
386
|
if isinstance(channel, int):
|
|
387
387
|
# Select by index
|
|
388
|
-
|
|
389
|
-
if channel < 0 or channel >= len(
|
|
388
|
+
channels = sorted(waveforms.keys())
|
|
389
|
+
if channel < 0 or channel >= len(channels):
|
|
390
390
|
raise LoaderError(
|
|
391
391
|
f"Channel index {channel} out of range",
|
|
392
392
|
file_path=str(path),
|
|
393
|
-
fix_hint=f"Available channels: {', '.join(
|
|
393
|
+
fix_hint=f"Available channels: {', '.join(channels)} (indices 0-{len(channels) - 1})",
|
|
394
394
|
)
|
|
395
|
-
|
|
396
|
-
return waveforms[
|
|
395
|
+
channel = channels[channel]
|
|
396
|
+
return waveforms[channel], channel
|
|
397
397
|
|
|
398
398
|
# Select by name (case-insensitive)
|
|
399
399
|
channel_lower = channel.lower()
|
|
@@ -426,26 +426,19 @@ def _enrich_metadata_from_session(
|
|
|
426
426
|
Trace with enriched metadata.
|
|
427
427
|
"""
|
|
428
428
|
# Create new metadata with session information
|
|
429
|
-
from dataclasses import replace
|
|
430
429
|
|
|
431
430
|
metadata = trace.metadata
|
|
432
431
|
|
|
433
|
-
#
|
|
434
|
-
metadata = replace(metadata, source_file=source_file)
|
|
435
|
-
|
|
436
|
-
# Add trigger info from session if available
|
|
437
|
-
if "trigger" in session_metadata and metadata.trigger_info is None:
|
|
438
|
-
metadata = replace(metadata, trigger_info=session_metadata["trigger"])
|
|
432
|
+
# Note: source_file and trigger_info attributes removed from TraceMetadata
|
|
439
433
|
|
|
440
434
|
# Return trace with updated metadata
|
|
441
435
|
if isinstance(trace, WaveformTrace):
|
|
442
436
|
return WaveformTrace(data=trace.data, metadata=metadata)
|
|
443
437
|
if isinstance(trace, DigitalTrace):
|
|
444
|
-
return DigitalTrace(data=trace.data, metadata=metadata
|
|
438
|
+
return DigitalTrace(data=trace.data, metadata=metadata)
|
|
445
439
|
# IQTrace
|
|
446
440
|
return IQTrace(
|
|
447
|
-
|
|
448
|
-
q_data=trace.q_data,
|
|
441
|
+
data=trace.data,
|
|
449
442
|
metadata=metadata,
|
|
450
443
|
)
|
|
451
444
|
|
oscura/loaders/validation.py
CHANGED
|
@@ -475,24 +475,31 @@ class PacketValidator:
|
|
|
475
475
|
|
|
476
476
|
@staticmethod
|
|
477
477
|
def _crc32(data: bytes, poly: int = 0xEDB88320) -> int:
|
|
478
|
-
"""Compute CRC-32 checksum.
|
|
478
|
+
"""Compute CRC-32 checksum using native implementation.
|
|
479
479
|
|
|
480
480
|
Args:
|
|
481
481
|
data: Data to checksum.
|
|
482
482
|
poly: CRC polynomial (default: 0xEDB88320 for CRC-32).
|
|
483
|
+
Note: Only standard CRC-32 polynomial is supported by native implementation.
|
|
483
484
|
|
|
484
485
|
Returns:
|
|
485
486
|
CRC-32 value.
|
|
487
|
+
|
|
488
|
+
Note:
|
|
489
|
+
Uses zlib.crc32() for performance (~100x faster than pure Python).
|
|
490
|
+
Custom polynomials are not supported - raises ValueError if non-standard poly provided.
|
|
486
491
|
"""
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
import zlib
|
|
493
|
+
|
|
494
|
+
# Verify standard CRC-32 polynomial (zlib only supports this)
|
|
495
|
+
if poly != 0xEDB88320:
|
|
496
|
+
raise ValueError(
|
|
497
|
+
f"Non-standard CRC polynomial {poly:#x} not supported by native implementation. "
|
|
498
|
+
"Only standard CRC-32 (0xEDB88320) is available."
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# zlib.crc32 returns signed int on some platforms, mask to unsigned
|
|
502
|
+
return zlib.crc32(data) & 0xFFFFFFFF
|
|
496
503
|
|
|
497
504
|
def get_statistics(self) -> ValidationStats:
|
|
498
505
|
"""Get aggregate validation statistics.
|
oscura/loaders/vcd.py
CHANGED
|
@@ -16,7 +16,7 @@ import mmap
|
|
|
16
16
|
import re
|
|
17
17
|
from dataclasses import dataclass, field
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import TYPE_CHECKING
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
from numpy.typing import NDArray
|
|
@@ -130,7 +130,7 @@ def load_vcd(
|
|
|
130
130
|
data, edges = _changes_to_samples(changes, header.timescale, sample_rate)
|
|
131
131
|
metadata = _build_trace_metadata(path, target_var, header, sample_rate)
|
|
132
132
|
|
|
133
|
-
return DigitalTrace(data=data.astype(np.bool_), metadata=metadata
|
|
133
|
+
return DigitalTrace(data=data.astype(np.bool_), metadata=metadata)
|
|
134
134
|
|
|
135
135
|
except UnicodeDecodeError as e:
|
|
136
136
|
raise FormatError(
|
|
@@ -239,15 +239,20 @@ def _build_trace_metadata(
|
|
|
239
239
|
path: Path, target_var: VCDVariable, header: VCDHeader, sample_rate: float
|
|
240
240
|
) -> TraceMetadata:
|
|
241
241
|
"""Build trace metadata from VCD information."""
|
|
242
|
+
# Build trigger_info from VCD header
|
|
243
|
+
trigger_info: dict[str, Any] = {}
|
|
244
|
+
if header.timescale is not None:
|
|
245
|
+
trigger_info["timescale"] = header.timescale
|
|
246
|
+
if header.date:
|
|
247
|
+
trigger_info["date"] = header.date
|
|
248
|
+
if header.version:
|
|
249
|
+
trigger_info["version"] = header.version
|
|
250
|
+
|
|
242
251
|
return TraceMetadata(
|
|
243
252
|
sample_rate=sample_rate,
|
|
253
|
+
channel=target_var.name,
|
|
244
254
|
source_file=str(path),
|
|
245
|
-
|
|
246
|
-
trigger_info={
|
|
247
|
-
"timescale": header.timescale,
|
|
248
|
-
"var_type": target_var.var_type,
|
|
249
|
-
"bit_width": target_var.size,
|
|
250
|
-
},
|
|
255
|
+
trigger_info=trigger_info if trigger_info else None,
|
|
251
256
|
)
|
|
252
257
|
|
|
253
258
|
|
oscura/loaders/wav.py
CHANGED
|
@@ -250,13 +250,8 @@ def load_wav(
|
|
|
250
250
|
# Build metadata
|
|
251
251
|
metadata = TraceMetadata(
|
|
252
252
|
sample_rate=float(sample_rate),
|
|
253
|
+
channel=channel_name,
|
|
253
254
|
source_file=str(path),
|
|
254
|
-
channel_name=channel_name,
|
|
255
|
-
trigger_info={
|
|
256
|
-
"original_dtype": str(data.dtype),
|
|
257
|
-
"n_channels": data.shape[1] if data.ndim == 2 else 1,
|
|
258
|
-
"normalized": normalize,
|
|
259
|
-
},
|
|
260
255
|
)
|
|
261
256
|
|
|
262
257
|
return WaveformTrace(data=audio_data, metadata=metadata)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""YAML-based pipeline system for Oscura.
|
|
2
|
+
|
|
3
|
+
This package provides a complete pipeline execution framework with:
|
|
4
|
+
- YAML configuration for declarative workflows
|
|
5
|
+
- 60+ built-in handlers for common operations
|
|
6
|
+
- Transaction semantics with automatic rollback
|
|
7
|
+
- Conditional logic and parallel execution
|
|
8
|
+
- Template variables and composition
|
|
9
|
+
|
|
10
|
+
Quick Start:
|
|
11
|
+
>>> from oscura.pipeline import Pipeline, register_all_handlers
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Load and execute a pipeline
|
|
14
|
+
>>> pipeline = Pipeline.load("analysis.yaml")
|
|
15
|
+
>>> register_all_handlers(pipeline)
|
|
16
|
+
>>> results = pipeline.execute()
|
|
17
|
+
>>>
|
|
18
|
+
>>> # Access results
|
|
19
|
+
>>> trace = results.outputs["load_trace"]["trace"]
|
|
20
|
+
>>> frames = results.outputs["decode_uart"]["frames"]
|
|
21
|
+
|
|
22
|
+
Example YAML:
|
|
23
|
+
```yaml
|
|
24
|
+
pipeline:
|
|
25
|
+
name: uart_analysis
|
|
26
|
+
version: 1.0.0
|
|
27
|
+
steps:
|
|
28
|
+
- name: load_trace
|
|
29
|
+
type: input.file
|
|
30
|
+
params:
|
|
31
|
+
path: ${input_file}
|
|
32
|
+
outputs:
|
|
33
|
+
trace: waveform
|
|
34
|
+
|
|
35
|
+
- name: decode_uart
|
|
36
|
+
type: decoder.uart
|
|
37
|
+
inputs:
|
|
38
|
+
trace: load_trace.waveform
|
|
39
|
+
params:
|
|
40
|
+
baud_rate: 115200
|
|
41
|
+
outputs:
|
|
42
|
+
frames: decoded_frames
|
|
43
|
+
```
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from oscura.core.config.pipeline import (
|
|
47
|
+
Pipeline,
|
|
48
|
+
PipelineDefinition,
|
|
49
|
+
PipelineExecutionError,
|
|
50
|
+
PipelineResult,
|
|
51
|
+
PipelineStep,
|
|
52
|
+
PipelineValidationError,
|
|
53
|
+
)
|
|
54
|
+
from oscura.pipeline.handlers import (
|
|
55
|
+
get_all_handlers,
|
|
56
|
+
get_handler,
|
|
57
|
+
list_handler_types,
|
|
58
|
+
register_all_handlers,
|
|
59
|
+
register_handler,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
# Core classes
|
|
64
|
+
"Pipeline",
|
|
65
|
+
"PipelineDefinition",
|
|
66
|
+
"PipelineExecutionError",
|
|
67
|
+
"PipelineResult",
|
|
68
|
+
"PipelineStep",
|
|
69
|
+
"PipelineValidationError",
|
|
70
|
+
"get_all_handlers",
|
|
71
|
+
"get_handler",
|
|
72
|
+
"list_handler_types",
|
|
73
|
+
"register_all_handlers",
|
|
74
|
+
# Registry functions
|
|
75
|
+
"register_handler",
|
|
76
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Pipeline handler registry system.
|
|
2
|
+
|
|
3
|
+
This module provides a centralized registry for pipeline step handlers.
|
|
4
|
+
Handlers are functions that process specific step types (e.g., "input.file",
|
|
5
|
+
"decoder.uart") and return outputs that can be used by subsequent steps.
|
|
6
|
+
|
|
7
|
+
Handler Signature:
|
|
8
|
+
All handlers must follow this signature:
|
|
9
|
+
|
|
10
|
+
def handler(inputs: dict[str, Any], params: dict[str, Any], step_name: str) -> dict[str, Any]:
|
|
11
|
+
'''Handler for step type.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
inputs: Input data from previous steps
|
|
15
|
+
params: Step parameters from YAML
|
|
16
|
+
step_name: Name of step being executed (for error reporting)
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Dictionary of outputs to pass to next steps
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
PipelineExecutionError: If step execution fails
|
|
23
|
+
'''
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> from oscura.pipeline.handlers import register_handler
|
|
27
|
+
>>>
|
|
28
|
+
>>> @register_handler("custom.step")
|
|
29
|
+
>>> def handle_custom(inputs: dict[str, Any], params: dict[str, Any], step_name: str) -> dict[str, Any]:
|
|
30
|
+
... result = do_custom_processing(inputs, params)
|
|
31
|
+
... return {"result": result}
|
|
32
|
+
>>>
|
|
33
|
+
>>> # Auto-register all handlers
|
|
34
|
+
>>> from oscura.core.config.pipeline import Pipeline
|
|
35
|
+
>>> from oscura.pipeline.handlers import register_all_handlers
|
|
36
|
+
>>>
|
|
37
|
+
>>> pipeline = Pipeline.load("analysis.yaml")
|
|
38
|
+
>>> register_all_handlers(pipeline)
|
|
39
|
+
>>> results = pipeline.execute()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
from typing import TYPE_CHECKING, Any
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING:
|
|
47
|
+
from collections.abc import Callable
|
|
48
|
+
|
|
49
|
+
from oscura.core.config.pipeline import Pipeline
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Global handler registry
|
|
53
|
+
_HANDLER_REGISTRY: dict[str, Callable[[dict[str, Any], dict[str, Any], str], dict[str, Any]]] = {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def register_handler(
|
|
57
|
+
step_type: str,
|
|
58
|
+
) -> Callable[
|
|
59
|
+
[Callable[[dict[str, Any], dict[str, Any], str], dict[str, Any]]],
|
|
60
|
+
Callable[[dict[str, Any], dict[str, Any], str], dict[str, Any]],
|
|
61
|
+
]:
|
|
62
|
+
"""Decorator to register a handler for a step type.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
step_type: Step type identifier (e.g., "input.file", "decoder.uart")
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Decorator function
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> @register_handler("input.file")
|
|
72
|
+
>>> def handle_input_file(inputs: dict[str, Any], params: dict[str, Any], step_name: str) -> dict[str, Any]:
|
|
73
|
+
... # Load file and return trace
|
|
74
|
+
... return {"trace": loaded_trace}
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def decorator(
|
|
78
|
+
func: Callable[[dict[str, Any], dict[str, Any], str], dict[str, Any]],
|
|
79
|
+
) -> Callable[[dict[str, Any], dict[str, Any], str], dict[str, Any]]:
|
|
80
|
+
"""Register handler function."""
|
|
81
|
+
_HANDLER_REGISTRY[step_type] = func
|
|
82
|
+
return func
|
|
83
|
+
|
|
84
|
+
return decorator
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_handler(
|
|
88
|
+
step_type: str,
|
|
89
|
+
) -> Callable[[dict[str, Any], dict[str, Any], str], dict[str, Any]] | None:
|
|
90
|
+
"""Get handler for a step type.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
step_type: Step type identifier
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Handler function or None if not found
|
|
97
|
+
"""
|
|
98
|
+
return _HANDLER_REGISTRY.get(step_type)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def get_all_handlers() -> dict[
|
|
102
|
+
str, Callable[[dict[str, Any], dict[str, Any], str], dict[str, Any]]
|
|
103
|
+
]:
|
|
104
|
+
"""Get all registered handlers.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Dictionary mapping step types to handler functions
|
|
108
|
+
"""
|
|
109
|
+
return _HANDLER_REGISTRY.copy()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def list_handler_types() -> list[str]:
|
|
113
|
+
"""List all registered handler types.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Sorted list of step type identifiers
|
|
117
|
+
"""
|
|
118
|
+
return sorted(_HANDLER_REGISTRY.keys())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def register_all_handlers(pipeline: Pipeline) -> None:
|
|
122
|
+
"""Register all handlers with a pipeline.
|
|
123
|
+
|
|
124
|
+
This is the main entry point for setting up a pipeline with all
|
|
125
|
+
available handlers. Import this after importing handler modules.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
pipeline: Pipeline instance to register handlers with
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
>>> from oscura.core.config.pipeline import Pipeline
|
|
132
|
+
>>> from oscura.pipeline.handlers import register_all_handlers
|
|
133
|
+
>>>
|
|
134
|
+
>>> pipeline = Pipeline.load("analysis.yaml")
|
|
135
|
+
>>> register_all_handlers(pipeline)
|
|
136
|
+
>>> results = pipeline.execute()
|
|
137
|
+
"""
|
|
138
|
+
for step_type, handler in _HANDLER_REGISTRY.items():
|
|
139
|
+
pipeline.register_handler(step_type, handler)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Import all handler modules to trigger registration
|
|
143
|
+
# These imports must come after the registry is defined
|
|
144
|
+
from oscura.pipeline.handlers import (
|
|
145
|
+
analyzers,
|
|
146
|
+
decoders,
|
|
147
|
+
exporters,
|
|
148
|
+
filters,
|
|
149
|
+
loaders,
|
|
150
|
+
transforms,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
__all__ = [
|
|
154
|
+
"analyzers",
|
|
155
|
+
"decoders",
|
|
156
|
+
"exporters",
|
|
157
|
+
"filters",
|
|
158
|
+
"get_all_handlers",
|
|
159
|
+
"get_handler",
|
|
160
|
+
"list_handler_types",
|
|
161
|
+
"loaders",
|
|
162
|
+
"register_all_handlers",
|
|
163
|
+
"register_handler",
|
|
164
|
+
"transforms",
|
|
165
|
+
]
|