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,1166 @@
1
+ """Database backend for storing and querying analysis results.
2
+
3
+ This module provides a comprehensive database abstraction for persisting
4
+ hardware reverse engineering session data including protocols, messages,
5
+ and analysis results.
6
+
7
+ Example:
8
+ >>> from oscura.utils.storage import DatabaseBackend, DatabaseConfig
9
+ >>>
10
+ >>> # SQLite (default, no dependencies)
11
+ >>> config = DatabaseConfig(url="sqlite:///analysis.db")
12
+ >>> db = DatabaseBackend(config)
13
+ >>>
14
+ >>> # PostgreSQL (optional)
15
+ >>> config = DatabaseConfig(
16
+ ... url="postgresql://user:pass@localhost/oscura",
17
+ ... pool_size=10
18
+ ... )
19
+ >>> db = DatabaseBackend(config)
20
+ >>>
21
+ >>> # Store analysis workflow
22
+ >>> project_id = db.create_project("CAN Bus RE", "Automotive reverse engineering")
23
+ >>> session_id = db.create_session(project_id, "can", {"bus": "HS-CAN"})
24
+ >>> protocol_id = db.store_protocol(session_id, "UDS", spec_json, confidence=0.9)
25
+ >>> db.store_message(protocol_id, timestamp=1.5, data=b"\\x02\\x10\\x01", decoded)
26
+ >>>
27
+ >>> # Query historical data
28
+ >>> protocols = db.find_protocols(name_pattern="UDS%", min_confidence=0.8)
29
+ >>> messages = db.query_messages(protocol_id, time_range=(0.0, 10.0))
30
+ >>> results = db.get_analysis_results(session_id, analysis_type="dpa")
31
+
32
+ Architecture:
33
+ - SQLite by default (serverless, file-based)
34
+ - PostgreSQL optional (production deployments)
35
+ - Raw SQL fallback (no ORM dependencies)
36
+ - Connection pooling for performance
37
+ - Automatic schema migration
38
+ - Transaction support
39
+
40
+ Database Schema:
41
+ projects: Project metadata and descriptions
42
+ sessions: Analysis sessions per project
43
+ protocols: Discovered protocols per session
44
+ messages: Decoded messages per protocol
45
+ analysis_results: DPA, timing, entropy, etc.
46
+
47
+ References:
48
+ V0.6.0_COMPLETE_COMPREHENSIVE_PLAN.md: Phase 5 Feature 45
49
+ SQLite Documentation: https://www.sqlite.org/docs.html
50
+ PostgreSQL Documentation: https://www.postgresql.org/docs/
51
+ """
52
+
53
+ from __future__ import annotations
54
+
55
+ import json
56
+ import logging
57
+ import sqlite3
58
+ from dataclasses import asdict, dataclass, field
59
+ from datetime import datetime
60
+ from pathlib import Path
61
+ from typing import Any
62
+
63
+ # Optional PostgreSQL support
64
+ try:
65
+ import psycopg2 # type: ignore[import-untyped]
66
+ from psycopg2.pool import SimpleConnectionPool # type: ignore[import-untyped]
67
+
68
+ HAS_POSTGRES = True
69
+ except ImportError:
70
+ psycopg2 = None
71
+ SimpleConnectionPool = None
72
+ HAS_POSTGRES = False
73
+
74
+
75
+ logger = logging.getLogger(__name__)
76
+
77
+ # SQL Schema constants for SQLite
78
+ _SQL_CREATE_PROJECTS_SQLITE = """
79
+ CREATE TABLE IF NOT EXISTS projects (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ name TEXT NOT NULL,
82
+ description TEXT,
83
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
84
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
85
+ metadata TEXT
86
+ )
87
+ """
88
+
89
+ _SQL_CREATE_SESSIONS_SQLITE = """
90
+ CREATE TABLE IF NOT EXISTS sessions (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ project_id INTEGER NOT NULL,
93
+ session_type TEXT NOT NULL,
94
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
95
+ metadata TEXT,
96
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
97
+ )
98
+ """
99
+
100
+ _SQL_CREATE_PROTOCOLS_SQLITE = """
101
+ CREATE TABLE IF NOT EXISTS protocols (
102
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
103
+ session_id INTEGER NOT NULL,
104
+ name TEXT NOT NULL,
105
+ spec_json TEXT NOT NULL,
106
+ confidence REAL NOT NULL,
107
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
108
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
109
+ )
110
+ """
111
+
112
+ _SQL_CREATE_MESSAGES_SQLITE = """
113
+ CREATE TABLE IF NOT EXISTS messages (
114
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
115
+ protocol_id INTEGER NOT NULL,
116
+ timestamp REAL NOT NULL,
117
+ data TEXT NOT NULL,
118
+ decoded_fields TEXT,
119
+ FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
120
+ )
121
+ """
122
+
123
+ _SQL_CREATE_ANALYSIS_SQLITE = """
124
+ CREATE TABLE IF NOT EXISTS analysis_results (
125
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
126
+ session_id INTEGER NOT NULL,
127
+ analysis_type TEXT NOT NULL,
128
+ results_json TEXT NOT NULL,
129
+ metrics TEXT,
130
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
131
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
132
+ )
133
+ """
134
+
135
+ # SQL Schema constants for PostgreSQL
136
+ _SQL_CREATE_PROJECTS_POSTGRES = """
137
+ CREATE TABLE IF NOT EXISTS projects (
138
+ id SERIAL PRIMARY KEY,
139
+ name TEXT NOT NULL,
140
+ description TEXT,
141
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
142
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
143
+ metadata JSONB
144
+ )
145
+ """
146
+
147
+ _SQL_CREATE_SESSIONS_POSTGRES = """
148
+ CREATE TABLE IF NOT EXISTS sessions (
149
+ id SERIAL PRIMARY KEY,
150
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
151
+ session_type TEXT NOT NULL,
152
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
153
+ metadata JSONB
154
+ )
155
+ """
156
+
157
+ _SQL_CREATE_PROTOCOLS_POSTGRES = """
158
+ CREATE TABLE IF NOT EXISTS protocols (
159
+ id SERIAL PRIMARY KEY,
160
+ session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
161
+ name TEXT NOT NULL,
162
+ spec_json JSONB NOT NULL,
163
+ confidence REAL NOT NULL,
164
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
165
+ )
166
+ """
167
+
168
+ _SQL_CREATE_MESSAGES_POSTGRES = """
169
+ CREATE TABLE IF NOT EXISTS messages (
170
+ id SERIAL PRIMARY KEY,
171
+ protocol_id INTEGER NOT NULL REFERENCES protocols(id) ON DELETE CASCADE,
172
+ timestamp REAL NOT NULL,
173
+ data TEXT NOT NULL,
174
+ decoded_fields JSONB
175
+ )
176
+ """
177
+
178
+ _SQL_CREATE_ANALYSIS_POSTGRES = """
179
+ CREATE TABLE IF NOT EXISTS analysis_results (
180
+ id SERIAL PRIMARY KEY,
181
+ session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
182
+ analysis_type TEXT NOT NULL,
183
+ results_json JSONB NOT NULL,
184
+ metrics JSONB,
185
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
186
+ )
187
+ """
188
+
189
+ # Index creation statements
190
+ _SQL_CREATE_INDEXES = [
191
+ "CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id)",
192
+ "CREATE INDEX IF NOT EXISTS idx_protocols_session ON protocols(session_id)",
193
+ "CREATE INDEX IF NOT EXISTS idx_protocols_name ON protocols(name)",
194
+ "CREATE INDEX IF NOT EXISTS idx_messages_protocol ON messages(protocol_id)",
195
+ "CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)",
196
+ "CREATE INDEX IF NOT EXISTS idx_analysis_session ON analysis_results(session_id)",
197
+ ]
198
+
199
+
200
+ @dataclass
201
+ class DatabaseConfig:
202
+ """Database configuration.
203
+
204
+ Attributes:
205
+ url: Database URL (sqlite:///path.db or postgresql://...)
206
+ pool_size: Connection pool size (PostgreSQL only)
207
+ timeout: Connection timeout in seconds
208
+ echo_sql: Log SQL statements for debugging
209
+
210
+ Example:
211
+ >>> # SQLite (default)
212
+ >>> config = DatabaseConfig(url="sqlite:///analysis.db")
213
+ >>>
214
+ >>> # PostgreSQL
215
+ >>> config = DatabaseConfig(
216
+ ... url="postgresql://user:pass@localhost/oscura",
217
+ ... pool_size=10,
218
+ ... timeout=30.0
219
+ ... )
220
+ """
221
+
222
+ url: str = "sqlite:///oscura_analysis.db"
223
+ pool_size: int = 5
224
+ timeout: float = 30.0
225
+ echo_sql: bool = False
226
+
227
+
228
+ @dataclass
229
+ class Project:
230
+ """Project metadata.
231
+
232
+ Attributes:
233
+ id: Project ID (auto-assigned)
234
+ name: Project name
235
+ description: Project description
236
+ created_at: Creation timestamp
237
+ updated_at: Last update timestamp
238
+ metadata: Additional metadata
239
+
240
+ Example:
241
+ >>> project = Project(
242
+ ... id=1,
243
+ ... name="Automotive CAN",
244
+ ... description="CAN bus protocol analysis",
245
+ ... created_at=datetime.now(UTC),
246
+ ... updated_at=datetime.now(UTC)
247
+ ... )
248
+ """
249
+
250
+ id: int | None = None
251
+ name: str = ""
252
+ description: str = ""
253
+ created_at: datetime | None = None
254
+ updated_at: datetime | None = None
255
+ metadata: dict[str, Any] = field(default_factory=dict)
256
+
257
+
258
+ @dataclass
259
+ class Session:
260
+ """Analysis session.
261
+
262
+ Attributes:
263
+ id: Session ID (auto-assigned)
264
+ project_id: Parent project ID
265
+ session_type: Session type (blackbox, can, uart, etc.)
266
+ timestamp: Session timestamp
267
+ metadata: Session-specific metadata
268
+
269
+ Example:
270
+ >>> session = Session(
271
+ ... id=1,
272
+ ... project_id=1,
273
+ ... session_type="blackbox",
274
+ ... timestamp=datetime.now(UTC),
275
+ ... metadata={"capture_file": "device.bin"}
276
+ ... )
277
+ """
278
+
279
+ id: int | None = None
280
+ project_id: int | None = None
281
+ session_type: str = ""
282
+ timestamp: datetime | None = None
283
+ metadata: dict[str, Any] = field(default_factory=dict)
284
+
285
+
286
+ @dataclass
287
+ class Protocol:
288
+ """Discovered protocol.
289
+
290
+ Attributes:
291
+ id: Protocol ID (auto-assigned)
292
+ session_id: Parent session ID
293
+ name: Protocol name
294
+ spec_json: Protocol specification as JSON
295
+ confidence: Confidence score (0.0-1.0)
296
+ created_at: Creation timestamp
297
+
298
+ Example:
299
+ >>> protocol = Protocol(
300
+ ... id=1,
301
+ ... session_id=1,
302
+ ... name="IoT Protocol",
303
+ ... spec_json={"fields": [...]},
304
+ ... confidence=0.85
305
+ ... )
306
+ """
307
+
308
+ id: int | None = None
309
+ session_id: int | None = None
310
+ name: str = ""
311
+ spec_json: dict[str, Any] = field(default_factory=dict)
312
+ confidence: float = 0.0
313
+ created_at: datetime | None = None
314
+
315
+
316
+ @dataclass
317
+ class Message:
318
+ """Decoded message.
319
+
320
+ Attributes:
321
+ id: Message ID (auto-assigned)
322
+ protocol_id: Parent protocol ID
323
+ timestamp: Message timestamp
324
+ data: Raw message data (hex string)
325
+ decoded_fields: Decoded field values
326
+
327
+ Example:
328
+ >>> message = Message(
329
+ ... id=1,
330
+ ... protocol_id=1,
331
+ ... timestamp=1.5,
332
+ ... data="aa5501",
333
+ ... decoded_fields={"id": 1, "counter": 0}
334
+ ... )
335
+ """
336
+
337
+ id: int | None = None
338
+ protocol_id: int | None = None
339
+ timestamp: float = 0.0
340
+ data: str = ""
341
+ decoded_fields: dict[str, Any] = field(default_factory=dict)
342
+
343
+
344
+ @dataclass
345
+ class AnalysisResult:
346
+ """Analysis result.
347
+
348
+ Attributes:
349
+ id: Result ID (auto-assigned)
350
+ session_id: Parent session ID
351
+ analysis_type: Analysis type (dpa, timing, entropy, etc.)
352
+ results_json: Analysis results as JSON
353
+ metrics: Computed metrics
354
+ created_at: Creation timestamp
355
+
356
+ Example:
357
+ >>> result = AnalysisResult(
358
+ ... id=1,
359
+ ... session_id=1,
360
+ ... analysis_type="dpa",
361
+ ... results_json={"recovered_key": "0x1234..."},
362
+ ... metrics={"confidence": 0.95}
363
+ ... )
364
+ """
365
+
366
+ id: int | None = None
367
+ session_id: int | None = None
368
+ analysis_type: str = ""
369
+ results_json: dict[str, Any] = field(default_factory=dict)
370
+ metrics: dict[str, Any] = field(default_factory=dict)
371
+ created_at: datetime | None = None
372
+
373
+
374
+ @dataclass
375
+ class QueryResult:
376
+ """Paginated query result.
377
+
378
+ Attributes:
379
+ items: Result items
380
+ total: Total number of results
381
+ page: Current page number (0-indexed)
382
+ page_size: Items per page
383
+
384
+ Example:
385
+ >>> result = QueryResult(
386
+ ... items=[msg1, msg2, msg3],
387
+ ... total=100,
388
+ ... page=0,
389
+ ... page_size=10
390
+ ... )
391
+ >>> print(f"Page 1/{result.total_pages}: {len(result.items)} items")
392
+ """
393
+
394
+ items: list[Any] = field(default_factory=list)
395
+ total: int = 0
396
+ page: int = 0
397
+ page_size: int = 100
398
+
399
+ @property
400
+ def total_pages(self) -> int:
401
+ """Calculate total number of pages.
402
+
403
+ Returns:
404
+ Number of pages (at least 1)
405
+ """
406
+ return max(1, (self.total + self.page_size - 1) // self.page_size)
407
+
408
+ @property
409
+ def has_next(self) -> bool:
410
+ """Check if there is a next page.
411
+
412
+ Returns:
413
+ True if more pages available
414
+ """
415
+ return self.page < self.total_pages - 1
416
+
417
+ @property
418
+ def has_prev(self) -> bool:
419
+ """Check if there is a previous page.
420
+
421
+ Returns:
422
+ True if previous pages exist
423
+ """
424
+ return self.page > 0
425
+
426
+
427
+ class DatabaseBackend:
428
+ """Database backend for storing analysis results.
429
+
430
+ Supports SQLite (default) and PostgreSQL (optional).
431
+ Uses raw SQL for simplicity and graceful degradation.
432
+
433
+ Example:
434
+ >>> config = DatabaseConfig(url="sqlite:///analysis.db")
435
+ >>> db = DatabaseBackend(config)
436
+ >>>
437
+ >>> # Create project hierarchy
438
+ >>> proj_id = db.create_project("IoT RE", "Device protocol analysis")
439
+ >>> sess_id = db.create_session(proj_id, "blackbox", {"file": "capture.bin"})
440
+ >>> prot_id = db.store_protocol(sess_id, "IoT", {"fields": []}, 0.9)
441
+ >>>
442
+ >>> # Store messages
443
+ >>> db.store_message(prot_id, 0.0, b"\\xaa\\x55", {"id": 1})
444
+ >>>
445
+ >>> # Query
446
+ >>> protocols = db.find_protocols(min_confidence=0.8)
447
+ >>> messages = db.query_messages(prot_id, limit=10)
448
+ """
449
+
450
+ def __init__(self, config: DatabaseConfig | None = None) -> None:
451
+ """Initialize database backend.
452
+
453
+ Args:
454
+ config: Database configuration (default: SQLite)
455
+
456
+ Raises:
457
+ ValueError: If PostgreSQL URL but psycopg2 not installed
458
+ sqlite3.Error: If SQLite database creation fails
459
+ """
460
+ self.config = config or DatabaseConfig()
461
+ self._conn: Any = None
462
+ self._pool: Any = None
463
+
464
+ # Determine backend type
465
+ self._is_postgres = self.config.url.startswith("postgresql://")
466
+
467
+ if self._is_postgres and not HAS_POSTGRES:
468
+ raise ValueError(
469
+ "PostgreSQL URL specified but psycopg2 not installed. "
470
+ "Install with: pip install psycopg2-binary"
471
+ )
472
+
473
+ # Initialize connection/pool
474
+ self._init_connection()
475
+
476
+ # Create schema
477
+ self._create_schema()
478
+
479
+ def _init_connection(self) -> None:
480
+ """Initialize database connection or pool."""
481
+ if self._is_postgres:
482
+ # PostgreSQL connection pool
483
+ self._pool = SimpleConnectionPool(
484
+ 1,
485
+ self.config.pool_size,
486
+ self.config.url,
487
+ connect_timeout=int(self.config.timeout),
488
+ )
489
+ else:
490
+ # SQLite connection
491
+ db_path = self.config.url.replace("sqlite:///", "")
492
+ Path(db_path).parent.mkdir(parents=True, exist_ok=True)
493
+ self._conn = sqlite3.connect(
494
+ db_path,
495
+ timeout=self.config.timeout,
496
+ check_same_thread=False,
497
+ )
498
+ self._conn.row_factory = sqlite3.Row
499
+
500
+ def _get_connection(self) -> Any:
501
+ """Get database connection.
502
+
503
+ Returns:
504
+ Connection object (sqlite3.Connection or psycopg2.connection)
505
+ """
506
+ if self._is_postgres:
507
+ return self._pool.getconn()
508
+ return self._conn
509
+
510
+ def _return_connection(self, conn: Any) -> None:
511
+ """Return connection to pool (PostgreSQL only).
512
+
513
+ Args:
514
+ conn: Connection to return
515
+ """
516
+ if self._is_postgres:
517
+ self._pool.putconn(conn)
518
+
519
+ def _execute(self, sql: str, params: tuple[Any, ...] = ()) -> Any:
520
+ """Execute SQL statement.
521
+
522
+ Args:
523
+ sql: SQL statement
524
+ params: Query parameters
525
+
526
+ Returns:
527
+ Cursor after execution
528
+ """
529
+ conn = self._get_connection()
530
+ try:
531
+ cursor = conn.cursor()
532
+ if self.config.echo_sql:
533
+ logger.debug(f"SQL: {sql}")
534
+ logger.debug(f"Params: {params}")
535
+ cursor.execute(sql, params)
536
+ conn.commit()
537
+ return cursor
538
+ finally:
539
+ self._return_connection(conn)
540
+
541
+ def _fetchall(self, sql: str, params: tuple[Any, ...] = ()) -> list[Any]:
542
+ """Execute query and fetch all results.
543
+
544
+ Args:
545
+ sql: SQL query
546
+ params: Query parameters
547
+
548
+ Returns:
549
+ List of row dictionaries
550
+ """
551
+ conn = self._get_connection()
552
+ try:
553
+ cursor = conn.cursor()
554
+ if self.config.echo_sql:
555
+ logger.debug(f"SQL: {sql}")
556
+ logger.debug(f"Params: {params}")
557
+ cursor.execute(sql, params)
558
+
559
+ if self._is_postgres:
560
+ columns = [desc[0] for desc in cursor.description]
561
+ return [dict(zip(columns, row, strict=True)) for row in cursor.fetchall()]
562
+ else:
563
+ return [dict(row) for row in cursor.fetchall()]
564
+ finally:
565
+ self._return_connection(conn)
566
+
567
+ def _fetchone(self, sql: str, params: tuple[Any, ...] = ()) -> dict[str, Any] | None:
568
+ """Execute query and fetch one result.
569
+
570
+ Args:
571
+ sql: SQL query
572
+ params: Query parameters
573
+
574
+ Returns:
575
+ Row dictionary or None
576
+ """
577
+ results = self._fetchall(sql, params)
578
+ return results[0] if results else None
579
+
580
+ def _create_schema(self) -> None:
581
+ """Create database schema if not exists."""
582
+ tables = [
583
+ ("projects", _SQL_CREATE_PROJECTS_SQLITE, _SQL_CREATE_PROJECTS_POSTGRES),
584
+ ("sessions", _SQL_CREATE_SESSIONS_SQLITE, _SQL_CREATE_SESSIONS_POSTGRES),
585
+ ("protocols", _SQL_CREATE_PROTOCOLS_SQLITE, _SQL_CREATE_PROTOCOLS_POSTGRES),
586
+ ("messages", _SQL_CREATE_MESSAGES_SQLITE, _SQL_CREATE_MESSAGES_POSTGRES),
587
+ ("analysis_results", _SQL_CREATE_ANALYSIS_SQLITE, _SQL_CREATE_ANALYSIS_POSTGRES),
588
+ ]
589
+
590
+ for _, sqlite_sql, postgres_sql in tables:
591
+ self._execute(sqlite_sql if not self._is_postgres else postgres_sql)
592
+
593
+ # Create indexes
594
+ for idx_sql in _SQL_CREATE_INDEXES:
595
+ self._execute(idx_sql)
596
+
597
+ def create_project(
598
+ self, name: str, description: str = "", metadata: dict[str, Any] | None = None
599
+ ) -> int:
600
+ """Create new project.
601
+
602
+ Args:
603
+ name: Project name
604
+ description: Project description
605
+ metadata: Additional metadata
606
+
607
+ Returns:
608
+ Project ID
609
+
610
+ Example:
611
+ >>> db = DatabaseBackend()
612
+ >>> project_id = db.create_project("IoT RE", "Unknown device protocol")
613
+ """
614
+ metadata_json = json.dumps(metadata or {})
615
+ cursor = self._execute(
616
+ "INSERT INTO projects (name, description, metadata) VALUES (?, ?, ?)",
617
+ (name, description, metadata_json),
618
+ )
619
+ result: int = cursor.lastrowid
620
+ return result
621
+
622
+ def get_project(self, project_id: int) -> Project | None:
623
+ """Get project by ID.
624
+
625
+ Args:
626
+ project_id: Project ID
627
+
628
+ Returns:
629
+ Project or None if not found
630
+
631
+ Example:
632
+ >>> project = db.get_project(1)
633
+ >>> print(project.name)
634
+ """
635
+ row = self._fetchone("SELECT * FROM projects WHERE id = ?", (project_id,))
636
+ if not row:
637
+ return None
638
+
639
+ return Project(
640
+ id=row["id"],
641
+ name=row["name"],
642
+ description=row["description"] or "",
643
+ created_at=datetime.fromisoformat(row["created_at"]),
644
+ updated_at=datetime.fromisoformat(row["updated_at"]),
645
+ metadata=json.loads(row["metadata"]) if row["metadata"] else {},
646
+ )
647
+
648
+ def list_projects(self) -> list[Project]:
649
+ """List all projects.
650
+
651
+ Returns:
652
+ List of projects
653
+
654
+ Example:
655
+ >>> projects = db.list_projects()
656
+ >>> for proj in projects:
657
+ ... print(f"{proj.id}: {proj.name}")
658
+ """
659
+ rows = self._fetchall("SELECT * FROM projects ORDER BY updated_at DESC")
660
+ return [
661
+ Project(
662
+ id=row["id"],
663
+ name=row["name"],
664
+ description=row["description"] or "",
665
+ created_at=datetime.fromisoformat(row["created_at"]),
666
+ updated_at=datetime.fromisoformat(row["updated_at"]),
667
+ metadata=json.loads(row["metadata"]) if row["metadata"] else {},
668
+ )
669
+ for row in rows
670
+ ]
671
+
672
+ def create_session(
673
+ self,
674
+ project_id: int,
675
+ session_type: str,
676
+ metadata: dict[str, Any] | None = None,
677
+ ) -> int:
678
+ """Create new session.
679
+
680
+ Args:
681
+ project_id: Parent project ID
682
+ session_type: Session type (blackbox, can, uart, etc.)
683
+ metadata: Session metadata
684
+
685
+ Returns:
686
+ Session ID
687
+
688
+ Example:
689
+ >>> session_id = db.create_session(
690
+ ... project_id=1,
691
+ ... session_type="blackbox",
692
+ ... metadata={"capture": "device.bin"}
693
+ ... )
694
+ """
695
+ metadata_json = json.dumps(metadata or {})
696
+ cursor = self._execute(
697
+ "INSERT INTO sessions (project_id, session_type, metadata) VALUES (?, ?, ?)",
698
+ (project_id, session_type, metadata_json),
699
+ )
700
+ result: int = cursor.lastrowid
701
+ return result
702
+
703
+ def get_sessions(self, project_id: int) -> list[Session]:
704
+ """Get all sessions for project.
705
+
706
+ Args:
707
+ project_id: Project ID
708
+
709
+ Returns:
710
+ List of sessions
711
+
712
+ Example:
713
+ >>> sessions = db.get_sessions(project_id=1)
714
+ >>> for sess in sessions:
715
+ ... print(f"{sess.id}: {sess.session_type}")
716
+ """
717
+ rows = self._fetchall(
718
+ "SELECT * FROM sessions WHERE project_id = ? ORDER BY timestamp DESC",
719
+ (project_id,),
720
+ )
721
+ return [
722
+ Session(
723
+ id=row["id"],
724
+ project_id=row["project_id"],
725
+ session_type=row["session_type"],
726
+ timestamp=datetime.fromisoformat(row["timestamp"]),
727
+ metadata=json.loads(row["metadata"]) if row["metadata"] else {},
728
+ )
729
+ for row in rows
730
+ ]
731
+
732
+ def store_protocol(
733
+ self,
734
+ session_id: int,
735
+ name: str,
736
+ spec_json: dict[str, Any],
737
+ confidence: float,
738
+ ) -> int:
739
+ """Store discovered protocol.
740
+
741
+ Args:
742
+ session_id: Parent session ID
743
+ name: Protocol name
744
+ spec_json: Protocol specification
745
+ confidence: Confidence score (0.0-1.0)
746
+
747
+ Returns:
748
+ Protocol ID
749
+
750
+ Example:
751
+ >>> protocol_id = db.store_protocol(
752
+ ... session_id=1,
753
+ ... name="IoT Protocol",
754
+ ... spec_json={"fields": [...]},
755
+ ... confidence=0.85
756
+ ... )
757
+ """
758
+ spec_str = json.dumps(spec_json)
759
+ cursor = self._execute(
760
+ "INSERT INTO protocols (session_id, name, spec_json, confidence) VALUES (?, ?, ?, ?)",
761
+ (session_id, name, spec_str, confidence),
762
+ )
763
+ result: int = cursor.lastrowid
764
+ return result
765
+
766
+ def find_protocols(
767
+ self,
768
+ name_pattern: str | None = None,
769
+ min_confidence: float | None = None,
770
+ ) -> list[Protocol]:
771
+ """Find protocols by criteria.
772
+
773
+ Args:
774
+ name_pattern: SQL LIKE pattern (e.g., "UDS%")
775
+ min_confidence: Minimum confidence threshold
776
+
777
+ Returns:
778
+ List of matching protocols
779
+
780
+ Example:
781
+ >>> # Find all UDS protocols with confidence > 0.8
782
+ >>> protocols = db.find_protocols(name_pattern="UDS%", min_confidence=0.8)
783
+ """
784
+ conditions = []
785
+ params: list[Any] = []
786
+
787
+ if name_pattern:
788
+ conditions.append("name LIKE ?")
789
+ params.append(name_pattern)
790
+
791
+ if min_confidence is not None:
792
+ conditions.append("confidence >= ?")
793
+ params.append(min_confidence)
794
+
795
+ where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
796
+ sql = f"SELECT * FROM protocols {where_clause} ORDER BY confidence DESC"
797
+
798
+ rows = self._fetchall(sql, tuple(params))
799
+ return [
800
+ Protocol(
801
+ id=row["id"],
802
+ session_id=row["session_id"],
803
+ name=row["name"],
804
+ spec_json=json.loads(row["spec_json"]),
805
+ confidence=row["confidence"],
806
+ created_at=datetime.fromisoformat(row["created_at"]),
807
+ )
808
+ for row in rows
809
+ ]
810
+
811
+ def store_message(
812
+ self,
813
+ protocol_id: int,
814
+ timestamp: float,
815
+ data: bytes,
816
+ decoded_fields: dict[str, Any] | None = None,
817
+ ) -> int:
818
+ """Store decoded message.
819
+
820
+ Args:
821
+ protocol_id: Parent protocol ID
822
+ timestamp: Message timestamp
823
+ data: Raw message bytes
824
+ decoded_fields: Decoded field values
825
+
826
+ Returns:
827
+ Message ID
828
+
829
+ Example:
830
+ >>> msg_id = db.store_message(
831
+ ... protocol_id=1,
832
+ ... timestamp=1.5,
833
+ ... data=b"\\xaa\\x55\\x01",
834
+ ... decoded_fields={"id": 1, "counter": 0}
835
+ ... )
836
+ """
837
+ data_hex = data.hex()
838
+ fields_json = json.dumps(decoded_fields or {})
839
+ cursor = self._execute(
840
+ "INSERT INTO messages (protocol_id, timestamp, data, decoded_fields) "
841
+ "VALUES (?, ?, ?, ?)",
842
+ (protocol_id, timestamp, data_hex, fields_json),
843
+ )
844
+ result: int = cursor.lastrowid
845
+ return result
846
+
847
+ def query_messages(
848
+ self,
849
+ protocol_id: int,
850
+ time_range: tuple[float, float] | None = None,
851
+ field_filters: dict[str, Any] | None = None,
852
+ limit: int = 100,
853
+ offset: int = 0,
854
+ ) -> QueryResult:
855
+ """Query messages with filtering and pagination.
856
+
857
+ Args:
858
+ protocol_id: Protocol ID
859
+ time_range: (start_time, end_time) tuple
860
+ field_filters: Field name -> value filters
861
+ limit: Maximum results per page
862
+ offset: Result offset for pagination
863
+
864
+ Returns:
865
+ Paginated query results
866
+
867
+ Example:
868
+ >>> # Get first 10 messages between t=0 and t=10
869
+ >>> result = db.query_messages(
870
+ ... protocol_id=1,
871
+ ... time_range=(0.0, 10.0),
872
+ ... limit=10
873
+ ... )
874
+ >>> print(f"Page {result.page + 1}/{result.total_pages}")
875
+ >>> for msg in result.items:
876
+ ... print(msg.decoded_fields)
877
+ """
878
+ conditions = ["protocol_id = ?"]
879
+ params: list[Any] = [protocol_id]
880
+
881
+ if time_range:
882
+ conditions.append("timestamp >= ? AND timestamp <= ?")
883
+ params.extend(time_range)
884
+
885
+ where_clause = f"WHERE {' AND '.join(conditions)}"
886
+
887
+ # Count total
888
+ count_sql = f"SELECT COUNT(*) as total FROM messages {where_clause}"
889
+ count_row = self._fetchone(count_sql, tuple(params))
890
+ total = count_row["total"] if count_row else 0
891
+
892
+ # Fetch page
893
+ sql = f"SELECT * FROM messages {where_clause} ORDER BY timestamp LIMIT ? OFFSET ?"
894
+ params.extend([limit, offset])
895
+ rows = self._fetchall(sql, tuple(params))
896
+
897
+ messages = [
898
+ Message(
899
+ id=row["id"],
900
+ protocol_id=row["protocol_id"],
901
+ timestamp=row["timestamp"],
902
+ data=row["data"],
903
+ decoded_fields=json.loads(row["decoded_fields"]) if row["decoded_fields"] else {},
904
+ )
905
+ for row in rows
906
+ ]
907
+
908
+ # Apply field filters (client-side for SQLite)
909
+ if field_filters:
910
+ messages = [
911
+ msg
912
+ for msg in messages
913
+ if all(msg.decoded_fields.get(k) == v for k, v in field_filters.items())
914
+ ]
915
+
916
+ return QueryResult(
917
+ items=messages,
918
+ total=total,
919
+ page=offset // limit,
920
+ page_size=limit,
921
+ )
922
+
923
+ def store_analysis_result(
924
+ self,
925
+ session_id: int,
926
+ analysis_type: str,
927
+ results_json: dict[str, Any],
928
+ metrics: dict[str, Any] | None = None,
929
+ ) -> int:
930
+ """Store analysis result.
931
+
932
+ Args:
933
+ session_id: Parent session ID
934
+ analysis_type: Analysis type (dpa, timing, entropy, etc.)
935
+ results_json: Analysis results
936
+ metrics: Computed metrics
937
+
938
+ Returns:
939
+ Result ID
940
+
941
+ Example:
942
+ >>> result_id = db.store_analysis_result(
943
+ ... session_id=1,
944
+ ... analysis_type="dpa",
945
+ ... results_json={"recovered_key": "0x1234..."},
946
+ ... metrics={"confidence": 0.95}
947
+ ... )
948
+ """
949
+ results_str = json.dumps(results_json)
950
+ metrics_str = json.dumps(metrics or {})
951
+ cursor = self._execute(
952
+ "INSERT INTO analysis_results (session_id, analysis_type, results_json, metrics) "
953
+ "VALUES (?, ?, ?, ?)",
954
+ (session_id, analysis_type, results_str, metrics_str),
955
+ )
956
+ result: int = cursor.lastrowid
957
+ return result
958
+
959
+ def get_analysis_results(
960
+ self, session_id: int, analysis_type: str | None = None
961
+ ) -> list[AnalysisResult]:
962
+ """Get analysis results for session.
963
+
964
+ Args:
965
+ session_id: Session ID
966
+ analysis_type: Filter by analysis type (optional)
967
+
968
+ Returns:
969
+ List of analysis results
970
+
971
+ Example:
972
+ >>> # Get all DPA results
973
+ >>> results = db.get_analysis_results(session_id=1, analysis_type="dpa")
974
+ >>> for result in results:
975
+ ... print(result.metrics["confidence"])
976
+ """
977
+ if analysis_type:
978
+ sql = (
979
+ "SELECT * FROM analysis_results "
980
+ "WHERE session_id = ? AND analysis_type = ? "
981
+ "ORDER BY created_at DESC"
982
+ )
983
+ params: tuple[Any, ...] = (session_id, analysis_type)
984
+ else:
985
+ sql = "SELECT * FROM analysis_results WHERE session_id = ? ORDER BY created_at DESC"
986
+ params = (session_id,)
987
+
988
+ rows = self._fetchall(sql, params)
989
+ return [
990
+ AnalysisResult(
991
+ id=row["id"],
992
+ session_id=row["session_id"],
993
+ analysis_type=row["analysis_type"],
994
+ results_json=json.loads(row["results_json"]),
995
+ metrics=json.loads(row["metrics"]) if row["metrics"] else {},
996
+ created_at=datetime.fromisoformat(row["created_at"]),
997
+ )
998
+ for row in rows
999
+ ]
1000
+
1001
+ def export_to_sql(self, output_path: str | Path) -> None:
1002
+ """Export database to SQL dump.
1003
+
1004
+ Args:
1005
+ output_path: Output SQL file path
1006
+
1007
+ Example:
1008
+ >>> db.export_to_sql("backup.sql")
1009
+ """
1010
+ output_path = Path(output_path)
1011
+
1012
+ if self._is_postgres:
1013
+ raise NotImplementedError("PostgreSQL export via pg_dump recommended")
1014
+
1015
+ # SQLite dump
1016
+ with open(output_path, "w") as f:
1017
+ for line in self._conn.iterdump():
1018
+ f.write(f"{line}\n")
1019
+
1020
+ def export_to_json(self, output_path: str | Path, project_id: int | None = None) -> None:
1021
+ """Export database contents to JSON.
1022
+
1023
+ Args:
1024
+ output_path: Output JSON file path
1025
+ project_id: Export specific project (optional)
1026
+
1027
+ Example:
1028
+ >>> db.export_to_json("export.json", project_id=1)
1029
+ """
1030
+ output_path = Path(output_path)
1031
+
1032
+ projects_list: list[Project | None]
1033
+ if project_id:
1034
+ projects_list = [self.get_project(project_id)]
1035
+ else:
1036
+ projects_list = list(self.list_projects())
1037
+
1038
+ export_data = []
1039
+ for proj in projects_list:
1040
+ if proj is None:
1041
+ continue
1042
+
1043
+ proj_data = asdict(proj)
1044
+ proj_data["sessions"] = []
1045
+
1046
+ sessions = self.get_sessions(proj.id) # type: ignore[arg-type]
1047
+ for sess in sessions:
1048
+ sess_data = asdict(sess)
1049
+ sess_data["protocols"] = []
1050
+ sess_data["analysis_results"] = []
1051
+
1052
+ # Get protocols
1053
+ protocols = self.find_protocols()
1054
+ for prot in protocols:
1055
+ if prot.session_id == sess.id:
1056
+ prot_data = asdict(prot)
1057
+ # Get messages
1058
+ msgs = self.query_messages(prot.id, limit=1000) # type: ignore[arg-type]
1059
+ prot_data["messages"] = [asdict(msg) for msg in msgs.items]
1060
+ sess_data["protocols"].append(prot_data)
1061
+
1062
+ # Get analysis results
1063
+ results = self.get_analysis_results(sess.id) # type: ignore[arg-type]
1064
+ sess_data["analysis_results"] = [asdict(r) for r in results]
1065
+
1066
+ proj_data["sessions"].append(sess_data)
1067
+
1068
+ export_data.append(proj_data)
1069
+
1070
+ with open(output_path, "w") as f:
1071
+ json.dump(export_data, f, indent=2, default=str)
1072
+
1073
+ def export_to_csv(self, output_dir: str | Path, project_id: int | None = None) -> None:
1074
+ """Export database to CSV files.
1075
+
1076
+ Args:
1077
+ output_dir: Output directory for CSV files
1078
+ project_id: Export specific project (optional)
1079
+
1080
+ Example:
1081
+ >>> db.export_to_csv("csv_export/", project_id=1)
1082
+ """
1083
+ import csv
1084
+
1085
+ output_dir = Path(output_dir)
1086
+ output_dir.mkdir(parents=True, exist_ok=True)
1087
+
1088
+ projects_list: list[Project | None]
1089
+ if project_id:
1090
+ projects_list = [self.get_project(project_id)]
1091
+ else:
1092
+ projects_list = list(self.list_projects())
1093
+
1094
+ # Export projects
1095
+ with open(output_dir / "projects.csv", "w", newline="") as f:
1096
+ writer = csv.DictWriter(
1097
+ f, fieldnames=["id", "name", "description", "created_at", "updated_at"]
1098
+ )
1099
+ writer.writeheader()
1100
+ for proj in projects_list:
1101
+ if proj:
1102
+ writer.writerow(
1103
+ {
1104
+ "id": proj.id,
1105
+ "name": proj.name,
1106
+ "description": proj.description,
1107
+ "created_at": proj.created_at,
1108
+ "updated_at": proj.updated_at,
1109
+ }
1110
+ )
1111
+
1112
+ # Export sessions
1113
+ all_sessions = []
1114
+ for proj in projects_list:
1115
+ if proj:
1116
+ all_sessions.extend(self.get_sessions(proj.id)) # type: ignore[arg-type]
1117
+
1118
+ with open(output_dir / "sessions.csv", "w", newline="") as f:
1119
+ writer = csv.DictWriter(f, fieldnames=["id", "project_id", "session_type", "timestamp"])
1120
+ writer.writeheader()
1121
+ for sess in all_sessions:
1122
+ writer.writerow(
1123
+ {
1124
+ "id": sess.id,
1125
+ "project_id": sess.project_id,
1126
+ "session_type": sess.session_type,
1127
+ "timestamp": sess.timestamp,
1128
+ }
1129
+ )
1130
+
1131
+ # Export protocols
1132
+ all_protocols = self.find_protocols()
1133
+ with open(output_dir / "protocols.csv", "w", newline="") as f:
1134
+ writer = csv.DictWriter(
1135
+ f, fieldnames=["id", "session_id", "name", "confidence", "created_at"]
1136
+ )
1137
+ writer.writeheader()
1138
+ for prot in all_protocols:
1139
+ writer.writerow(
1140
+ {
1141
+ "id": prot.id,
1142
+ "session_id": prot.session_id,
1143
+ "name": prot.name,
1144
+ "confidence": prot.confidence,
1145
+ "created_at": prot.created_at,
1146
+ }
1147
+ )
1148
+
1149
+ def close(self) -> None:
1150
+ """Close database connection/pool.
1151
+
1152
+ Example:
1153
+ >>> db.close()
1154
+ """
1155
+ if self._is_postgres and self._pool:
1156
+ self._pool.closeall()
1157
+ elif self._conn:
1158
+ self._conn.close()
1159
+
1160
+ def __enter__(self) -> DatabaseBackend:
1161
+ """Context manager entry."""
1162
+ return self
1163
+
1164
+ def __exit__(self, *args: Any) -> None:
1165
+ """Context manager exit."""
1166
+ self.close()