oscura 0.1.2__py3-none-any.whl → 0.4.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.
Files changed (116) hide show
  1. oscura/__init__.py +1 -7
  2. oscura/acquisition/__init__.py +147 -0
  3. oscura/acquisition/file.py +255 -0
  4. oscura/acquisition/hardware.py +186 -0
  5. oscura/acquisition/saleae.py +340 -0
  6. oscura/acquisition/socketcan.py +315 -0
  7. oscura/acquisition/streaming.py +38 -0
  8. oscura/acquisition/synthetic.py +229 -0
  9. oscura/acquisition/visa.py +376 -0
  10. oscura/analyzers/__init__.py +3 -0
  11. oscura/analyzers/digital/clock.py +9 -1
  12. oscura/analyzers/digital/edges.py +1 -1
  13. oscura/analyzers/digital/timing.py +41 -11
  14. oscura/analyzers/packet/payload_extraction.py +2 -4
  15. oscura/analyzers/packet/stream.py +5 -5
  16. oscura/analyzers/patterns/__init__.py +4 -3
  17. oscura/analyzers/patterns/clustering.py +3 -1
  18. oscura/analyzers/power/ac_power.py +0 -2
  19. oscura/analyzers/power/basic.py +0 -2
  20. oscura/analyzers/power/ripple.py +0 -2
  21. oscura/analyzers/side_channel/__init__.py +52 -0
  22. oscura/analyzers/side_channel/power.py +690 -0
  23. oscura/analyzers/side_channel/timing.py +369 -0
  24. oscura/analyzers/signal_integrity/embedding.py +0 -2
  25. oscura/analyzers/signal_integrity/sparams.py +28 -206
  26. oscura/analyzers/spectral/fft.py +0 -2
  27. oscura/analyzers/statistical/__init__.py +3 -3
  28. oscura/analyzers/statistical/checksum.py +2 -0
  29. oscura/analyzers/statistical/classification.py +2 -0
  30. oscura/analyzers/statistical/entropy.py +11 -9
  31. oscura/analyzers/statistical/ngrams.py +4 -2
  32. oscura/api/fluent.py +2 -2
  33. oscura/automotive/__init__.py +4 -4
  34. oscura/automotive/can/__init__.py +0 -2
  35. oscura/automotive/can/patterns.py +3 -1
  36. oscura/automotive/can/session.py +277 -78
  37. oscura/automotive/can/state_machine.py +5 -2
  38. oscura/automotive/dbc/__init__.py +0 -2
  39. oscura/automotive/dtc/__init__.py +0 -2
  40. oscura/automotive/dtc/data.json +2763 -0
  41. oscura/automotive/dtc/database.py +37 -2769
  42. oscura/automotive/j1939/__init__.py +0 -2
  43. oscura/automotive/loaders/__init__.py +0 -2
  44. oscura/automotive/loaders/asc.py +0 -2
  45. oscura/automotive/loaders/blf.py +0 -2
  46. oscura/automotive/loaders/csv_can.py +0 -2
  47. oscura/automotive/obd/__init__.py +0 -2
  48. oscura/automotive/uds/__init__.py +0 -2
  49. oscura/automotive/uds/models.py +0 -2
  50. oscura/builders/__init__.py +9 -11
  51. oscura/builders/signal_builder.py +99 -191
  52. oscura/cli/main.py +0 -2
  53. oscura/cli/shell.py +0 -2
  54. oscura/config/loader.py +0 -2
  55. oscura/core/backend_selector.py +1 -1
  56. oscura/core/correlation.py +0 -2
  57. oscura/core/exceptions.py +61 -3
  58. oscura/core/lazy.py +5 -3
  59. oscura/core/memory_limits.py +0 -2
  60. oscura/core/numba_backend.py +5 -7
  61. oscura/core/uncertainty.py +3 -3
  62. oscura/dsl/interpreter.py +2 -0
  63. oscura/dsl/parser.py +8 -6
  64. oscura/exploratory/error_recovery.py +3 -3
  65. oscura/exploratory/parse.py +2 -0
  66. oscura/exploratory/recovery.py +2 -0
  67. oscura/exploratory/sync.py +2 -0
  68. oscura/export/wireshark/generator.py +1 -1
  69. oscura/export/wireshark/type_mapping.py +2 -0
  70. oscura/exporters/hdf5.py +1 -3
  71. oscura/extensibility/templates.py +0 -8
  72. oscura/inference/active_learning/lstar.py +2 -4
  73. oscura/inference/active_learning/observation_table.py +0 -2
  74. oscura/inference/active_learning/oracle.py +3 -1
  75. oscura/inference/active_learning/teachers/simulator.py +1 -3
  76. oscura/inference/alignment.py +2 -0
  77. oscura/inference/message_format.py +2 -0
  78. oscura/inference/protocol_dsl.py +7 -5
  79. oscura/inference/sequences.py +12 -14
  80. oscura/inference/state_machine.py +2 -0
  81. oscura/integrations/llm.py +3 -1
  82. oscura/jupyter/display.py +0 -2
  83. oscura/loaders/__init__.py +68 -51
  84. oscura/loaders/chipwhisperer.py +393 -0
  85. oscura/loaders/pcap.py +1 -1
  86. oscura/loaders/touchstone.py +221 -0
  87. oscura/math/arithmetic.py +0 -2
  88. oscura/optimization/parallel.py +9 -6
  89. oscura/pipeline/composition.py +0 -2
  90. oscura/plugins/cli.py +0 -2
  91. oscura/reporting/comparison.py +0 -2
  92. oscura/reporting/config.py +1 -1
  93. oscura/reporting/formatting/emphasis.py +2 -0
  94. oscura/reporting/formatting/numbers.py +0 -2
  95. oscura/reporting/output.py +1 -3
  96. oscura/reporting/sections.py +0 -2
  97. oscura/search/anomaly.py +2 -0
  98. oscura/session/session.py +91 -16
  99. oscura/sessions/__init__.py +70 -0
  100. oscura/sessions/base.py +323 -0
  101. oscura/sessions/blackbox.py +640 -0
  102. oscura/sessions/generic.py +189 -0
  103. oscura/testing/synthetic.py +2 -0
  104. oscura/ui/formatters.py +4 -2
  105. oscura/utils/buffer.py +2 -2
  106. oscura/utils/lazy.py +5 -5
  107. oscura/utils/memory_advanced.py +2 -2
  108. oscura/utils/memory_extensions.py +2 -2
  109. oscura/visualization/colors.py +0 -2
  110. oscura/visualization/power.py +2 -0
  111. oscura/workflows/multi_trace.py +2 -0
  112. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/METADATA +122 -20
  113. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/RECORD +116 -98
  114. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/WHEEL +0 -0
  115. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/entry_points.txt +0 -0
  116. {oscura-0.1.2.dist-info → oscura-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -35,8 +35,6 @@ Example:
35
35
  >>> result = sum_of_squares(data) # Fast on second call
36
36
  """
37
37
 
38
- from __future__ import annotations
39
-
40
38
  import functools
41
39
  from collections.abc import Callable
42
40
  from typing import Any, TypeVar
@@ -45,11 +43,11 @@ import numpy as np
45
43
 
46
44
  # Try to import Numba
47
45
  try:
48
- from numba import guvectorize as _numba_guvectorize # type: ignore[import-untyped]
49
- from numba import jit as _numba_jit # type: ignore[import-untyped]
50
- from numba import njit as _numba_njit # type: ignore[import-untyped]
51
- from numba import prange as _numba_prange # type: ignore[import-untyped]
52
- from numba import vectorize as _numba_vectorize # type: ignore[import-untyped]
46
+ from numba import guvectorize as _numba_guvectorize # type: ignore[import-not-found]
47
+ from numba import jit as _numba_jit # type: ignore[import-not-found]
48
+ from numba import njit as _numba_njit # type: ignore[import-not-found]
49
+ from numba import prange as _numba_prange # type: ignore[import-not-found]
50
+ from numba import vectorize as _numba_vectorize # type: ignore[import-not-found]
53
51
 
54
52
  HAS_NUMBA = True
55
53
  except ImportError:
@@ -108,7 +108,7 @@ class MeasurementWithUncertainty:
108
108
  JCGM 100:2008 Section 5.1.6
109
109
  """
110
110
  if self.value == 0:
111
- return np.inf
111
+ return float(np.inf) # type: ignore[no-any-return]
112
112
  return abs(self.uncertainty / self.value)
113
113
 
114
114
  @property
@@ -173,7 +173,7 @@ class UncertaintyEstimator:
173
173
  JCGM 100:2008 Section 4.2 (Type A evaluation)
174
174
  """
175
175
  if len(data) < 2:
176
- return np.nan
176
+ return float(np.nan) # type: ignore[no-any-return]
177
177
  return float(np.std(data, ddof=1)) # Sample std (Bessel correction)
178
178
 
179
179
  @staticmethod
@@ -190,7 +190,7 @@ class UncertaintyEstimator:
190
190
  JCGM 100:2008 Section 4.2.3
191
191
  """
192
192
  if len(data) < 2:
193
- return np.nan
193
+ return float(np.nan) # type: ignore[no-any-return]
194
194
  return float(np.std(data, ddof=1) / np.sqrt(len(data)))
195
195
 
196
196
  @staticmethod
oscura/dsl/interpreter.py CHANGED
@@ -3,6 +3,8 @@
3
3
  Executes parsed DSL programs.
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  from collections.abc import Callable
7
9
  from pathlib import Path
8
10
  from typing import Any
oscura/dsl/parser.py CHANGED
@@ -3,6 +3,8 @@
3
3
  Implements simple domain-specific language for trace analysis workflows.
4
4
  """
5
5
 
6
+ from __future__ import annotations
7
+
6
8
  from dataclasses import dataclass
7
9
  from enum import Enum, auto
8
10
  from typing import Any, Union
@@ -352,14 +354,14 @@ class Assignment(ASTNode):
352
354
  """Variable assignment: $var = expr."""
353
355
 
354
356
  variable: str
355
- expression: "Expression"
357
+ expression: Expression
356
358
 
357
359
 
358
360
  @dataclass
359
361
  class Pipeline(ASTNode):
360
362
  """Pipeline expression: expr | command | command."""
361
363
 
362
- stages: list["Expression"]
364
+ stages: list[Expression]
363
365
 
364
366
 
365
367
  @dataclass
@@ -367,7 +369,7 @@ class Command(ASTNode):
367
369
  """Command invocation: command arg1 arg2."""
368
370
 
369
371
  name: str
370
- args: list["Expression"]
372
+ args: list[Expression]
371
373
 
372
374
 
373
375
  @dataclass
@@ -375,7 +377,7 @@ class FunctionCall(ASTNode):
375
377
  """Function call: func(arg1, arg2)."""
376
378
 
377
379
  name: str
378
- args: list["Expression"]
380
+ args: list[Expression]
379
381
 
380
382
 
381
383
  @dataclass
@@ -397,8 +399,8 @@ class ForLoop(ASTNode):
397
399
  """For loop: for $var in expr: body."""
398
400
 
399
401
  variable: str
400
- iterable: "Expression"
401
- body: list["Statement"]
402
+ iterable: Expression
403
+ body: list[Statement]
402
404
 
403
405
 
404
406
  # Type aliases
@@ -20,6 +20,8 @@ import numpy as np
20
20
 
21
21
  from oscura.core.types import WaveformTrace
22
22
 
23
+ T = TypeVar("T")
24
+
23
25
  logger = logging.getLogger(__name__)
24
26
 
25
27
  if TYPE_CHECKING:
@@ -27,8 +29,6 @@ if TYPE_CHECKING:
27
29
 
28
30
  from numpy.typing import NDArray
29
31
 
30
- T = TypeVar("T")
31
-
32
32
 
33
33
  @dataclass
34
34
  class RecoveryStats:
@@ -547,7 +547,7 @@ class RetryResult:
547
547
  adjustments_made: list[str]
548
548
 
549
549
 
550
- def retry_with_adjustment[T](
550
+ def retry_with_adjustment(
551
551
  func: Callable[..., T],
552
552
  trace: WaveformTrace,
553
553
  initial_params: dict[str, Any],
@@ -5,6 +5,8 @@ This module provides robust protocol decoding that continues after errors
5
5
  and timestamp correction for jittery captures.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  from dataclasses import dataclass
9
11
  from enum import Enum
10
12
  from typing import Any, Literal
@@ -5,6 +5,8 @@ This module characterizes bit error patterns to diagnose capture quality
5
5
  issues (EMI, USB problems, clock jitter) and suggests likely causes.
6
6
  """
7
7
 
8
+ from __future__ import annotations
9
+
8
10
  from dataclasses import dataclass
9
11
  from enum import Enum
10
12
 
@@ -6,6 +6,8 @@ in noisy or corrupted logic analyzer captures, with configurable bit error
6
6
  tolerance using Hamming distance.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  from dataclasses import dataclass
10
12
  from enum import Enum
11
13
  from typing import Literal
@@ -106,7 +106,7 @@ class WiresharkDissectorGenerator:
106
106
 
107
107
  # Render template
108
108
  template = self.env.get_template("dissector.lua.j2")
109
- return template.render(**context)
109
+ return str(template.render(**context)) # type: ignore[no-any-return]
110
110
 
111
111
  def _build_template_context(self, protocol: ProtocolDefinition) -> dict[str, Any]:
112
112
  """Build context dictionary for template rendering.
@@ -7,6 +7,8 @@ References:
7
7
  https://wiki.wireshark.org/LuaAPI/Proto
8
8
  """
9
9
 
10
+ from __future__ import annotations
11
+
10
12
  from typing import Literal
11
13
 
12
14
  # Map Oscura field types to Wireshark ProtoField types
oscura/exporters/hdf5.py CHANGED
@@ -11,8 +11,6 @@ References:
11
11
  HDF5 specification (https://www.hdfgroup.org/)
12
12
  """
13
13
 
14
- from __future__ import annotations
15
-
16
14
  from datetime import datetime
17
15
  from pathlib import Path
18
16
  from typing import Any
@@ -81,7 +79,7 @@ def export_hdf5(
81
79
 
82
80
 
83
81
  def _write_trace_dataset(
84
- f: h5py.File,
82
+ f: "h5py.File",
85
83
  name: str,
86
84
  trace: WaveformTrace | DigitalTrace,
87
85
  compression: str | None,
@@ -622,8 +622,6 @@ def _generate_analyzer_stub(template: PluginTemplate, class_name: str) -> str:
622
622
  References:
623
623
  PLUG-008: Plugin Template Generator
624
624
  """
625
- from __future__ import annotations
626
-
627
625
  import numpy as np
628
626
  from numpy.typing import NDArray
629
627
 
@@ -697,8 +695,6 @@ def _generate_loader_stub(template: PluginTemplate, class_name: str) -> str:
697
695
  References:
698
696
  PLUG-008: Plugin Template Generator
699
697
  """
700
- from __future__ import annotations
701
-
702
698
  import numpy as np
703
699
  from numpy.typing import NDArray
704
700
  from pathlib import Path
@@ -782,8 +778,6 @@ def _generate_exporter_stub(template: PluginTemplate, class_name: str) -> str:
782
778
  References:
783
779
  PLUG-008: Plugin Template Generator
784
780
  """
785
- from __future__ import annotations
786
-
787
781
  import numpy as np
788
782
  from numpy.typing import NDArray
789
783
  from pathlib import Path
@@ -871,8 +865,6 @@ def _generate_generic_stub(template: PluginTemplate, class_name: str) -> str:
871
865
  References:
872
866
  PLUG-008: Plugin Template Generator
873
867
  """
874
- from __future__ import annotations
875
-
876
868
 
877
869
  class {class_name}:
878
870
  """Generic plugin implementation.
@@ -26,11 +26,9 @@ Complexity:
26
26
  - Produces minimal DFA (fewest states)
27
27
  """
28
28
 
29
- from __future__ import annotations
30
-
31
29
  from oscura.inference.active_learning.observation_table import ObservationTable
32
- from oscura.inference.active_learning.oracle import Oracle # noqa: TC001
33
- from oscura.inference.state_machine import FiniteAutomaton # noqa: TC001
30
+ from oscura.inference.active_learning.oracle import Oracle
31
+ from oscura.inference.state_machine import FiniteAutomaton
34
32
 
35
33
 
36
34
  class LStarLearner:
@@ -8,8 +8,6 @@ References:
8
8
  Information and Computation, 75(2), 87-106.
9
9
  """
10
10
 
11
- from __future__ import annotations
12
-
13
11
  from dataclasses import dataclass, field
14
12
 
15
13
  from oscura.inference.state_machine import FiniteAutomaton, State, Transition
@@ -11,8 +11,10 @@ References:
11
11
  from __future__ import annotations
12
12
 
13
13
  from abc import ABC, abstractmethod
14
+ from typing import TYPE_CHECKING
14
15
 
15
- from oscura.inference.state_machine import FiniteAutomaton # noqa: TC001
16
+ if TYPE_CHECKING:
17
+ from oscura.inference.state_machine import FiniteAutomaton
16
18
 
17
19
 
18
20
  class Oracle(ABC):
@@ -7,10 +7,8 @@ requiring a live system.
7
7
  The simulator teacher treats traces as examples of valid protocol sequences.
8
8
  """
9
9
 
10
- from __future__ import annotations
11
-
12
10
  from oscura.inference.active_learning.oracle import Oracle
13
- from oscura.inference.state_machine import FiniteAutomaton # noqa: TC001
11
+ from oscura.inference.state_machine import FiniteAutomaton
14
12
 
15
13
 
16
14
  class SimulatorTeacher(Oracle):
@@ -12,6 +12,8 @@ Key capabilities:
12
12
  - Conserved/variable region detection
13
13
  """
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from dataclasses import dataclass
16
18
  from typing import Any, Literal
17
19
 
@@ -18,6 +18,8 @@ References:
18
18
  Discoverer: Automatic Protocol Reverse Engineering. USENIX Security 2007.
19
19
  """
20
20
 
21
+ from __future__ import annotations
22
+
21
23
  from dataclasses import dataclass
22
24
  from dataclasses import field as dataclass_field
23
25
  from typing import Any, Literal
@@ -13,6 +13,8 @@ Key capabilities:
13
13
  - Comprehensive error reporting
14
14
  """
15
15
 
16
+ from __future__ import annotations
17
+
16
18
  import ast
17
19
  import operator
18
20
  import struct
@@ -68,7 +70,7 @@ class FieldDefinition:
68
70
  element: dict[str, Any] | None = None # Element definition for arrays
69
71
  count_field: str | None = None # Field containing array count
70
72
  count: int | None = None # Fixed array count
71
- fields: list["FieldDefinition"] | None = None # Nested fields for struct type
73
+ fields: list[FieldDefinition] | None = None # Nested fields for struct type
72
74
 
73
75
  def __post_init__(self) -> None:
74
76
  """Handle size_ref as alias for size."""
@@ -117,7 +119,7 @@ class ProtocolDefinition:
117
119
  encoding: dict[str, Any] = field(default_factory=dict)
118
120
 
119
121
  @classmethod
120
- def from_yaml(cls, path: str | Path) -> "ProtocolDefinition":
122
+ def from_yaml(cls, path: str | Path) -> ProtocolDefinition:
121
123
  """Load protocol definition from YAML file.
122
124
 
123
125
  : YAML parsing.
@@ -134,7 +136,7 @@ class ProtocolDefinition:
134
136
  return cls.from_dict(config)
135
137
 
136
138
  @classmethod
137
- def from_dict(cls, config: dict[str, Any]) -> "ProtocolDefinition":
139
+ def from_dict(cls, config: dict[str, Any]) -> ProtocolDefinition:
138
140
  """Create from dictionary.
139
141
 
140
142
  : Configuration parsing.
@@ -168,7 +170,7 @@ class ProtocolDefinition:
168
170
  @classmethod
169
171
  def _parse_field_definition(
170
172
  cls, field_dict: dict[str, Any], default_endian: str
171
- ) -> "FieldDefinition":
173
+ ) -> FieldDefinition:
172
174
  """Parse a single field definition from dictionary.
173
175
 
174
176
  Args:
@@ -365,7 +367,7 @@ class ProtocolDecoder:
365
367
  self._endian_map: dict[str, str] = {"big": ">", "little": "<"}
366
368
 
367
369
  @classmethod
368
- def load(cls, path: str | Path) -> "ProtocolDecoder":
370
+ def load(cls, path: str | Path) -> ProtocolDecoder:
369
371
  """Load decoder from YAML protocol definition.
370
372
 
371
373
  : Load decoder from file.
@@ -8,8 +8,6 @@ streams, identifying request-response pairs, and analyzing communication
8
8
  flows.
9
9
  """
10
10
 
11
- from __future__ import annotations
12
-
13
11
  from collections import defaultdict
14
12
  from collections.abc import Callable, Sequence
15
13
  from dataclasses import dataclass, field
@@ -83,7 +81,7 @@ class CommunicationFlow:
83
81
 
84
82
  flow_id: int
85
83
  messages: list[Any]
86
- pairs: list[RequestResponsePair]
84
+ pairs: list["RequestResponsePair"]
87
85
  direction: str
88
86
  participants: list[str]
89
87
  duration: float
@@ -127,7 +125,7 @@ class SequencePatternDetector:
127
125
  messages: Sequence[Any],
128
126
  key: Callable[[Any], Any] | None = None,
129
127
  timestamp_key: Callable[[Any], float] | None = None,
130
- ) -> list[SequencePattern]:
128
+ ) -> list["SequencePattern"]:
131
129
  """Detect sequential patterns in message stream.
132
130
 
133
131
  Implements RE-SEQ-002: Pattern detection workflow.
@@ -222,7 +220,7 @@ class SequencePatternDetector:
222
220
  messages: Sequence[Any],
223
221
  key: Callable[[Any], Any] | None = None,
224
222
  timestamp_key: Callable[[Any], float] | None = None,
225
- ) -> list[SequencePattern]:
223
+ ) -> list["SequencePattern"]:
226
224
  """Detect patterns that occur at regular intervals.
227
225
 
228
226
  Implements RE-SEQ-002: Periodic pattern detection.
@@ -287,7 +285,7 @@ class SequencePatternDetector:
287
285
  candidates: dict[tuple[Any, ...], list[int]],
288
286
  identifiers: list[Any],
289
287
  timestamps: list[float] | None,
290
- ) -> list[SequencePattern]:
288
+ ) -> list["SequencePattern"]:
291
289
  """Score candidate patterns.
292
290
 
293
291
  Args:
@@ -386,7 +384,7 @@ class RequestResponseCorrelator:
386
384
  request_filter: Callable[[Any], bool] | None = None,
387
385
  response_filter: Callable[[Any], bool] | None = None,
388
386
  timestamp_key: Callable[[Any], float] | None = None,
389
- ) -> list[RequestResponsePair]:
387
+ ) -> list["RequestResponsePair"]:
390
388
  """Correlate requests with responses.
391
389
 
392
390
  Implements RE-SEQ-003: Request-response correlation workflow.
@@ -441,7 +439,7 @@ class RequestResponseCorrelator:
441
439
  messages: Sequence[Any],
442
440
  content_key: Callable[[Any], bytes],
443
441
  timestamp_key: Callable[[Any], float] | None = None,
444
- ) -> list[RequestResponsePair]:
442
+ ) -> list["RequestResponsePair"]:
445
443
  """Correlate by analyzing message content similarity.
446
444
 
447
445
  Implements RE-SEQ-003: Content-based correlation.
@@ -500,10 +498,10 @@ class RequestResponseCorrelator:
500
498
 
501
499
  def extract_flows(
502
500
  self,
503
- pairs: Sequence[RequestResponsePair],
501
+ pairs: Sequence["RequestResponsePair"],
504
502
  messages: Sequence[Any],
505
503
  flow_key: Callable[[Any], str] | None = None,
506
- ) -> list[CommunicationFlow]:
504
+ ) -> list["CommunicationFlow"]:
507
505
  """Extract communication flows from pairs.
508
506
 
509
507
  Implements RE-SEQ-003: Flow extraction.
@@ -569,7 +567,7 @@ class RequestResponseCorrelator:
569
567
  self,
570
568
  requests: list[tuple[int, Any, float, Any]],
571
569
  responses: list[tuple[int, Any, float, Any]],
572
- ) -> list[RequestResponsePair]:
570
+ ) -> list["RequestResponsePair"]:
573
571
  """Match request and response messages.
574
572
 
575
573
  Args:
@@ -664,7 +662,7 @@ def detect_sequence_patterns(
664
662
  min_length: int = 2,
665
663
  max_length: int = 10,
666
664
  min_frequency: int = 2,
667
- ) -> list[SequencePattern]:
665
+ ) -> list["SequencePattern"]:
668
666
  """Detect sequential patterns in messages.
669
667
 
670
668
  Implements RE-SEQ-002: Sequence Pattern Detection.
@@ -699,7 +697,7 @@ def correlate_requests(
699
697
  response_filter: Callable[[Any], bool],
700
698
  timestamp_key: Callable[[Any], float] | None = None,
701
699
  max_latency: float = 10.0,
702
- ) -> list[RequestResponsePair]:
700
+ ) -> list["RequestResponsePair"]:
703
701
  """Correlate request and response messages.
704
702
 
705
703
  Implements RE-SEQ-003: Request-Response Correlation.
@@ -760,7 +758,7 @@ def find_message_dependencies(
760
758
 
761
759
 
762
760
  def calculate_latency_stats(
763
- pairs: Sequence[RequestResponsePair],
761
+ pairs: Sequence["RequestResponsePair"],
764
762
  ) -> dict[str, float]:
765
763
  """Calculate latency statistics for request-response pairs.
766
764
 
@@ -12,6 +12,8 @@ Key capabilities:
12
12
  - Export to NetworkX graph for analysis
13
13
  """
14
14
 
15
+ from __future__ import annotations
16
+
15
17
  from copy import deepcopy
16
18
  from dataclasses import dataclass
17
19
  from typing import Any
@@ -23,6 +23,8 @@ Examples:
23
23
  ... )
24
24
  """
25
25
 
26
+ from __future__ import annotations
27
+
26
28
  import hashlib
27
29
  import json
28
30
  import os
@@ -1545,7 +1547,7 @@ def get_client_auto(**config_kwargs: Any) -> LLMClient:
1545
1547
 
1546
1548
  def get_client_with_failover(
1547
1549
  providers: list[str] | None = None, **config_kwargs: Any
1548
- ) -> "FailoverLLMClient":
1550
+ ) -> FailoverLLMClient:
1549
1551
  """Get LLM client with automatic failover between providers.
1550
1552
 
1551
1553
  Failover logic (try OpenAI, fallback to Anthropic).
oscura/jupyter/display.py CHANGED
@@ -12,8 +12,6 @@ Example:
12
12
  In [2]: display_trace(trace) # Shows rich HTML summary
13
13
  """
14
14
 
15
- from __future__ import annotations
16
-
17
15
  from typing import Any
18
16
 
19
17
  try:
@@ -20,10 +20,74 @@ from __future__ import annotations
20
20
  import logging
21
21
  import warnings
22
22
  from pathlib import Path
23
- from typing import TYPE_CHECKING, Any
23
+ from typing import TYPE_CHECKING, Any, cast
24
24
 
25
25
  from oscura.core.exceptions import LoaderError, UnsupportedFormatError
26
- from oscura.core.types import DigitalTrace, WaveformTrace
26
+ from oscura.core.types import DigitalTrace, IQTrace, WaveformTrace
27
+
28
+ # Loader registry for cleaner dispatch
29
+ _LOADER_REGISTRY: dict[str, tuple[str, str]] = {
30
+ "tektronix": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
31
+ "tek": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
32
+ "rigol": ("oscura.loaders.rigol", "load_rigol_wfm"),
33
+ "numpy": ("oscura.loaders.numpy_loader", "load_npz"),
34
+ "csv": ("oscura.loaders.csv_loader", "load_csv"),
35
+ "hdf5": ("oscura.loaders.hdf5_loader", "load_hdf5"),
36
+ "sigrok": ("oscura.loaders.sigrok", "load_sigrok"),
37
+ "vcd": ("oscura.loaders.vcd", "load_vcd"),
38
+ "pcap": ("oscura.loaders.pcap", "load_pcap"),
39
+ "wav": ("oscura.loaders.wav", "load_wav"),
40
+ "tdms": ("oscura.loaders.tdms", "load_tdms"),
41
+ "touchstone": ("oscura.loaders.touchstone", "load_touchstone"),
42
+ "chipwhisperer": ("oscura.loaders.chipwhisperer", "load_chipwhisperer"),
43
+ }
44
+
45
+
46
+ def _dispatch_loader(
47
+ loader_name: str, path: Path, **kwargs: Any
48
+ ) -> WaveformTrace | DigitalTrace | IQTrace:
49
+ """Dispatch to registered loader.
50
+
51
+ Args:
52
+ loader_name: Name of loader to use.
53
+ path: Path to file.
54
+ **kwargs: Additional arguments for loader.
55
+
56
+ Returns:
57
+ Loaded data.
58
+
59
+ Raises:
60
+ UnsupportedFormatError: If loader not registered.
61
+ """
62
+ if loader_name not in _LOADER_REGISTRY:
63
+ raise UnsupportedFormatError(
64
+ loader_name,
65
+ list(_LOADER_REGISTRY.keys()),
66
+ file_path=str(path),
67
+ )
68
+
69
+ module_path, func_name = _LOADER_REGISTRY[loader_name]
70
+
71
+ # Dynamically import the module
72
+ import importlib
73
+ import inspect
74
+
75
+ module = importlib.import_module(module_path)
76
+ loader_func = getattr(module, func_name)
77
+
78
+ # Filter kwargs to only include parameters the function accepts
79
+ sig = inspect.signature(loader_func)
80
+ valid_kwargs = {}
81
+ for key, value in kwargs.items():
82
+ if key in sig.parameters or any(
83
+ p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
84
+ ):
85
+ valid_kwargs[key] = value
86
+
87
+ # Call loader with appropriate arguments
88
+ result = loader_func(path, **valid_kwargs)
89
+ return cast("WaveformTrace | DigitalTrace | IQTrace", result)
90
+
27
91
 
28
92
  # Import alias modules for DSL compatibility
29
93
  from oscura.loaders import (
@@ -180,56 +244,9 @@ def load(
180
244
  # Dispatch to appropriate loader
181
245
  if loader_name == "auto_wfm":
182
246
  return _load_wfm_auto(path, channel=channel, **kwargs)
183
- elif loader_name in ("tektronix", "tek"):
184
- from oscura.loaders.tektronix import load_tektronix_wfm
185
-
186
- return load_tektronix_wfm(path, **kwargs)
187
- elif loader_name == "rigol":
188
- from oscura.loaders.rigol import load_rigol_wfm
189
-
190
- return load_rigol_wfm(path, **kwargs)
191
- elif loader_name == "numpy":
192
- from oscura.loaders.numpy_loader import load_npz
193
-
194
- return load_npz(path, channel=channel, **kwargs)
195
- elif loader_name == "csv":
196
- from oscura.loaders.csv_loader import load_csv
197
-
198
- return load_csv(path, **kwargs) # type: ignore[return-value]
199
- elif loader_name == "hdf5":
200
- from oscura.loaders.hdf5_loader import load_hdf5
201
-
202
- return load_hdf5(path, channel=channel, **kwargs) # type: ignore[return-value]
203
- elif loader_name == "sigrok":
204
- from oscura.loaders.sigrok import load_sigrok
205
-
206
- return load_sigrok(path, channel=channel, **kwargs)
207
- elif loader_name == "vcd":
208
- from oscura.loaders.vcd import load_vcd
209
-
210
- return load_vcd(path, **kwargs)
211
- elif loader_name == "pcap":
212
- from oscura.loaders.pcap import load_pcap
213
-
214
- return load_pcap(path, **kwargs) # type: ignore[return-value]
215
- elif loader_name == "wav":
216
- from oscura.loaders.wav import load_wav
217
-
218
- return load_wav(path, channel=channel, **kwargs)
219
- elif loader_name == "tdms":
220
- from oscura.loaders.tdms import load_tdms
221
-
222
- return load_tdms(path, channel=channel, **kwargs)
223
- elif loader_name == "touchstone":
224
- from oscura.analyzers.signal_integrity.sparams import load_touchstone
225
-
226
- return load_touchstone(path) # type: ignore[return-value]
227
247
  else:
228
- raise UnsupportedFormatError(
229
- loader_name,
230
- list(SUPPORTED_FORMATS.keys()),
231
- file_path=str(path),
232
- )
248
+ # Use registry-based dispatch for all other loaders
249
+ return _dispatch_loader(loader_name, path, channel=channel, **kwargs)
233
250
 
234
251
 
235
252
  def _load_wfm_auto(