oscura 0.5.0__py3-none-any.whl → 0.6.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 (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,783 @@
1
+ """Legacy session management for backward compatibility.
2
+
3
+ This module provides backward compatibility with the old Session API,
4
+ which has been superseded by the AnalysisSession hierarchy.
5
+
6
+ For new code, use:
7
+ - GenericSession for general waveform analysis
8
+ - BlackBoxSession for protocol reverse engineering
9
+ - Or extend AnalysisSession for custom workflows
10
+
11
+ This module exists only to support existing code and tests.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import gzip
17
+ import hashlib
18
+ import hmac
19
+ import pickle
20
+ from dataclasses import dataclass, field
21
+ from datetime import datetime
22
+ from enum import Enum
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from oscura.core.exceptions import SecurityError
27
+
28
+ # Session file format constants
29
+ _SESSION_MAGIC = b"OSC1" # Magic bytes for new format with signature
30
+ _SESSION_SIGNATURE_SIZE = 32 # SHA256 hash size in bytes
31
+ _SECURITY_KEY = hashlib.sha256(b"oscura-session-v1").digest()
32
+
33
+
34
+ class AnnotationType(Enum):
35
+ """Types of annotations."""
36
+
37
+ POINT = "point" # Single time point
38
+ RANGE = "range" # Time range
39
+ VERTICAL = "vertical" # Vertical line
40
+ HORIZONTAL = "horizontal" # Horizontal line
41
+ REGION = "region" # 2D region (time + amplitude)
42
+ TEXT = "text" # Free-floating text
43
+
44
+
45
+ @dataclass
46
+ class Annotation:
47
+ """Single annotation on a trace.
48
+
49
+ Attributes:
50
+ text: Annotation text/label
51
+ time: Time point (for point annotations)
52
+ time_range: (start, end) time range
53
+ amplitude: Amplitude value (for horizontal lines)
54
+ amplitude_range: (min, max) amplitude range
55
+ annotation_type: Type of annotation
56
+ color: Display color (hex or name)
57
+ style: Line style ('solid', 'dashed', 'dotted')
58
+ visible: Whether annotation is visible
59
+ created_at: Creation timestamp
60
+ metadata: Additional metadata
61
+ """
62
+
63
+ text: str
64
+ time: float | None = None
65
+ time_range: tuple[float, float] | None = None
66
+ amplitude: float | None = None
67
+ amplitude_range: tuple[float, float] | None = None
68
+ annotation_type: AnnotationType = AnnotationType.POINT
69
+ color: str = "#FF6B6B"
70
+ style: str = "solid"
71
+ visible: bool = True
72
+ created_at: datetime = field(default_factory=datetime.now)
73
+ metadata: dict[str, Any] = field(default_factory=dict)
74
+
75
+ def __post_init__(self) -> None:
76
+ """Infer annotation type from provided parameters."""
77
+ if self.annotation_type == AnnotationType.POINT:
78
+ if self.amplitude_range is not None and self.time_range is not None:
79
+ self.annotation_type = AnnotationType.REGION
80
+ elif self.time_range is not None:
81
+ self.annotation_type = AnnotationType.RANGE
82
+ elif self.amplitude is not None and self.time is None:
83
+ self.annotation_type = AnnotationType.HORIZONTAL
84
+
85
+ @property
86
+ def start_time(self) -> float | None:
87
+ """Get start time for range annotations."""
88
+ if self.time_range:
89
+ return self.time_range[0]
90
+ return self.time
91
+
92
+ @property
93
+ def end_time(self) -> float | None:
94
+ """Get end time for range annotations."""
95
+ if self.time_range:
96
+ return self.time_range[1]
97
+ return self.time
98
+
99
+ def to_dict(self) -> dict[str, Any]:
100
+ """Convert to dictionary for serialization."""
101
+ return {
102
+ "text": self.text,
103
+ "time": self.time,
104
+ "time_range": self.time_range,
105
+ "amplitude": self.amplitude,
106
+ "amplitude_range": self.amplitude_range,
107
+ "annotation_type": self.annotation_type.value,
108
+ "color": self.color,
109
+ "style": self.style,
110
+ "visible": self.visible,
111
+ "created_at": self.created_at.isoformat(),
112
+ "metadata": self.metadata,
113
+ }
114
+
115
+ @classmethod
116
+ def from_dict(cls, data: dict[str, Any]) -> Annotation:
117
+ """Create from dictionary."""
118
+ data = data.copy()
119
+ data["annotation_type"] = AnnotationType(data.get("annotation_type", "point"))
120
+ if "created_at" in data and isinstance(data["created_at"], str):
121
+ data["created_at"] = datetime.fromisoformat(data["created_at"])
122
+ return cls(**data)
123
+
124
+
125
+ @dataclass
126
+ class AnnotationLayer:
127
+ """Collection of related annotations.
128
+
129
+ Attributes:
130
+ name: Layer name
131
+ annotations: List of annotations
132
+ visible: Whether layer is visible
133
+ locked: Whether layer is locked (read-only)
134
+ color: Default color for new annotations
135
+ description: Layer description
136
+ """
137
+
138
+ name: str
139
+ annotations: list[Annotation] = field(default_factory=list)
140
+ visible: bool = True
141
+ locked: bool = False
142
+ color: str = "#FF6B6B"
143
+ description: str = ""
144
+
145
+ def add(
146
+ self,
147
+ annotation: Annotation | None = None,
148
+ *,
149
+ text: str = "",
150
+ time: float | None = None,
151
+ time_range: tuple[float, float] | None = None,
152
+ **kwargs: Any,
153
+ ) -> Annotation:
154
+ """Add annotation to layer.
155
+
156
+ Args:
157
+ annotation: Pre-built Annotation object.
158
+ text: Annotation text (if not using pre-built).
159
+ time: Time point.
160
+ time_range: Time range.
161
+ **kwargs: Additional Annotation parameters.
162
+
163
+ Returns:
164
+ Added annotation.
165
+
166
+ Raises:
167
+ ValueError: If layer is locked.
168
+ """
169
+ if self.locked:
170
+ raise ValueError(f"Layer '{self.name}' is locked")
171
+
172
+ if annotation is None:
173
+ annotation = Annotation(
174
+ text=text,
175
+ time=time,
176
+ time_range=time_range,
177
+ color=kwargs.pop("color", self.color),
178
+ **kwargs,
179
+ )
180
+
181
+ self.annotations.append(annotation)
182
+ return annotation
183
+
184
+ def remove(self, annotation: Annotation) -> bool:
185
+ """Remove annotation from layer.
186
+
187
+ Args:
188
+ annotation: Annotation to remove.
189
+
190
+ Returns:
191
+ True if removed, False if not found.
192
+
193
+ Raises:
194
+ ValueError: If layer is locked.
195
+ """
196
+ if self.locked:
197
+ raise ValueError(f"Layer '{self.name}' is locked")
198
+
199
+ try:
200
+ self.annotations.remove(annotation)
201
+ return True
202
+ except ValueError:
203
+ return False
204
+
205
+ def find_at_time(
206
+ self,
207
+ time: float,
208
+ tolerance: float = 0.0,
209
+ ) -> list[Annotation]:
210
+ """Find annotations at or near a specific time.
211
+
212
+ Args:
213
+ time: Time to search.
214
+ tolerance: Time tolerance for matching.
215
+
216
+ Returns:
217
+ List of matching annotations.
218
+ """
219
+ matches = []
220
+ for ann in self.annotations:
221
+ if ann.time is not None:
222
+ if abs(ann.time - time) <= tolerance:
223
+ matches.append(ann)
224
+ elif ann.time_range is not None and (
225
+ ann.time_range[0] - tolerance <= time <= ann.time_range[1] + tolerance
226
+ ):
227
+ matches.append(ann)
228
+ return matches
229
+
230
+ def find_in_range(
231
+ self,
232
+ start_time: float,
233
+ end_time: float,
234
+ ) -> list[Annotation]:
235
+ """Find annotations within a time range.
236
+
237
+ Args:
238
+ start_time: Range start.
239
+ end_time: Range end.
240
+
241
+ Returns:
242
+ List of annotations within range.
243
+ """
244
+ matches = []
245
+ for ann in self.annotations:
246
+ ann_start = ann.start_time
247
+ ann_end = ann.end_time
248
+
249
+ if ann_start is not None and (
250
+ start_time <= ann_start <= end_time
251
+ or (ann_end is not None and ann_start <= end_time and ann_end >= start_time)
252
+ ):
253
+ matches.append(ann)
254
+
255
+ return matches
256
+
257
+ def clear(self) -> int:
258
+ """Remove all annotations.
259
+
260
+ Returns:
261
+ Number of annotations removed.
262
+
263
+ Raises:
264
+ ValueError: If layer is locked.
265
+ """
266
+ if self.locked:
267
+ raise ValueError(f"Layer '{self.name}' is locked")
268
+
269
+ count = len(self.annotations)
270
+ self.annotations.clear()
271
+ return count
272
+
273
+ def to_dict(self) -> dict[str, Any]:
274
+ """Convert to dictionary for serialization."""
275
+ return {
276
+ "name": self.name,
277
+ "annotations": [ann.to_dict() for ann in self.annotations],
278
+ "visible": self.visible,
279
+ "locked": self.locked,
280
+ "color": self.color,
281
+ "description": self.description,
282
+ }
283
+
284
+ @classmethod
285
+ def from_dict(cls, data: dict[str, Any]) -> AnnotationLayer:
286
+ """Create from dictionary."""
287
+ data = data.copy()
288
+ annotations_data = data.pop("annotations", [])
289
+ layer = cls(**data)
290
+ layer.annotations = [Annotation.from_dict(ann) for ann in annotations_data]
291
+ return layer
292
+
293
+
294
+ @dataclass
295
+ class HistoryEntry:
296
+ """Single history entry recording an operation.
297
+
298
+ Attributes:
299
+ operation: Operation name (function/method called)
300
+ parameters: Input parameters
301
+ result: Operation result (summary)
302
+ timestamp: When operation was performed
303
+ duration_ms: Operation duration in milliseconds
304
+ success: Whether operation succeeded
305
+ error_message: Error message if failed
306
+ metadata: Additional metadata
307
+ """
308
+
309
+ operation: str
310
+ parameters: dict[str, Any] = field(default_factory=dict)
311
+ result: Any = None
312
+ timestamp: datetime = field(default_factory=datetime.now)
313
+ duration_ms: float = 0.0
314
+ success: bool = True
315
+ error_message: str | None = None
316
+ metadata: dict[str, Any] = field(default_factory=dict)
317
+
318
+ def to_dict(self) -> dict[str, Any]:
319
+ """Convert to dictionary for serialization."""
320
+ return {
321
+ "operation": self.operation,
322
+ "parameters": self.parameters,
323
+ "result": self._serialize_result(self.result),
324
+ "timestamp": self.timestamp.isoformat(),
325
+ "duration_ms": self.duration_ms,
326
+ "success": self.success,
327
+ "error_message": self.error_message,
328
+ "metadata": self.metadata,
329
+ }
330
+
331
+ @staticmethod
332
+ def _serialize_result(result: Any) -> Any:
333
+ """Serialize result for JSON storage."""
334
+ if result is None:
335
+ return None
336
+ if isinstance(result, str | int | float | bool):
337
+ return result
338
+ if isinstance(result, dict):
339
+ return {k: HistoryEntry._serialize_result(v) for k, v in result.items()}
340
+ if isinstance(result, list | tuple):
341
+ return [HistoryEntry._serialize_result(v) for v in result]
342
+ # For complex objects, store string representation
343
+ return str(result)
344
+
345
+ @classmethod
346
+ def from_dict(cls, data: dict[str, Any]) -> HistoryEntry:
347
+ """Create from dictionary."""
348
+ data = data.copy()
349
+ if "timestamp" in data and isinstance(data["timestamp"], str):
350
+ data["timestamp"] = datetime.fromisoformat(data["timestamp"])
351
+ return cls(**data)
352
+
353
+ def to_code(self) -> str:
354
+ """Generate Python code to replay this operation.
355
+
356
+ Returns:
357
+ Python code string.
358
+ """
359
+ # Format parameters
360
+ params = []
361
+ for k, v in self.parameters.items():
362
+ if isinstance(v, str):
363
+ params.append(f'{k}="{v}"')
364
+ else:
365
+ params.append(f"{k}={v!r}")
366
+
367
+ param_str = ", ".join(params)
368
+ return f"osc.{self.operation}({param_str})"
369
+
370
+
371
+ @dataclass
372
+ class OperationHistory:
373
+ """History of analysis operations.
374
+
375
+ Supports recording, replaying, and exporting operation history.
376
+
377
+ Attributes:
378
+ entries: List of history entries
379
+ max_entries: Maximum entries to keep (0 = unlimited)
380
+ auto_record: Whether to automatically record operations
381
+ """
382
+
383
+ entries: list[HistoryEntry] = field(default_factory=list)
384
+ max_entries: int = 0
385
+ auto_record: bool = True
386
+ _current_session_start: datetime = field(default_factory=datetime.now)
387
+
388
+ def record(
389
+ self,
390
+ operation: str,
391
+ parameters: dict[str, Any] | None = None,
392
+ result: Any = None,
393
+ duration_ms: float = 0.0,
394
+ success: bool = True,
395
+ error_message: str | None = None,
396
+ **metadata: Any,
397
+ ) -> HistoryEntry:
398
+ """Record an operation.
399
+
400
+ Args:
401
+ operation: Operation name.
402
+ parameters: Input parameters.
403
+ result: Operation result.
404
+ duration_ms: Duration in milliseconds.
405
+ success: Whether operation succeeded.
406
+ error_message: Error message if failed.
407
+ **metadata: Additional metadata.
408
+
409
+ Returns:
410
+ Created history entry.
411
+ """
412
+ entry = HistoryEntry(
413
+ operation=operation,
414
+ parameters=parameters or {},
415
+ result=result,
416
+ duration_ms=duration_ms,
417
+ success=success,
418
+ error_message=error_message,
419
+ metadata=metadata,
420
+ )
421
+
422
+ self.entries.append(entry)
423
+
424
+ # Trim if exceeded max entries
425
+ if self.max_entries > 0 and len(self.entries) > self.max_entries:
426
+ self.entries = self.entries[-self.max_entries :]
427
+
428
+ return entry
429
+
430
+ def clear(self) -> None:
431
+ """Clear all history entries."""
432
+ self.entries.clear()
433
+
434
+ def to_script(self, include_imports: bool = True) -> str:
435
+ """Generate Python script to replay operations.
436
+
437
+ Args:
438
+ include_imports: Whether to include import statement.
439
+
440
+ Returns:
441
+ Python script as string.
442
+ """
443
+ lines = []
444
+
445
+ if include_imports:
446
+ lines.append("import oscura as osc")
447
+ lines.append("")
448
+
449
+ for entry in self.entries:
450
+ if entry.success:
451
+ lines.append(entry.to_code())
452
+
453
+ return "\n".join(lines)
454
+
455
+ def to_dict(self) -> dict[str, Any]:
456
+ """Convert to dictionary for serialization."""
457
+ return {
458
+ "entries": [entry.to_dict() for entry in self.entries],
459
+ "max_entries": self.max_entries,
460
+ "auto_record": self.auto_record,
461
+ "session_start": self._current_session_start.isoformat(),
462
+ }
463
+
464
+ @classmethod
465
+ def from_dict(cls, data: dict[str, Any]) -> OperationHistory:
466
+ """Create from dictionary."""
467
+ data = data.copy()
468
+ entries_data = data.pop("entries", [])
469
+ if "session_start" in data:
470
+ data["_current_session_start"] = datetime.fromisoformat(data.pop("session_start"))
471
+ history = cls(**data)
472
+ history.entries = [HistoryEntry.from_dict(entry) for entry in entries_data]
473
+ return history
474
+
475
+
476
+ @dataclass
477
+ class Session:
478
+ """Analysis session container (legacy API).
479
+
480
+ NOTE: This is the legacy Session API for backward compatibility.
481
+ For new code, use:
482
+ - GenericSession for general waveform analysis
483
+ - BlackBoxSession for protocol reverse engineering
484
+ - Or extend AnalysisSession for custom workflows
485
+
486
+ Manages traces, annotations, measurements, and history for a complete
487
+ analysis session. Sessions can be saved and restored.
488
+
489
+ Attributes:
490
+ name: Session name
491
+ traces: Dictionary of loaded traces (name -> trace)
492
+ annotation_layers: Annotation layers
493
+ measurements: Recorded measurements
494
+ history: Operation history
495
+ metadata: Session metadata
496
+ created_at: Creation timestamp
497
+ modified_at: Last modification timestamp
498
+ """
499
+
500
+ name: str = "Untitled Session"
501
+ traces: dict[str, Any] = field(default_factory=dict)
502
+ annotation_layers: dict[str, AnnotationLayer] = field(default_factory=dict)
503
+ measurements: dict[str, Any] = field(default_factory=dict)
504
+ history: OperationHistory = field(default_factory=OperationHistory)
505
+ metadata: dict[str, Any] = field(default_factory=dict)
506
+ created_at: datetime = field(default_factory=datetime.now)
507
+ modified_at: datetime = field(default_factory=datetime.now)
508
+ _file_path: Path | None = None
509
+
510
+ def __post_init__(self) -> None:
511
+ """Initialize default annotation layer."""
512
+ if "default" not in self.annotation_layers:
513
+ self.annotation_layers["default"] = AnnotationLayer("Default")
514
+
515
+ def load_trace(
516
+ self,
517
+ path: str | Path,
518
+ name: str | None = None,
519
+ **load_kwargs: Any,
520
+ ) -> Any:
521
+ """Load a trace into the session.
522
+
523
+ Args:
524
+ path: Path to trace file.
525
+ name: Name for trace in session (default: filename).
526
+ **load_kwargs: Additional arguments for load().
527
+
528
+ Returns:
529
+ Loaded trace.
530
+ """
531
+ from oscura.loaders import load
532
+
533
+ path = Path(path)
534
+ trace = load(str(path), **load_kwargs)
535
+
536
+ if name is None:
537
+ name = path.stem
538
+
539
+ self.traces[name] = trace
540
+ self._mark_modified()
541
+
542
+ self.history.record(
543
+ "load_trace",
544
+ {"path": str(path), "name": name},
545
+ result=f"Loaded {name}",
546
+ )
547
+
548
+ return trace
549
+
550
+ def add_trace(
551
+ self,
552
+ name: str,
553
+ trace: Any,
554
+ ) -> None:
555
+ """Add an in-memory trace to the session.
556
+
557
+ Args:
558
+ name: Name for the trace in the session.
559
+ trace: Trace object (WaveformTrace, DigitalTrace, etc.).
560
+
561
+ Raises:
562
+ ValueError: If name is empty or already exists.
563
+ TypeError: If trace doesn't have expected attributes.
564
+ """
565
+ if not name:
566
+ raise ValueError("Trace name cannot be empty")
567
+
568
+ if not hasattr(trace, "data"):
569
+ raise TypeError("Trace must have a 'data' attribute")
570
+
571
+ self.traces[name] = trace
572
+ self._mark_modified()
573
+
574
+ self.history.record(
575
+ "add_trace",
576
+ {"name": name, "type": type(trace).__name__},
577
+ result=f"Added {name}",
578
+ )
579
+
580
+ def remove_trace(self, name: str) -> None:
581
+ """Remove a trace from the session.
582
+
583
+ Args:
584
+ name: Name of the trace to remove.
585
+
586
+ Raises:
587
+ KeyError: If trace not found.
588
+ """
589
+ if name not in self.traces:
590
+ raise KeyError(f"Trace '{name}' not found in session")
591
+
592
+ del self.traces[name]
593
+ self._mark_modified()
594
+
595
+ self.history.record(
596
+ "remove_trace",
597
+ {"name": name},
598
+ result=f"Removed {name}",
599
+ )
600
+
601
+ def get_trace(self, name: str) -> Any:
602
+ """Get trace by name.
603
+
604
+ Args:
605
+ name: Trace name.
606
+
607
+ Returns:
608
+ Trace object.
609
+ """
610
+ return self.traces[name]
611
+
612
+ def list_traces(self) -> list[str]:
613
+ """List all trace names."""
614
+ return list(self.traces.keys())
615
+
616
+ def annotate(
617
+ self,
618
+ text: str,
619
+ *,
620
+ time: float | None = None,
621
+ time_range: tuple[float, float] | None = None,
622
+ layer: str = "default",
623
+ **kwargs: Any,
624
+ ) -> None:
625
+ """Add annotation to session.
626
+
627
+ Args:
628
+ text: Annotation text.
629
+ time: Time point for annotation.
630
+ time_range: Time range for annotation.
631
+ layer: Annotation layer name.
632
+ **kwargs: Additional annotation parameters.
633
+ """
634
+ if layer not in self.annotation_layers:
635
+ self.annotation_layers[layer] = AnnotationLayer(layer)
636
+
637
+ self.annotation_layers[layer].add(
638
+ text=text,
639
+ time=time,
640
+ time_range=time_range,
641
+ **kwargs,
642
+ )
643
+
644
+ self._mark_modified()
645
+
646
+ def save(self, path: str | Path, *, compress: bool = True) -> None:
647
+ """Save session to file.
648
+
649
+ Args:
650
+ path: Output file path (.tks extension).
651
+ compress: Whether to compress with gzip.
652
+
653
+ Raises:
654
+ SecurityError: If session verification fails.
655
+ """
656
+ path = Path(path)
657
+ self._file_path = path
658
+
659
+ # Prepare session data
660
+ session_data = {
661
+ "name": self.name,
662
+ "traces": self.traces,
663
+ "annotation_layers": {
664
+ name: layer.to_dict() for name, layer in self.annotation_layers.items()
665
+ },
666
+ "measurements": self.measurements,
667
+ "history": self.history.to_dict(),
668
+ "metadata": self.metadata,
669
+ "created_at": self.created_at.isoformat(),
670
+ "modified_at": datetime.now().isoformat(),
671
+ }
672
+
673
+ # Serialize
674
+ pickled = pickle.dumps(session_data)
675
+
676
+ # Sign the data
677
+ signature = hmac.new(_SECURITY_KEY, pickled, hashlib.sha256).digest()
678
+
679
+ # Combine magic + signature + data
680
+ full_data = _SESSION_MAGIC + signature + pickled
681
+
682
+ # Write
683
+ if compress:
684
+ with gzip.open(path, "wb") as f:
685
+ f.write(full_data)
686
+ else:
687
+ path.write_bytes(full_data)
688
+
689
+ def _mark_modified(self) -> None:
690
+ """Mark session as modified."""
691
+ self.modified_at = datetime.now()
692
+
693
+ def to_dict(self) -> dict[str, Any]:
694
+ """Convert session to dictionary for export."""
695
+ return {
696
+ "name": self.name,
697
+ "traces": {name: str(type(trace).__name__) for name, trace in self.traces.items()},
698
+ "annotation_layers": {
699
+ name: layer.to_dict() for name, layer in self.annotation_layers.items()
700
+ },
701
+ "measurements": self.measurements,
702
+ "history": self.history.to_dict(),
703
+ "metadata": self.metadata,
704
+ "created_at": self.created_at.isoformat(),
705
+ "modified_at": self.modified_at.isoformat(),
706
+ }
707
+
708
+
709
+ def load_session(path: str | Path) -> Session:
710
+ """Load session from file.
711
+
712
+ Args:
713
+ path: Path to session file (.tks).
714
+
715
+ Returns:
716
+ Loaded Session object.
717
+
718
+ Raises:
719
+ SecurityError: If session verification fails or file format is invalid.
720
+ """
721
+ path = Path(path)
722
+
723
+ # Read file as raw bytes first
724
+ raw_data = path.read_bytes()
725
+
726
+ # Detect and decompress gzip files (magic bytes: 0x1f 0x8b)
727
+ if raw_data[:2] == b"\x1f\x8b":
728
+ import io
729
+
730
+ with gzip.open(io.BytesIO(raw_data), "rb") as f:
731
+ full_data = f.read()
732
+ else:
733
+ full_data = raw_data
734
+
735
+ # Check magic - missing magic bytes is a security issue (file lacks HMAC signature)
736
+ if not full_data.startswith(_SESSION_MAGIC):
737
+ raise SecurityError(
738
+ "Invalid session file format: missing HMAC signature (magic bytes not found)"
739
+ )
740
+
741
+ # Extract signature and data
742
+ signature = full_data[len(_SESSION_MAGIC) : len(_SESSION_MAGIC) + _SESSION_SIGNATURE_SIZE]
743
+ pickled = full_data[len(_SESSION_MAGIC) + _SESSION_SIGNATURE_SIZE :]
744
+
745
+ # Verify signature
746
+ expected_signature = hmac.new(_SECURITY_KEY, pickled, hashlib.sha256).digest()
747
+ if not hmac.compare_digest(signature, expected_signature):
748
+ raise SecurityError("Session file signature verification failed (data may be tampered)")
749
+
750
+ # Deserialize
751
+ session_data = pickle.loads(pickled)
752
+
753
+ # Reconstruct session
754
+ session = Session(
755
+ name=session_data["name"],
756
+ traces=session_data.get("traces", {}),
757
+ measurements=session_data.get("measurements", {}),
758
+ metadata=session_data.get("metadata", {}),
759
+ created_at=datetime.fromisoformat(session_data["created_at"]),
760
+ modified_at=datetime.fromisoformat(session_data["modified_at"]),
761
+ )
762
+
763
+ # Restore annotation layers
764
+ for name, layer_data in session_data.get("annotation_layers", {}).items():
765
+ session.annotation_layers[name] = AnnotationLayer.from_dict(layer_data)
766
+
767
+ # Restore history
768
+ session.history = OperationHistory.from_dict(session_data["history"])
769
+
770
+ session._file_path = path
771
+
772
+ return session
773
+
774
+
775
+ __all__ = [
776
+ "Annotation",
777
+ "AnnotationLayer",
778
+ "AnnotationType",
779
+ "HistoryEntry",
780
+ "OperationHistory",
781
+ "Session",
782
+ "load_session",
783
+ ]