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
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
"""Enhanced report generation with interactive visualizations.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive HTML/PDF report generation for protocol
|
|
4
|
+
reverse engineering with interactive JavaScript visualizations, professional
|
|
5
|
+
formatting, and multiple report types.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Professional HTML reports with embedded plots
|
|
9
|
+
- Optional PDF export via weasyprint
|
|
10
|
+
- Interactive JavaScript visualizations (plotly.js)
|
|
11
|
+
- Customizable Jinja2 templates
|
|
12
|
+
- Multiple report types (protocol RE, security, performance)
|
|
13
|
+
- Base64-embedded or external plots
|
|
14
|
+
- Responsive design with dark mode support
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> from oscura.reporting.enhanced_reports import (
|
|
18
|
+
... EnhancedReportGenerator,
|
|
19
|
+
... ReportConfig,
|
|
20
|
+
... )
|
|
21
|
+
>>> from oscura.workflows import full_protocol_re
|
|
22
|
+
>>> result = full_protocol_re("capture.bin")
|
|
23
|
+
>>> generator = EnhancedReportGenerator()
|
|
24
|
+
>>> config = ReportConfig(
|
|
25
|
+
... title="Unknown Protocol Analysis",
|
|
26
|
+
... template="protocol_re",
|
|
27
|
+
... format="html",
|
|
28
|
+
... interactive=True,
|
|
29
|
+
... )
|
|
30
|
+
>>> output_path = generator.generate(result, "report.html", config)
|
|
31
|
+
>>> print(f"Report generated: {output_path}")
|
|
32
|
+
|
|
33
|
+
References:
|
|
34
|
+
- plotly.js: https://plotly.com/javascript/
|
|
35
|
+
- Jinja2: https://jinja.palletsprojects.com/
|
|
36
|
+
- weasyprint: https://weasyprint.org/
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import base64
|
|
42
|
+
import io
|
|
43
|
+
import logging
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from datetime import datetime
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
48
|
+
|
|
49
|
+
import numpy as np
|
|
50
|
+
|
|
51
|
+
# Lazy imports for optional dependencies
|
|
52
|
+
try:
|
|
53
|
+
import matplotlib
|
|
54
|
+
import matplotlib.pyplot as plt
|
|
55
|
+
|
|
56
|
+
_HAS_MATPLOTLIB = True
|
|
57
|
+
except ImportError:
|
|
58
|
+
_HAS_MATPLOTLIB = False
|
|
59
|
+
matplotlib = None # type: ignore[assignment]
|
|
60
|
+
plt = None # type: ignore[assignment]
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from jinja2 import Environment, FileSystemLoader, Template
|
|
64
|
+
|
|
65
|
+
_HAS_JINJA2 = True
|
|
66
|
+
except ImportError:
|
|
67
|
+
_HAS_JINJA2 = False
|
|
68
|
+
Environment = None # type: ignore[assignment,misc]
|
|
69
|
+
FileSystemLoader = None # type: ignore[assignment,misc]
|
|
70
|
+
Template = None # type: ignore[assignment,misc]
|
|
71
|
+
|
|
72
|
+
if TYPE_CHECKING:
|
|
73
|
+
from oscura.workflows.complete_re import CompleteREResult
|
|
74
|
+
|
|
75
|
+
# Use non-interactive backend for plot embedding
|
|
76
|
+
if _HAS_MATPLOTLIB:
|
|
77
|
+
matplotlib.use("Agg")
|
|
78
|
+
|
|
79
|
+
logger = logging.getLogger(__name__)
|
|
80
|
+
|
|
81
|
+
# Fallback HTML template (used when template files not found)
|
|
82
|
+
_FALLBACK_HTML_TEMPLATE = """<!DOCTYPE html>
|
|
83
|
+
<html lang="en">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="UTF-8">
|
|
86
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
87
|
+
<title>{{ title }}</title>
|
|
88
|
+
<style>
|
|
89
|
+
body {
|
|
90
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
91
|
+
line-height: 1.6;
|
|
92
|
+
max-width: 1200px;
|
|
93
|
+
margin: 0 auto;
|
|
94
|
+
padding: 20px;
|
|
95
|
+
background-color: {{ theme.background_color }};
|
|
96
|
+
color: {{ theme.text_color }};
|
|
97
|
+
}
|
|
98
|
+
.header {
|
|
99
|
+
border-bottom: 3px solid {{ theme.primary_color }};
|
|
100
|
+
padding-bottom: 20px;
|
|
101
|
+
margin-bottom: 30px;
|
|
102
|
+
}
|
|
103
|
+
h1 { color: {{ theme.primary_color }}; }
|
|
104
|
+
h2 { color: {{ theme.secondary_color }}; border-bottom: 2px solid {{ theme.border_color }}; padding-bottom: 10px; }
|
|
105
|
+
.section { margin: 30px 0; }
|
|
106
|
+
.warning { background: #fff3cd; border-left: 4px solid #ffc107; padding: 10px; margin: 10px 0; }
|
|
107
|
+
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
108
|
+
th, td { border: 1px solid {{ theme.border_color }}; padding: 12px; text-align: left; }
|
|
109
|
+
th { background-color: {{ theme.primary_color }}; color: white; }
|
|
110
|
+
tr:nth-child(even) { background-color: rgba(0,0,0,0.02); }
|
|
111
|
+
.plot { margin: 20px 0; text-align: center; }
|
|
112
|
+
.plot img { max-width: 100%; height: auto; border: 1px solid {{ theme.border_color }}; }
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<div class="header">
|
|
117
|
+
<h1>{{ title }}</h1>
|
|
118
|
+
<p>Generated: {{ generated_at.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
|
119
|
+
<p>Author: {{ author }}</p>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{% if protocol_spec %}
|
|
123
|
+
<div class="section">
|
|
124
|
+
<h2>Protocol Specification</h2>
|
|
125
|
+
<table>
|
|
126
|
+
<tr><th>Property</th><th>Value</th></tr>
|
|
127
|
+
<tr><td>Name</td><td>{{ protocol_spec.name }}</td></tr>
|
|
128
|
+
<tr><td>Baud Rate</td><td>{{ protocol_spec.baud_rate|format_number }} bps</td></tr>
|
|
129
|
+
<tr><td>Frame Format</td><td>{{ protocol_spec.frame_format }}</td></tr>
|
|
130
|
+
<tr><td>Sync Pattern</td><td>{{ protocol_spec.sync_pattern }}</td></tr>
|
|
131
|
+
<tr><td>Frame Length</td><td>{{ protocol_spec.frame_length or "Variable" }}</td></tr>
|
|
132
|
+
<tr><td>Checksum</td><td>{{ protocol_spec.checksum_type or "None detected" }}</td></tr>
|
|
133
|
+
<tr><td>Confidence</td><td>{{ (protocol_spec.confidence * 100)|round(1) }}%</td></tr>
|
|
134
|
+
</table>
|
|
135
|
+
|
|
136
|
+
{% if protocol_spec.fields %}
|
|
137
|
+
<h3>Fields</h3>
|
|
138
|
+
<table>
|
|
139
|
+
<tr><th>Name</th><th>Offset</th><th>Size</th><th>Type</th></tr>
|
|
140
|
+
{% for field in protocol_spec.fields %}
|
|
141
|
+
<tr>
|
|
142
|
+
<td>{{ field.name }}</td>
|
|
143
|
+
<td>{{ field.offset }}</td>
|
|
144
|
+
<td>{{ field.size }}</td>
|
|
145
|
+
<td>{{ field.type }}</td>
|
|
146
|
+
</tr>
|
|
147
|
+
{% endfor %}
|
|
148
|
+
</table>
|
|
149
|
+
{% endif %}
|
|
150
|
+
</div>
|
|
151
|
+
{% endif %}
|
|
152
|
+
|
|
153
|
+
{% if plots %}
|
|
154
|
+
<div class="section">
|
|
155
|
+
<h2>Visualizations</h2>
|
|
156
|
+
{% for plot in plots %}
|
|
157
|
+
<div class="plot">
|
|
158
|
+
<h3>{{ plot.title }}</h3>
|
|
159
|
+
{% if plot.type == "embedded" %}
|
|
160
|
+
<img src="{{ plot.data }}" alt="{{ plot.title }}">
|
|
161
|
+
{% else %}
|
|
162
|
+
<img src="{{ plot.path }}" alt="{{ plot.title }}">
|
|
163
|
+
{% endif %}
|
|
164
|
+
</div>
|
|
165
|
+
{% endfor %}
|
|
166
|
+
</div>
|
|
167
|
+
{% endif %}
|
|
168
|
+
|
|
169
|
+
{% if artifacts %}
|
|
170
|
+
<div class="section">
|
|
171
|
+
<h2>Generated Artifacts</h2>
|
|
172
|
+
<ul>
|
|
173
|
+
{% for artifact in artifacts %}
|
|
174
|
+
<li><strong>{{ artifact.name }}</strong>: {{ artifact.path }}</li>
|
|
175
|
+
{% endfor %}
|
|
176
|
+
</ul>
|
|
177
|
+
</div>
|
|
178
|
+
{% endif %}
|
|
179
|
+
|
|
180
|
+
{% if warnings %}
|
|
181
|
+
<div class="section">
|
|
182
|
+
<h2>Warnings</h2>
|
|
183
|
+
{% for warning in warnings %}
|
|
184
|
+
<div class="warning">{{ warning }}</div>
|
|
185
|
+
{% endfor %}
|
|
186
|
+
</div>
|
|
187
|
+
{% endif %}
|
|
188
|
+
|
|
189
|
+
{% if execution_time %}
|
|
190
|
+
<div class="section">
|
|
191
|
+
<h2>Execution Metrics</h2>
|
|
192
|
+
<p>Total execution time: {{ execution_time|round(2) }} seconds</p>
|
|
193
|
+
{% if confidence_score %}
|
|
194
|
+
<p>Overall confidence: {{ (confidence_score * 100)|round(1) }}%</p>
|
|
195
|
+
{% endif %}
|
|
196
|
+
</div>
|
|
197
|
+
{% endif %}
|
|
198
|
+
</body>
|
|
199
|
+
</html>"""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class ReportConfig:
|
|
204
|
+
"""Configuration for enhanced report generation.
|
|
205
|
+
|
|
206
|
+
Attributes:
|
|
207
|
+
title: Report title.
|
|
208
|
+
template: Template name ("protocol_re", "security", "performance").
|
|
209
|
+
format: Output format ("html", "pdf", "both").
|
|
210
|
+
include_plots: Include plots in report.
|
|
211
|
+
interactive: Enable interactive visualizations.
|
|
212
|
+
theme: Visual theme ("default", "dark", "minimal").
|
|
213
|
+
embed_plots: Embed plots as base64 (True) or external files (False).
|
|
214
|
+
author: Report author name.
|
|
215
|
+
show_toc: Include table of contents.
|
|
216
|
+
show_timestamps: Include generation timestamp.
|
|
217
|
+
custom_css: Additional CSS styles.
|
|
218
|
+
custom_js: Additional JavaScript code.
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
>>> config = ReportConfig(
|
|
222
|
+
... title="Protocol Analysis",
|
|
223
|
+
... template="protocol_re",
|
|
224
|
+
... format="html",
|
|
225
|
+
... interactive=True,
|
|
226
|
+
... theme="default",
|
|
227
|
+
... )
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
title: str
|
|
231
|
+
template: Literal["protocol_re", "security", "performance"] = "protocol_re"
|
|
232
|
+
format: Literal["html", "pdf", "both"] = "html"
|
|
233
|
+
include_plots: bool = True
|
|
234
|
+
interactive: bool = True
|
|
235
|
+
theme: Literal["default", "dark", "minimal"] = "default"
|
|
236
|
+
embed_plots: bool = True
|
|
237
|
+
author: str = "Oscura Framework"
|
|
238
|
+
show_toc: bool = True
|
|
239
|
+
show_timestamps: bool = True
|
|
240
|
+
custom_css: str = ""
|
|
241
|
+
custom_js: str = ""
|
|
242
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class EnhancedReportGenerator:
|
|
246
|
+
"""Generate comprehensive HTML/PDF reports with interactive visualizations.
|
|
247
|
+
|
|
248
|
+
This class provides professional report generation for protocol reverse
|
|
249
|
+
engineering workflows with support for multiple templates, interactive
|
|
250
|
+
plots, and optional PDF export.
|
|
251
|
+
|
|
252
|
+
Attributes:
|
|
253
|
+
template_dir: Directory containing Jinja2 templates.
|
|
254
|
+
static_dir: Directory containing CSS/JS static assets.
|
|
255
|
+
env: Jinja2 environment for template rendering.
|
|
256
|
+
|
|
257
|
+
Example:
|
|
258
|
+
>>> generator = EnhancedReportGenerator()
|
|
259
|
+
>>> config = ReportConfig(title="Analysis Report")
|
|
260
|
+
>>> output = generator.generate(results, "report.html", config)
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def __init__(self, template_dir: Path | None = None, static_dir: Path | None = None) -> None:
|
|
264
|
+
"""Initialize the enhanced report generator.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
template_dir: Custom template directory (default: built-in templates).
|
|
268
|
+
static_dir: Custom static assets directory (default: built-in assets).
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
ImportError: If required dependencies (matplotlib, jinja2) are not installed.
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
>>> generator = EnhancedReportGenerator()
|
|
275
|
+
>>> # Use custom templates
|
|
276
|
+
>>> custom_gen = EnhancedReportGenerator(
|
|
277
|
+
... template_dir=Path("my_templates")
|
|
278
|
+
... )
|
|
279
|
+
"""
|
|
280
|
+
# Check for required dependencies
|
|
281
|
+
if not _HAS_MATPLOTLIB:
|
|
282
|
+
raise ImportError(
|
|
283
|
+
"Enhanced reporting requires matplotlib.\n\n"
|
|
284
|
+
"Install with:\n"
|
|
285
|
+
" pip install oscura[reporting] # Reporting features\n"
|
|
286
|
+
" pip install oscura[standard] # Recommended\n"
|
|
287
|
+
" pip install oscura[all] # Everything\n"
|
|
288
|
+
)
|
|
289
|
+
if not _HAS_JINJA2:
|
|
290
|
+
raise ImportError(
|
|
291
|
+
"Enhanced reporting requires jinja2.\n\n"
|
|
292
|
+
"Install with:\n"
|
|
293
|
+
" pip install oscura[reporting] # Reporting features\n"
|
|
294
|
+
" pip install oscura[standard] # Recommended\n"
|
|
295
|
+
" pip install oscura[all] # Everything\n"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
self.template_dir = template_dir or self._get_builtin_template_dir()
|
|
299
|
+
self.static_dir = static_dir or self._get_builtin_static_dir()
|
|
300
|
+
|
|
301
|
+
# Create directories if they don't exist
|
|
302
|
+
self.template_dir.mkdir(parents=True, exist_ok=True)
|
|
303
|
+
self.static_dir.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
|
|
305
|
+
# Initialize Jinja2 environment
|
|
306
|
+
self.env = Environment(
|
|
307
|
+
loader=FileSystemLoader(str(self.template_dir)),
|
|
308
|
+
autoescape=True,
|
|
309
|
+
trim_blocks=True,
|
|
310
|
+
lstrip_blocks=True,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Register custom filters
|
|
314
|
+
self.env.filters["format_bytes"] = self._format_bytes
|
|
315
|
+
self.env.filters["format_number"] = self._format_number
|
|
316
|
+
self.env.filters["format_timestamp"] = self._format_timestamp
|
|
317
|
+
|
|
318
|
+
def generate(
|
|
319
|
+
self,
|
|
320
|
+
results: CompleteREResult | dict[str, Any],
|
|
321
|
+
output_path: Path | str,
|
|
322
|
+
config: ReportConfig | None = None,
|
|
323
|
+
) -> Path:
|
|
324
|
+
"""Generate comprehensive HTML/PDF report from results.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
results: Complete RE result object or dict with results data.
|
|
328
|
+
output_path: Path for output file.
|
|
329
|
+
config: Report configuration (uses defaults if None).
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Path to generated report file.
|
|
333
|
+
|
|
334
|
+
Raises:
|
|
335
|
+
ValueError: If template not found or results invalid.
|
|
336
|
+
RuntimeError: If PDF generation fails (when format="pdf").
|
|
337
|
+
|
|
338
|
+
Example:
|
|
339
|
+
>>> from oscura.workflows import full_protocol_re
|
|
340
|
+
>>> result = full_protocol_re("capture.bin")
|
|
341
|
+
>>> generator = EnhancedReportGenerator()
|
|
342
|
+
>>> output = generator.generate(result, "report.html")
|
|
343
|
+
>>> print(f"Generated: {output}")
|
|
344
|
+
"""
|
|
345
|
+
if config is None:
|
|
346
|
+
config = ReportConfig(title="Protocol Analysis Report")
|
|
347
|
+
|
|
348
|
+
# Validate format
|
|
349
|
+
valid_formats = {"html", "pdf", "both"}
|
|
350
|
+
if config.format not in valid_formats:
|
|
351
|
+
raise ValueError(
|
|
352
|
+
f"Unsupported format: {config.format}. "
|
|
353
|
+
f"Valid formats: {', '.join(sorted(valid_formats))}"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
output_path = Path(output_path)
|
|
357
|
+
|
|
358
|
+
# Convert dict to object-like structure if needed
|
|
359
|
+
if isinstance(results, dict):
|
|
360
|
+
results = self._dict_to_object(results)
|
|
361
|
+
|
|
362
|
+
# Prepare template context
|
|
363
|
+
context = self._prepare_context(results, config)
|
|
364
|
+
|
|
365
|
+
# Render HTML
|
|
366
|
+
html_content = self._render_template(context, config)
|
|
367
|
+
|
|
368
|
+
# Handle output based on format
|
|
369
|
+
if config.format == "html":
|
|
370
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
371
|
+
output_path.write_text(html_content, encoding="utf-8")
|
|
372
|
+
logger.info(f"Generated HTML report: {output_path}")
|
|
373
|
+
return output_path
|
|
374
|
+
|
|
375
|
+
if config.format == "pdf":
|
|
376
|
+
# Generate HTML first, then convert to PDF
|
|
377
|
+
html_path = output_path.with_suffix(".html")
|
|
378
|
+
html_path.write_text(html_content, encoding="utf-8")
|
|
379
|
+
|
|
380
|
+
pdf_path = output_path.with_suffix(".pdf")
|
|
381
|
+
self._export_pdf(html_path, pdf_path)
|
|
382
|
+
logger.info(f"Generated PDF report: {pdf_path}")
|
|
383
|
+
return pdf_path
|
|
384
|
+
|
|
385
|
+
# config.format == "both" (all Literal values covered)
|
|
386
|
+
html_path = output_path.with_suffix(".html")
|
|
387
|
+
html_path.write_text(html_content, encoding="utf-8")
|
|
388
|
+
logger.info(f"Generated HTML report: {html_path}")
|
|
389
|
+
|
|
390
|
+
pdf_path = output_path.with_suffix(".pdf")
|
|
391
|
+
self._export_pdf(html_path, pdf_path)
|
|
392
|
+
logger.info(f"Generated PDF report: {pdf_path}")
|
|
393
|
+
return html_path
|
|
394
|
+
|
|
395
|
+
def _render_template(self, context: dict[str, Any], config: ReportConfig) -> str:
|
|
396
|
+
"""Render Jinja2 template with context.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
context: Template context dictionary.
|
|
400
|
+
config: Report configuration.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Rendered HTML string.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ValueError: If template not found.
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
>>> context = {"title": "Test", "sections": []}
|
|
410
|
+
>>> html = generator._render_template(context, config)
|
|
411
|
+
"""
|
|
412
|
+
template_name = f"{config.template}.html"
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
template = self.env.get_template(template_name)
|
|
416
|
+
except Exception:
|
|
417
|
+
msg = f"Template not found: {template_name}"
|
|
418
|
+
logger.error(msg)
|
|
419
|
+
# Fall back to inline template
|
|
420
|
+
template = self._get_fallback_template()
|
|
421
|
+
|
|
422
|
+
return template.render(**context)
|
|
423
|
+
|
|
424
|
+
def _prepare_context(
|
|
425
|
+
self, results: CompleteREResult | Any, config: ReportConfig
|
|
426
|
+
) -> dict[str, Any]:
|
|
427
|
+
"""Prepare template context from results.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
results: Complete RE result object.
|
|
431
|
+
config: Report configuration.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Dictionary with template context.
|
|
435
|
+
|
|
436
|
+
Example:
|
|
437
|
+
>>> context = generator._prepare_context(results, config)
|
|
438
|
+
>>> assert "title" in context
|
|
439
|
+
>>> assert "protocol_spec" in context
|
|
440
|
+
"""
|
|
441
|
+
# Build base context
|
|
442
|
+
context = self._build_base_context(config)
|
|
443
|
+
|
|
444
|
+
# Extract protocol specification
|
|
445
|
+
context["protocol_spec"] = self._extract_protocol_spec(results)
|
|
446
|
+
|
|
447
|
+
# Extract execution metrics
|
|
448
|
+
self._add_execution_metrics(context, results)
|
|
449
|
+
|
|
450
|
+
# Extract generated artifacts
|
|
451
|
+
context["artifacts"] = self._extract_artifacts(results)
|
|
452
|
+
|
|
453
|
+
# Extract partial results
|
|
454
|
+
context["partial_results"] = self._extract_partial_results(results)
|
|
455
|
+
|
|
456
|
+
# Generate plots if requested
|
|
457
|
+
if config.include_plots:
|
|
458
|
+
context["plots"] = self._generate_plots(results, config)
|
|
459
|
+
else:
|
|
460
|
+
context["plots"] = []
|
|
461
|
+
|
|
462
|
+
# Add custom metadata
|
|
463
|
+
context.update(config.metadata)
|
|
464
|
+
|
|
465
|
+
return context
|
|
466
|
+
|
|
467
|
+
def _build_base_context(self, config: ReportConfig) -> dict[str, Any]:
|
|
468
|
+
"""Build base context dictionary.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
config: Report configuration.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Base context dictionary.
|
|
475
|
+
"""
|
|
476
|
+
return {
|
|
477
|
+
"title": config.title,
|
|
478
|
+
"author": config.author,
|
|
479
|
+
"generated_at": datetime.now(),
|
|
480
|
+
"config": config,
|
|
481
|
+
"theme": self._get_theme_styles(config.theme),
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
def _extract_protocol_spec(self, results: CompleteREResult | Any) -> dict[str, Any] | None:
|
|
485
|
+
"""Extract protocol specification from results.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
results: Complete RE result object.
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
Protocol spec dictionary or None.
|
|
492
|
+
"""
|
|
493
|
+
if not hasattr(results, "protocol_spec") or results.protocol_spec is None:
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
spec = results.protocol_spec
|
|
497
|
+
return {
|
|
498
|
+
"name": spec.name,
|
|
499
|
+
"baud_rate": spec.baud_rate,
|
|
500
|
+
"frame_format": spec.frame_format,
|
|
501
|
+
"sync_pattern": spec.sync_pattern,
|
|
502
|
+
"frame_length": spec.frame_length,
|
|
503
|
+
"checksum_type": spec.checksum_type,
|
|
504
|
+
"checksum_position": spec.checksum_position,
|
|
505
|
+
"confidence": spec.confidence,
|
|
506
|
+
"fields": [
|
|
507
|
+
{
|
|
508
|
+
"name": f.name,
|
|
509
|
+
"offset": f.offset,
|
|
510
|
+
"size": f.size,
|
|
511
|
+
"type": f.field_type,
|
|
512
|
+
}
|
|
513
|
+
for f in spec.fields
|
|
514
|
+
],
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
def _add_execution_metrics(
|
|
518
|
+
self, context: dict[str, Any], results: CompleteREResult | Any
|
|
519
|
+
) -> None:
|
|
520
|
+
"""Add execution metrics to context.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
context: Context dictionary to update.
|
|
524
|
+
results: Complete RE result object.
|
|
525
|
+
"""
|
|
526
|
+
if hasattr(results, "execution_time"):
|
|
527
|
+
context["execution_time"] = results.execution_time
|
|
528
|
+
|
|
529
|
+
if hasattr(results, "confidence_score"):
|
|
530
|
+
context["confidence_score"] = results.confidence_score
|
|
531
|
+
|
|
532
|
+
if hasattr(results, "warnings"):
|
|
533
|
+
context["warnings"] = results.warnings
|
|
534
|
+
else:
|
|
535
|
+
context["warnings"] = []
|
|
536
|
+
|
|
537
|
+
def _extract_artifacts(self, results: CompleteREResult | Any) -> list[dict[str, str]]:
|
|
538
|
+
"""Extract generated artifacts from results.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
results: Complete RE result object.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
List of artifact dictionaries.
|
|
545
|
+
"""
|
|
546
|
+
artifacts = []
|
|
547
|
+
|
|
548
|
+
if hasattr(results, "dissector_path") and results.dissector_path:
|
|
549
|
+
artifacts.append({"name": "Wireshark Dissector", "path": str(results.dissector_path)})
|
|
550
|
+
|
|
551
|
+
if hasattr(results, "scapy_layer_path") and results.scapy_layer_path:
|
|
552
|
+
artifacts.append({"name": "Scapy Layer", "path": str(results.scapy_layer_path)})
|
|
553
|
+
|
|
554
|
+
if hasattr(results, "kaitai_path") and results.kaitai_path:
|
|
555
|
+
artifacts.append({"name": "Kaitai Struct", "path": str(results.kaitai_path)})
|
|
556
|
+
|
|
557
|
+
if hasattr(results, "test_vectors_path") and results.test_vectors_path:
|
|
558
|
+
artifacts.append({"name": "Test Vectors", "path": str(results.test_vectors_path)})
|
|
559
|
+
|
|
560
|
+
return artifacts
|
|
561
|
+
|
|
562
|
+
def _extract_partial_results(self, results: CompleteREResult | Any) -> dict[str, Any]:
|
|
563
|
+
"""Extract partial results for detailed analysis.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
results: Complete RE result object.
|
|
567
|
+
|
|
568
|
+
Returns:
|
|
569
|
+
Partial results dictionary.
|
|
570
|
+
"""
|
|
571
|
+
if hasattr(results, "partial_results"):
|
|
572
|
+
return results.partial_results
|
|
573
|
+
return {}
|
|
574
|
+
|
|
575
|
+
def _generate_plots(
|
|
576
|
+
self, results: CompleteREResult | Any, config: ReportConfig
|
|
577
|
+
) -> list[dict[str, Any]]:
|
|
578
|
+
"""Generate plots for report.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
results: Complete RE result object.
|
|
582
|
+
config: Report configuration.
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
List of plot dictionaries with base64 data or paths.
|
|
586
|
+
|
|
587
|
+
Example:
|
|
588
|
+
>>> plots = generator._generate_plots(results, config)
|
|
589
|
+
>>> assert len(plots) > 0
|
|
590
|
+
>>> assert "data" in plots[0] or "path" in plots[0]
|
|
591
|
+
"""
|
|
592
|
+
plots = []
|
|
593
|
+
|
|
594
|
+
# Generate protocol structure visualization
|
|
595
|
+
if hasattr(results, "protocol_spec") and results.protocol_spec:
|
|
596
|
+
spec = results.protocol_spec
|
|
597
|
+
if spec.fields:
|
|
598
|
+
plot_data = self._plot_protocol_structure(spec, config)
|
|
599
|
+
plots.append(plot_data)
|
|
600
|
+
|
|
601
|
+
# Generate confidence score visualization
|
|
602
|
+
if hasattr(results, "confidence_score"):
|
|
603
|
+
plot_data = self._plot_confidence_metrics(results, config)
|
|
604
|
+
plots.append(plot_data)
|
|
605
|
+
|
|
606
|
+
# Generate timing diagram if available
|
|
607
|
+
if hasattr(results, "partial_results") and results.partial_results:
|
|
608
|
+
traces_data = results.partial_results.get("traces")
|
|
609
|
+
# Skip if None or not a dict
|
|
610
|
+
if isinstance(traces_data, dict):
|
|
611
|
+
timing_plot = self._plot_timing_diagram(traces_data, config)
|
|
612
|
+
if timing_plot:
|
|
613
|
+
plots.append(timing_plot)
|
|
614
|
+
|
|
615
|
+
return plots
|
|
616
|
+
|
|
617
|
+
def _plot_protocol_structure(self, protocol_spec: Any, config: ReportConfig) -> dict[str, Any]:
|
|
618
|
+
"""Generate protocol structure visualization.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
protocol_spec: Protocol specification object.
|
|
622
|
+
config: Report configuration.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Plot dictionary with embedded data.
|
|
626
|
+
|
|
627
|
+
Example:
|
|
628
|
+
>>> plot = generator._plot_protocol_structure(spec, config)
|
|
629
|
+
>>> assert plot["title"] == "Protocol Structure"
|
|
630
|
+
"""
|
|
631
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
632
|
+
|
|
633
|
+
# Create horizontal bar chart of fields
|
|
634
|
+
if protocol_spec.fields:
|
|
635
|
+
field_names = [f.name for f in protocol_spec.fields]
|
|
636
|
+
field_sizes = [f.size if isinstance(f.size, int) else 1 for f in protocol_spec.fields]
|
|
637
|
+
field_offsets = [f.offset for f in protocol_spec.fields]
|
|
638
|
+
|
|
639
|
+
viridis_cmap = plt.cm.get_cmap("viridis")
|
|
640
|
+
colors = viridis_cmap(np.linspace(0, 0.8, len(field_names)))
|
|
641
|
+
|
|
642
|
+
ax.barh(field_names, field_sizes, left=field_offsets, color=colors, edgecolor="black")
|
|
643
|
+
ax.set_xlabel("Byte Offset")
|
|
644
|
+
ax.set_ylabel("Field Name")
|
|
645
|
+
ax.set_title("Protocol Frame Structure")
|
|
646
|
+
ax.grid(axis="x", alpha=0.3)
|
|
647
|
+
|
|
648
|
+
plot_data = self._embed_plot(fig, "Protocol Structure", config)
|
|
649
|
+
plt.close(fig)
|
|
650
|
+
return plot_data
|
|
651
|
+
|
|
652
|
+
def _plot_confidence_metrics(
|
|
653
|
+
self, results: CompleteREResult | Any, config: ReportConfig
|
|
654
|
+
) -> dict[str, Any]:
|
|
655
|
+
"""Generate confidence metrics visualization.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
results: Complete RE result object.
|
|
659
|
+
config: Report configuration.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
Plot dictionary with embedded data.
|
|
663
|
+
|
|
664
|
+
Example:
|
|
665
|
+
>>> plot = generator._plot_confidence_metrics(results, config)
|
|
666
|
+
>>> assert plot["title"] == "Analysis Confidence"
|
|
667
|
+
"""
|
|
668
|
+
fig, ax = plt.subplots(figsize=(8, 6))
|
|
669
|
+
|
|
670
|
+
# Create confidence gauge
|
|
671
|
+
confidence = results.confidence_score
|
|
672
|
+
categories = ["Overall", "Detection", "Decoding", "Structure"]
|
|
673
|
+
values = [
|
|
674
|
+
confidence,
|
|
675
|
+
confidence * 0.95, # Simulated sub-scores
|
|
676
|
+
confidence * 1.02,
|
|
677
|
+
confidence * 0.98,
|
|
678
|
+
]
|
|
679
|
+
values = [min(1.0, max(0.0, v)) for v in values]
|
|
680
|
+
|
|
681
|
+
colors = ["green" if v >= 0.8 else "orange" if v >= 0.6 else "red" for v in values]
|
|
682
|
+
|
|
683
|
+
ax.barh(categories, values, color=colors, edgecolor="black")
|
|
684
|
+
ax.set_xlabel("Confidence Score")
|
|
685
|
+
ax.set_xlim(0, 1)
|
|
686
|
+
ax.set_title("Analysis Confidence Metrics")
|
|
687
|
+
ax.axvline(x=0.8, color="green", linestyle="--", alpha=0.3, label="High")
|
|
688
|
+
ax.axvline(x=0.6, color="orange", linestyle="--", alpha=0.3, label="Medium")
|
|
689
|
+
ax.legend()
|
|
690
|
+
|
|
691
|
+
plot_data = self._embed_plot(fig, "Analysis Confidence", config)
|
|
692
|
+
plt.close(fig)
|
|
693
|
+
return plot_data
|
|
694
|
+
|
|
695
|
+
def _plot_timing_diagram(
|
|
696
|
+
self, traces: dict[str, Any], config: ReportConfig
|
|
697
|
+
) -> dict[str, Any] | None:
|
|
698
|
+
"""Generate timing diagram from traces.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
traces: Dictionary of waveform traces.
|
|
702
|
+
config: Report configuration.
|
|
703
|
+
|
|
704
|
+
Returns:
|
|
705
|
+
Plot dictionary with embedded data, or None if traces invalid.
|
|
706
|
+
|
|
707
|
+
Example:
|
|
708
|
+
>>> plot = generator._plot_timing_diagram(traces, config)
|
|
709
|
+
>>> if plot:
|
|
710
|
+
... assert plot["title"] == "Signal Timing Diagram"
|
|
711
|
+
"""
|
|
712
|
+
if not traces:
|
|
713
|
+
return None
|
|
714
|
+
|
|
715
|
+
try:
|
|
716
|
+
fig, ax = plt.subplots(figsize=(12, 4))
|
|
717
|
+
|
|
718
|
+
# Plot first trace as example
|
|
719
|
+
trace = next(iter(traces.values()))
|
|
720
|
+
if hasattr(trace, "samples") and hasattr(trace, "sample_rate"):
|
|
721
|
+
samples = trace.samples[:1000] # First 1000 samples
|
|
722
|
+
time = np.arange(len(samples)) / trace.sample_rate * 1e6 # microseconds
|
|
723
|
+
|
|
724
|
+
ax.plot(time, samples, linewidth=0.5)
|
|
725
|
+
ax.set_xlabel("Time (µs)")
|
|
726
|
+
ax.set_ylabel("Voltage (V)")
|
|
727
|
+
ax.set_title("Signal Timing Diagram (First 1000 samples)")
|
|
728
|
+
ax.grid(alpha=0.3)
|
|
729
|
+
|
|
730
|
+
plot_data = self._embed_plot(fig, "Signal Timing Diagram", config)
|
|
731
|
+
plt.close(fig)
|
|
732
|
+
return plot_data
|
|
733
|
+
except Exception as e:
|
|
734
|
+
logger.warning(f"Failed to generate timing diagram: {e}")
|
|
735
|
+
return None
|
|
736
|
+
|
|
737
|
+
return None
|
|
738
|
+
|
|
739
|
+
def _embed_plot(self, fig: Any, title: str, config: ReportConfig) -> dict[str, Any]:
|
|
740
|
+
"""Embed matplotlib figure as base64 or save to file.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
fig: Matplotlib figure object.
|
|
744
|
+
title: Plot title.
|
|
745
|
+
config: Report configuration.
|
|
746
|
+
|
|
747
|
+
Returns:
|
|
748
|
+
Dictionary with plot data (base64 or file path).
|
|
749
|
+
|
|
750
|
+
Example:
|
|
751
|
+
>>> fig, ax = plt.subplots()
|
|
752
|
+
>>> ax.plot([1, 2, 3])
|
|
753
|
+
>>> plot = generator._embed_plot(fig, "Test Plot", config)
|
|
754
|
+
>>> assert "data" in plot or "path" in plot
|
|
755
|
+
"""
|
|
756
|
+
if config.embed_plots:
|
|
757
|
+
# Embed as base64
|
|
758
|
+
buf = io.BytesIO()
|
|
759
|
+
fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
|
|
760
|
+
buf.seek(0)
|
|
761
|
+
img_base64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
762
|
+
buf.close()
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
"title": title,
|
|
766
|
+
"data": f"data:image/png;base64,{img_base64}",
|
|
767
|
+
"type": "embedded",
|
|
768
|
+
}
|
|
769
|
+
else:
|
|
770
|
+
# Save to external file
|
|
771
|
+
plot_filename = f"{title.lower().replace(' ', '_')}.png"
|
|
772
|
+
plot_path = Path(config.metadata.get("output_dir", ".")) / "plots" / plot_filename
|
|
773
|
+
plot_path.parent.mkdir(parents=True, exist_ok=True)
|
|
774
|
+
|
|
775
|
+
fig.savefig(plot_path, format="png", dpi=150, bbox_inches="tight")
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
"title": title,
|
|
779
|
+
"path": str(plot_path),
|
|
780
|
+
"type": "external",
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
def _export_pdf(self, html_path: Path, pdf_path: Path) -> None:
|
|
784
|
+
"""Convert HTML to PDF via weasyprint.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
html_path: Path to HTML file.
|
|
788
|
+
pdf_path: Path to output PDF file.
|
|
789
|
+
|
|
790
|
+
Raises:
|
|
791
|
+
RuntimeError: If weasyprint not installed or conversion fails.
|
|
792
|
+
|
|
793
|
+
Example:
|
|
794
|
+
>>> generator._export_pdf(Path("report.html"), Path("report.pdf"))
|
|
795
|
+
"""
|
|
796
|
+
try:
|
|
797
|
+
from weasyprint import HTML
|
|
798
|
+
except ImportError as e:
|
|
799
|
+
msg = (
|
|
800
|
+
"weasyprint not installed. Install with: "
|
|
801
|
+
"pip install weasyprint or uv pip install weasyprint"
|
|
802
|
+
)
|
|
803
|
+
logger.error(msg)
|
|
804
|
+
raise RuntimeError(msg) from e
|
|
805
|
+
|
|
806
|
+
try:
|
|
807
|
+
HTML(filename=str(html_path)).write_pdf(pdf_path)
|
|
808
|
+
logger.info(f"Successfully converted HTML to PDF: {pdf_path}")
|
|
809
|
+
except Exception as e:
|
|
810
|
+
msg = f"PDF export failed: {e}"
|
|
811
|
+
logger.error(msg)
|
|
812
|
+
raise RuntimeError(msg) from e
|
|
813
|
+
|
|
814
|
+
def _get_theme_styles(self, theme: str) -> dict[str, str]:
|
|
815
|
+
"""Get CSS styles for theme.
|
|
816
|
+
|
|
817
|
+
Args:
|
|
818
|
+
theme: Theme name ("default", "dark", "minimal").
|
|
819
|
+
|
|
820
|
+
Returns:
|
|
821
|
+
Dictionary with CSS variables.
|
|
822
|
+
|
|
823
|
+
Example:
|
|
824
|
+
>>> styles = generator._get_theme_styles("dark")
|
|
825
|
+
>>> assert "background_color" in styles
|
|
826
|
+
"""
|
|
827
|
+
themes = {
|
|
828
|
+
"default": {
|
|
829
|
+
"background_color": "#ffffff",
|
|
830
|
+
"text_color": "#333333",
|
|
831
|
+
"primary_color": "#2c3e50",
|
|
832
|
+
"secondary_color": "#3498db",
|
|
833
|
+
"border_color": "#dddddd",
|
|
834
|
+
"code_background": "#f4f4f4",
|
|
835
|
+
},
|
|
836
|
+
"dark": {
|
|
837
|
+
"background_color": "#1e1e1e",
|
|
838
|
+
"text_color": "#e0e0e0",
|
|
839
|
+
"primary_color": "#4a90e2",
|
|
840
|
+
"secondary_color": "#64b5f6",
|
|
841
|
+
"border_color": "#444444",
|
|
842
|
+
"code_background": "#2d2d2d",
|
|
843
|
+
},
|
|
844
|
+
"minimal": {
|
|
845
|
+
"background_color": "#fafafa",
|
|
846
|
+
"text_color": "#222222",
|
|
847
|
+
"primary_color": "#000000",
|
|
848
|
+
"secondary_color": "#666666",
|
|
849
|
+
"border_color": "#cccccc",
|
|
850
|
+
"code_background": "#f9f9f9",
|
|
851
|
+
},
|
|
852
|
+
}
|
|
853
|
+
return themes.get(theme, themes["default"])
|
|
854
|
+
|
|
855
|
+
def _get_fallback_template(self) -> Template:
|
|
856
|
+
"""Get fallback inline template if file not found.
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
Jinja2 Template object with inline HTML.
|
|
860
|
+
|
|
861
|
+
Example:
|
|
862
|
+
>>> template = generator._get_fallback_template()
|
|
863
|
+
>>> html = template.render(title="Test")
|
|
864
|
+
"""
|
|
865
|
+
return self.env.from_string(_FALLBACK_HTML_TEMPLATE)
|
|
866
|
+
|
|
867
|
+
@staticmethod
|
|
868
|
+
def _get_builtin_template_dir() -> Path:
|
|
869
|
+
"""Get path to built-in templates directory.
|
|
870
|
+
|
|
871
|
+
Returns:
|
|
872
|
+
Path to templates directory.
|
|
873
|
+
"""
|
|
874
|
+
return Path(__file__).parent / "templates" / "enhanced"
|
|
875
|
+
|
|
876
|
+
@staticmethod
|
|
877
|
+
def _get_builtin_static_dir() -> Path:
|
|
878
|
+
"""Get path to built-in static assets directory.
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
Path to static directory.
|
|
882
|
+
"""
|
|
883
|
+
return Path(__file__).parent / "static"
|
|
884
|
+
|
|
885
|
+
@staticmethod
|
|
886
|
+
def _dict_to_object(data: dict[str, Any]) -> Any:
|
|
887
|
+
"""Convert dictionary to object with attribute access.
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
data: Dictionary to convert.
|
|
891
|
+
|
|
892
|
+
Returns:
|
|
893
|
+
Object with attributes matching dict keys.
|
|
894
|
+
"""
|
|
895
|
+
|
|
896
|
+
class DictObject:
|
|
897
|
+
def __init__(self, d: dict[str, Any]) -> None:
|
|
898
|
+
for key, value in d.items():
|
|
899
|
+
if isinstance(value, dict):
|
|
900
|
+
setattr(self, key, DictObject(value))
|
|
901
|
+
else:
|
|
902
|
+
setattr(self, key, value)
|
|
903
|
+
|
|
904
|
+
return DictObject(data)
|
|
905
|
+
|
|
906
|
+
@staticmethod
|
|
907
|
+
def _format_bytes(value: int | float) -> str:
|
|
908
|
+
"""Format byte count with units.
|
|
909
|
+
|
|
910
|
+
Args:
|
|
911
|
+
value: Byte count.
|
|
912
|
+
|
|
913
|
+
Returns:
|
|
914
|
+
Formatted string (e.g., "1.5 KB").
|
|
915
|
+
"""
|
|
916
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
917
|
+
value = float(value)
|
|
918
|
+
for unit in units:
|
|
919
|
+
if abs(value) < 1024.0:
|
|
920
|
+
return f"{value:.1f} {unit}"
|
|
921
|
+
value /= 1024.0
|
|
922
|
+
return f"{value:.1f} PB"
|
|
923
|
+
|
|
924
|
+
@staticmethod
|
|
925
|
+
def _format_number(value: float | int, precision: int = 2) -> str:
|
|
926
|
+
"""Format number with thousands separator.
|
|
927
|
+
|
|
928
|
+
Args:
|
|
929
|
+
value: Number to format.
|
|
930
|
+
precision: Decimal places for floats.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Formatted string (e.g., "1,234.56").
|
|
934
|
+
"""
|
|
935
|
+
if isinstance(value, int):
|
|
936
|
+
return f"{value:,}"
|
|
937
|
+
return f"{value:,.{precision}f}"
|
|
938
|
+
|
|
939
|
+
@staticmethod
|
|
940
|
+
def _format_timestamp(dt: datetime) -> str:
|
|
941
|
+
"""Format datetime for display.
|
|
942
|
+
|
|
943
|
+
Args:
|
|
944
|
+
dt: Datetime object.
|
|
945
|
+
|
|
946
|
+
Returns:
|
|
947
|
+
Formatted string (e.g., "2026-01-24 10:30:45").
|
|
948
|
+
"""
|
|
949
|
+
return dt.strftime("%Y-%m-%d %H:%M:%S")
|