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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
oscura/reporting/export.py
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
-
|
|
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()
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
462
|
-
|
|
509
|
+
if section.visible:
|
|
510
|
+
section_html = _render_section(section, collapsible)
|
|
511
|
+
content.append(section_html)
|
|
463
512
|
|
|
464
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|
-
|
|
521
|
+
# Add section header
|
|
522
|
+
parts.append(_render_section_header(section, collapsible))
|
|
501
523
|
|
|
502
|
-
|
|
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
|
-
|
|
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
|
-
|
|
634
|
+
html_parts.append(f'<img src="{fig_obj}" alt="{caption}" style="width: 100%;">')
|
|
554
635
|
else:
|
|
555
636
|
# Placeholder for matplotlib figures
|
|
556
|
-
|
|
637
|
+
html_parts.append(f'<div class="figure-placeholder">[Figure: {caption}]</div>')
|
|
557
638
|
|
|
558
639
|
if caption:
|
|
559
|
-
|
|
640
|
+
html_parts.append(f"<figcaption>{caption}</figcaption>")
|
|
560
641
|
|
|
561
|
-
|
|
562
|
-
return
|
|
642
|
+
html_parts.append("</figure>")
|
|
643
|
+
return "".join(html_parts)
|
|
563
644
|
|
|
564
645
|
|
|
565
646
|
def save_html_report(
|