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,458 @@
|
|
|
1
|
+
"""Schema migration system for TraceKit configuration files.
|
|
2
|
+
|
|
3
|
+
This module provides schema migration functionality to automatically upgrade
|
|
4
|
+
configuration files between schema versions while preserving user data.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.config.migration import migrate_config, register_migration
|
|
9
|
+
>>> # Register a migration function
|
|
10
|
+
>>> def migrate_1_0_to_1_1(config: dict) -> dict:
|
|
11
|
+
... config['new_field'] = 'default_value'
|
|
12
|
+
... return config
|
|
13
|
+
>>> register_migration("1.0.0", "1.1.0", migrate_1_0_to_1_1)
|
|
14
|
+
>>> # Migrate config to latest version
|
|
15
|
+
>>> old_config = {"version": "1.0.0", "name": "test"}
|
|
16
|
+
>>> new_config = migrate_config(old_config)
|
|
17
|
+
>>> print(new_config["version"])
|
|
18
|
+
1.1.0
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import copy
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from oscura.core.exceptions import ConfigurationError
|
|
29
|
+
|
|
30
|
+
# Type alias for migration functions
|
|
31
|
+
MigrationFunction = Callable[[dict[str, Any]], dict[str, Any]]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Migration:
|
|
36
|
+
"""Schema migration definition.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
from_version: Source schema version (semver).
|
|
40
|
+
to_version: Target schema version (semver).
|
|
41
|
+
migrate_fn: Function to perform migration.
|
|
42
|
+
description: Human-readable description of changes.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from_version: str
|
|
46
|
+
to_version: str
|
|
47
|
+
migrate_fn: MigrationFunction
|
|
48
|
+
description: str = ""
|
|
49
|
+
|
|
50
|
+
def __post_init__(self) -> None:
|
|
51
|
+
"""Validate migration after initialization."""
|
|
52
|
+
if not self.from_version:
|
|
53
|
+
raise ValueError("from_version cannot be empty")
|
|
54
|
+
if not self.to_version:
|
|
55
|
+
raise ValueError("to_version cannot be empty")
|
|
56
|
+
if not callable(self.migrate_fn):
|
|
57
|
+
raise ValueError("migrate_fn must be callable")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SchemaMigration:
|
|
61
|
+
"""Schema migration manager with version tracking.
|
|
62
|
+
|
|
63
|
+
Manages registration of migration functions and execution of migration
|
|
64
|
+
paths from older schema versions to newer ones.
|
|
65
|
+
|
|
66
|
+
Supports forward migrations only (not backward) to maintain data integrity.
|
|
67
|
+
Preserves unknown keys during migration to avoid data loss.
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
>>> migration = SchemaMigration()
|
|
71
|
+
>>> migration.register_migration("1.0.0", "1.1.0", upgrade_fn)
|
|
72
|
+
>>> config = {"version": "1.0.0", "data": "value"}
|
|
73
|
+
>>> migrated = migration.migrate_config(config, "1.1.0")
|
|
74
|
+
>>> print(migrated["version"])
|
|
75
|
+
1.1.0
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self) -> None:
|
|
79
|
+
"""Initialize empty migration registry."""
|
|
80
|
+
# Map of (from_version, to_version) -> Migration
|
|
81
|
+
self._migrations: dict[tuple[str, str], Migration] = {}
|
|
82
|
+
# Map of from_version -> list of to_versions for path finding
|
|
83
|
+
self._version_graph: dict[str, list[str]] = {}
|
|
84
|
+
|
|
85
|
+
def register_migration(
|
|
86
|
+
self,
|
|
87
|
+
from_version: str,
|
|
88
|
+
to_version: str,
|
|
89
|
+
migrate_fn: MigrationFunction,
|
|
90
|
+
*,
|
|
91
|
+
description: str = "",
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Register a migration function.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
from_version: Source schema version (semver).
|
|
97
|
+
to_version: Target schema version (semver).
|
|
98
|
+
migrate_fn: Function that takes config dict and returns migrated dict.
|
|
99
|
+
description: Human-readable description of migration.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: If migration already registered for this version pair.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> migration = SchemaMigration()
|
|
106
|
+
>>> def upgrade(cfg): return {**cfg, "new_field": "default"}
|
|
107
|
+
>>> migration.register_migration("1.0.0", "1.1.0", upgrade)
|
|
108
|
+
"""
|
|
109
|
+
key = (from_version, to_version)
|
|
110
|
+
|
|
111
|
+
if key in self._migrations:
|
|
112
|
+
raise ValueError(f"Migration from {from_version} to {to_version} already registered")
|
|
113
|
+
|
|
114
|
+
mig = Migration(
|
|
115
|
+
from_version=from_version,
|
|
116
|
+
to_version=to_version,
|
|
117
|
+
migrate_fn=migrate_fn,
|
|
118
|
+
description=description,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self._migrations[key] = mig
|
|
122
|
+
|
|
123
|
+
# Update graph for path finding
|
|
124
|
+
if from_version not in self._version_graph:
|
|
125
|
+
self._version_graph[from_version] = []
|
|
126
|
+
self._version_graph[from_version].append(to_version)
|
|
127
|
+
|
|
128
|
+
def migrate_config(
|
|
129
|
+
self,
|
|
130
|
+
config: dict[str, Any],
|
|
131
|
+
target_version: str | None = None,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
"""Migrate config to target version.
|
|
134
|
+
|
|
135
|
+
Automatically finds migration path and applies migrations in sequence.
|
|
136
|
+
Preserves unknown keys during migration.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
config: Configuration dictionary to migrate.
|
|
140
|
+
target_version: Target schema version. If None, migrate to latest.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
Migrated configuration dictionary.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
ConfigurationError: If migration path not found or migration fails.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
>>> migration = SchemaMigration()
|
|
150
|
+
>>> config = {"version": "1.0.0", "name": "test"}
|
|
151
|
+
>>> migrated = migration.migrate_config(config, "2.0.0")
|
|
152
|
+
"""
|
|
153
|
+
# Make a deep copy to avoid mutating input
|
|
154
|
+
result = copy.deepcopy(config)
|
|
155
|
+
|
|
156
|
+
# Get current version
|
|
157
|
+
current_version = self.get_config_version(result)
|
|
158
|
+
|
|
159
|
+
# If no version field, add default
|
|
160
|
+
if current_version is None:
|
|
161
|
+
result["version"] = "1.0.0"
|
|
162
|
+
current_version = "1.0.0"
|
|
163
|
+
|
|
164
|
+
# If no target specified, use latest available
|
|
165
|
+
if target_version is None:
|
|
166
|
+
target_version = self._get_latest_version(current_version)
|
|
167
|
+
if target_version is None:
|
|
168
|
+
# No migrations available, return as-is
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
# Already at target version
|
|
172
|
+
if current_version == target_version:
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
# Find migration path
|
|
176
|
+
path = self._find_migration_path(current_version, target_version)
|
|
177
|
+
|
|
178
|
+
if path is None:
|
|
179
|
+
available = self.list_migrations()
|
|
180
|
+
raise ConfigurationError(
|
|
181
|
+
f"No migration path from {current_version} to {target_version}",
|
|
182
|
+
details=f"Available migrations: {available}",
|
|
183
|
+
fix_hint=f"Register migrations to connect {current_version} to {target_version}",
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Apply migrations in sequence
|
|
187
|
+
for from_ver, to_ver in path:
|
|
188
|
+
migration = self._migrations[(from_ver, to_ver)]
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
result = migration.migrate_fn(result)
|
|
192
|
+
# Update version field
|
|
193
|
+
result["version"] = to_ver
|
|
194
|
+
except Exception as e:
|
|
195
|
+
raise ConfigurationError(
|
|
196
|
+
f"Migration from {from_ver} to {to_ver} failed",
|
|
197
|
+
details=str(e),
|
|
198
|
+
fix_hint="Check migration function implementation",
|
|
199
|
+
) from e
|
|
200
|
+
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def get_config_version(self, config: dict[str, Any]) -> str | None:
|
|
204
|
+
"""Extract version from config.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
config: Configuration dictionary.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Version string or None if not present.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
>>> migration = SchemaMigration()
|
|
214
|
+
>>> config = {"version": "1.2.3", "data": "value"}
|
|
215
|
+
>>> migration.get_config_version(config)
|
|
216
|
+
'1.2.3'
|
|
217
|
+
"""
|
|
218
|
+
return config.get("version")
|
|
219
|
+
|
|
220
|
+
def list_migrations(self) -> list[tuple[str, str]]:
|
|
221
|
+
"""List available migrations.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of (from_version, to_version) tuples.
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
>>> migration = SchemaMigration()
|
|
228
|
+
>>> migration.register_migration("1.0.0", "1.1.0", lambda c: c)
|
|
229
|
+
>>> migration.list_migrations()
|
|
230
|
+
[('1.0.0', '1.1.0')]
|
|
231
|
+
"""
|
|
232
|
+
return sorted(self._migrations.keys())
|
|
233
|
+
|
|
234
|
+
def has_migration(self, from_version: str, to_version: str) -> bool:
|
|
235
|
+
"""Check if migration exists.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
from_version: Source version.
|
|
239
|
+
to_version: Target version.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
True if migration exists.
|
|
243
|
+
"""
|
|
244
|
+
return (from_version, to_version) in self._migrations
|
|
245
|
+
|
|
246
|
+
def _find_migration_path(
|
|
247
|
+
self,
|
|
248
|
+
from_version: str,
|
|
249
|
+
to_version: str,
|
|
250
|
+
) -> list[tuple[str, str]] | None:
|
|
251
|
+
"""Find shortest migration path using BFS.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
from_version: Source version.
|
|
255
|
+
to_version: Target version.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
List of (from, to) version pairs representing migration path,
|
|
259
|
+
or None if no path exists.
|
|
260
|
+
"""
|
|
261
|
+
if from_version == to_version:
|
|
262
|
+
return []
|
|
263
|
+
|
|
264
|
+
# BFS to find shortest path
|
|
265
|
+
queue: list[tuple[str, list[tuple[str, str]]]] = [(from_version, [])]
|
|
266
|
+
visited = {from_version}
|
|
267
|
+
|
|
268
|
+
while queue:
|
|
269
|
+
current, path = queue.pop(0)
|
|
270
|
+
|
|
271
|
+
# Get all possible next versions from current
|
|
272
|
+
if current in self._version_graph:
|
|
273
|
+
for next_version in self._version_graph[current]:
|
|
274
|
+
if next_version in visited:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
new_path = [*path, (current, next_version)]
|
|
278
|
+
|
|
279
|
+
if next_version == to_version:
|
|
280
|
+
return new_path
|
|
281
|
+
|
|
282
|
+
visited.add(next_version)
|
|
283
|
+
queue.append((next_version, new_path))
|
|
284
|
+
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
def _get_latest_version(self, from_version: str) -> str | None:
|
|
288
|
+
"""Get latest version reachable from given version.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
from_version: Starting version.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Latest version string or None if no migrations available.
|
|
295
|
+
"""
|
|
296
|
+
if from_version not in self._version_graph:
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
# Find all reachable versions
|
|
300
|
+
reachable = set()
|
|
301
|
+
queue = [from_version]
|
|
302
|
+
visited = {from_version}
|
|
303
|
+
|
|
304
|
+
while queue:
|
|
305
|
+
current = queue.pop(0)
|
|
306
|
+
|
|
307
|
+
if current in self._version_graph:
|
|
308
|
+
for next_version in self._version_graph[current]:
|
|
309
|
+
if next_version not in visited:
|
|
310
|
+
visited.add(next_version)
|
|
311
|
+
reachable.add(next_version)
|
|
312
|
+
queue.append(next_version)
|
|
313
|
+
|
|
314
|
+
if not reachable:
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
# Sort versions and return latest (simple lexicographic sort)
|
|
318
|
+
# For proper semver comparison, could use packaging.version
|
|
319
|
+
return sorted(reachable, key=_parse_version)[-1]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# Global migration registry
|
|
323
|
+
_global_migration: SchemaMigration | None = None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_migration_registry() -> SchemaMigration:
|
|
327
|
+
"""Get the global migration registry.
|
|
328
|
+
|
|
329
|
+
Initializes with built-in migrations on first call.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Global SchemaMigration instance.
|
|
333
|
+
"""
|
|
334
|
+
global _global_migration
|
|
335
|
+
|
|
336
|
+
if _global_migration is None:
|
|
337
|
+
_global_migration = SchemaMigration()
|
|
338
|
+
_register_builtin_migrations(_global_migration)
|
|
339
|
+
|
|
340
|
+
return _global_migration
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def register_migration(
|
|
344
|
+
from_version: str,
|
|
345
|
+
to_version: str,
|
|
346
|
+
migrate_fn: MigrationFunction,
|
|
347
|
+
*,
|
|
348
|
+
description: str = "",
|
|
349
|
+
) -> None:
|
|
350
|
+
"""Register a migration with the global registry.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
from_version: Source schema version.
|
|
354
|
+
to_version: Target schema version.
|
|
355
|
+
migrate_fn: Migration function.
|
|
356
|
+
description: Human-readable description.
|
|
357
|
+
"""
|
|
358
|
+
get_migration_registry().register_migration(
|
|
359
|
+
from_version, to_version, migrate_fn, description=description
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def migrate_config(
|
|
364
|
+
config: dict[str, Any],
|
|
365
|
+
target_version: str | None = None,
|
|
366
|
+
) -> dict[str, Any]:
|
|
367
|
+
"""Migrate configuration to target version using global registry.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
config: Configuration to migrate.
|
|
371
|
+
target_version: Target version or None for latest.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Migrated configuration.
|
|
375
|
+
"""
|
|
376
|
+
return get_migration_registry().migrate_config(config, target_version)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def get_config_version(config: dict[str, Any]) -> str | None:
|
|
380
|
+
"""Get version from configuration.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
config: Configuration dictionary.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Version string or None.
|
|
387
|
+
"""
|
|
388
|
+
return get_migration_registry().get_config_version(config)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def list_migrations() -> list[tuple[str, str]]:
|
|
392
|
+
"""List all registered migrations.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
List of (from_version, to_version) tuples.
|
|
396
|
+
"""
|
|
397
|
+
return get_migration_registry().list_migrations()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _parse_version(version: str) -> tuple[int, ...]:
|
|
401
|
+
"""Parse semver version string into tuple for comparison.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
version: Version string (e.g., "1.2.3").
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Tuple of integers (e.g., (1, 2, 3)).
|
|
408
|
+
"""
|
|
409
|
+
try:
|
|
410
|
+
return tuple(int(part) for part in version.split("."))
|
|
411
|
+
except (ValueError, AttributeError):
|
|
412
|
+
# Return (0, 0, 0) for invalid versions
|
|
413
|
+
return (0, 0, 0)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _register_builtin_migrations(migration: SchemaMigration) -> None:
|
|
417
|
+
"""Register built-in migrations for core schemas.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
migration: Migration registry to populate.
|
|
421
|
+
"""
|
|
422
|
+
|
|
423
|
+
# Example migration for protocol schema (1.0.0 -> 1.1.0)
|
|
424
|
+
# This is a placeholder - real migrations would be added as needed
|
|
425
|
+
def _migrate_protocol_1_0_to_1_1(config: dict[str, Any]) -> dict[str, Any]:
|
|
426
|
+
"""Migrate protocol config from 1.0.0 to 1.1.0.
|
|
427
|
+
|
|
428
|
+
Example migration that preserves all existing fields.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
config: Configuration dictionary to migrate.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Migrated configuration dictionary.
|
|
435
|
+
"""
|
|
436
|
+
# Keep all existing fields (preserves unknown keys)
|
|
437
|
+
# Add new optional fields with defaults if needed
|
|
438
|
+
return config
|
|
439
|
+
|
|
440
|
+
# Register when actual schema changes are needed
|
|
441
|
+
# migration.register_migration(
|
|
442
|
+
# "1.0.0",
|
|
443
|
+
# "1.1.0",
|
|
444
|
+
# _migrate_protocol_1_0_to_1_1,
|
|
445
|
+
# description="Protocol schema update for new features",
|
|
446
|
+
# )
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
__all__ = [
|
|
450
|
+
"Migration",
|
|
451
|
+
"MigrationFunction",
|
|
452
|
+
"SchemaMigration",
|
|
453
|
+
"get_config_version",
|
|
454
|
+
"get_migration_registry",
|
|
455
|
+
"list_migrations",
|
|
456
|
+
"migrate_config",
|
|
457
|
+
"register_migration",
|
|
458
|
+
]
|