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
oscura/reporting/index.py CHANGED
@@ -312,26 +312,61 @@ class IndexGenerator:
312
312
 
313
313
  Requirements:
314
314
  """
315
- # Extract timestamp properly from output_dir name
316
- # Format is: YYYYMMDD_HHMMSS_name_analysis
317
- dir_name = result.output_dir.name
318
- timestamp = "N/A"
319
- if "_" in dir_name:
320
- parts = dir_name.split("_")
321
- if len(parts) >= 2:
322
- date_part = parts[0] # YYYYMMDD
323
- time_part = parts[1] # HHMMSS
324
- if len(date_part) == 8 and len(time_part) == 6:
325
- try:
326
- timestamp = (
327
- f"{date_part[:4]}-{date_part[4:6]}-{date_part[6:8]} "
328
- f"{time_part[:2]}:{time_part[2:4]}:{time_part[4:6]}"
329
- )
330
- except (IndexError, ValueError):
331
- timestamp = f"{date_part}_{time_part}"
332
-
333
- # Basic metadata
334
- context: dict[str, Any] = {
315
+ # Extract timestamp and basic metadata
316
+ timestamp = self._extract_timestamp(result.output_dir.name)
317
+ context = self._build_basic_metadata(result, timestamp)
318
+
319
+ # Build domain information
320
+ domains = self._build_domains_info(result)
321
+ context["domains"] = domains
322
+
323
+ # Build error information
324
+ if result.errors:
325
+ context["errors"] = self._build_errors_info(result.errors)
326
+
327
+ return context
328
+
329
+ def _extract_timestamp(self, dir_name: str) -> str:
330
+ """Extract formatted timestamp from directory name.
331
+
332
+ Args:
333
+ dir_name: Directory name in format YYYYMMDD_HHMMSS_name_analysis.
334
+
335
+ Returns:
336
+ Formatted timestamp string.
337
+ """
338
+ if "_" not in dir_name:
339
+ return "N/A"
340
+
341
+ parts = dir_name.split("_")
342
+ if len(parts) < 2:
343
+ return "N/A"
344
+
345
+ date_part = parts[0] # YYYYMMDD
346
+ time_part = parts[1] # HHMMSS
347
+
348
+ if len(date_part) == 8 and len(time_part) == 6:
349
+ try:
350
+ return (
351
+ f"{date_part[:4]}-{date_part[4:6]}-{date_part[6:8]} "
352
+ f"{time_part[:2]}:{time_part[2:4]}:{time_part[4:6]}"
353
+ )
354
+ except (IndexError, ValueError):
355
+ return f"{date_part}_{time_part}"
356
+
357
+ return "N/A"
358
+
359
+ def _build_basic_metadata(self, result: AnalysisResult, timestamp: str) -> dict[str, Any]:
360
+ """Build basic metadata context.
361
+
362
+ Args:
363
+ result: Analysis result.
364
+ timestamp: Formatted timestamp.
365
+
366
+ Returns:
367
+ Dictionary with basic metadata.
368
+ """
369
+ return {
335
370
  "title": "Analysis Report",
336
371
  "input_name": result.input_file or "In-Memory Data",
337
372
  "input_size": self._format_size(result.input_file),
@@ -345,44 +380,23 @@ class IndexGenerator:
345
380
  "has_errors": len(result.errors) > 0,
346
381
  }
347
382
 
348
- # Build domain information
349
- # domain_summaries contains {AnalysisDomain: {func_name: result, ...}}
383
+ def _build_domains_info(self, result: AnalysisResult) -> list[dict[str, Any]]:
384
+ """Build domain information list.
385
+
386
+ Args:
387
+ result: Analysis result.
388
+
389
+ Returns:
390
+ List of domain dictionaries.
391
+ """
350
392
  domains: list[dict[str, Any]] = []
393
+
351
394
  for domain, domain_results in result.domain_summaries.items():
352
- # Count successful analyses in this domain
353
- # domain_results is a dict of {function_name: result_value}
354
395
  analyses_count = len(domain_results) if isinstance(domain_results, dict) else 0
355
396
 
356
- # Find plots for this domain
357
- domain_plots = []
358
- if result.plot_paths:
359
- domain_id = domain.value
360
- for plot_path in result.plot_paths:
361
- # Check if plot belongs to this domain
362
- plot_str = str(plot_path)
363
- if f"/{domain_id}/" in plot_str or plot_str.startswith(domain_id):
364
- domain_plots.append(
365
- {
366
- "title": plot_path.stem.replace("_", " ").title(),
367
- "path": str(plot_path.name)
368
- if plot_path.parent == result.output_dir
369
- else str(plot_path.relative_to(result.output_dir)),
370
- "filename": plot_path.name,
371
- }
372
- )
373
-
374
- # Find data files for this domain
375
- domain_data_files = []
376
- domain_dir = result.domain_dirs.get(domain)
377
- if domain_dir and domain_dir.exists():
378
- for data_file in domain_dir.glob("*.json"):
379
- domain_data_files.append(
380
- {
381
- "filename": data_file.name,
382
- "path": str(data_file.relative_to(result.output_dir)),
383
- "format": "JSON",
384
- }
385
- )
397
+ # Find plots and data files for this domain
398
+ domain_plots = self._find_domain_plots(result, domain)
399
+ domain_data_files = self._find_domain_data_files(result, domain)
386
400
 
387
401
  # Build key findings from results
388
402
  key_findings = self._extract_key_findings(domain_results)
@@ -399,22 +413,80 @@ class IndexGenerator:
399
413
  }
400
414
  domains.append(domain_data)
401
415
 
402
- context["domains"] = domains
416
+ return domains
403
417
 
404
- # Build error information
405
- if result.errors:
406
- errors: list[dict[str, Any]] = []
407
- for error in result.errors:
408
- errors.append(
418
+ def _find_domain_plots(self, result: AnalysisResult, domain: Any) -> list[dict[str, Any]]:
419
+ """Find plots for a specific domain.
420
+
421
+ Args:
422
+ result: Analysis result.
423
+ domain: Analysis domain.
424
+
425
+ Returns:
426
+ List of plot dictionaries.
427
+ """
428
+ domain_plots: list[dict[str, Any]] = []
429
+ if not result.plot_paths:
430
+ return domain_plots
431
+
432
+ domain_id = domain.value
433
+ for plot_path in result.plot_paths:
434
+ plot_str = str(plot_path)
435
+ if f"/{domain_id}/" in plot_str or plot_str.startswith(domain_id):
436
+ domain_plots.append(
409
437
  {
410
- "domain": error.domain.value,
411
- "analysis_name": error.function,
412
- "error_message": error.error_message,
438
+ "title": plot_path.stem.replace("_", " ").title(),
439
+ "path": str(plot_path.name)
440
+ if plot_path.parent == result.output_dir
441
+ else str(plot_path.relative_to(result.output_dir)),
442
+ "filename": plot_path.name,
413
443
  }
414
444
  )
415
- context["errors"] = errors
416
445
 
417
- return context
446
+ return domain_plots
447
+
448
+ def _find_domain_data_files(self, result: AnalysisResult, domain: Any) -> list[dict[str, Any]]:
449
+ """Find data files for a specific domain.
450
+
451
+ Args:
452
+ result: Analysis result.
453
+ domain: Analysis domain.
454
+
455
+ Returns:
456
+ List of data file dictionaries.
457
+ """
458
+ domain_data_files = []
459
+ domain_dir = result.domain_dirs.get(domain)
460
+
461
+ if domain_dir and domain_dir.exists():
462
+ for data_file in domain_dir.glob("*.json"):
463
+ domain_data_files.append(
464
+ {
465
+ "filename": data_file.name,
466
+ "path": str(data_file.relative_to(result.output_dir)),
467
+ "format": "JSON",
468
+ }
469
+ )
470
+
471
+ return domain_data_files
472
+
473
+ def _build_errors_info(self, errors: list[Any]) -> list[dict[str, Any]]:
474
+ """Build error information list.
475
+
476
+ Args:
477
+ errors: List of error objects.
478
+
479
+ Returns:
480
+ List of error dictionaries.
481
+ """
482
+ return [
483
+ {
484
+ "domain": error.domain.value,
485
+ "analysis_name": error.function,
486
+ "error_message": error.error_message,
487
+ }
488
+ for error in errors
489
+ ]
418
490
 
419
491
  def _extract_key_findings(self, domain_results: dict[str, Any]) -> list[str]:
420
492
  """Extract key findings from domain results for display.
@@ -11,6 +11,7 @@ from typing import Any
11
11
 
12
12
  import numpy as np
13
13
  import yaml
14
+ from numpy.typing import NDArray
14
15
 
15
16
  from oscura.reporting.config import AnalysisDomain
16
17
 
@@ -39,88 +40,178 @@ def _sanitize_for_serialization(obj: Any, max_depth: int = 10) -> Any:
39
40
  # Don't sanitize Oscura types - let the JSONEncoder handle them
40
41
  if isinstance(obj, WaveformTrace | DigitalTrace | TraceMetadata):
41
42
  return obj
43
+
44
+ # Dispatch to type-specific sanitizers
42
45
  if isinstance(obj, dict):
43
- # Sanitize both keys and values, convert non-string keys to strings
44
- sanitized = {}
45
- for k, v in obj.items():
46
- # Convert bytes keys to hex strings
47
- if isinstance(k, bytes):
48
- k = f"0x{k.hex()}"
49
- # Convert other non-string keys to strings
50
- elif not isinstance(k, str | int | float | bool | type(None)):
51
- k = str(k)
52
- sanitized[k] = _sanitize_for_serialization(v, max_depth - 1)
53
- return sanitized
46
+ return _sanitize_dict(obj, max_depth)
54
47
  elif isinstance(obj, list | tuple):
55
- return [_sanitize_for_serialization(item, max_depth - 1) for item in obj]
48
+ return _sanitize_sequence(obj, max_depth)
56
49
  elif isinstance(obj, types.GeneratorType):
57
- # Convert generators to lists, but catch errors
58
- try:
59
- items = list(obj)
60
- return [_sanitize_for_serialization(item, max_depth - 1) for item in items]
61
- except Exception:
62
- # Return None for incompatible generators (cleaner than error string)
63
- return None
50
+ return _sanitize_generator(obj, max_depth)
64
51
  elif isinstance(obj, np.ndarray):
65
- # Limit large arrays
66
- if obj.size > 10000:
67
- return f"<ndarray shape={obj.shape} dtype={obj.dtype}>"
68
- return obj.tolist()
69
- elif isinstance(obj, np.generic):
70
- # Catch all numpy scalar types (int, float, complex, bool, str, etc.)
71
- # This includes np.integer, np.floating, np.bool_, np.complexfloating, etc.
72
- return obj.item()
73
- elif isinstance(obj, np.integer | np.floating):
74
- # Redundant but kept for clarity
75
- return obj.item()
76
- elif isinstance(obj, np.bool_):
77
- # Redundant but kept for clarity
78
- return bool(obj)
52
+ return _sanitize_ndarray(obj)
53
+ elif isinstance(obj, np.generic | np.integer | np.floating | np.bool_):
54
+ return _sanitize_numpy_scalar(obj)
79
55
  elif isinstance(obj, float):
80
- # Handle Python float inf/nan (not caught by JSONEncoder.default)
81
- import math
82
-
83
- if math.isinf(obj) or math.isnan(obj):
84
- return None
85
- return obj
56
+ return _sanitize_float(obj)
86
57
  elif isinstance(obj, complex):
87
- # Handle complex numbers with inf/nan components
88
- import math
89
-
90
- if (
91
- math.isinf(obj.real)
92
- or math.isnan(obj.real)
93
- or math.isinf(obj.imag)
94
- or math.isnan(obj.imag)
95
- ):
96
- return None
97
- return {"real": obj.real, "imag": obj.imag}
58
+ return _sanitize_complex(obj)
98
59
  elif isinstance(obj, bytes):
99
- # Limit large byte sequences
100
- if len(obj) > 1000:
101
- return f"<bytes len={len(obj)}>"
102
- return obj.hex()
60
+ return _sanitize_bytes(obj)
103
61
  elif hasattr(obj, "__dict__") and not isinstance(obj, type):
104
- # Convert dataclasses and objects to dicts
105
- try:
106
- return {
107
- k: _sanitize_for_serialization(v, max_depth - 1)
108
- for k, v in obj.__dict__.items()
109
- }
110
- except Exception:
111
- return str(obj)
62
+ return _sanitize_object(obj, max_depth)
112
63
  elif callable(obj):
113
64
  return f"<callable: {getattr(obj, '__name__', str(obj))}>"
114
65
  else:
115
- # Try to convert to string as last resort
116
- try:
117
- return obj
118
- except Exception:
119
- return str(obj)
66
+ return obj
67
+
120
68
  except Exception as e:
121
69
  return f"<error: {type(e).__name__}: {str(e)[:50]}>"
122
70
 
123
71
 
72
+ def _sanitize_dict(obj: dict[Any, Any], max_depth: int) -> dict[Any, Any]:
73
+ """Sanitize dictionary for serialization.
74
+
75
+ Args:
76
+ obj: Dictionary to sanitize.
77
+ max_depth: Remaining recursion depth.
78
+
79
+ Returns:
80
+ Sanitized dictionary with string keys.
81
+ """
82
+ sanitized = {}
83
+ for k, v in obj.items():
84
+ # Convert bytes keys to hex strings
85
+ if isinstance(k, bytes):
86
+ k = f"0x{k.hex()}"
87
+ # Convert other non-string keys to strings
88
+ elif not isinstance(k, str | int | float | bool | type(None)):
89
+ k = str(k)
90
+ sanitized[k] = _sanitize_for_serialization(v, max_depth - 1)
91
+ return sanitized
92
+
93
+
94
+ def _sanitize_sequence(obj: list[Any] | tuple[Any, ...], max_depth: int) -> list[Any]:
95
+ """Sanitize list or tuple for serialization.
96
+
97
+ Args:
98
+ obj: Sequence to sanitize.
99
+ max_depth: Remaining recursion depth.
100
+
101
+ Returns:
102
+ Sanitized list.
103
+ """
104
+ return [_sanitize_for_serialization(item, max_depth - 1) for item in obj]
105
+
106
+
107
+ def _sanitize_generator(obj: Any, max_depth: int) -> list[Any] | None:
108
+ """Sanitize generator for serialization.
109
+
110
+ Args:
111
+ obj: Generator to sanitize.
112
+ max_depth: Remaining recursion depth.
113
+
114
+ Returns:
115
+ Sanitized list or None if generator fails.
116
+ """
117
+ try:
118
+ items = list(obj)
119
+ return [_sanitize_for_serialization(item, max_depth - 1) for item in items]
120
+ except Exception:
121
+ return None
122
+
123
+
124
+ def _sanitize_ndarray(obj: NDArray[Any]) -> str | list[Any]:
125
+ """Sanitize numpy array for serialization.
126
+
127
+ Args:
128
+ obj: Numpy array to sanitize.
129
+
130
+ Returns:
131
+ String representation for large arrays, list for small arrays.
132
+ """
133
+ if obj.size > 10000:
134
+ return f"<ndarray shape={obj.shape} dtype={obj.dtype}>"
135
+ result: list[Any] = obj.tolist()
136
+ return result
137
+
138
+
139
+ def _sanitize_numpy_scalar(obj: Any) -> Any:
140
+ """Sanitize numpy scalar types for serialization.
141
+
142
+ Args:
143
+ obj: Numpy scalar to sanitize.
144
+
145
+ Returns:
146
+ Python native type.
147
+ """
148
+ if isinstance(obj, np.bool_):
149
+ return bool(obj)
150
+ return obj.item()
151
+
152
+
153
+ def _sanitize_float(obj: float) -> float | None:
154
+ """Sanitize Python float for serialization.
155
+
156
+ Args:
157
+ obj: Float to sanitize.
158
+
159
+ Returns:
160
+ Float or None if inf/nan.
161
+ """
162
+ import math
163
+
164
+ if math.isinf(obj) or math.isnan(obj):
165
+ return None
166
+ return obj
167
+
168
+
169
+ def _sanitize_complex(obj: complex) -> dict[str, float] | None:
170
+ """Sanitize complex number for serialization.
171
+
172
+ Args:
173
+ obj: Complex number to sanitize.
174
+
175
+ Returns:
176
+ Dictionary with real/imag or None if inf/nan components.
177
+ """
178
+ import math
179
+
180
+ if math.isinf(obj.real) or math.isnan(obj.real) or math.isinf(obj.imag) or math.isnan(obj.imag):
181
+ return None
182
+ return {"real": obj.real, "imag": obj.imag}
183
+
184
+
185
+ def _sanitize_bytes(obj: bytes) -> str:
186
+ """Sanitize bytes for serialization.
187
+
188
+ Args:
189
+ obj: Bytes to sanitize.
190
+
191
+ Returns:
192
+ Hex string or size placeholder for large sequences.
193
+ """
194
+ if len(obj) > 1000:
195
+ return f"<bytes len={len(obj)}>"
196
+ return obj.hex()
197
+
198
+
199
+ def _sanitize_object(obj: Any, max_depth: int) -> dict[str, Any] | str:
200
+ """Sanitize generic object for serialization.
201
+
202
+ Args:
203
+ obj: Object to sanitize.
204
+ max_depth: Remaining recursion depth.
205
+
206
+ Returns:
207
+ Sanitized dictionary or string representation.
208
+ """
209
+ try:
210
+ return {k: _sanitize_for_serialization(v, max_depth - 1) for k, v in obj.__dict__.items()}
211
+ except Exception:
212
+ return str(obj)
213
+
214
+
124
215
  class OutputManager:
125
216
  """Manages output directory structure and file operations for analysis reports.
126
217
 
oscura/reporting/pdf.py CHANGED
@@ -132,108 +132,73 @@ def generate_pdf_report(
132
132
  def _create_styles() -> dict[str, ParagraphStyle]:
133
133
  """Create PDF paragraph styles."""
134
134
  base_styles = getSampleStyleSheet()
135
-
136
- # Create a new dict to hold our custom styles
137
- styles: dict[str, ParagraphStyle] = {}
138
-
139
- # Copy base styles we want to keep
140
- styles["Normal"] = base_styles["Normal"]
141
-
142
- # Title style (24pt) - override default
143
- styles["Title"] = ParagraphStyle(
144
- name="Title",
145
- parent=base_styles["Normal"],
146
- fontSize=24,
147
- textColor=colors.HexColor("#2c3e50"),
148
- spaceAfter=12,
149
- alignment=1, # Center
150
- fontName="Helvetica-Bold",
151
- )
152
-
153
- # Heading styles. - override defaults
154
- styles["Heading1"] = ParagraphStyle(
155
- name="Heading1",
156
- parent=base_styles["Normal"],
157
- fontSize=18,
158
- textColor=colors.HexColor("#2c3e50"),
159
- spaceBefore=12,
160
- spaceAfter=6,
161
- fontName="Helvetica-Bold",
162
- )
163
-
164
- styles["Heading2"] = ParagraphStyle(
165
- name="Heading2",
166
- parent=base_styles["Normal"],
167
- fontSize=14,
168
- textColor=colors.HexColor("#34495e"),
169
- spaceBefore=10,
170
- spaceAfter=4,
171
- fontName="Helvetica-Bold",
172
- )
173
-
174
- styles["Heading3"] = ParagraphStyle(
175
- name="Heading3",
176
- parent=base_styles["Normal"],
177
- fontSize=12,
178
- textColor=colors.HexColor("#34495e"),
179
- spaceBefore=8,
180
- spaceAfter=4,
181
- fontName="Helvetica-Bold",
182
- )
183
-
184
- # Body text (10pt, serif, 1.5 line spacing)
185
- styles["Body"] = ParagraphStyle(
186
- name="Body",
187
- parent=base_styles["Normal"],
188
- fontSize=10,
189
- leading=15, # 1.5 line spacing
190
- fontName="Times-Roman",
191
- )
192
-
193
- # Metadata style
194
- styles["Metadata"] = ParagraphStyle(
195
- name="Metadata",
196
- parent=base_styles["Normal"],
197
- fontSize=9,
198
- textColor=colors.HexColor("#555555"),
199
- fontName="Helvetica",
200
- )
201
-
202
- # TOC style
203
- styles["TOC"] = ParagraphStyle(
204
- name="TOC",
205
- parent=base_styles["Normal"],
206
- fontSize=10,
207
- leftIndent=20,
208
- spaceAfter=4,
209
- )
210
-
211
- # Pass/Fail styles with visual emphasis.
212
- styles["Pass"] = ParagraphStyle(
213
- name="Pass",
214
- parent=base_styles["Normal"],
215
- fontSize=10,
216
- textColor=colors.HexColor("#27ae60"),
217
- fontName="Helvetica-Bold",
218
- )
219
-
220
- styles["Fail"] = ParagraphStyle(
221
- name="Fail",
222
- parent=base_styles["Normal"],
223
- fontSize=10,
224
- textColor=colors.HexColor("#e74c3c"),
225
- fontName="Helvetica-Bold",
226
- )
227
-
228
- styles["Warning"] = ParagraphStyle(
229
- name="Warning",
230
- parent=base_styles["Normal"],
231
- fontSize=10,
232
- textColor=colors.HexColor("#f39c12"),
233
- fontName="Helvetica-Bold",
234
- )
235
-
236
- return styles
135
+ base = base_styles["Normal"]
136
+
137
+ return {
138
+ "Normal": base,
139
+ "Title": ParagraphStyle(
140
+ "Title",
141
+ base,
142
+ fontSize=24,
143
+ textColor=colors.HexColor("#2c3e50"),
144
+ spaceAfter=12,
145
+ alignment=1,
146
+ fontName="Helvetica-Bold",
147
+ ),
148
+ "Heading1": ParagraphStyle(
149
+ "Heading1",
150
+ base,
151
+ fontSize=18,
152
+ textColor=colors.HexColor("#2c3e50"),
153
+ spaceBefore=12,
154
+ spaceAfter=6,
155
+ fontName="Helvetica-Bold",
156
+ ),
157
+ "Heading2": ParagraphStyle(
158
+ "Heading2",
159
+ base,
160
+ fontSize=14,
161
+ textColor=colors.HexColor("#34495e"),
162
+ spaceBefore=10,
163
+ spaceAfter=4,
164
+ fontName="Helvetica-Bold",
165
+ ),
166
+ "Heading3": ParagraphStyle(
167
+ "Heading3",
168
+ base,
169
+ fontSize=12,
170
+ textColor=colors.HexColor("#34495e"),
171
+ spaceBefore=8,
172
+ spaceAfter=4,
173
+ fontName="Helvetica-Bold",
174
+ ),
175
+ "Body": ParagraphStyle("Body", base, fontSize=10, leading=15, fontName="Times-Roman"),
176
+ "Metadata": ParagraphStyle(
177
+ "Metadata", base, fontSize=9, textColor=colors.HexColor("#555555"), fontName="Helvetica"
178
+ ),
179
+ "TOC": ParagraphStyle("TOC", base, fontSize=10, leftIndent=20, spaceAfter=4),
180
+ "Pass": ParagraphStyle(
181
+ "Pass",
182
+ base,
183
+ fontSize=10,
184
+ textColor=colors.HexColor("#27ae60"),
185
+ fontName="Helvetica-Bold",
186
+ ),
187
+ "Fail": ParagraphStyle(
188
+ "Fail",
189
+ base,
190
+ fontSize=10,
191
+ textColor=colors.HexColor("#e74c3c"),
192
+ fontName="Helvetica-Bold",
193
+ ),
194
+ "Warning": ParagraphStyle(
195
+ "Warning",
196
+ base,
197
+ fontSize=10,
198
+ textColor=colors.HexColor("#f39c12"),
199
+ fontName="Helvetica-Bold",
200
+ ),
201
+ }
237
202
 
238
203
 
239
204
  def _format_metadata(report: Report) -> str: