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
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: