oscura 0.6.0__py3-none-any.whl → 0.7.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 CHANGED
@@ -53,7 +53,7 @@ try:
53
53
  __version__ = version("oscura")
54
54
  except Exception:
55
55
  # Fallback for development/testing when package not installed
56
- __version__ = "0.6.0"
56
+ __version__ = "0.7.0"
57
57
 
58
58
  __author__ = "Oscura Contributors"
59
59
 
@@ -49,7 +49,7 @@ try:
49
49
  __version__ = version("oscura")
50
50
  except Exception:
51
51
  # Fallback for development/testing when package not installed
52
- __version__ = "0.6.0"
52
+ __version__ = "0.7.0"
53
53
 
54
54
  __all__ = [
55
55
  "CANMessage",
@@ -266,12 +266,7 @@
266
266
  "category": "Powertrain",
267
267
  "severity": "High",
268
268
  "system": "Throttle Control",
269
- "possible_causes": [
270
- "TPS circuit shorted to voltage",
271
- "Faulty TPS sensor",
272
- "Wiring harness open",
273
- "ECM problem"
274
- ]
269
+ "possible_causes": ["TPS circuit shorted to voltage", "Faulty TPS sensor", "Wiring harness open", "ECM problem"]
275
270
  },
276
271
  "P0125": {
277
272
  "code": "P0125",
@@ -865,12 +860,7 @@
865
860
  "category": "Powertrain",
866
861
  "severity": "Medium",
867
862
  "system": "Emissions Control",
868
- "possible_causes": [
869
- "EGR valve stuck closed",
870
- "EGR passages clogged",
871
- "Faulty EGR valve",
872
- "Vacuum leak"
873
- ]
863
+ "possible_causes": ["EGR valve stuck closed", "EGR passages clogged", "Faulty EGR valve", "Vacuum leak"]
874
864
  },
875
865
  "P0401": {
876
866
  "code": "P0401",
@@ -891,12 +881,7 @@
891
881
  "category": "Powertrain",
892
882
  "severity": "Medium",
893
883
  "system": "Emissions Control",
894
- "possible_causes": [
895
- "EGR valve stuck open",
896
- "Faulty EGR valve",
897
- "EGR vacuum solenoid fault",
898
- "ECM problem"
899
- ]
884
+ "possible_causes": ["EGR valve stuck open", "Faulty EGR valve", "EGR vacuum solenoid fault", "ECM problem"]
900
885
  },
901
886
  "P0403": {
902
887
  "code": "P0403",
@@ -958,12 +943,7 @@
958
943
  "category": "Powertrain",
959
944
  "severity": "Low",
960
945
  "system": "Emissions Control",
961
- "possible_causes": [
962
- "Loose or missing fuel cap",
963
- "EVAP system leak",
964
- "Faulty purge valve",
965
- "Faulty vent valve"
966
- ]
946
+ "possible_causes": ["Loose or missing fuel cap", "EVAP system leak", "Faulty purge valve", "Faulty vent valve"]
967
947
  },
968
948
  "P0441": {
969
949
  "code": "P0441",
@@ -1075,12 +1055,7 @@
1075
1055
  "category": "Powertrain",
1076
1056
  "severity": "Low",
1077
1057
  "system": "Idle Control",
1078
- "possible_causes": [
1079
- "Vacuum leak",
1080
- "IAC valve fault",
1081
- "Dirty throttle body",
1082
- "PCV valve problem"
1083
- ]
1058
+ "possible_causes": ["Vacuum leak", "IAC valve fault", "Dirty throttle body", "PCV valve problem"]
1084
1059
  },
1085
1060
  "P0507": {
1086
1061
  "code": "P0507",
@@ -1088,12 +1063,7 @@
1088
1063
  "category": "Powertrain",
1089
1064
  "severity": "Low",
1090
1065
  "system": "Idle Control",
1091
- "possible_causes": [
1092
- "Vacuum leak",
1093
- "IAC valve stuck open",
1094
- "PCV valve stuck open",
1095
- "EVAP purge valve leaking"
1096
- ]
1066
+ "possible_causes": ["Vacuum leak", "IAC valve stuck open", "PCV valve stuck open", "EVAP purge valve leaking"]
1097
1067
  },
1098
1068
  "P0600": {
1099
1069
  "code": "P0600",
@@ -1127,12 +1097,7 @@
1127
1097
  "category": "Powertrain",
1128
1098
  "severity": "Critical",
1129
1099
  "system": "Engine Control Module",
1130
- "possible_causes": [
1131
- "ECM not programmed",
1132
- "ECM programming incomplete",
1133
- "Wrong software version",
1134
- "ECM fault"
1135
- ]
1100
+ "possible_causes": ["ECM not programmed", "ECM programming incomplete", "Wrong software version", "ECM fault"]
1136
1101
  },
1137
1102
  "P0603": {
1138
1103
  "code": "P0603",
@@ -1205,12 +1170,7 @@
1205
1170
  "category": "Powertrain",
1206
1171
  "severity": "Medium",
1207
1172
  "system": "Charging System",
1208
- "possible_causes": [
1209
- "Faulty alternator",
1210
- "Wiring harness problem",
1211
- "Poor electrical connection",
1212
- "ECM fault"
1213
- ]
1173
+ "possible_causes": ["Faulty alternator", "Wiring harness problem", "Poor electrical connection", "ECM fault"]
1214
1174
  },
1215
1175
  "P0625": {
1216
1176
  "code": "P0625",
@@ -1283,12 +1243,7 @@
1283
1243
  "category": "Powertrain",
1284
1244
  "severity": "High",
1285
1245
  "system": "Transmission",
1286
- "possible_causes": [
1287
- "Faulty input speed sensor",
1288
- "Wiring harness problem",
1289
- "Sensor reluctor damaged",
1290
- "TCM fault"
1291
- ]
1246
+ "possible_causes": ["Faulty input speed sensor", "Wiring harness problem", "Sensor reluctor damaged", "TCM fault"]
1292
1247
  },
1293
1248
  "P0720": {
1294
1249
  "code": "P0720",
@@ -1491,12 +1446,7 @@
1491
1446
  "category": "Chassis",
1492
1447
  "severity": "High",
1493
1448
  "system": "ABS",
1494
- "possible_causes": [
1495
- "Faulty valve relay",
1496
- "Relay circuit problem",
1497
- "ABS module fault",
1498
- "Wiring harness issue"
1499
- ]
1449
+ "possible_causes": ["Faulty valve relay", "Relay circuit problem", "ABS module fault", "Wiring harness issue"]
1500
1450
  },
1501
1451
  "C0161": {
1502
1452
  "code": "C0161",
@@ -2206,12 +2156,7 @@
2206
2156
  "category": "Body",
2207
2157
  "severity": "Low",
2208
2158
  "system": "Lighting System",
2209
- "possible_causes": [
2210
- "Burned out bulb",
2211
- "Wiring harness problem",
2212
- "Lamp socket corrosion",
2213
- "BCM fault"
2214
- ]
2159
+ "possible_causes": ["Burned out bulb", "Wiring harness problem", "Lamp socket corrosion", "BCM fault"]
2215
2160
  },
2216
2161
  "B0601": {
2217
2162
  "code": "B0601",
@@ -2232,12 +2177,7 @@
2232
2177
  "category": "Body",
2233
2178
  "severity": "Low",
2234
2179
  "system": "Lighting System",
2235
- "possible_causes": [
2236
- "Burned out turn signal bulb",
2237
- "Wiring harness problem",
2238
- "Flasher relay fault",
2239
- "BCM fault"
2240
- ]
2180
+ "possible_causes": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
2241
2181
  },
2242
2182
  "B0603": {
2243
2183
  "code": "B0603",
@@ -2245,12 +2185,7 @@
2245
2185
  "category": "Body",
2246
2186
  "severity": "Low",
2247
2187
  "system": "Lighting System",
2248
- "possible_causes": [
2249
- "Burned out turn signal bulb",
2250
- "Wiring harness problem",
2251
- "Flasher relay fault",
2252
- "BCM fault"
2253
- ]
2188
+ "possible_causes": ["Burned out turn signal bulb", "Wiring harness problem", "Flasher relay fault", "BCM fault"]
2254
2189
  },
2255
2190
  "B0604": {
2256
2191
  "code": "B0604",
@@ -2362,12 +2297,7 @@
2362
2297
  "category": "Body",
2363
2298
  "severity": "Low",
2364
2299
  "system": "Keyless Entry",
2365
- "possible_causes": [
2366
- "Key fob battery weak",
2367
- "Key fob not synchronized",
2368
- "BCM fault",
2369
- "Receiver antenna fault"
2370
- ]
2300
+ "possible_causes": ["Key fob battery weak", "Key fob not synchronized", "BCM fault", "Receiver antenna fault"]
2371
2301
  },
2372
2302
  "B1300": {
2373
2303
  "code": "B1300",
@@ -2466,12 +2396,7 @@
2466
2396
  "category": "Network",
2467
2397
  "severity": "Critical",
2468
2398
  "system": "CAN Bus",
2469
- "possible_causes": [
2470
- "TCM not powered",
2471
- "CAN bus wiring problem",
2472
- "TCM internal fault",
2473
- "CAN bus short circuit"
2474
- ]
2399
+ "possible_causes": ["TCM not powered", "CAN bus wiring problem", "TCM internal fault", "CAN bus short circuit"]
2475
2400
  },
2476
2401
  "U0102": {
2477
2402
  "code": "U0102",
@@ -2544,12 +2469,7 @@
2544
2469
  "category": "Network",
2545
2470
  "severity": "High",
2546
2471
  "system": "CAN Bus",
2547
- "possible_causes": [
2548
- "BCM not powered",
2549
- "CAN bus wiring problem",
2550
- "BCM internal fault",
2551
- "Ground connection issue"
2552
- ]
2472
+ "possible_causes": ["BCM not powered", "CAN bus wiring problem", "BCM internal fault", "Ground connection issue"]
2553
2473
  },
2554
2474
  "U0141": {
2555
2475
  "code": "U0141",
@@ -2557,12 +2477,7 @@
2557
2477
  "category": "Network",
2558
2478
  "severity": "High",
2559
2479
  "system": "CAN Bus",
2560
- "possible_causes": [
2561
- "BCM not powered",
2562
- "CAN bus wiring problem",
2563
- "Module internal fault",
2564
- "Connector problem"
2565
- ]
2480
+ "possible_causes": ["BCM not powered", "CAN bus wiring problem", "Module internal fault", "Connector problem"]
2566
2481
  },
2567
2482
  "U0151": {
2568
2483
  "code": "U0151",
@@ -149,20 +149,14 @@
149
149
  "type": "array",
150
150
  "description": "Device IDs to include (whitelist)",
151
151
  "items": {
152
- "oneOf": [
153
- { "type": "integer" },
154
- { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }
155
- ]
152
+ "oneOf": [{ "type": "integer" }, { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }]
156
153
  }
157
154
  },
158
155
  "exclude_devices": {
159
156
  "type": "array",
160
157
  "description": "Device IDs to exclude (blacklist)",
161
158
  "items": {
162
- "oneOf": [
163
- { "type": "integer" },
164
- { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }
165
- ]
159
+ "oneOf": [{ "type": "integer" }, { "type": "string", "pattern": "^0[xX][0-9A-Fa-f]+$" }]
166
160
  }
167
161
  },
168
162
  "include_categories": {
@@ -118,10 +118,7 @@
118
118
  },
119
119
  "value": {
120
120
  "description": "Expected constant value for validation",
121
- "oneOf": [
122
- { "type": "integer" },
123
- { "type": "array", "items": { "type": "integer" } }
124
- ]
121
+ "oneOf": [{ "type": "integer" }, { "type": "array", "items": { "type": "integer" } }]
125
122
  },
126
123
  "description": {
127
124
  "type": "string",
@@ -188,18 +185,7 @@
188
185
  },
189
186
  "type": {
190
187
  "type": "string",
191
- "enum": [
192
- "uint8",
193
- "uint16",
194
- "uint32",
195
- "uint64",
196
- "int8",
197
- "int16",
198
- "int32",
199
- "int64",
200
- "float32",
201
- "float64"
202
- ],
188
+ "enum": ["uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float32", "float64"],
203
189
  "description": "Sample data type"
204
190
  },
205
191
  "endian": {
@@ -303,10 +289,7 @@
303
289
  },
304
290
  "expected": {
305
291
  "description": "Expected value",
306
- "oneOf": [
307
- { "type": "integer" },
308
- { "type": "array", "items": { "type": "integer" } }
309
- ]
292
+ "oneOf": [{ "type": "integer" }, { "type": "array", "items": { "type": "integer" } }]
310
293
  },
311
294
  "on_failure": {
312
295
  "type": "string",
@@ -379,10 +362,7 @@
379
362
  },
380
363
  "pattern": {
381
364
  "description": "Idle pattern to detect",
382
- "oneOf": [
383
- { "type": "string", "enum": ["auto", "zeros", "ones"] },
384
- { "type": "integer" }
385
- ]
365
+ "oneOf": [{ "type": "string", "enum": ["auto", "zeros", "ones"] }, { "type": "integer" }]
386
366
  },
387
367
  "min_duration": {
388
368
  "type": "integer",
@@ -241,12 +241,7 @@
241
241
  },
242
242
  "value": {
243
243
  "description": "Expected constant value for validation",
244
- "oneOf": [
245
- { "type": "integer" },
246
- { "type": "number" },
247
- { "type": "string" },
248
- { "type": "array" }
249
- ]
244
+ "oneOf": [{ "type": "integer" }, { "type": "number" }, { "type": "string" }, { "type": "array" }]
250
245
  },
251
246
  "condition": {
252
247
  "type": "string",
@@ -331,12 +326,7 @@
331
326
  },
332
327
  "expected": {
333
328
  "description": "Expected value",
334
- "oneOf": [
335
- { "type": "integer" },
336
- { "type": "number" },
337
- { "type": "string" },
338
- { "type": "array" }
339
- ]
329
+ "oneOf": [{ "type": "integer" }, { "type": "number" }, { "type": "string" }, { "type": "array" }]
340
330
  },
341
331
  "on_mismatch": {
342
332
  "type": "string",
@@ -29,6 +29,7 @@ from oscura.core.types import DigitalTrace, IQTrace, WaveformTrace
29
29
  _LOADER_REGISTRY: dict[str, tuple[str, str]] = {
30
30
  "tektronix": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
31
31
  "tek": ("oscura.loaders.tektronix", "load_tektronix_wfm"),
32
+ "tss": ("oscura.loaders.tss", "load_tss"),
32
33
  "rigol": ("oscura.loaders.rigol", "load_rigol_wfm"),
33
34
  "numpy": ("oscura.loaders.numpy_loader", "load_npz"),
34
35
  "csv": ("oscura.loaders.csv_loader", "load_csv"),
@@ -180,6 +181,7 @@ logger = logging.getLogger(__name__)
180
181
  # Supported format extensions mapped to loader names
181
182
  SUPPORTED_FORMATS: dict[str, str] = {
182
183
  ".wfm": "auto_wfm", # Auto-detect Tektronix vs Rigol
184
+ ".tss": "tss", # Tektronix session files
183
185
  ".npz": "numpy",
184
186
  ".csv": "csv",
185
187
  ".h5": "hdf5",
@@ -392,8 +394,8 @@ def load_all_channels(
392
394
  )
393
395
  loader_name = SUPPORTED_FORMATS[ext]
394
396
 
395
- # Currently only supports Tektronix WFM for multi-channel loading
396
- if loader_name in ("auto_wfm", "tektronix", "tek"):
397
+ # Currently only supports Tektronix WFM and TSS for multi-channel loading
398
+ if loader_name in ("auto_wfm", "tektronix", "tek", "tss"):
397
399
  return _load_all_channels_tektronix(path)
398
400
  else:
399
401
  # For other formats, try loading as single channel
@@ -405,10 +407,10 @@ def load_all_channels(
405
407
  def _load_all_channels_tektronix(
406
408
  path: Path,
407
409
  ) -> dict[str, WaveformTrace | DigitalTrace | IQTrace]:
408
- """Load all channels from a Tektronix WFM file.
410
+ """Load all channels from a Tektronix WFM or TSS file.
409
411
 
410
412
  Args:
411
- path: Path to the Tektronix .wfm file.
413
+ path: Path to the Tektronix .wfm or .tss file.
412
414
 
413
415
  Returns:
414
416
  Dictionary mapping channel names to traces.
@@ -416,6 +418,12 @@ def _load_all_channels_tektronix(
416
418
  Raises:
417
419
  LoaderError: If the file cannot be read or parsed.
418
420
  """
421
+ # Check if this is a .tss session file
422
+ if path.suffix.lower() == ".tss":
423
+ from oscura.loaders.tss import load_all_channels_tss
424
+
425
+ return load_all_channels_tss(path)
426
+
419
427
  wfm = _read_tektronix_file(path)
420
428
  channels: dict[str, WaveformTrace | DigitalTrace | IQTrace] = {}
421
429
 
oscura/loaders/tss.py ADDED
@@ -0,0 +1,456 @@
1
+ """Tektronix Session File (.tss) Loader.
2
+
3
+ This module provides loading functionality for Tektronix session files (.tss),
4
+ which are ZIP archives containing multiple waveform captures, instrument
5
+ configuration, measurements, and annotations.
6
+
7
+ A .tss session file typically contains:
8
+ - Multiple .wfm waveform files (one per channel/capture)
9
+ - session.json: Instrument configuration and setup
10
+ - measurements.json: Stored measurement results (optional)
11
+ - annotations.json: User annotations and markers (optional)
12
+
13
+ Example:
14
+ >>> import oscura as osc
15
+ >>> trace = osc.load("oscilloscope_session.tss")
16
+ >>> print(f"Channel: {trace.metadata.channel_name}")
17
+ >>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
18
+
19
+ >>> # Load specific channel
20
+ >>> trace = osc.load("session.tss", channel="CH2")
21
+
22
+ >>> # Load all channels
23
+ >>> channels = osc.load_all_channels("session.tss")
24
+ >>> for name, trace in channels.items():
25
+ ... print(f"{name}: {len(trace.data)} samples")
26
+
27
+ References:
28
+ Tektronix Programming Manual for Session Files
29
+ TekScope PC Analysis Software Documentation
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import json
35
+ import tempfile
36
+ import zipfile
37
+ from os import PathLike
38
+ from pathlib import Path
39
+ from typing import Any
40
+
41
+ from oscura.core.exceptions import FormatError, LoaderError
42
+ from oscura.core.types import DigitalTrace, IQTrace, WaveformTrace
43
+
44
+
45
+ def load_tss(
46
+ path: str | PathLike[str],
47
+ *,
48
+ channel: str | int | None = None,
49
+ ) -> WaveformTrace | DigitalTrace | IQTrace:
50
+ """Load a Tektronix session file (.tss).
51
+
52
+ Tektronix session files are ZIP archives containing multiple waveform
53
+ captures along with instrument configuration and analysis results.
54
+
55
+ Args:
56
+ path: Path to the Tektronix .tss session file.
57
+ channel: Optional channel name or index to load. If None,
58
+ loads the first waveform found (alphabetically).
59
+ String names are case-insensitive (e.g., "ch1", "CH1").
60
+ Integer index is 0-based.
61
+
62
+ Returns:
63
+ WaveformTrace, DigitalTrace, or IQTrace containing the channel data.
64
+
65
+ Raises:
66
+ LoaderError: If the file cannot be loaded or doesn't exist.
67
+ FormatError: If the file is not a valid Tektronix session.
68
+
69
+ Example:
70
+ >>> # Load first channel (default)
71
+ >>> trace = load_tss("session.tss")
72
+
73
+ >>> # Load specific channel by name
74
+ >>> trace = load_tss("session.tss", channel="CH2")
75
+
76
+ >>> # Load by index
77
+ >>> trace = load_tss("session.tss", channel=1) # Second channel
78
+ """
79
+ path = Path(path)
80
+
81
+ # Validate file
82
+ _validate_tss_file(path)
83
+
84
+ # Load all waveforms and select requested channel
85
+ waveforms = _load_all_waveforms(path)
86
+
87
+ if not waveforms:
88
+ raise FormatError(
89
+ "No waveforms found in session file",
90
+ file_path=str(path),
91
+ expected="At least one .wfm file in the archive",
92
+ got="Empty session or no waveform files",
93
+ )
94
+
95
+ # Select channel
96
+ trace, channel_name = _select_channel(waveforms, channel, path)
97
+
98
+ # Enrich metadata with session information
99
+ try:
100
+ with zipfile.ZipFile(path, "r") as zf:
101
+ session_metadata = _parse_session_metadata(zf, path)
102
+ trace = _enrich_metadata_from_session(trace, session_metadata, str(path))
103
+ except Exception:
104
+ # Session metadata is optional, continue with waveform metadata
105
+ pass
106
+
107
+ return trace
108
+
109
+
110
+ def load_all_channels_tss(
111
+ path: Path,
112
+ ) -> dict[str, WaveformTrace | DigitalTrace | IQTrace]:
113
+ """Load all channels from a Tektronix session file.
114
+
115
+ Args:
116
+ path: Path to the .tss session file.
117
+
118
+ Returns:
119
+ Dictionary mapping channel names to traces.
120
+ Channel names are derived from .wfm filenames (e.g., "ch1", "ch2").
121
+
122
+ Raises:
123
+ LoaderError: If the file cannot be loaded.
124
+ FormatError: If no waveforms found in session.
125
+
126
+ Example:
127
+ >>> channels = load_all_channels_tss(Path("session.tss"))
128
+ >>> for name, trace in channels.items():
129
+ ... print(f"{name}: {trace.metadata.sample_rate} Hz")
130
+ """
131
+ # Validate file
132
+ _validate_tss_file(path)
133
+
134
+ # Load all waveforms
135
+ waveforms = _load_all_waveforms(path)
136
+
137
+ if not waveforms:
138
+ raise FormatError(
139
+ "No waveforms found in session file",
140
+ file_path=str(path),
141
+ expected="At least one .wfm file",
142
+ got="Empty archive",
143
+ )
144
+
145
+ # Enrich all traces with session metadata
146
+ try:
147
+ with zipfile.ZipFile(path, "r") as zf:
148
+ session_metadata = _parse_session_metadata(zf, path)
149
+ for name in waveforms:
150
+ waveforms[name] = _enrich_metadata_from_session(
151
+ waveforms[name], session_metadata, str(path)
152
+ )
153
+ except Exception:
154
+ # Session metadata is optional
155
+ pass
156
+
157
+ return waveforms
158
+
159
+
160
+ def _validate_tss_file(path: Path) -> None:
161
+ """Validate that file exists and is a valid ZIP archive.
162
+
163
+ Args:
164
+ path: Path to validate.
165
+
166
+ Raises:
167
+ LoaderError: If file doesn't exist or can't be read.
168
+ FormatError: If file is not a ZIP archive.
169
+ """
170
+ if not path.exists():
171
+ raise LoaderError(
172
+ "File not found",
173
+ file_path=str(path),
174
+ fix_hint="Ensure the file path is correct and the file exists.",
175
+ )
176
+
177
+ if not zipfile.is_zipfile(path):
178
+ raise FormatError(
179
+ "Not a valid ZIP archive",
180
+ file_path=str(path),
181
+ expected="Tektronix session file (.tss) is a ZIP archive",
182
+ got="File is not a ZIP file",
183
+ )
184
+
185
+ try:
186
+ with zipfile.ZipFile(path, "r") as zf:
187
+ # Test archive integrity
188
+ bad_file = zf.testzip()
189
+ if bad_file is not None:
190
+ raise FormatError(
191
+ f"Corrupted ZIP archive: {bad_file}",
192
+ file_path=str(path),
193
+ expected="Valid ZIP archive",
194
+ got="Corrupted file detected",
195
+ )
196
+ except zipfile.BadZipFile as e:
197
+ raise FormatError(
198
+ "Corrupted or invalid ZIP archive",
199
+ file_path=str(path),
200
+ details=str(e),
201
+ ) from e
202
+
203
+
204
+ def _parse_session_metadata(zf: zipfile.ZipFile, path: Path) -> dict[str, Any]:
205
+ """Parse session.json metadata from the archive.
206
+
207
+ Args:
208
+ zf: Open ZipFile object.
209
+ path: Path to session file (for error messages).
210
+
211
+ Returns:
212
+ Dictionary containing session metadata.
213
+ Returns empty dict if session.json not found (non-fatal).
214
+ """
215
+ # Look for session metadata files
216
+ metadata_files = [
217
+ "session.json",
218
+ "Session.json",
219
+ "metadata.json",
220
+ "Metadata.json",
221
+ ]
222
+
223
+ for metadata_file in metadata_files:
224
+ try:
225
+ with zf.open(metadata_file) as f:
226
+ data: dict[str, Any] = json.load(f)
227
+ return data
228
+ except KeyError:
229
+ continue
230
+ except json.JSONDecodeError as e:
231
+ # Non-fatal: log warning and continue
232
+ import warnings
233
+
234
+ warnings.warn(
235
+ f"Failed to parse {metadata_file} in {path.name}: {e}",
236
+ stacklevel=2,
237
+ )
238
+ return {}
239
+
240
+ # Session metadata is optional
241
+ return {}
242
+
243
+
244
+ def _extract_waveform_list(zf: zipfile.ZipFile) -> list[str]:
245
+ """Extract list of .wfm files in the session.
246
+
247
+ Args:
248
+ zf: Open ZipFile object.
249
+
250
+ Returns:
251
+ List of .wfm file paths within the archive.
252
+ Excludes macOS metadata files (__MACOSX).
253
+ """
254
+ return [
255
+ name
256
+ for name in zf.namelist()
257
+ if name.lower().endswith(".wfm") and not name.startswith("__MACOSX")
258
+ ]
259
+
260
+
261
+ def _load_wfm_from_archive(
262
+ zf: zipfile.ZipFile,
263
+ wfm_name: str,
264
+ path: Path,
265
+ ) -> WaveformTrace | DigitalTrace | IQTrace:
266
+ """Extract and load a .wfm file from the archive.
267
+
268
+ Args:
269
+ zf: Open ZipFile object.
270
+ wfm_name: Name of .wfm file within archive.
271
+ path: Path to session file (for error messages).
272
+
273
+ Returns:
274
+ Loaded trace from the waveform file.
275
+
276
+ Raises:
277
+ LoaderError: If waveform cannot be loaded.
278
+ """
279
+ try:
280
+ # Extract waveform to temporary file
281
+ # (load_tektronix_wfm expects file path, not bytes)
282
+ wfm_bytes = zf.read(wfm_name)
283
+
284
+ with tempfile.NamedTemporaryFile(suffix=".wfm", delete=True) as tmp:
285
+ tmp.write(wfm_bytes)
286
+ tmp.flush()
287
+
288
+ # Use existing Tektronix loader
289
+ from oscura.loaders.tektronix import load_tektronix_wfm
290
+
291
+ trace = load_tektronix_wfm(tmp.name)
292
+
293
+ return trace
294
+
295
+ except Exception as e:
296
+ raise LoaderError(
297
+ f"Failed to load waveform from session: {wfm_name}",
298
+ file_path=str(path),
299
+ details=str(e),
300
+ fix_hint="Waveform file may be corrupted or incompatible.",
301
+ ) from e
302
+
303
+
304
+ def _load_all_waveforms(path: Path) -> dict[str, WaveformTrace | DigitalTrace | IQTrace]:
305
+ """Load all waveforms from the session file.
306
+
307
+ Args:
308
+ path: Path to .tss session file.
309
+
310
+ Returns:
311
+ Dictionary mapping channel names to traces.
312
+ """
313
+ waveforms: dict[str, WaveformTrace | DigitalTrace | IQTrace] = {}
314
+
315
+ with zipfile.ZipFile(path, "r") as zf:
316
+ wfm_files = _extract_waveform_list(zf)
317
+
318
+ for wfm_name in sorted(wfm_files): # Sort for consistent ordering
319
+ # Derive channel name from filename
320
+ channel_name = _derive_channel_name(wfm_name)
321
+
322
+ # Load waveform
323
+ trace = _load_wfm_from_archive(zf, wfm_name, path)
324
+
325
+ # Store with normalized channel name
326
+ waveforms[channel_name] = trace
327
+
328
+ return waveforms
329
+
330
+
331
+ def _derive_channel_name(wfm_filename: str) -> str:
332
+ """Derive channel name from .wfm filename.
333
+
334
+ Args:
335
+ wfm_filename: Filename like "CH1.wfm", "CH2_Voltage.wfm", etc.
336
+
337
+ Returns:
338
+ Normalized channel name (lowercase, e.g., "ch1", "ch2", "d0").
339
+
340
+ Examples:
341
+ >>> _derive_channel_name("CH1.wfm")
342
+ 'ch1'
343
+ >>> _derive_channel_name("subdir/CH2_Voltage.wfm")
344
+ 'ch2'
345
+ >>> _derive_channel_name("D0.wfm")
346
+ 'd0'
347
+ >>> _derive_channel_name("MATH1.wfm")
348
+ 'math1'
349
+ """
350
+ # Get base filename without path
351
+ basename = Path(wfm_filename).stem # Remove extension
352
+
353
+ # Remove path components if nested
354
+ basename = basename.split("/")[-1].split("\\")[-1]
355
+
356
+ # Extract channel identifier (first part before underscore)
357
+ channel_id = basename.split("_")[0]
358
+
359
+ # Normalize to lowercase
360
+ return channel_id.lower()
361
+
362
+
363
+ def _select_channel(
364
+ waveforms: dict[str, WaveformTrace | DigitalTrace | IQTrace],
365
+ channel: str | int | None,
366
+ path: Path,
367
+ ) -> tuple[WaveformTrace | DigitalTrace | IQTrace, str]:
368
+ """Select specific channel from waveforms dictionary.
369
+
370
+ Args:
371
+ waveforms: Dictionary of channel name to trace.
372
+ channel: Channel selector (name, index, or None for first).
373
+ path: Path to session file (for error messages).
374
+
375
+ Returns:
376
+ Tuple of (selected_trace, channel_name).
377
+
378
+ Raises:
379
+ LoaderError: If channel not found or index out of range.
380
+ """
381
+ if channel is None:
382
+ # Default: first channel (alphabetically sorted)
383
+ channel_name = sorted(waveforms.keys())[0]
384
+ return waveforms[channel_name], channel_name
385
+
386
+ if isinstance(channel, int):
387
+ # Select by index
388
+ channel_names = sorted(waveforms.keys())
389
+ if channel < 0 or channel >= len(channel_names):
390
+ raise LoaderError(
391
+ f"Channel index {channel} out of range",
392
+ file_path=str(path),
393
+ fix_hint=f"Available channels: {', '.join(channel_names)} (indices 0-{len(channel_names) - 1})",
394
+ )
395
+ channel_name = channel_names[channel]
396
+ return waveforms[channel_name], channel_name
397
+
398
+ # Select by name (case-insensitive)
399
+ channel_lower = channel.lower()
400
+ for name, trace in waveforms.items():
401
+ if name.lower() == channel_lower:
402
+ return trace, name
403
+
404
+ # Channel not found
405
+ available = ", ".join(sorted(waveforms.keys()))
406
+ raise LoaderError(
407
+ f"Channel '{channel}' not found in session",
408
+ file_path=str(path),
409
+ fix_hint=f"Available channels: {available}",
410
+ )
411
+
412
+
413
+ def _enrich_metadata_from_session(
414
+ trace: WaveformTrace | DigitalTrace | IQTrace,
415
+ session_metadata: dict[str, Any],
416
+ source_file: str,
417
+ ) -> WaveformTrace | DigitalTrace | IQTrace:
418
+ """Enrich waveform metadata with session-level information.
419
+
420
+ Args:
421
+ trace: Original trace from .wfm file.
422
+ session_metadata: Session metadata from session.json.
423
+ source_file: Path to .tss file (for source_file metadata).
424
+
425
+ Returns:
426
+ Trace with enriched metadata.
427
+ """
428
+ # Create new metadata with session information
429
+ from dataclasses import replace
430
+
431
+ metadata = trace.metadata
432
+
433
+ # Update source file to point to .tss instead of temp .wfm
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"])
439
+
440
+ # Return trace with updated metadata
441
+ if isinstance(trace, WaveformTrace):
442
+ return WaveformTrace(data=trace.data, metadata=metadata)
443
+ if isinstance(trace, DigitalTrace):
444
+ return DigitalTrace(data=trace.data, metadata=metadata, edges=trace.edges)
445
+ # IQTrace
446
+ return IQTrace(
447
+ i_data=trace.i_data,
448
+ q_data=trace.q_data,
449
+ metadata=metadata,
450
+ )
451
+
452
+
453
+ __all__ = [
454
+ "load_all_channels_tss",
455
+ "load_tss",
456
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oscura
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Unified hardware reverse engineering framework. Extract all information from any system through signals and data. Unknown protocol discovery, state machine extraction, CRC recovery, security analysis. 16+ protocols, IEEE-compliant measurements.
5
5
  Project-URL: Homepage, https://github.com/oscura-re/oscura
6
6
  Project-URL: Documentation, https://github.com/oscura-re/oscura/tree/main/docs
@@ -292,7 +292,7 @@ result = osc.auto_decode(trace)
292
292
  print(f"Detected {result.protocol}: {len(result.frames)} frames decoded")
293
293
  ```
294
294
 
295
- [**6 working examples**](examples/) demonstrating core workflows and analysis patterns.
295
+ **[See demos/README.md](demos/README.md)** for 105+ comprehensive demonstrations organized by skill level.
296
296
 
297
297
  ---
298
298
 
@@ -439,32 +439,50 @@ Built for extensibility:
439
439
 
440
440
  ## Learn By Doing
441
441
 
442
- ### Working Examples
442
+ ### Working Demonstrations
443
443
 
444
- Core functionality demonstrated with working code:
444
+ **105+ comprehensive demos** organized into 12 categories covering:
445
445
 
446
- - [Side-Channel Analysis](examples/side_channel_analysis_demo.py) - DPA/CPA attacks, trace analysis
447
- - [ML Signal Classification](examples/ml_signal_classification_demo.py) - Machine learning for signal identification
448
- - [Wireshark Dissector Generation](examples/export/wireshark_dissector_demo.py) - Auto-generate protocol dissectors
449
- - [DBC File Generation](examples/automotive/dbc_generation_example.py) - CAN bus DBC export
450
- - [LIN Bus Analysis](examples/automotive/lin_analysis_example.py) - LIN protocol decoding
451
- - [Web Dashboard](examples/web_dashboard_example.py) - Interactive visualization
446
+ - **Data Loading** - All file format loaders (oscilloscopes, logic analyzers, automotive, scientific)
447
+ - **Basic Analysis** - Waveform measurements, digital analysis, spectral analysis, filtering
448
+ - **Protocol Decoding** - UART, SPI, I2C, CAN, LIN, FlexRay, JTAG, SWD, I2S, USB
449
+ - **Advanced Analysis** - Jitter, eye diagrams, power analysis, signal integrity, TDR
450
+ - **Domain Specific** - Automotive diagnostics, EMC compliance, side-channel analysis, IEEE 181 timing
451
+ - **Reverse Engineering** - CRC recovery, state machines, Wireshark dissectors, ML classification
452
+ - **Advanced Features** - Lazy loading, memory management, performance optimization, batch processing
453
+ - **Extensibility** - Custom analyzers, plugins, templates
454
+ - **Integration** - CI/CD, hardware, external tools, web dashboards
455
+ - **Export & Visualization** - All export formats, plotting, reporting
456
+ - **Complete Workflows** - End-to-end production pipelines
457
+ - **Standards Compliance** - IEEE 181/1241/1459/2414, automotive standards
452
458
 
453
- ### Run Your First Example
459
+ ### Comprehensive Demonstrations
460
+
461
+ **33+ in-depth demos** organized by skill level and domain:
462
+
463
+ - **[Getting Started](demos/README.md#beginner-path-2-4-hours)** - File loading, basic measurements, format export (Beginner, 2-4 hours)
464
+ - **[Protocol Decoding](demos/README.md#intermediate-path-6-10-hours)** - UART, SPI, I2C, Manchester, JTAG, USB, PCAP (Intermediate, 6-10 hours)
465
+ - **[Reverse Engineering](demos/README.md#advanced-path-12-20-hours)** - CRC recovery, state machines, Wireshark dissectors, automotive protocols (Advanced, 12-20 hours)
466
+ - **[Standards Compliance](demos/README.md#advanced-path-12-20-hours)** - IEEE 181/1241/1459/2414, CISPR 32, IEC 61000 (Advanced/Expert)
467
+ - **[Complete Workflows](demos/README.md#expert-path-20-40-hours)** - End-to-end production pipelines with ML inference (Expert, 20-40 hours)
468
+
469
+ **Categories**: Waveform Analysis | File I/O | Custom DAQ | Serial Protocols | Protocol Decoding | UDP Analysis | Protocol Inference | Automotive | Timing | Mixed Signal | Spectral | Jitter | Power | Signal Integrity | EMC | Signal RE | Advanced Inference | Complete Workflows
470
+
471
+ [**See full demo catalog with learning paths**](demos/README.md)
472
+
473
+ ### Run Your First Demo
454
474
 
455
475
  ```bash
456
476
  # Install development dependencies
457
477
  ./scripts/setup.sh
458
478
 
459
- # Side-channel analysis demo
460
- python examples/side_channel_analysis_demo.py
479
+ # Run your first demo
480
+ python demos/00_getting_started/00_hello_world.py
461
481
 
462
- # ML signal classification
463
- python examples/ml_signal_classification_demo.py
482
+ # Or try a specific topic
483
+ python demos/05_domain_specific/05_side_channel_basics.py
464
484
  ```
465
485
 
466
- [**Browse all examples**](examples/)
467
-
468
486
  ---
469
487
 
470
488
  ## Command-Line Interface
@@ -556,7 +574,7 @@ python3 .claude/hooks/validate_all.py # Must show 5/5 passing
556
574
  | **Real-World Validation** | Test on your captures, report issues | Ensure reliability across use cases |
557
575
  | **Documentation & Case Studies** | Tutorials, sanitized RE workflows, academic papers using Oscura | Lower entry barrier, demonstrate capabilities |
558
576
 
559
- [**Contributing Guide**](CONTRIBUTING.md) | [Architecture Documentation](docs/architecture/)
577
+ [**Contributing Guide**](CONTRIBUTING.md) | [Architecture Documentation](docs/developer-guide/architecture.md)
560
578
 
561
579
  ### Community
562
580
 
@@ -584,7 +602,7 @@ python3 .claude/hooks/validate_all.py # Must show 5/5 passing
584
602
 
585
603
  ### Development
586
604
 
587
- - [Architecture](docs/architecture/) - Design principles and patterns
605
+ - [Architecture](docs/developer-guide/architecture.md) - Design principles and patterns
588
606
  - [Testing Guide](docs/testing/) - Test suite architecture
589
607
  - [CHANGELOG](CHANGELOG.md) - Version history and migration guides
590
608
 
@@ -1,4 +1,4 @@
1
- oscura/__init__.py,sha256=sZMIZXUx6o16YS0E_1XC19L0dIduZHJgTFa5960TenQ,19022
1
+ oscura/__init__.py,sha256=cB_SbWzKA60qDD2BQHesVvh_GOgWI7XOGfOOwd42YyY,19022
2
2
  oscura/__main__.py,sha256=l1rnaD-tpI1W3cp1VHGhSdq9NDqw6Gxjf_gnqXf-zzE,11786
3
3
  oscura/convenience.py,sha256=tAABQMwgKjftSNUJLlYnchAwVSO-no0rBfSZteXfZnI,16361
4
4
  oscura/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -159,7 +159,7 @@ oscura/api/server/templates/reports.html,sha256=18xoAZPIg7eQANs9k-tmGzVs_-tmS5m6
159
159
  oscura/api/server/templates/session_detail.html,sha256=32HnJ0o8ptlZEHQVO1c96j6PoxS0856kPjW2MVn0D8o,3693
160
160
  oscura/api/server/templates/sessions.html,sha256=kVcpLOlXAx0PizP7Vc1NlhsYg5swTj77fKCZxoTTCYU,3146
161
161
  oscura/api/server/templates/waveforms.html,sha256=5XfpHkgRpmV0MeFb9pBkVDa63phT8zGWnrmA0UlUwZE,2269
162
- oscura/automotive/__init__.py,sha256=MhHsef7rFKAn_0souQIh2BQPcIlKnBzBOryTr5oww-I,2767
162
+ oscura/automotive/__init__.py,sha256=PCTYjEm5mVPrBiOmYyhPul9utHa955ASz-Oq7h_nYy8,2767
163
163
  oscura/automotive/visualization.py,sha256=LD6Gia3kDqoB_jQkOeVmyRVLv1BPuTUVj6lJMVtbQWg,10664
164
164
  oscura/automotive/can/__init__.py,sha256=ZYxC8tMMi9Drm9adkH5yklypBTfIbf3D7BYA2SUp9fE,1491
165
165
  oscura/automotive/can/analysis.py,sha256=0z6MycxuJGusqDQhYEpHnGRmDw3OMf5yDbS8zI6GJO0,11251
@@ -177,7 +177,7 @@ oscura/automotive/dbc/__init__.py,sha256=HZlM2WAaaglpX4C9tJVkF7uZiaHW4iUPQAbhfJY
177
177
  oscura/automotive/dbc/generator.py,sha256=Dxg6fFHYNxtqExKxf0S63MeZsHriisA6R-Ii7WwsfPY,6724
178
178
  oscura/automotive/dbc/parser.py,sha256=C5SL1bfJFZp0CYsw-ff8ysJ_57Us9dBoHZ1t6LVLGbo,4421
179
179
  oscura/automotive/dtc/__init__.py,sha256=09CgvnClJTQIvzgDOQ22CbcjGAg_zOhPlbm1-k-gl30,895
180
- oscura/automotive/dtc/data.json,sha256=OrymnbjQkVMdJlIeQ6MUDIihkd9Kea-YvAizZwOZWcc,80011
180
+ oscura/automotive/dtc/data.json,sha256=5pddTolfBJjwimwp5epiMjUqSngZ1dur9LQlcHfdjrA,79331
181
181
  oscura/automotive/dtc/database.py,sha256=Ee1Jd4CbP5T4LON0Y2PdOkgdISHa8jf8c6znb0S_L00,9582
182
182
  oscura/automotive/flexray/__init__.py,sha256=2RuW8TR4M3tSDEfcj-pJQpGGSxGaw5GdyGSvjMnU8Lk,938
183
183
  oscura/automotive/flexray/analyzer.py,sha256=UG3e9tJSHGV7aR3WKwfVCrNDSVlT7ZgFm7vgCcw0qOo,16727
@@ -284,9 +284,9 @@ oscura/core/plugins/registry.py,sha256=XNtq0rBuppC1Z5V5RH74afW-LBnWSLc33lCE6BDZK
284
284
  oscura/core/plugins/versioning.py,sha256=d18sXMbgeWy_dVNaM7JWvCVIF6VgpnjIbjP8gdFfgSs,10785
285
285
  oscura/core/schemas/__init__.py,sha256=W33kQp0A8v9qZevqjm3T-1Fjrnou5cz0WCAbLRkwbgY,4327
286
286
  oscura/core/schemas/bus_configuration.json,sha256=gpcDsg04760KCaLeIDuWvP6RzRUcPZuQplJbe7xpc8E,9562
287
- oscura/core/schemas/device_mapping.json,sha256=sYOcc2zSe0rmMQN_vtg3Y5XeeDMktAO1ar8vAWl-E1M,5499
288
- oscura/core/schemas/packet_format.json,sha256=y2KavMGeOUlmjDq7AW_85bfTk3nHlGPuistpuVDdlAk,12893
289
- oscura/core/schemas/protocol_definition.json,sha256=VPHkgY4fAI-hUiRBtiqZvDNUGjp6_7O4xdi1z36IhCI,11256
287
+ oscura/core/schemas/device_mapping.json,sha256=I87-MWnXldVWIXRC1H-WgzNLszymRdnaxwE3B1z8qys,5415
288
+ oscura/core/schemas/packet_format.json,sha256=NVa4RtwSye9QgD1pBi67nuCDRsVljudCpBBGjmZsMqk,12567
289
+ oscura/core/schemas/protocol_definition.json,sha256=lThVXTWzh7GxN-XNb3h8Bh20l1NZUigivWBb04xvogs,11116
290
290
  oscura/correlation/__init__.py,sha256=4H5lCJqBBG3_10oIPsdIOyCij8FlKtMxLp3SAYvSsRg,1537
291
291
  oscura/correlation/multi_protocol.py,sha256=X3sTdAc6etsY_9GIbXISCeNMNWM8imkGzHn-UhK7rcw,28288
292
292
  oscura/discovery/__init__.py,sha256=ytYa_s4PfKxRRopOdUGsmzUIDPevH25ze_VytBEX2Cw,1232
@@ -376,7 +376,7 @@ oscura/jupyter/exploratory/unknown.py,sha256=KaxpoP9tW9E7Ld16_3VaXaKszfcU2uw62Id
376
376
  oscura/jupyter/ui/__init__.py,sha256=XQJh89b_xRe1CB8jssjIdbVgQeLCplFzSkCiXQYKXFU,935
377
377
  oscura/jupyter/ui/formatters.py,sha256=roXZhaNLgypb30T6EGF805cDkrqsAh_RQ978iGLtlAU,13580
378
378
  oscura/jupyter/ui/progressive_display.py,sha256=Ew-xXek_f6-XkR0CZymyzYbYkbXn8qCZS6Usk2n86dQ,11993
379
- oscura/loaders/__init__.py,sha256=6XL9e9V3c5sE4tNyNVE4NNEvk9g16lA8nGfyewbtfzs,19927
379
+ oscura/loaders/__init__.py,sha256=f1yakezyiv6IjrsK_L7H1jFCwQUAGNCvjv5DIKHDgzo,20237
380
380
  oscura/loaders/binary.py,sha256=USQTTdWjbrn3b-Ly4v-E5eLhga6kM73QlNKxnLm1YUw,5062
381
381
  oscura/loaders/chipwhisperer.py,sha256=gkxgakx0YXxJ4dKKkSmlOZYa-VKdWFldAbz3PPAvWyw,12756
382
382
  oscura/loaders/configurable.py,sha256=QyyqN_hMBKafx1wyMCcQrykRFY_TfWFU8XLYXztjnFY,44483
@@ -394,6 +394,7 @@ oscura/loaders/sigrok.py,sha256=BliW7KuLIKpyRkupaZTGlSczVoHWbK0-HBs2n4r5oVM,1302
394
394
  oscura/loaders/tdms.py,sha256=J1qDLiQMDs_7ayR04g9hsP39hs7LkYy7jJEc57BUPzw,11114
395
395
  oscura/loaders/tektronix.py,sha256=V-ns3yCqsXSkeZ1zNXbOZZuViMzYagsiiHr12WeKc4s,26189
396
396
  oscura/loaders/touchstone.py,sha256=Cj4KlQQMcSEZuf5M0hg3lFvXvnuPZkcLa8-X-qKEObA,8898
397
+ oscura/loaders/tss.py,sha256=c0xL2h0x09u765qFAxOKpFE8ggOGpc9DwUyRi6r41Y0,13874
397
398
  oscura/loaders/validation.py,sha256=JA_vVYQJB4gC9KFKa2lsGV5D-_agmIjP9kE_dXpv4Wk,17835
398
399
  oscura/loaders/vcd.py,sha256=y1mFZXTEqJEFfvMf3MYgGk5e16qG7mNlyAQgVLZNwfU,17722
399
400
  oscura/loaders/wav.py,sha256=xv6eELWMYflP7OormysAe6-yAiYNzWMInzC6cuUZZmo,9391
@@ -583,8 +584,8 @@ oscura/workflows/batch/logging.py,sha256=6YkTrOA2OSiRjYHmtRn-lL2wyNpya0sggVkgxsz
583
584
  oscura/workflows/batch/metrics.py,sha256=InbxaZmFQrANpp_RCcx5F3va55UaiQjhFJ-_SEKAS20,17322
584
585
  oscura/workflows/legacy/__init__.py,sha256=PqHw8AIWfP4-1lJEjCDC4c_O8GXYOcWodK9ciOnGJaU,409
585
586
  oscura/workflows/legacy/dag.py,sha256=tEhiWmOSdcEYKmrwYUETC0P5yN-_Cy9fEysPwTDVZ3A,12337
586
- oscura-0.6.0.dist-info/METADATA,sha256=CtuxIm7lD1gUhOP9qjFRhIm4WhucoiUSjCxtzOHqLNE,32904
587
- oscura-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
588
- oscura-0.6.0.dist-info/entry_points.txt,sha256=QLBxd-iTjBQ5HidaVSkLBwvUsqxSG1ZTJ6i-0juu960,48
589
- oscura-0.6.0.dist-info/licenses/LICENSE,sha256=p1_oEK-oqWDXMFSv5mKbyYkgW-CPbCnFUvdICu490aY,1077
590
- oscura-0.6.0.dist-info/RECORD,,
587
+ oscura-0.7.0.dist-info/METADATA,sha256=s6BHYA3LswuBulGUa6yeH-08oWJKWE-gwZkp9n19aIw,34518
588
+ oscura-0.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
589
+ oscura-0.7.0.dist-info/entry_points.txt,sha256=QLBxd-iTjBQ5HidaVSkLBwvUsqxSG1ZTJ6i-0juu960,48
590
+ oscura-0.7.0.dist-info/licenses/LICENSE,sha256=p1_oEK-oqWDXMFSv5mKbyYkgW-CPbCnFUvdICu490aY,1077
591
+ oscura-0.7.0.dist-info/RECORD,,
File without changes