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
oscura/batch/logging.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""Aggregate logging for batch processing operations.
|
|
2
|
+
|
|
3
|
+
This module provides consolidated logging for parallel batch workers
|
|
4
|
+
with job-level summaries and per-file tracking.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.batch.logging import BatchLogger
|
|
9
|
+
>>> logger = BatchLogger(batch_id="job-001")
|
|
10
|
+
>>> with logger.file_context("capture1.wfm") as file_log:
|
|
11
|
+
... file_log.info("Processing file")
|
|
12
|
+
... result = analyze(file)
|
|
13
|
+
>>> logger.summary()
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
LOG-011,
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from collections import defaultdict
|
|
26
|
+
from contextlib import contextmanager
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from datetime import UTC, datetime
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from collections.abc import Iterator
|
|
33
|
+
|
|
34
|
+
from oscura.core.logging import format_timestamp, get_logger
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class FileLogEntry:
|
|
39
|
+
"""Log entry for a single file in a batch job.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
file_id: Unique identifier for this file.
|
|
43
|
+
filename: Path to the file.
|
|
44
|
+
start_time: Processing start time.
|
|
45
|
+
end_time: Processing end time.
|
|
46
|
+
status: Processing status (pending, processing, success, error).
|
|
47
|
+
error_message: Error message if status is 'error'.
|
|
48
|
+
log_messages: List of log messages for this file.
|
|
49
|
+
|
|
50
|
+
References:
|
|
51
|
+
LOG-011: Aggregate Logging for Batch Processing
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
file_id: str
|
|
55
|
+
filename: str
|
|
56
|
+
start_time: float | None = None
|
|
57
|
+
end_time: float | None = None
|
|
58
|
+
status: str = "pending"
|
|
59
|
+
error_message: str | None = None
|
|
60
|
+
log_messages: list[dict[str, Any]] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def duration(self) -> float | None:
|
|
64
|
+
"""Get processing duration in seconds."""
|
|
65
|
+
if self.start_time is not None and self.end_time is not None:
|
|
66
|
+
return self.end_time - self.start_time
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict[str, Any]:
|
|
70
|
+
"""Convert to dictionary representation."""
|
|
71
|
+
return {
|
|
72
|
+
"file_id": self.file_id,
|
|
73
|
+
"filename": self.filename,
|
|
74
|
+
"start_time": format_timestamp(datetime.fromtimestamp(self.start_time, tz=UTC))
|
|
75
|
+
if self.start_time
|
|
76
|
+
else None,
|
|
77
|
+
"end_time": format_timestamp(datetime.fromtimestamp(self.end_time, tz=UTC))
|
|
78
|
+
if self.end_time
|
|
79
|
+
else None,
|
|
80
|
+
"duration_seconds": self.duration,
|
|
81
|
+
"status": self.status,
|
|
82
|
+
"error_message": self.error_message,
|
|
83
|
+
"log_count": len(self.log_messages),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class BatchSummary:
|
|
89
|
+
"""Summary of batch processing results.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
batch_id: Unique identifier for the batch job.
|
|
93
|
+
total_files: Total number of files in the batch.
|
|
94
|
+
success_count: Number of successfully processed files.
|
|
95
|
+
error_count: Number of files that failed.
|
|
96
|
+
total_duration: Total processing time in seconds.
|
|
97
|
+
errors_by_type: Count of errors grouped by type.
|
|
98
|
+
|
|
99
|
+
References:
|
|
100
|
+
LOG-011: Aggregate Logging for Batch Processing
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
batch_id: str
|
|
104
|
+
total_files: int
|
|
105
|
+
success_count: int
|
|
106
|
+
error_count: int
|
|
107
|
+
total_duration: float
|
|
108
|
+
start_time: str
|
|
109
|
+
end_time: str
|
|
110
|
+
errors_by_type: dict[str, int] = field(default_factory=dict)
|
|
111
|
+
files_per_second: float = 0.0
|
|
112
|
+
average_duration_per_file: float = 0.0
|
|
113
|
+
|
|
114
|
+
def to_dict(self) -> dict[str, Any]:
|
|
115
|
+
"""Convert to dictionary representation."""
|
|
116
|
+
return {
|
|
117
|
+
"batch_id": self.batch_id,
|
|
118
|
+
"total_files": self.total_files,
|
|
119
|
+
"success_count": self.success_count,
|
|
120
|
+
"error_count": self.error_count,
|
|
121
|
+
"success_rate": self.success_count / self.total_files if self.total_files > 0 else 0.0,
|
|
122
|
+
"total_duration_seconds": self.total_duration,
|
|
123
|
+
"start_time": self.start_time,
|
|
124
|
+
"end_time": self.end_time,
|
|
125
|
+
"files_per_second": self.files_per_second,
|
|
126
|
+
"average_duration_per_file": self.average_duration_per_file,
|
|
127
|
+
"errors_by_type": self.errors_by_type,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class FileLogger:
|
|
132
|
+
"""Logger for individual file processing within a batch.
|
|
133
|
+
|
|
134
|
+
Provides logging methods that automatically tag logs with
|
|
135
|
+
batch_id and file_id for aggregation.
|
|
136
|
+
|
|
137
|
+
References:
|
|
138
|
+
LOG-011: Aggregate Logging for Batch Processing
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
entry: FileLogEntry,
|
|
144
|
+
batch_id: str,
|
|
145
|
+
parent_logger: logging.Logger,
|
|
146
|
+
):
|
|
147
|
+
"""Initialize file logger.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
entry: FileLogEntry for this file.
|
|
151
|
+
batch_id: Batch job identifier.
|
|
152
|
+
parent_logger: Parent logger for output.
|
|
153
|
+
"""
|
|
154
|
+
self._entry = entry
|
|
155
|
+
self._batch_id = batch_id
|
|
156
|
+
self._logger = parent_logger
|
|
157
|
+
|
|
158
|
+
def _log(self, level: int, message: str, **kwargs: Any) -> None:
|
|
159
|
+
"""Log a message with batch/file context."""
|
|
160
|
+
log_entry = {
|
|
161
|
+
"timestamp": format_timestamp(),
|
|
162
|
+
"level": logging.getLevelName(level),
|
|
163
|
+
"message": message,
|
|
164
|
+
"batch_id": self._batch_id,
|
|
165
|
+
"file_id": self._entry.file_id,
|
|
166
|
+
"filename": self._entry.filename,
|
|
167
|
+
**kwargs,
|
|
168
|
+
}
|
|
169
|
+
self._entry.log_messages.append(log_entry)
|
|
170
|
+
self._logger.log(
|
|
171
|
+
level,
|
|
172
|
+
message,
|
|
173
|
+
extra={
|
|
174
|
+
"batch_id": self._batch_id,
|
|
175
|
+
"file_id": self._entry.file_id,
|
|
176
|
+
**kwargs,
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def debug(self, message: str, **kwargs: Any) -> None:
|
|
181
|
+
"""Log debug message."""
|
|
182
|
+
self._log(logging.DEBUG, message, **kwargs)
|
|
183
|
+
|
|
184
|
+
def info(self, message: str, **kwargs: Any) -> None:
|
|
185
|
+
"""Log info message."""
|
|
186
|
+
self._log(logging.INFO, message, **kwargs)
|
|
187
|
+
|
|
188
|
+
def warning(self, message: str, **kwargs: Any) -> None:
|
|
189
|
+
"""Log warning message."""
|
|
190
|
+
self._log(logging.WARNING, message, **kwargs)
|
|
191
|
+
|
|
192
|
+
def error(self, message: str, **kwargs: Any) -> None:
|
|
193
|
+
"""Log error message."""
|
|
194
|
+
self._log(logging.ERROR, message, **kwargs)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class BatchLogger:
|
|
198
|
+
"""Aggregate logger for batch processing operations.
|
|
199
|
+
|
|
200
|
+
Consolidates logs from parallel batch workers with job-level
|
|
201
|
+
summaries and per-file tracking.
|
|
202
|
+
|
|
203
|
+
Example:
|
|
204
|
+
>>> logger = BatchLogger(batch_id="job-001")
|
|
205
|
+
>>> with logger.file_context("capture1.wfm") as file_log:
|
|
206
|
+
... file_log.info("Loading file")
|
|
207
|
+
... result = analyze(file)
|
|
208
|
+
... file_log.info("Analysis complete", result=result)
|
|
209
|
+
>>> summary = logger.summary()
|
|
210
|
+
|
|
211
|
+
References:
|
|
212
|
+
LOG-011: Aggregate Logging for Batch Processing
|
|
213
|
+
LOG-013: Batch Job Correlation ID and Lineage
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
def __init__(
|
|
217
|
+
self,
|
|
218
|
+
batch_id: str | None = None,
|
|
219
|
+
logger_name: str = "oscura.batch",
|
|
220
|
+
):
|
|
221
|
+
"""Initialize batch logger.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
batch_id: Unique batch job identifier. Auto-generated if None.
|
|
225
|
+
logger_name: Name for the underlying logger.
|
|
226
|
+
"""
|
|
227
|
+
self.batch_id = batch_id or str(uuid.uuid4())
|
|
228
|
+
self._logger = get_logger(logger_name)
|
|
229
|
+
self._files: dict[str, FileLogEntry] = {}
|
|
230
|
+
self._lock = threading.Lock()
|
|
231
|
+
self._start_time: float | None = None
|
|
232
|
+
self._end_time: float | None = None
|
|
233
|
+
self._error_types: dict[str, int] = defaultdict(int)
|
|
234
|
+
|
|
235
|
+
def start(self) -> None:
|
|
236
|
+
"""Mark batch job as started.
|
|
237
|
+
|
|
238
|
+
Records the start time for duration calculation.
|
|
239
|
+
"""
|
|
240
|
+
self._start_time = time.time()
|
|
241
|
+
self._logger.info(
|
|
242
|
+
"Batch job started",
|
|
243
|
+
extra={"batch_id": self.batch_id},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
def finish(self) -> None:
|
|
247
|
+
"""Mark batch job as finished.
|
|
248
|
+
|
|
249
|
+
Records the end time and logs the completion summary.
|
|
250
|
+
"""
|
|
251
|
+
self._end_time = time.time()
|
|
252
|
+
summary = self.summary()
|
|
253
|
+
self._logger.info(
|
|
254
|
+
"Batch job completed",
|
|
255
|
+
extra={
|
|
256
|
+
"batch_id": self.batch_id,
|
|
257
|
+
"total_files": summary.total_files,
|
|
258
|
+
"success_count": summary.success_count,
|
|
259
|
+
"error_count": summary.error_count,
|
|
260
|
+
"duration": summary.total_duration,
|
|
261
|
+
},
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def register_file(self, filename: str) -> str:
|
|
265
|
+
"""Register a file for processing.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
filename: Path to the file.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Unique file_id for this file.
|
|
272
|
+
"""
|
|
273
|
+
file_id = str(uuid.uuid4())
|
|
274
|
+
with self._lock:
|
|
275
|
+
self._files[file_id] = FileLogEntry(
|
|
276
|
+
file_id=file_id,
|
|
277
|
+
filename=filename,
|
|
278
|
+
)
|
|
279
|
+
return file_id
|
|
280
|
+
|
|
281
|
+
@contextmanager
|
|
282
|
+
def file_context(self, filename: str) -> Iterator[FileLogger]:
|
|
283
|
+
"""Context manager for file processing.
|
|
284
|
+
|
|
285
|
+
Automatically tracks start/end time and status.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
filename: Path to the file being processed.
|
|
289
|
+
|
|
290
|
+
Yields:
|
|
291
|
+
FileLogger for logging within this file's context.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
Exception: Re-raises any exception from the processing context.
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
>>> with batch_logger.file_context("data.wfm") as log:
|
|
298
|
+
... log.info("Processing started")
|
|
299
|
+
... result = process_file("data.wfm")
|
|
300
|
+
"""
|
|
301
|
+
file_id = self.register_file(filename)
|
|
302
|
+
entry = self._files[file_id]
|
|
303
|
+
|
|
304
|
+
entry.start_time = time.time()
|
|
305
|
+
entry.status = "processing"
|
|
306
|
+
|
|
307
|
+
file_logger = FileLogger(entry, self.batch_id, self._logger)
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
yield file_logger
|
|
311
|
+
entry.status = "success"
|
|
312
|
+
except Exception as e:
|
|
313
|
+
entry.status = "error"
|
|
314
|
+
entry.error_message = str(e)
|
|
315
|
+
error_type = type(e).__name__
|
|
316
|
+
with self._lock:
|
|
317
|
+
self._error_types[error_type] += 1
|
|
318
|
+
file_logger.error("Processing failed: %s", e, exception_type=error_type) # type: ignore[call-arg]
|
|
319
|
+
raise
|
|
320
|
+
finally:
|
|
321
|
+
entry.end_time = time.time()
|
|
322
|
+
|
|
323
|
+
def mark_success(self, file_id: str) -> None:
|
|
324
|
+
"""Mark a file as successfully processed.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
file_id: File identifier from register_file.
|
|
328
|
+
"""
|
|
329
|
+
with self._lock:
|
|
330
|
+
if file_id in self._files:
|
|
331
|
+
self._files[file_id].status = "success"
|
|
332
|
+
self._files[file_id].end_time = time.time()
|
|
333
|
+
|
|
334
|
+
def mark_error(self, file_id: str, error: str, error_type: str = "Unknown") -> None:
|
|
335
|
+
"""Mark a file as failed.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
file_id: File identifier from register_file.
|
|
339
|
+
error: Error message.
|
|
340
|
+
error_type: Type of error for aggregation.
|
|
341
|
+
"""
|
|
342
|
+
with self._lock:
|
|
343
|
+
if file_id in self._files:
|
|
344
|
+
self._files[file_id].status = "error"
|
|
345
|
+
self._files[file_id].error_message = error
|
|
346
|
+
self._files[file_id].end_time = time.time()
|
|
347
|
+
self._error_types[error_type] += 1
|
|
348
|
+
|
|
349
|
+
def summary(self) -> BatchSummary:
|
|
350
|
+
"""Generate batch processing summary.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
BatchSummary with aggregated statistics.
|
|
354
|
+
|
|
355
|
+
References:
|
|
356
|
+
LOG-011: Aggregate Logging for Batch Processing
|
|
357
|
+
"""
|
|
358
|
+
with self._lock:
|
|
359
|
+
files = list(self._files.values())
|
|
360
|
+
errors_by_type = dict(self._error_types)
|
|
361
|
+
|
|
362
|
+
total_files = len(files)
|
|
363
|
+
success_count = sum(1 for f in files if f.status == "success")
|
|
364
|
+
error_count = sum(1 for f in files if f.status == "error")
|
|
365
|
+
|
|
366
|
+
# Calculate timing
|
|
367
|
+
start_time = self._start_time or (
|
|
368
|
+
min((f.start_time for f in files if f.start_time), default=0)
|
|
369
|
+
)
|
|
370
|
+
end_time = self._end_time or (max((f.end_time for f in files if f.end_time), default=0))
|
|
371
|
+
total_duration = end_time - start_time if start_time and end_time else 0.0
|
|
372
|
+
|
|
373
|
+
# Calculate per-file metrics
|
|
374
|
+
durations = [f.duration for f in files if f.duration is not None]
|
|
375
|
+
avg_duration = sum(durations) / len(durations) if durations else 0.0
|
|
376
|
+
files_per_second = total_files / total_duration if total_duration > 0 else 0.0
|
|
377
|
+
|
|
378
|
+
return BatchSummary(
|
|
379
|
+
batch_id=self.batch_id,
|
|
380
|
+
total_files=total_files,
|
|
381
|
+
success_count=success_count,
|
|
382
|
+
error_count=error_count,
|
|
383
|
+
total_duration=total_duration,
|
|
384
|
+
start_time=format_timestamp(datetime.fromtimestamp(start_time, tz=UTC))
|
|
385
|
+
if start_time
|
|
386
|
+
else "",
|
|
387
|
+
end_time=format_timestamp(datetime.fromtimestamp(end_time, tz=UTC)) if end_time else "",
|
|
388
|
+
errors_by_type=errors_by_type,
|
|
389
|
+
files_per_second=files_per_second,
|
|
390
|
+
average_duration_per_file=avg_duration,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
def get_file_logs(self, file_id: str) -> list[dict[str, Any]]:
|
|
394
|
+
"""Get all log messages for a specific file.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
file_id: File identifier.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
List of log message dictionaries.
|
|
401
|
+
"""
|
|
402
|
+
with self._lock:
|
|
403
|
+
if file_id in self._files:
|
|
404
|
+
return list(self._files[file_id].log_messages)
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
def get_all_files(self) -> list[dict[str, Any]]:
|
|
408
|
+
"""Get summary information for all files.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
List of file summary dictionaries.
|
|
412
|
+
"""
|
|
413
|
+
with self._lock:
|
|
414
|
+
return [f.to_dict() for f in self._files.values()]
|
|
415
|
+
|
|
416
|
+
def get_errors(self) -> list[dict[str, Any]]:
|
|
417
|
+
"""Get all files that encountered errors.
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
List of error file dictionaries with error details.
|
|
421
|
+
"""
|
|
422
|
+
with self._lock:
|
|
423
|
+
return [
|
|
424
|
+
{
|
|
425
|
+
**f.to_dict(),
|
|
426
|
+
"logs": f.log_messages,
|
|
427
|
+
}
|
|
428
|
+
for f in self._files.values()
|
|
429
|
+
if f.status == "error"
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def aggregate_batch_logs(
|
|
434
|
+
batch_loggers: list[BatchLogger],
|
|
435
|
+
) -> dict[str, Any]:
|
|
436
|
+
"""Aggregate logs from multiple batch loggers.
|
|
437
|
+
|
|
438
|
+
Combines summaries from multiple batch jobs into a single
|
|
439
|
+
aggregate report.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
batch_loggers: List of BatchLogger instances.
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Aggregated summary dictionary.
|
|
446
|
+
|
|
447
|
+
References:
|
|
448
|
+
LOG-011: Aggregate Logging for Batch Processing
|
|
449
|
+
"""
|
|
450
|
+
total_files = 0
|
|
451
|
+
total_success = 0
|
|
452
|
+
total_errors = 0
|
|
453
|
+
total_duration = 0.0
|
|
454
|
+
all_errors_by_type: dict[str, int] = defaultdict(int)
|
|
455
|
+
batch_summaries = []
|
|
456
|
+
|
|
457
|
+
for logger in batch_loggers:
|
|
458
|
+
summary = logger.summary()
|
|
459
|
+
batch_summaries.append(summary.to_dict())
|
|
460
|
+
total_files += summary.total_files
|
|
461
|
+
total_success += summary.success_count
|
|
462
|
+
total_errors += summary.error_count
|
|
463
|
+
total_duration += summary.total_duration
|
|
464
|
+
for error_type, count in summary.errors_by_type.items():
|
|
465
|
+
all_errors_by_type[error_type] += count
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
"aggregate": {
|
|
469
|
+
"total_batches": len(batch_loggers),
|
|
470
|
+
"total_files": total_files,
|
|
471
|
+
"total_success": total_success,
|
|
472
|
+
"total_errors": total_errors,
|
|
473
|
+
"total_duration_seconds": total_duration,
|
|
474
|
+
"overall_success_rate": total_success / total_files if total_files > 0 else 0.0,
|
|
475
|
+
"errors_by_type": dict(all_errors_by_type),
|
|
476
|
+
},
|
|
477
|
+
"batches": batch_summaries,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
__all__ = [
|
|
482
|
+
"BatchLogger",
|
|
483
|
+
"BatchSummary",
|
|
484
|
+
"FileLogEntry",
|
|
485
|
+
"FileLogger",
|
|
486
|
+
"aggregate_batch_logs",
|
|
487
|
+
]
|