oscura 0.0.1__py3-none-any.whl → 0.1.1__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.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Default configuration values and injection.
|
|
2
|
+
|
|
3
|
+
This module provides default configuration values and utilities
|
|
4
|
+
for injecting defaults into user configurations.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.config.defaults import inject_defaults
|
|
9
|
+
>>> config = {"name": "test"}
|
|
10
|
+
>>> full_config = inject_defaults(config, "protocol")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import copy
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# Default configuration values
|
|
19
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
20
|
+
"version": "1.0",
|
|
21
|
+
"defaults": {
|
|
22
|
+
"sample_rate": 1e6, # 1 MHz default
|
|
23
|
+
"window_function": "hann",
|
|
24
|
+
"fft_size": 1024,
|
|
25
|
+
},
|
|
26
|
+
"loaders": {
|
|
27
|
+
"auto_detect": True,
|
|
28
|
+
"formats": ["wfm", "csv", "npz", "hdf5", "tdms", "vcd", "sr", "wav", "pcap"],
|
|
29
|
+
"tektronix": {"byte_order": "little"},
|
|
30
|
+
"csv": {"delimiter": ",", "skip_header": 0},
|
|
31
|
+
},
|
|
32
|
+
"measurements": {
|
|
33
|
+
"rise_time": {"ref_levels": [0.1, 0.9]},
|
|
34
|
+
"fall_time": {"ref_levels": [0.9, 0.1]},
|
|
35
|
+
"frequency": {"min_periods": 3},
|
|
36
|
+
},
|
|
37
|
+
"spectral": {
|
|
38
|
+
"default_window": "hann",
|
|
39
|
+
"overlap": 0.5,
|
|
40
|
+
"nfft": None, # Auto-determine from signal length
|
|
41
|
+
},
|
|
42
|
+
"visualization": {
|
|
43
|
+
"default_style": "seaborn",
|
|
44
|
+
"figure_size": [10, 6],
|
|
45
|
+
"dpi": 100,
|
|
46
|
+
"colormap": "viridis",
|
|
47
|
+
},
|
|
48
|
+
"export": {
|
|
49
|
+
"csv": {"precision": 6},
|
|
50
|
+
"hdf5": {"compression": "gzip", "compression_opts": 4},
|
|
51
|
+
},
|
|
52
|
+
"logging": {
|
|
53
|
+
"level": "INFO",
|
|
54
|
+
"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Schema-specific default values
|
|
60
|
+
SCHEMA_DEFAULTS: dict[str, dict[str, Any]] = {
|
|
61
|
+
"protocol": {
|
|
62
|
+
"version": "1.0.0",
|
|
63
|
+
"timing": {
|
|
64
|
+
"data_bits": [8],
|
|
65
|
+
"stop_bits": [1],
|
|
66
|
+
"parity": ["none"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
"pipeline": {
|
|
70
|
+
"version": "1.0.0",
|
|
71
|
+
"parallel_groups": [],
|
|
72
|
+
},
|
|
73
|
+
"logic_family": {
|
|
74
|
+
"temperature_range": {
|
|
75
|
+
"min": 0,
|
|
76
|
+
"max": 70,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
"threshold_profile": {
|
|
80
|
+
"tolerance": 0,
|
|
81
|
+
"overrides": {},
|
|
82
|
+
},
|
|
83
|
+
"preferences": {
|
|
84
|
+
"defaults": {
|
|
85
|
+
"sample_rate": 1e6,
|
|
86
|
+
"window_function": "hann",
|
|
87
|
+
},
|
|
88
|
+
"visualization": {
|
|
89
|
+
"style": "seaborn",
|
|
90
|
+
"dpi": 100,
|
|
91
|
+
},
|
|
92
|
+
"export": {
|
|
93
|
+
"default_format": "csv",
|
|
94
|
+
"precision": 6,
|
|
95
|
+
},
|
|
96
|
+
"logging": {
|
|
97
|
+
"level": "INFO",
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
104
|
+
"""Recursively merge two dictionaries.
|
|
105
|
+
|
|
106
|
+
Values from override take precedence. Nested dictionaries are
|
|
107
|
+
merged recursively.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
base: Base dictionary.
|
|
111
|
+
override: Dictionary with values to override.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Merged dictionary (new instance).
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> base = {"a": 1, "b": {"c": 2}}
|
|
118
|
+
>>> override = {"b": {"d": 3}}
|
|
119
|
+
>>> deep_merge(base, override)
|
|
120
|
+
{'a': 1, 'b': {'c': 2, 'd': 3}}
|
|
121
|
+
"""
|
|
122
|
+
result = copy.deepcopy(base)
|
|
123
|
+
|
|
124
|
+
for key, value in override.items():
|
|
125
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
126
|
+
result[key] = deep_merge(result[key], value)
|
|
127
|
+
else:
|
|
128
|
+
result[key] = copy.deepcopy(value)
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def inject_defaults(
|
|
134
|
+
config: dict[str, Any],
|
|
135
|
+
schema_name: str,
|
|
136
|
+
) -> dict[str, Any]:
|
|
137
|
+
"""Inject default values into configuration.
|
|
138
|
+
|
|
139
|
+
Adds default values for missing fields based on schema type.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
config: User configuration dictionary.
|
|
143
|
+
schema_name: Schema name to determine defaults.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Configuration with defaults injected.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
>>> config = {"name": "uart", "timing": {"baud_rates": [9600]}}
|
|
150
|
+
>>> full = inject_defaults(config, "protocol")
|
|
151
|
+
>>> print(full["timing"]["data_bits"])
|
|
152
|
+
[8]
|
|
153
|
+
"""
|
|
154
|
+
defaults = SCHEMA_DEFAULTS.get(schema_name, {})
|
|
155
|
+
|
|
156
|
+
if not defaults:
|
|
157
|
+
return copy.deepcopy(config)
|
|
158
|
+
|
|
159
|
+
# Merge defaults with config (config takes precedence)
|
|
160
|
+
return deep_merge(defaults, config)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_effective_config(
|
|
164
|
+
user_config: dict[str, Any] | None = None,
|
|
165
|
+
schema_name: str | None = None,
|
|
166
|
+
) -> dict[str, Any]:
|
|
167
|
+
"""Get effective configuration with all defaults applied.
|
|
168
|
+
|
|
169
|
+
Combines base defaults, schema-specific defaults, and user configuration.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
user_config: User-provided configuration.
|
|
173
|
+
schema_name: Schema to apply defaults for.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Complete configuration with all defaults.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> config = get_effective_config({"defaults": {"sample_rate": 2e6}})
|
|
180
|
+
>>> print(config["defaults"]["sample_rate"])
|
|
181
|
+
2000000.0
|
|
182
|
+
"""
|
|
183
|
+
# Start with base defaults
|
|
184
|
+
result = copy.deepcopy(DEFAULT_CONFIG)
|
|
185
|
+
|
|
186
|
+
# Add schema-specific defaults
|
|
187
|
+
if schema_name and schema_name in SCHEMA_DEFAULTS:
|
|
188
|
+
schema_defaults = SCHEMA_DEFAULTS[schema_name]
|
|
189
|
+
result = deep_merge(result, schema_defaults)
|
|
190
|
+
|
|
191
|
+
# Apply user configuration
|
|
192
|
+
if user_config:
|
|
193
|
+
result = deep_merge(result, user_config)
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_default(
|
|
199
|
+
key_path: str,
|
|
200
|
+
schema_name: str | None = None,
|
|
201
|
+
) -> Any:
|
|
202
|
+
"""Get default value for a configuration key.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
key_path: Dot-separated path (e.g., "defaults.sample_rate").
|
|
206
|
+
schema_name: Optional schema for schema-specific defaults.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Default value or None if not found.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> get_default("defaults.sample_rate")
|
|
213
|
+
1000000.0
|
|
214
|
+
"""
|
|
215
|
+
# Check schema-specific defaults first
|
|
216
|
+
if schema_name and schema_name in SCHEMA_DEFAULTS:
|
|
217
|
+
value = _get_nested(SCHEMA_DEFAULTS[schema_name], key_path)
|
|
218
|
+
if value is not None:
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
# Fall back to base defaults
|
|
222
|
+
return _get_nested(DEFAULT_CONFIG, key_path)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _get_nested(config: dict[str, Any], key_path: str) -> Any:
|
|
226
|
+
"""Get nested value by dot-separated path.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
config: Configuration dictionary.
|
|
230
|
+
key_path: Dot-separated path.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Value or None if not found.
|
|
234
|
+
"""
|
|
235
|
+
keys = key_path.split(".")
|
|
236
|
+
value = config
|
|
237
|
+
|
|
238
|
+
for key in keys:
|
|
239
|
+
if isinstance(value, dict) and key in value:
|
|
240
|
+
value = value[key]
|
|
241
|
+
else:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
return value
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
__all__ = [
|
|
248
|
+
"DEFAULT_CONFIG",
|
|
249
|
+
"SCHEMA_DEFAULTS",
|
|
250
|
+
"deep_merge",
|
|
251
|
+
"get_default",
|
|
252
|
+
"get_effective_config",
|
|
253
|
+
"inject_defaults",
|
|
254
|
+
]
|
oscura/config/loader.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Configuration file loading utilities.
|
|
2
|
+
|
|
3
|
+
This module provides unified configuration loading from YAML and JSON files
|
|
4
|
+
with support for schema validation, default injection, and path resolution.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.config.loader import load_config_file
|
|
9
|
+
>>> config = load_config_file("pipeline.yaml", schema="pipeline")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from oscura.core.exceptions import ConfigurationError
|
|
19
|
+
|
|
20
|
+
# Try to import yaml
|
|
21
|
+
try:
|
|
22
|
+
import yaml
|
|
23
|
+
|
|
24
|
+
YAML_AVAILABLE = True
|
|
25
|
+
except ImportError:
|
|
26
|
+
YAML_AVAILABLE = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_config_file(
|
|
30
|
+
path: str | Path,
|
|
31
|
+
*,
|
|
32
|
+
schema: str | None = None,
|
|
33
|
+
validate: bool = True,
|
|
34
|
+
inject_defaults: bool = True,
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
"""Load configuration from YAML or JSON file.
|
|
37
|
+
|
|
38
|
+
Automatically detects file format from extension.
|
|
39
|
+
Optionally validates against schema and injects defaults.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
path: Path to configuration file.
|
|
43
|
+
schema: Schema name to validate against (e.g., "protocol").
|
|
44
|
+
validate: If True and schema provided, validate configuration.
|
|
45
|
+
inject_defaults: If True, inject default values from schema.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Configuration dictionary.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ConfigurationError: If file cannot be loaded or parsed.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> config = load_config_file("uart.yaml", schema="protocol")
|
|
55
|
+
>>> print(config["name"])
|
|
56
|
+
'uart'
|
|
57
|
+
"""
|
|
58
|
+
path = Path(path).expanduser().resolve()
|
|
59
|
+
|
|
60
|
+
if not path.exists():
|
|
61
|
+
raise ConfigurationError(
|
|
62
|
+
"Configuration file not found",
|
|
63
|
+
config_key=str(path),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Determine format from extension
|
|
67
|
+
ext = path.suffix.lower()
|
|
68
|
+
|
|
69
|
+
if ext in (".yaml", ".yml"):
|
|
70
|
+
config = _load_yaml(path)
|
|
71
|
+
elif ext == ".json":
|
|
72
|
+
config = _load_json(path)
|
|
73
|
+
else:
|
|
74
|
+
# Try YAML first, then JSON
|
|
75
|
+
try:
|
|
76
|
+
config = _load_yaml(path)
|
|
77
|
+
except ConfigurationError:
|
|
78
|
+
try:
|
|
79
|
+
config = _load_json(path)
|
|
80
|
+
except ConfigurationError:
|
|
81
|
+
raise ConfigurationError( # noqa: B904
|
|
82
|
+
f"Unsupported configuration format: {ext}",
|
|
83
|
+
config_key=str(path),
|
|
84
|
+
fix_hint="Use .yaml, .yml, or .json extension",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Validate against schema if requested
|
|
88
|
+
if validate and schema is not None:
|
|
89
|
+
from oscura.config.schema import validate_against_schema
|
|
90
|
+
|
|
91
|
+
validate_against_schema(config, schema)
|
|
92
|
+
|
|
93
|
+
# Inject defaults if requested
|
|
94
|
+
if inject_defaults and schema is not None:
|
|
95
|
+
from oscura.config.defaults import inject_defaults as do_inject
|
|
96
|
+
|
|
97
|
+
config = do_inject(config, schema)
|
|
98
|
+
|
|
99
|
+
return config
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_yaml(path: Path) -> dict[str, Any]:
|
|
103
|
+
"""Load YAML configuration file.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
path: Path to YAML file.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Parsed configuration dictionary.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
ConfigurationError: If YAML parsing fails.
|
|
113
|
+
"""
|
|
114
|
+
if not YAML_AVAILABLE:
|
|
115
|
+
raise ConfigurationError(
|
|
116
|
+
"YAML support not available",
|
|
117
|
+
fix_hint="Install PyYAML: pip install pyyaml",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
with open(path, encoding="utf-8") as f:
|
|
122
|
+
content = yaml.safe_load(f)
|
|
123
|
+
|
|
124
|
+
if content is None:
|
|
125
|
+
return {}
|
|
126
|
+
|
|
127
|
+
if not isinstance(content, dict):
|
|
128
|
+
raise ConfigurationError(
|
|
129
|
+
"Configuration must be a dictionary",
|
|
130
|
+
config_key=str(path),
|
|
131
|
+
expected_type="object",
|
|
132
|
+
actual_value=type(content).__name__,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return content
|
|
136
|
+
|
|
137
|
+
except yaml.YAMLError as e:
|
|
138
|
+
# Extract line number from YAML error
|
|
139
|
+
line = None
|
|
140
|
+
if hasattr(e, "problem_mark") and e.problem_mark is not None:
|
|
141
|
+
line = e.problem_mark.line + 1
|
|
142
|
+
|
|
143
|
+
raise ConfigurationError(
|
|
144
|
+
"Failed to parse YAML configuration",
|
|
145
|
+
config_key=str(path),
|
|
146
|
+
details=f"Line {line}: {e}" if line else str(e),
|
|
147
|
+
) from e
|
|
148
|
+
except OSError as e:
|
|
149
|
+
raise ConfigurationError(
|
|
150
|
+
"Failed to read configuration file",
|
|
151
|
+
config_key=str(path),
|
|
152
|
+
details=str(e),
|
|
153
|
+
) from e
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _load_json(path: Path) -> dict[str, Any]:
|
|
157
|
+
"""Load JSON configuration file.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
path: Path to JSON file.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Parsed configuration dictionary.
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
ConfigurationError: If JSON parsing fails.
|
|
167
|
+
"""
|
|
168
|
+
try:
|
|
169
|
+
with open(path, encoding="utf-8") as f:
|
|
170
|
+
content = json.load(f)
|
|
171
|
+
|
|
172
|
+
if content is None:
|
|
173
|
+
return {}
|
|
174
|
+
|
|
175
|
+
if not isinstance(content, dict):
|
|
176
|
+
raise ConfigurationError(
|
|
177
|
+
"Configuration must be a dictionary",
|
|
178
|
+
config_key=str(path),
|
|
179
|
+
expected_type="object",
|
|
180
|
+
actual_value=type(content).__name__,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return content
|
|
184
|
+
|
|
185
|
+
except json.JSONDecodeError as e:
|
|
186
|
+
raise ConfigurationError(
|
|
187
|
+
"Failed to parse JSON configuration",
|
|
188
|
+
config_key=str(path),
|
|
189
|
+
details=f"Line {e.lineno}, column {e.colno}: {e.msg}",
|
|
190
|
+
) from e
|
|
191
|
+
except OSError as e:
|
|
192
|
+
raise ConfigurationError(
|
|
193
|
+
"Failed to read configuration file",
|
|
194
|
+
config_key=str(path),
|
|
195
|
+
details=str(e),
|
|
196
|
+
) from e
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def load_config(
|
|
200
|
+
config_path: str | Path | None = None,
|
|
201
|
+
*,
|
|
202
|
+
use_defaults: bool = True,
|
|
203
|
+
) -> dict[str, Any]:
|
|
204
|
+
"""Load Oscura configuration.
|
|
205
|
+
|
|
206
|
+
Searches for configuration in standard locations if no path provided.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
config_path: Path to configuration file. If None, searches
|
|
210
|
+
standard locations.
|
|
211
|
+
use_defaults: If True, merge with default configuration.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Configuration dictionary.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
>>> config = load_config() # Auto-find config
|
|
218
|
+
>>> config = load_config("~/.oscura/config.yaml")
|
|
219
|
+
"""
|
|
220
|
+
from oscura.config.defaults import DEFAULT_CONFIG, deep_merge
|
|
221
|
+
|
|
222
|
+
config: dict[str, Any] = {}
|
|
223
|
+
|
|
224
|
+
if use_defaults:
|
|
225
|
+
import copy
|
|
226
|
+
|
|
227
|
+
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
228
|
+
|
|
229
|
+
if config_path is None:
|
|
230
|
+
# Search standard locations
|
|
231
|
+
search_paths = [
|
|
232
|
+
Path.cwd() / "oscura.yaml",
|
|
233
|
+
Path.cwd() / ".oscura.yaml",
|
|
234
|
+
Path.cwd() / "oscura.json",
|
|
235
|
+
Path.home() / ".oscura" / "config.yaml",
|
|
236
|
+
Path.home() / ".config" / "oscura" / "config.yaml",
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
for path in search_paths:
|
|
240
|
+
if path.exists():
|
|
241
|
+
config_path = path
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if config_path is not None:
|
|
245
|
+
user_config = load_config_file(
|
|
246
|
+
config_path,
|
|
247
|
+
validate=False,
|
|
248
|
+
inject_defaults=False,
|
|
249
|
+
)
|
|
250
|
+
config = deep_merge(config, user_config) if use_defaults else user_config
|
|
251
|
+
|
|
252
|
+
return config
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def save_config(
|
|
256
|
+
config: dict[str, Any],
|
|
257
|
+
path: str | Path,
|
|
258
|
+
*,
|
|
259
|
+
format: str | None = None,
|
|
260
|
+
) -> None:
|
|
261
|
+
"""Save configuration to file.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
config: Configuration dictionary to save.
|
|
265
|
+
path: Output file path.
|
|
266
|
+
format: Output format ("yaml" or "json"). Auto-detected from
|
|
267
|
+
extension if not specified.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
ConfigurationError: If configuration cannot be saved.
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
>>> save_config(config, "config.yaml")
|
|
274
|
+
>>> save_config(config, "config.json")
|
|
275
|
+
"""
|
|
276
|
+
path = Path(path).expanduser().resolve()
|
|
277
|
+
|
|
278
|
+
# Determine format
|
|
279
|
+
if format is None:
|
|
280
|
+
ext = path.suffix.lower()
|
|
281
|
+
if ext in (".yaml", ".yml"):
|
|
282
|
+
format = "yaml"
|
|
283
|
+
elif ext == ".json":
|
|
284
|
+
format = "json"
|
|
285
|
+
else:
|
|
286
|
+
format = "yaml" # Default
|
|
287
|
+
|
|
288
|
+
# Create parent directory
|
|
289
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
if format == "yaml":
|
|
293
|
+
if not YAML_AVAILABLE:
|
|
294
|
+
raise ConfigurationError(
|
|
295
|
+
"YAML support not available",
|
|
296
|
+
fix_hint="Install PyYAML: pip install pyyaml",
|
|
297
|
+
)
|
|
298
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
299
|
+
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
300
|
+
else:
|
|
301
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
302
|
+
json.dump(config, f, indent=2)
|
|
303
|
+
|
|
304
|
+
except OSError as e:
|
|
305
|
+
raise ConfigurationError(
|
|
306
|
+
"Failed to save configuration",
|
|
307
|
+
config_key=str(path),
|
|
308
|
+
details=str(e),
|
|
309
|
+
) from e
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def get_config_value(
|
|
313
|
+
config: dict[str, Any],
|
|
314
|
+
key_path: str,
|
|
315
|
+
default: Any = None,
|
|
316
|
+
) -> Any:
|
|
317
|
+
"""Get configuration value by dot-separated path.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
config: Configuration dictionary.
|
|
321
|
+
key_path: Dot-separated path (e.g., "defaults.sample_rate").
|
|
322
|
+
default: Default value if key not found.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Configuration value or default.
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
>>> get_config_value(config, "defaults.sample_rate", 1e6)
|
|
329
|
+
1000000.0
|
|
330
|
+
"""
|
|
331
|
+
keys = key_path.split(".")
|
|
332
|
+
value = config
|
|
333
|
+
|
|
334
|
+
for key in keys:
|
|
335
|
+
if isinstance(value, dict) and key in value:
|
|
336
|
+
value = value[key]
|
|
337
|
+
else:
|
|
338
|
+
return default
|
|
339
|
+
|
|
340
|
+
return value
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
__all__ = [
|
|
344
|
+
"get_config_value",
|
|
345
|
+
"load_config",
|
|
346
|
+
"load_config_file",
|
|
347
|
+
"save_config",
|
|
348
|
+
]
|