oscura 0.0.1__py3-none-any.whl → 0.1.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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
"""Index file generation for comprehensive analysis reports.
|
|
2
|
+
|
|
3
|
+
This module provides HTML and Markdown index generation from analysis results
|
|
4
|
+
using a simple template engine (no external dependencies like Jinja2).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from oscura.reporting.config import AnalysisResult
|
|
15
|
+
from oscura.reporting.output import OutputManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TemplateEngine:
|
|
19
|
+
"""Simple template engine for variable substitution and control flow.
|
|
20
|
+
|
|
21
|
+
Supports:
|
|
22
|
+
- {{variable}} - Variable substitution
|
|
23
|
+
- {{#if condition}}...{{/if}} - Conditional blocks
|
|
24
|
+
- {{#each items}}...{{/each}} - Iteration blocks
|
|
25
|
+
- {{this}} - Current item in iteration
|
|
26
|
+
|
|
27
|
+
Requirements:
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
"""Initialize template engine."""
|
|
32
|
+
self._var_pattern = re.compile(r"\{\{([^#/}][^}]*)\}\}")
|
|
33
|
+
self._if_pattern = re.compile(r"\{\{#if\s+([^}]+)\}\}(.*?)\{\{/if\}\}", re.DOTALL)
|
|
34
|
+
self._each_pattern = re.compile(r"\{\{#each\s+([^}]+)\}\}(.*?)\{\{/each\}\}", re.DOTALL)
|
|
35
|
+
|
|
36
|
+
def render(self, template: str, context: dict[str, Any]) -> str:
|
|
37
|
+
"""Render template with context.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
template: Template string with placeholders.
|
|
41
|
+
context: Context dictionary for variable substitution.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Rendered template string.
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
>>> engine = TemplateEngine()
|
|
48
|
+
>>> engine.render("Hello {{name}}", {"name": "World"})
|
|
49
|
+
'Hello World'
|
|
50
|
+
>>> engine.render("{{#if show}}visible{{/if}}", {"show": True})
|
|
51
|
+
'visible'
|
|
52
|
+
>>> engine.render("{{#each items}}{{this}} {{/each}}", {"items": [1, 2]})
|
|
53
|
+
'1 2 '
|
|
54
|
+
"""
|
|
55
|
+
# Process each blocks first (innermost to outermost)
|
|
56
|
+
result = self._process_each_blocks(template, context)
|
|
57
|
+
|
|
58
|
+
# Process if blocks
|
|
59
|
+
result = self._process_if_blocks(result, context)
|
|
60
|
+
|
|
61
|
+
# Process variables
|
|
62
|
+
result = self._process_variables(result, context)
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
def _process_variables(self, template: str, context: dict[str, Any]) -> str:
|
|
67
|
+
"""Replace {{variable}} with values from context.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
template: Template string.
|
|
71
|
+
context: Context dictionary.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Template with variables replaced.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def replace_var(match: re.Match[str]) -> str:
|
|
78
|
+
var_name = match.group(1).strip()
|
|
79
|
+
|
|
80
|
+
# Handle "this" for current iteration item
|
|
81
|
+
if var_name == "this":
|
|
82
|
+
return str(context.get("_current_item", ""))
|
|
83
|
+
|
|
84
|
+
# Handle nested access like "domain.value"
|
|
85
|
+
value: Any = context
|
|
86
|
+
for key in var_name.split("."):
|
|
87
|
+
if isinstance(value, dict):
|
|
88
|
+
value = value.get(key, "")
|
|
89
|
+
elif hasattr(value, key):
|
|
90
|
+
value = getattr(value, key)
|
|
91
|
+
else:
|
|
92
|
+
value = ""
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
# Handle enum values
|
|
96
|
+
if hasattr(value, "value"):
|
|
97
|
+
value = value.value
|
|
98
|
+
|
|
99
|
+
return str(value) if value is not None else ""
|
|
100
|
+
|
|
101
|
+
return self._var_pattern.sub(replace_var, template)
|
|
102
|
+
|
|
103
|
+
def _process_if_blocks(self, template: str, context: dict[str, Any]) -> str:
|
|
104
|
+
"""Process {{#if condition}}...{{/if}} blocks.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
template: Template string.
|
|
108
|
+
context: Context dictionary.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Template with conditionals processed.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def replace_if(match: re.Match[str]) -> str:
|
|
115
|
+
condition = match.group(1).strip()
|
|
116
|
+
content = match.group(2)
|
|
117
|
+
|
|
118
|
+
# Evaluate condition
|
|
119
|
+
value = context.get(condition, False)
|
|
120
|
+
|
|
121
|
+
# Truthy check
|
|
122
|
+
if value and value != 0 and value != "" and value != []:
|
|
123
|
+
return content
|
|
124
|
+
return ""
|
|
125
|
+
|
|
126
|
+
return self._if_pattern.sub(replace_if, template)
|
|
127
|
+
|
|
128
|
+
def _process_each_blocks(self, template: str, context: dict[str, Any]) -> str:
|
|
129
|
+
"""Process {{#each items}}...{{/each}} blocks.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
template: Template string.
|
|
133
|
+
context: Context dictionary.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Template with iterations processed.
|
|
137
|
+
"""
|
|
138
|
+
# Manually find and process each blocks to handle nesting
|
|
139
|
+
result = []
|
|
140
|
+
pos = 0
|
|
141
|
+
|
|
142
|
+
while pos < len(template):
|
|
143
|
+
# Look for next {{#each}}
|
|
144
|
+
start_match = re.search(r"\{\{#each\s+([^}]+)\}\}", template[pos:])
|
|
145
|
+
if not start_match:
|
|
146
|
+
# No more each blocks
|
|
147
|
+
result.append(template[pos:])
|
|
148
|
+
break
|
|
149
|
+
|
|
150
|
+
# Add everything before this block
|
|
151
|
+
result.append(template[pos : pos + start_match.start()])
|
|
152
|
+
|
|
153
|
+
# Find the matching {{/each}} accounting for nesting
|
|
154
|
+
items_name = start_match.group(1).strip()
|
|
155
|
+
block_start = pos + start_match.end()
|
|
156
|
+
block_end = self._find_matching_end(template, block_start, "each")
|
|
157
|
+
|
|
158
|
+
if block_end == -1:
|
|
159
|
+
# No matching end tag, skip this
|
|
160
|
+
result.append(start_match.group(0))
|
|
161
|
+
pos = block_start
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
# Extract the item template
|
|
165
|
+
item_template = template[block_start:block_end]
|
|
166
|
+
|
|
167
|
+
# Get the items
|
|
168
|
+
items = context.get(items_name, [])
|
|
169
|
+
if not items:
|
|
170
|
+
# Empty result
|
|
171
|
+
pass
|
|
172
|
+
else:
|
|
173
|
+
# Render each item
|
|
174
|
+
for item in items:
|
|
175
|
+
# Create context for this iteration
|
|
176
|
+
if isinstance(item, dict):
|
|
177
|
+
item_context = {**context, **item, "_current_item": item}
|
|
178
|
+
else:
|
|
179
|
+
item_context = {**context, "this": item, "_current_item": item}
|
|
180
|
+
|
|
181
|
+
# Recursively process nested blocks
|
|
182
|
+
rendered = self._process_each_blocks(item_template, item_context)
|
|
183
|
+
rendered = self._process_if_blocks(rendered, item_context)
|
|
184
|
+
rendered = self._process_variables(rendered, item_context)
|
|
185
|
+
result.append(rendered)
|
|
186
|
+
|
|
187
|
+
# Move past the {{/each}}
|
|
188
|
+
pos = block_end + len("{{/each}}")
|
|
189
|
+
|
|
190
|
+
return "".join(result)
|
|
191
|
+
|
|
192
|
+
def _find_matching_end(self, template: str, start_pos: int, block_type: str) -> int:
|
|
193
|
+
"""Find matching end tag for a block, accounting for nesting.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
template: Template string.
|
|
197
|
+
start_pos: Position after the opening tag.
|
|
198
|
+
block_type: Block type (e.g., "each", "if").
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Position of the start of the matching {{/block_type}} tag, or -1 if not found.
|
|
202
|
+
"""
|
|
203
|
+
open_tag = f"{{{{#{block_type}"
|
|
204
|
+
close_tag = f"{{{{/{block_type}}}}}"
|
|
205
|
+
depth = 1
|
|
206
|
+
pos = start_pos
|
|
207
|
+
|
|
208
|
+
while pos < len(template) and depth > 0:
|
|
209
|
+
# Look for next open or close tag
|
|
210
|
+
next_open = template.find(open_tag, pos)
|
|
211
|
+
next_close = template.find(close_tag, pos)
|
|
212
|
+
|
|
213
|
+
if next_close == -1:
|
|
214
|
+
# No closing tag found
|
|
215
|
+
return -1
|
|
216
|
+
|
|
217
|
+
if next_open != -1 and next_open < next_close:
|
|
218
|
+
# Found nested open tag
|
|
219
|
+
depth += 1
|
|
220
|
+
pos = next_open + len(open_tag)
|
|
221
|
+
else:
|
|
222
|
+
# Found close tag
|
|
223
|
+
depth -= 1
|
|
224
|
+
if depth == 0:
|
|
225
|
+
return next_close
|
|
226
|
+
pos = next_close + len(close_tag)
|
|
227
|
+
|
|
228
|
+
return -1
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class IndexGenerator:
|
|
232
|
+
"""Generate HTML and Markdown index files from analysis results.
|
|
233
|
+
|
|
234
|
+
Creates navigable index pages that link to all analysis outputs including
|
|
235
|
+
plots, data files, and domain-specific results.
|
|
236
|
+
|
|
237
|
+
Attributes:
|
|
238
|
+
output_manager: Output manager for file operations.
|
|
239
|
+
|
|
240
|
+
Requirements:
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def __init__(self, output_manager: OutputManager) -> None:
|
|
244
|
+
"""Initialize index generator.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
output_manager: Output manager for file operations.
|
|
248
|
+
|
|
249
|
+
Examples:
|
|
250
|
+
>>> from pathlib import Path
|
|
251
|
+
>>> om = OutputManager(Path("/tmp/output"), "test")
|
|
252
|
+
>>> generator = IndexGenerator(om)
|
|
253
|
+
"""
|
|
254
|
+
self._output_manager = output_manager
|
|
255
|
+
self._engine = TemplateEngine()
|
|
256
|
+
|
|
257
|
+
# Template directory
|
|
258
|
+
self._template_dir = Path(__file__).parent / "templates"
|
|
259
|
+
|
|
260
|
+
def generate(
|
|
261
|
+
self,
|
|
262
|
+
result: AnalysisResult,
|
|
263
|
+
include_formats: list[str] | None = None,
|
|
264
|
+
) -> dict[str, Path]:
|
|
265
|
+
"""Generate index files in requested formats.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
result: Analysis result containing all output metadata.
|
|
269
|
+
include_formats: Formats to generate (e.g., ["html", "md"]).
|
|
270
|
+
Defaults to ["html", "md"] if None.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Dictionary mapping format name to generated file path.
|
|
274
|
+
|
|
275
|
+
Requirements:
|
|
276
|
+
|
|
277
|
+
Examples:
|
|
278
|
+
>>> # result = AnalysisResult(...)
|
|
279
|
+
>>> # generator = IndexGenerator(output_manager)
|
|
280
|
+
>>> # paths = generator.generate(result, ["html", "md"])
|
|
281
|
+
>>> # paths["html"] # Path to index.html
|
|
282
|
+
"""
|
|
283
|
+
if include_formats is None:
|
|
284
|
+
include_formats = ["html", "md"]
|
|
285
|
+
|
|
286
|
+
# Build context from result
|
|
287
|
+
context = self._build_context(result)
|
|
288
|
+
|
|
289
|
+
# Generate each format
|
|
290
|
+
outputs: dict[str, Path] = {}
|
|
291
|
+
|
|
292
|
+
if "html" in include_formats:
|
|
293
|
+
html_content = self._render_html(context)
|
|
294
|
+
html_path = self._output_manager.save_text("index.html", html_content)
|
|
295
|
+
outputs["html"] = html_path
|
|
296
|
+
|
|
297
|
+
if "md" in include_formats:
|
|
298
|
+
md_content = self._render_markdown(context)
|
|
299
|
+
md_path = self._output_manager.save_text("index.md", md_content)
|
|
300
|
+
outputs["md"] = md_path
|
|
301
|
+
|
|
302
|
+
return outputs
|
|
303
|
+
|
|
304
|
+
def _build_context(self, result: AnalysisResult) -> dict[str, Any]:
|
|
305
|
+
"""Build template context from AnalysisResult.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
result: Analysis result.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Context dictionary for template rendering.
|
|
312
|
+
|
|
313
|
+
Requirements:
|
|
314
|
+
"""
|
|
315
|
+
# Extract timestamp properly from output_dir name
|
|
316
|
+
# Format is: YYYYMMDD_HHMMSS_name_analysis
|
|
317
|
+
dir_name = result.output_dir.name
|
|
318
|
+
timestamp = "N/A"
|
|
319
|
+
if "_" in dir_name:
|
|
320
|
+
parts = dir_name.split("_")
|
|
321
|
+
if len(parts) >= 2:
|
|
322
|
+
date_part = parts[0] # YYYYMMDD
|
|
323
|
+
time_part = parts[1] # HHMMSS
|
|
324
|
+
if len(date_part) == 8 and len(time_part) == 6:
|
|
325
|
+
try:
|
|
326
|
+
timestamp = (
|
|
327
|
+
f"{date_part[:4]}-{date_part[4:6]}-{date_part[6:8]} "
|
|
328
|
+
f"{time_part[:2]}:{time_part[2:4]}:{time_part[4:6]}"
|
|
329
|
+
)
|
|
330
|
+
except (IndexError, ValueError):
|
|
331
|
+
timestamp = f"{date_part}_{time_part}"
|
|
332
|
+
|
|
333
|
+
# Basic metadata
|
|
334
|
+
context: dict[str, Any] = {
|
|
335
|
+
"title": "Analysis Report",
|
|
336
|
+
"input_name": result.input_file or "In-Memory Data",
|
|
337
|
+
"input_size": self._format_size(result.input_file),
|
|
338
|
+
"input_type": result.input_type.value,
|
|
339
|
+
"timestamp": timestamp,
|
|
340
|
+
"duration": self._format_duration(result.duration_seconds),
|
|
341
|
+
"total_analyses": result.total_analyses,
|
|
342
|
+
"successful": result.successful_analyses,
|
|
343
|
+
"failed": result.failed_analyses,
|
|
344
|
+
"domains_count": len(result.domain_summaries),
|
|
345
|
+
"has_errors": len(result.errors) > 0,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
# Build domain information
|
|
349
|
+
# domain_summaries contains {AnalysisDomain: {func_name: result, ...}}
|
|
350
|
+
domains: list[dict[str, Any]] = []
|
|
351
|
+
for domain, domain_results in result.domain_summaries.items():
|
|
352
|
+
# Count successful analyses in this domain
|
|
353
|
+
# domain_results is a dict of {function_name: result_value}
|
|
354
|
+
analyses_count = len(domain_results) if isinstance(domain_results, dict) else 0
|
|
355
|
+
|
|
356
|
+
# Find plots for this domain
|
|
357
|
+
domain_plots = []
|
|
358
|
+
if result.plot_paths:
|
|
359
|
+
domain_id = domain.value
|
|
360
|
+
for plot_path in result.plot_paths:
|
|
361
|
+
# Check if plot belongs to this domain
|
|
362
|
+
plot_str = str(plot_path)
|
|
363
|
+
if f"/{domain_id}/" in plot_str or plot_str.startswith(domain_id):
|
|
364
|
+
domain_plots.append(
|
|
365
|
+
{
|
|
366
|
+
"title": plot_path.stem.replace("_", " ").title(),
|
|
367
|
+
"path": str(plot_path.name)
|
|
368
|
+
if plot_path.parent == result.output_dir
|
|
369
|
+
else str(plot_path.relative_to(result.output_dir)),
|
|
370
|
+
"filename": plot_path.name,
|
|
371
|
+
}
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Find data files for this domain
|
|
375
|
+
domain_data_files = []
|
|
376
|
+
domain_dir = result.domain_dirs.get(domain)
|
|
377
|
+
if domain_dir and domain_dir.exists():
|
|
378
|
+
for data_file in domain_dir.glob("*.json"):
|
|
379
|
+
domain_data_files.append(
|
|
380
|
+
{
|
|
381
|
+
"filename": data_file.name,
|
|
382
|
+
"path": str(data_file.relative_to(result.output_dir)),
|
|
383
|
+
"format": "JSON",
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Build key findings from results
|
|
388
|
+
key_findings = self._extract_key_findings(domain_results)
|
|
389
|
+
|
|
390
|
+
domain_data: dict[str, Any] = {
|
|
391
|
+
"domain_id": domain.value,
|
|
392
|
+
"domain_name": domain.value.replace("_", " ").title(),
|
|
393
|
+
"analyses_count": analyses_count,
|
|
394
|
+
"plots_count": len(domain_plots),
|
|
395
|
+
"data_files_count": len(domain_data_files),
|
|
396
|
+
"key_findings": key_findings,
|
|
397
|
+
"plots": domain_plots,
|
|
398
|
+
"data_files": domain_data_files,
|
|
399
|
+
}
|
|
400
|
+
domains.append(domain_data)
|
|
401
|
+
|
|
402
|
+
context["domains"] = domains
|
|
403
|
+
|
|
404
|
+
# Build error information
|
|
405
|
+
if result.errors:
|
|
406
|
+
errors: list[dict[str, Any]] = []
|
|
407
|
+
for error in result.errors:
|
|
408
|
+
errors.append(
|
|
409
|
+
{
|
|
410
|
+
"domain": error.domain.value,
|
|
411
|
+
"analysis_name": error.function,
|
|
412
|
+
"error_message": error.error_message,
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
context["errors"] = errors
|
|
416
|
+
|
|
417
|
+
return context
|
|
418
|
+
|
|
419
|
+
def _extract_key_findings(self, domain_results: dict[str, Any]) -> list[str]:
|
|
420
|
+
"""Extract key findings from domain results for display.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
domain_results: Dictionary of analysis function results.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
List of key finding strings.
|
|
427
|
+
"""
|
|
428
|
+
findings = []
|
|
429
|
+
for func_name, result in domain_results.items():
|
|
430
|
+
# Extract function short name
|
|
431
|
+
short_name = func_name.split(".")[-1].replace("_", " ").title()
|
|
432
|
+
|
|
433
|
+
# Format result based on type
|
|
434
|
+
if result is None:
|
|
435
|
+
continue
|
|
436
|
+
elif isinstance(result, int | float):
|
|
437
|
+
if not (isinstance(result, float) and (result != result)): # Check for NaN
|
|
438
|
+
findings.append(
|
|
439
|
+
f"{short_name}: {result:.4g}"
|
|
440
|
+
if isinstance(result, float)
|
|
441
|
+
else f"{short_name}: {result}"
|
|
442
|
+
)
|
|
443
|
+
elif isinstance(result, dict) and len(result) <= 3:
|
|
444
|
+
# Show small dicts inline
|
|
445
|
+
items = [
|
|
446
|
+
f"{k}: {v:.4g}" if isinstance(v, float) else f"{k}: {v}"
|
|
447
|
+
for k, v in list(result.items())[:3]
|
|
448
|
+
if v is not None and not (isinstance(v, float) and v != v)
|
|
449
|
+
]
|
|
450
|
+
if items:
|
|
451
|
+
findings.append(f"{short_name}: {', '.join(items)}")
|
|
452
|
+
|
|
453
|
+
# Limit to most relevant findings
|
|
454
|
+
return findings[:5]
|
|
455
|
+
|
|
456
|
+
def _format_plots(self, plots: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|
457
|
+
"""Format plot information for templates.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
plots: List of plot dictionaries.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
Formatted plot data.
|
|
464
|
+
"""
|
|
465
|
+
formatted = []
|
|
466
|
+
for plot in plots:
|
|
467
|
+
formatted.append(
|
|
468
|
+
{
|
|
469
|
+
"title": plot.get("title", "Untitled"),
|
|
470
|
+
"path": str(plot.get("path", "")),
|
|
471
|
+
"filename": Path(plot.get("path", "")).name,
|
|
472
|
+
}
|
|
473
|
+
)
|
|
474
|
+
return formatted
|
|
475
|
+
|
|
476
|
+
def _format_data_files(self, data_files: list[dict[str, Any]]) -> list[dict[str, str]]:
|
|
477
|
+
"""Format data file information for templates.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
data_files: List of data file dictionaries.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Formatted data file data.
|
|
484
|
+
"""
|
|
485
|
+
formatted = []
|
|
486
|
+
for data_file in data_files:
|
|
487
|
+
path = Path(data_file.get("path", ""))
|
|
488
|
+
formatted.append(
|
|
489
|
+
{
|
|
490
|
+
"filename": path.name,
|
|
491
|
+
"path": str(path),
|
|
492
|
+
"format": path.suffix.lstrip(".").upper() or "DATA",
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
return formatted
|
|
496
|
+
|
|
497
|
+
def _format_size(self, filepath: str | None) -> str:
|
|
498
|
+
"""Format file size in human-readable format.
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
filepath: Path to file.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Formatted size string (e.g., "1.5 MB").
|
|
505
|
+
"""
|
|
506
|
+
if not filepath:
|
|
507
|
+
return "N/A"
|
|
508
|
+
|
|
509
|
+
try:
|
|
510
|
+
path = Path(filepath)
|
|
511
|
+
if not path.exists():
|
|
512
|
+
return "N/A"
|
|
513
|
+
|
|
514
|
+
size_bytes = path.stat().st_size
|
|
515
|
+
size_float = float(size_bytes)
|
|
516
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
517
|
+
if size_float < 1024.0:
|
|
518
|
+
return f"{size_float:.1f} {unit}"
|
|
519
|
+
size_float /= 1024.0
|
|
520
|
+
return f"{size_float:.1f} TB"
|
|
521
|
+
except Exception:
|
|
522
|
+
return "N/A"
|
|
523
|
+
|
|
524
|
+
def _format_duration(self, seconds: float) -> str:
|
|
525
|
+
"""Format duration in human-readable format.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
seconds: Duration in seconds.
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
Formatted duration string (e.g., "1m 30s").
|
|
532
|
+
"""
|
|
533
|
+
if seconds < 60:
|
|
534
|
+
return f"{seconds:.1f}s"
|
|
535
|
+
elif seconds < 3600:
|
|
536
|
+
minutes = int(seconds // 60)
|
|
537
|
+
secs = int(seconds % 60)
|
|
538
|
+
return f"{minutes}m {secs}s"
|
|
539
|
+
else:
|
|
540
|
+
hours = int(seconds // 3600)
|
|
541
|
+
minutes = int((seconds % 3600) // 60)
|
|
542
|
+
return f"{hours}h {minutes}m"
|
|
543
|
+
|
|
544
|
+
def _render_html(self, context: dict[str, Any]) -> str:
|
|
545
|
+
"""Render HTML index from template.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
context: Template context.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Rendered HTML string.
|
|
552
|
+
|
|
553
|
+
Raises:
|
|
554
|
+
FileNotFoundError: If HTML template file not found.
|
|
555
|
+
|
|
556
|
+
Requirements:
|
|
557
|
+
"""
|
|
558
|
+
template_path = self._template_dir / "index.html"
|
|
559
|
+
if not template_path.exists():
|
|
560
|
+
raise FileNotFoundError(f"HTML template not found: {template_path}")
|
|
561
|
+
|
|
562
|
+
template = template_path.read_text()
|
|
563
|
+
return self._engine.render(template, context)
|
|
564
|
+
|
|
565
|
+
def _render_markdown(self, context: dict[str, Any]) -> str:
|
|
566
|
+
"""Render Markdown index from template.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
context: Template context.
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Rendered Markdown string.
|
|
573
|
+
|
|
574
|
+
Raises:
|
|
575
|
+
FileNotFoundError: If Markdown template file not found.
|
|
576
|
+
|
|
577
|
+
Requirements:
|
|
578
|
+
"""
|
|
579
|
+
template_path = self._template_dir / "index.md"
|
|
580
|
+
if not template_path.exists():
|
|
581
|
+
raise FileNotFoundError(f"Markdown template not found: {template_path}")
|
|
582
|
+
|
|
583
|
+
template = template_path.read_text()
|
|
584
|
+
return self._engine.render(template, context)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
__all__ = [
|
|
588
|
+
"IndexGenerator",
|
|
589
|
+
"TemplateEngine",
|
|
590
|
+
]
|