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
@@ -142,68 +142,111 @@ def _export_docx(
142
142
  References:
143
143
  REPORT-019
144
144
  """
145
+ doc = _create_docx_document()
146
+ path = output_path.with_suffix(".docx")
147
+
148
+ _add_docx_header(doc, report)
149
+ _add_docx_sections(doc, report)
150
+
151
+ doc.save(str(path))
152
+ return path
153
+
154
+
155
+ def _create_docx_document() -> Any:
156
+ """Create and configure DOCX document.
157
+
158
+ Returns:
159
+ Document object from python-docx.
160
+
161
+ Raises:
162
+ ImportError: If python-docx not installed.
163
+ """
145
164
  try:
146
- from docx import Document # type: ignore[import-not-found]
147
- from docx.enum.text import ( # type: ignore[import-not-found]
148
- WD_ALIGN_PARAGRAPH, # type: ignore[import-not-found]
149
- )
150
- from docx.shared import ( # noqa: F401 # type: ignore[import-not-found]
151
- Inches,
152
- Pt,
153
- RGBColor,
154
- )
165
+ from docx import Document
155
166
  except ImportError:
156
- raise ImportError( # noqa: B904
167
+ raise ImportError(
157
168
  "python-docx is required for DOCX export. Install with: pip install python-docx"
158
169
  )
170
+ return Document()
159
171
 
160
- path = output_path.with_suffix(".docx")
161
- doc = Document()
162
172
 
163
- # Add title
173
+ def _add_docx_header(doc: Any, report: Report) -> None:
174
+ """Add title and metadata to DOCX document.
175
+
176
+ Args:
177
+ doc: Document object.
178
+ report: Report to extract metadata from.
179
+ """
180
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
181
+
164
182
  title = doc.add_heading(report.config.title, level=0)
165
183
  title.alignment = WD_ALIGN_PARAGRAPH.CENTER
166
184
 
167
- # Add metadata
168
185
  if report.config.author:
169
186
  doc.add_paragraph(f"Author: {report.config.author}")
170
187
  doc.add_paragraph(f"Date: {report.config.created.strftime('%Y-%m-%d %H:%M')}")
171
- doc.add_paragraph() # Blank line
188
+ doc.add_paragraph()
189
+
190
+
191
+ def _add_docx_sections(doc: Any, report: Report) -> None:
192
+ """Add all sections to DOCX document.
172
193
 
173
- # Add sections
194
+ Args:
195
+ doc: Document object.
196
+ report: Report with sections to add.
197
+ """
174
198
  for section in report.sections:
175
199
  if not section.visible:
176
200
  continue
177
201
 
178
- # Section heading
179
202
  doc.add_heading(section.title, level=section.level)
203
+ _add_docx_section_content(doc, section)
204
+ _add_docx_subsections(doc, section)
180
205
 
181
- # Section content
182
- if isinstance(section.content, str):
183
- doc.add_paragraph(section.content)
184
-
185
- elif isinstance(section.content, list):
186
- for item in section.content:
187
- if isinstance(item, dict):
188
- if item.get("type") == "table":
189
- _add_table_to_docx(doc, item)
190
- elif item.get("type") == "figure":
191
- # Placeholder for figures
192
- doc.add_paragraph(f"[Figure: {item.get('caption', 'N/A')}]")
193
- else:
194
- doc.add_paragraph(str(item))
195
-
196
- # Subsections
197
- for subsec in section.subsections:
198
- if not subsec.visible:
199
- continue
200
- doc.add_heading(subsec.title, level=subsec.level)
201
- if isinstance(subsec.content, str):
202
- doc.add_paragraph(subsec.content)
203
-
204
- # Save document
205
- doc.save(str(path))
206
- return path
206
+
207
+ def _add_docx_section_content(doc: Any, section: Any) -> None:
208
+ """Add section content to DOCX.
209
+
210
+ Args:
211
+ doc: Document object.
212
+ section: Section with content to add.
213
+ """
214
+ if isinstance(section.content, str):
215
+ doc.add_paragraph(section.content)
216
+ elif isinstance(section.content, list):
217
+ for item in section.content:
218
+ _add_docx_content_item(doc, item)
219
+
220
+
221
+ def _add_docx_content_item(doc: Any, item: Any) -> None:
222
+ """Add single content item to DOCX.
223
+
224
+ Args:
225
+ doc: Document object.
226
+ item: Content item (dict or other).
227
+ """
228
+ if isinstance(item, dict):
229
+ if item.get("type") == "table":
230
+ _add_table_to_docx(doc, item)
231
+ elif item.get("type") == "figure":
232
+ doc.add_paragraph(f"[Figure: {item.get('caption', 'N/A')}]")
233
+ else:
234
+ doc.add_paragraph(str(item))
235
+
236
+
237
+ def _add_docx_subsections(doc: Any, section: Any) -> None:
238
+ """Add subsections to DOCX document.
239
+
240
+ Args:
241
+ doc: Document object.
242
+ section: Section with subsections.
243
+ """
244
+ for subsec in section.subsections:
245
+ if not subsec.visible:
246
+ continue
247
+ doc.add_heading(subsec.title, level=subsec.level)
248
+ if isinstance(subsec.content, str):
249
+ doc.add_paragraph(subsec.content)
207
250
 
208
251
 
209
252
  def _add_table_to_docx(doc: Any, table_dict: dict[str, Any]) -> None:
@@ -70,6 +70,68 @@ class NumberFormatter:
70
70
  self.precision = self.sig_figs
71
71
  self.use_si = self.auto_scale
72
72
 
73
+ def _format_without_scaling(self, value: float, places: int, unit: str) -> str:
74
+ """Format value without SI scaling.
75
+
76
+ Args:
77
+ value: Value to format
78
+ places: Decimal places
79
+ unit: Unit string
80
+
81
+ Returns:
82
+ Formatted string
83
+ """
84
+ abs_val = abs(value)
85
+
86
+ if abs_val != 0 and abs_val < 1:
87
+ from math import floor, log10
88
+
89
+ order = floor(log10(abs_val))
90
+ decimal_places_needed = max(places, abs(order) + 1)
91
+ return f"{value:.{decimal_places_needed}f} {unit}".strip()
92
+
93
+ if abs_val >= 1e6:
94
+ return f"{value:.{places}e} {unit}".strip()
95
+
96
+ return f"{value:.{places}f} {unit}".strip()
97
+
98
+ def _get_si_scale(self, abs_val: float) -> tuple[float, int] | None:
99
+ """Get SI scale and exponent for value.
100
+
101
+ Args:
102
+ abs_val: Absolute value
103
+
104
+ Returns:
105
+ Tuple of (scale_factor, exponent) or None for extreme values
106
+ """
107
+ # Find the appropriate SI prefix by checking value ranges
108
+ # Each range is [lower_bound, upper_bound) for the prefix
109
+ if abs_val >= 1e15:
110
+ return (1e-15, 15)
111
+ if abs_val >= 1e12:
112
+ return (1e-12, 12)
113
+ if abs_val >= 1e9:
114
+ return (1e-9, 9)
115
+ if abs_val >= 1e6:
116
+ return (1e-6, 6)
117
+ if abs_val >= 1e3:
118
+ return (1e-3, 3)
119
+ if abs_val >= 1:
120
+ return (1, 0)
121
+ if abs_val >= 1e-3:
122
+ return (1e3, -3)
123
+ if abs_val >= 1e-6:
124
+ return (1e6, -6)
125
+ if abs_val >= 1e-9:
126
+ return (1e9, -9)
127
+ if abs_val >= 1e-12:
128
+ return (1e12, -12)
129
+ if abs_val >= 1e-15:
130
+ return (1e15, -15)
131
+
132
+ # Value too small, use scientific notation
133
+ return None
134
+
73
135
  def format(
74
136
  self,
75
137
  value: float,
@@ -93,64 +155,29 @@ class NumberFormatter:
93
155
  >>> fmt.format(1500000, "Hz")
94
156
  '1.500 MHz'
95
157
  """
96
- # Handle special float values
97
158
  if math.isnan(value):
98
159
  return f"NaN {unit}".strip()
99
160
  if math.isinf(value):
100
161
  sign = "-" if value < 0 else ""
101
162
  return f"{sign}Inf {unit}".strip()
102
163
 
103
- # Determine precision
104
164
  places = decimal_places if decimal_places is not None else self.sig_figs
105
165
 
106
166
  if not self.auto_scale:
107
- # No scaling - show full value with appropriate precision
108
- # Use more decimal places for small numbers to show actual value
109
- if abs(value) != 0 and abs(value) < 1:
110
- # Calculate needed decimal places to show significant figures
111
- from math import floor, log10
112
-
113
- order = floor(log10(abs(value)))
114
- # For value like 0.0023, order=-3, need at least 4 decimals
115
- decimal_places_needed = max(places, abs(order) + 1)
116
- return f"{value:.{decimal_places_needed}f} {unit}".strip()
117
- elif abs(value) >= 1e6:
118
- return f"{value:.{places}e} {unit}".strip()
119
- return f"{value:.{places}f} {unit}".strip()
120
-
121
- # Find appropriate SI prefix
167
+ return self._format_without_scaling(value, places, unit)
168
+
122
169
  if value == 0:
123
170
  return f"0.{'0' * places} {unit}".strip()
124
171
 
125
172
  abs_val = abs(value)
173
+ scale_result = self._get_si_scale(abs_val)
126
174
 
127
- # Determine the appropriate power of 10
128
- if abs_val < 1e-15:
129
- return f"{value:.{places}e} {unit}".strip()
130
- elif abs_val < 1e-12:
131
- scaled, exp = value * 1e15, -15
132
- elif abs_val < 1e-9:
133
- scaled, exp = value * 1e12, -12
134
- elif abs_val < 1e-6:
135
- scaled, exp = value * 1e9, -9
136
- elif abs_val < 1e-3:
137
- scaled, exp = value * 1e6, -6
138
- elif abs_val < 1:
139
- scaled, exp = value * 1e3, -3
140
- elif abs_val < 1e3:
141
- scaled, exp = value, 0
142
- elif abs_val < 1e6:
143
- scaled, exp = value / 1e3, 3
144
- elif abs_val < 1e9:
145
- scaled, exp = value / 1e6, 6
146
- elif abs_val < 1e12:
147
- scaled, exp = value / 1e9, 9
148
- elif abs_val < 1e15:
149
- scaled, exp = value / 1e12, 12
150
- else:
175
+ if scale_result is None:
151
176
  return f"{value:.{places}e} {unit}".strip()
152
177
 
153
- # Get prefix (unicode or ascii)
178
+ scale, exp = scale_result
179
+ scaled = value * scale
180
+
154
181
  prefix_idx = 0 if self.unicode_prefixes else 1
155
182
  prefix = self.SI_PREFIXES.get(exp, ("", ""))[prefix_idx]
156
183
 
oscura/reporting/html.py CHANGED
@@ -80,9 +80,23 @@ def _generate_html_header(report: Report, dark_mode: bool, responsive: bool) ->
80
80
 
81
81
  def _generate_html_styles(dark_mode: bool, responsive: bool) -> str:
82
82
  """Generate CSS styles for HTML report."""
83
- styles = """
83
+ base_styles = _generate_base_styles()
84
+ typography_styles = _generate_typography_styles()
85
+ component_styles = _generate_component_styles()
86
+ media_query_styles = _generate_media_query_styles()
87
+
88
+ return f"""
84
89
  <style>
85
- /* Professional Formatting Standards */
90
+ {base_styles}
91
+ {typography_styles}
92
+ {component_styles}
93
+ {media_query_styles}
94
+ </style>"""
95
+
96
+
97
+ def _generate_base_styles() -> str:
98
+ """Generate base CSS variables and reset."""
99
+ return """/* Professional Formatting Standards */
86
100
  :root {
87
101
  --primary-color: #2c3e50;
88
102
  --secondary-color: #3498db;
@@ -96,7 +110,6 @@ def _generate_html_styles(dark_mode: bool, responsive: bool) -> str:
96
110
  --table-alt-row-bg: #f9f9f9;
97
111
  }
98
112
 
99
- /* Dark mode support */
100
113
  @media (prefers-color-scheme: dark) {
101
114
  body.dark-mode {
102
115
  --bg-color: #1e1e1e;
@@ -127,8 +140,12 @@ body {
127
140
  max-width: 1200px;
128
141
  margin: 0 auto;
129
142
  padding: 1in;
130
- }
143
+ }"""
144
+
131
145
 
146
+ def _generate_typography_styles() -> str:
147
+ """Generate typography and text styling."""
148
+ return """
132
149
  /* Typography */
133
150
  h1, h2, h3, h4, h5, h6 {
134
151
  font-family: Arial, Helvetica, sans-serif;
@@ -153,8 +170,23 @@ code, pre {
153
170
  pre {
154
171
  padding: 10px;
155
172
  overflow-x: auto;
156
- }
173
+ }"""
174
+
175
+
176
+ def _generate_component_styles() -> str:
177
+ """Generate component CSS (tables, severity, navigation, etc)."""
178
+ emphasis = _generate_emphasis_styles()
179
+ tables = _generate_table_styles()
180
+ collapsible = _generate_collapsible_styles()
181
+ metadata = _generate_metadata_styles()
182
+ navigation = _generate_navigation_styles()
157
183
 
184
+ return f"{emphasis}\n{tables}\n{collapsible}\n{metadata}\n{navigation}"
185
+
186
+
187
+ def _generate_emphasis_styles() -> str:
188
+ """Generate visual emphasis and severity styles."""
189
+ return """
158
190
  /* Visual Emphasis */
159
191
  .pass {
160
192
  color: var(--success-color);
@@ -174,7 +206,6 @@ pre {
174
206
  .pass::before { content: '\\2713 '; }
175
207
  .fail::before { content: '\\2717 '; }
176
208
 
177
- /* Severity indicators */
178
209
  .severity-critical {
179
210
  background-color: rgba(231, 76, 60, 0.2);
180
211
  border-left: 4px solid var(--danger-color);
@@ -196,7 +227,6 @@ pre {
196
227
  margin: 10px 0;
197
228
  }
198
229
 
199
- /* Callout boxes */
200
230
  .callout {
201
231
  background-color: rgba(241, 196, 15, 0.15);
202
232
  border: 1px solid var(--warning-color);
@@ -208,8 +238,12 @@ pre {
208
238
  .callout-title {
209
239
  font-weight: bold;
210
240
  margin-bottom: 10px;
211
- }
241
+ }"""
242
+
212
243
 
244
+ def _generate_table_styles() -> str:
245
+ """Generate table CSS."""
246
+ return """
213
247
  /* Tables */
214
248
  table {
215
249
  border-collapse: collapse;
@@ -243,8 +277,12 @@ caption {
243
277
  font-style: italic;
244
278
  padding: 8px;
245
279
  text-align: left;
246
- }
280
+ }"""
247
281
 
282
+
283
+ def _generate_collapsible_styles() -> str:
284
+ """Generate collapsible section CSS."""
285
+ return """
248
286
  /* Collapsible sections */
249
287
  .collapsible {
250
288
  cursor: pointer;
@@ -272,8 +310,12 @@ caption {
272
310
 
273
311
  .collapsible-content.collapsed {
274
312
  max-height: 0;
275
- }
313
+ }"""
276
314
 
315
+
316
+ def _generate_metadata_styles() -> str:
317
+ """Generate metadata section CSS."""
318
+ return """
277
319
  /* Metadata section */
278
320
  .metadata {
279
321
  background-color: var(--table-alt-row-bg);
@@ -286,8 +328,12 @@ caption {
286
328
  .metadata-item {
287
329
  display: inline-block;
288
330
  margin-right: 20px;
289
- }
331
+ }"""
290
332
 
333
+
334
+ def _generate_navigation_styles() -> str:
335
+ """Generate navigation CSS."""
336
+ return """
291
337
  /* Navigation */
292
338
  nav {
293
339
  background-color: var(--primary-color);
@@ -312,8 +358,12 @@ nav a {
312
358
 
313
359
  nav a:hover {
314
360
  text-decoration: underline;
315
- }
361
+ }"""
316
362
 
363
+
364
+ def _generate_media_query_styles() -> str:
365
+ """Generate responsive and print media queries."""
366
+ return """
317
367
  /* Responsive design */
318
368
  @media (max-width: 768px) {
319
369
  .container {
@@ -348,9 +398,7 @@ nav a:hover {
348
398
  .collapsible-content {
349
399
  max-height: none !important;
350
400
  }
351
- }
352
- </style>"""
353
- return styles
401
+ }"""
354
402
 
355
403
 
356
404
  def _generate_html_scripts() -> str:
@@ -458,48 +506,80 @@ def _generate_html_content(report: Report, collapsible: bool) -> str:
458
506
  content = []
459
507
 
460
508
  for section in report.sections:
461
- if not section.visible:
462
- continue
509
+ if section.visible:
510
+ section_html = _render_section(section, collapsible)
511
+ content.append(section_html)
463
512
 
464
- section_id = section.title.lower().replace(" ", "-")
465
- content.append(f'<section id="{section_id}">')
513
+ return "\n".join(content)
466
514
 
467
- # Section header
468
- tag = f"h{min(section.level + 1, 6)}"
469
- if collapsible and section.collapsible:
470
- content.append(f'<{tag} class="collapsible">{section.title}</{tag}>')
471
- content.append('<div class="collapsible-content">')
472
- else:
473
- content.append(f"<{tag}>{section.title}</{tag}>")
474
-
475
- # Section content
476
- if isinstance(section.content, str):
477
- content.append(f"<p>{section.content}</p>")
478
- elif isinstance(section.content, list):
479
- for item in section.content:
480
- if isinstance(item, dict):
481
- if item.get("type") == "table":
482
- content.append(_table_to_html(item))
483
- elif item.get("type") == "figure":
484
- content.append(_figure_to_html(item))
485
- else:
486
- content.append(f"<p>{item}</p>")
487
-
488
- # Subsections
489
- for subsec in section.subsections:
490
- if not subsec.visible:
491
- continue
492
- sub_tag = f"h{min(subsec.level + 1, 6)}"
493
- content.append(f"<{sub_tag}>{subsec.title}</{sub_tag}>")
494
- if isinstance(subsec.content, str):
495
- content.append(f"<p>{subsec.content}</p>")
496
515
 
497
- if collapsible and section.collapsible:
498
- content.append("</div>")
516
+ def _render_section(section: Any, collapsible: bool) -> str:
517
+ """Render a single section with header and content."""
518
+ section_id = section.title.lower().replace(" ", "-")
519
+ parts = [f'<section id="{section_id}">']
499
520
 
500
- content.append("</section>")
521
+ # Add section header
522
+ parts.append(_render_section_header(section, collapsible))
501
523
 
502
- return "\n".join(content)
524
+ # Add section content
525
+ parts.append(_render_section_content(section))
526
+
527
+ # Add subsections
528
+ parts.append(_render_subsections(section))
529
+
530
+ # Close collapsible wrapper if needed
531
+ if collapsible and section.collapsible:
532
+ parts.append("</div>")
533
+
534
+ parts.append("</section>")
535
+ return "\n".join(parts)
536
+
537
+
538
+ def _render_section_header(section: Any, collapsible: bool) -> str:
539
+ """Render section header with optional collapsible class."""
540
+ tag = f"h{min(section.level + 1, 6)}"
541
+ if collapsible and section.collapsible:
542
+ return (
543
+ f'<{tag} class="collapsible">{section.title}</{tag}>\n<div class="collapsible-content">'
544
+ )
545
+ return f"<{tag}>{section.title}</{tag}>"
546
+
547
+
548
+ def _render_section_content(section: Any) -> str:
549
+ """Render section content (text, tables, figures)."""
550
+ if isinstance(section.content, str):
551
+ return f"<p>{section.content}</p>"
552
+
553
+ if isinstance(section.content, list):
554
+ return _render_content_list(section.content)
555
+
556
+ return ""
557
+
558
+
559
+ def _render_content_list(content_list: list[Any]) -> str:
560
+ """Render list of content items (tables, figures, text)."""
561
+ rendered = []
562
+ for item in content_list:
563
+ if isinstance(item, dict):
564
+ if item.get("type") == "table":
565
+ rendered.append(_table_to_html(item))
566
+ elif item.get("type") == "figure":
567
+ rendered.append(_figure_to_html(item))
568
+ else:
569
+ rendered.append(f"<p>{item}</p>")
570
+ return "\n".join(rendered)
571
+
572
+
573
+ def _render_subsections(section: Any) -> str:
574
+ """Render all subsections."""
575
+ subsection_html = []
576
+ for subsec in section.subsections:
577
+ if subsec.visible:
578
+ sub_tag = f"h{min(subsec.level + 1, 6)}"
579
+ subsection_html.append(f"<{sub_tag}>{subsec.title}</{sub_tag}>")
580
+ if isinstance(subsec.content, str):
581
+ subsection_html.append(f"<p>{subsec.content}</p>")
582
+ return "\n".join(subsection_html)
503
583
 
504
584
 
505
585
  def _table_to_html(table: dict[str, Any]) -> str:
@@ -544,22 +624,23 @@ def _figure_to_html(figure: dict[str, Any]) -> str:
544
624
  width = figure.get("width", "100%")
545
625
  caption = figure.get("caption", "")
546
626
 
547
- html = f'<figure style="max-width: {width}; margin: 20px auto;">'
627
+ # Use list + join for O(n) string building instead of O(n²) +=
628
+ html_parts = [f'<figure style="max-width: {width}; margin: 20px auto;">']
548
629
 
549
630
  # Handle different figure types
550
631
  fig_obj = figure.get("figure")
551
632
  if isinstance(fig_obj, str):
552
633
  # Assume it's a path to an image
553
- html += f'<img src="{fig_obj}" alt="{caption}" style="width: 100%;">'
634
+ html_parts.append(f'<img src="{fig_obj}" alt="{caption}" style="width: 100%;">')
554
635
  else:
555
636
  # Placeholder for matplotlib figures
556
- html += f'<div class="figure-placeholder">[Figure: {caption}]</div>'
637
+ html_parts.append(f'<div class="figure-placeholder">[Figure: {caption}]</div>')
557
638
 
558
639
  if caption:
559
- html += f"<figcaption>{caption}</figcaption>"
640
+ html_parts.append(f"<figcaption>{caption}</figcaption>")
560
641
 
561
- html += "</figure>"
562
- return html
642
+ html_parts.append("</figure>")
643
+ return "".join(html_parts)
563
644
 
564
645
 
565
646
  def save_html_report(