oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) 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/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.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()