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,518 @@
|
|
|
1
|
+
"""Interactive analysis wizard for guided workflows.
|
|
2
|
+
|
|
3
|
+
This module provides step-by-step guided workflows that walk non-experts
|
|
4
|
+
through signal analysis, asking simple questions and adapting based on
|
|
5
|
+
responses.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from oscura.guidance import AnalysisWizard
|
|
10
|
+
>>> wizard = AnalysisWizard(trace)
|
|
11
|
+
>>> result = wizard.run()
|
|
12
|
+
|
|
13
|
+
References:
|
|
14
|
+
Oscura Auto-Discovery Specification
|
|
15
|
+
Phase 34 Task-247
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
|
|
27
|
+
from oscura.core.types import WaveformTrace
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class WizardStep:
|
|
32
|
+
"""Single step in the analysis wizard.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
number: Step number (1-based).
|
|
36
|
+
id: Step identifier.
|
|
37
|
+
question: Question text in plain English.
|
|
38
|
+
options: List of answer options.
|
|
39
|
+
default: Default/recommended answer.
|
|
40
|
+
skip_if_confident: Skip if auto-detection confidence >= threshold.
|
|
41
|
+
user_response: User's answer.
|
|
42
|
+
confidence_before: Confidence before step.
|
|
43
|
+
confidence_after: Confidence after step.
|
|
44
|
+
preview: Preview of result.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
number: int
|
|
48
|
+
id: str
|
|
49
|
+
question: str
|
|
50
|
+
options: list[str] = field(default_factory=list)
|
|
51
|
+
default: str | None = None
|
|
52
|
+
skip_if_confident: bool = False
|
|
53
|
+
user_response: str | None = None
|
|
54
|
+
confidence_before: float = 0.0
|
|
55
|
+
confidence_after: float = 0.0
|
|
56
|
+
preview: Any = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class WizardResult:
|
|
61
|
+
"""Result from wizard analysis.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
summary: Summary of analysis.
|
|
65
|
+
signal_type: Detected signal type.
|
|
66
|
+
parameters: Signal parameters.
|
|
67
|
+
quality: Quality assessment.
|
|
68
|
+
decode: Decoded data (if applicable).
|
|
69
|
+
recommendations: Next step recommendations.
|
|
70
|
+
confidence: Overall confidence.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
summary: str
|
|
74
|
+
signal_type: str | None = None
|
|
75
|
+
parameters: dict | None = None # type: ignore[type-arg]
|
|
76
|
+
quality: Any = None
|
|
77
|
+
decode: Any = None
|
|
78
|
+
recommendations: list = field(default_factory=list) # type: ignore[type-arg]
|
|
79
|
+
confidence: float = 0.0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class AnalysisWizard:
|
|
83
|
+
"""Interactive analysis wizard for guided workflows.
|
|
84
|
+
|
|
85
|
+
Provides step-by-step guided analysis with smart defaults,
|
|
86
|
+
auto-skip based on confidence, progress tracking, and live previews.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
trace: Waveform being analyzed.
|
|
90
|
+
max_questions: Maximum questions to ask (default 5).
|
|
91
|
+
auto_detect_threshold: Confidence threshold for auto-skip (default 0.8).
|
|
92
|
+
enable_preview: Enable live result preview.
|
|
93
|
+
allow_backtrack: Allow back/forward navigation.
|
|
94
|
+
interactive: Enable interactive mode.
|
|
95
|
+
step_history: History of completed steps.
|
|
96
|
+
steps_completed: Number of steps completed.
|
|
97
|
+
questions_asked: Number of questions asked.
|
|
98
|
+
questions_skipped: Number of questions skipped.
|
|
99
|
+
session_duration_seconds: Total session duration.
|
|
100
|
+
_start_time: Session start time.
|
|
101
|
+
_current_state: Current analysis state.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
trace: WaveformTrace,
|
|
107
|
+
*,
|
|
108
|
+
max_questions: int = 5,
|
|
109
|
+
auto_detect_threshold: float = 0.8,
|
|
110
|
+
enable_preview: bool = True,
|
|
111
|
+
allow_backtrack: bool = True,
|
|
112
|
+
interactive: bool = True,
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Initialize analysis wizard.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
trace: Waveform to analyze.
|
|
118
|
+
max_questions: Maximum questions (default 5, range 3-7).
|
|
119
|
+
auto_detect_threshold: Skip question if confidence >= this.
|
|
120
|
+
enable_preview: Enable live result preview after each step.
|
|
121
|
+
allow_backtrack: Allow back/forward navigation.
|
|
122
|
+
interactive: Enable interactive mode (vs programmatic).
|
|
123
|
+
|
|
124
|
+
References:
|
|
125
|
+
DISC-006: Interactive Analysis Wizard
|
|
126
|
+
"""
|
|
127
|
+
self.trace = trace
|
|
128
|
+
self.max_questions = max(3, min(7, max_questions))
|
|
129
|
+
self.auto_detect_threshold = auto_detect_threshold
|
|
130
|
+
self.enable_preview = enable_preview
|
|
131
|
+
self.allow_backtrack = allow_backtrack
|
|
132
|
+
self.interactive = interactive
|
|
133
|
+
|
|
134
|
+
self.step_history: list[WizardStep] = []
|
|
135
|
+
self.steps_completed = 0
|
|
136
|
+
self.questions_asked = 0
|
|
137
|
+
self.questions_skipped = 0
|
|
138
|
+
self.session_duration_seconds = 0.0
|
|
139
|
+
|
|
140
|
+
self._start_time = datetime.now()
|
|
141
|
+
self._current_state: dict[str, Any] = {}
|
|
142
|
+
self._predefined_answers: dict[str, str] = {}
|
|
143
|
+
|
|
144
|
+
def add_custom_step(
|
|
145
|
+
self,
|
|
146
|
+
step_id: str,
|
|
147
|
+
*,
|
|
148
|
+
question: str,
|
|
149
|
+
options: list[str],
|
|
150
|
+
default: str | None = None,
|
|
151
|
+
skip_if_confident: bool = True,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Add a custom step to the wizard.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
step_id: Unique step identifier.
|
|
157
|
+
question: Question text in plain English.
|
|
158
|
+
options: List of answer options.
|
|
159
|
+
default: Default/recommended answer.
|
|
160
|
+
skip_if_confident: Skip if auto-detection confident.
|
|
161
|
+
"""
|
|
162
|
+
# Store for later use during run()
|
|
163
|
+
if not hasattr(self, "_custom_steps"):
|
|
164
|
+
self._custom_steps = []
|
|
165
|
+
|
|
166
|
+
self._custom_steps.append(
|
|
167
|
+
{
|
|
168
|
+
"id": step_id,
|
|
169
|
+
"question": question,
|
|
170
|
+
"options": options,
|
|
171
|
+
"default": default,
|
|
172
|
+
"skip_if_confident": skip_if_confident,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def set_answers(self, answers: dict[str, str]) -> None:
|
|
177
|
+
"""Set predefined answers for programmatic mode.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
answers: Dictionary mapping step IDs to answers.
|
|
181
|
+
"""
|
|
182
|
+
self._predefined_answers = answers
|
|
183
|
+
|
|
184
|
+
def run(
|
|
185
|
+
self,
|
|
186
|
+
*,
|
|
187
|
+
preview_callback: Callable[[Any], None] | None = None,
|
|
188
|
+
) -> WizardResult:
|
|
189
|
+
"""Run the analysis wizard.
|
|
190
|
+
|
|
191
|
+
Guides user through analysis steps, auto-skipping where confident,
|
|
192
|
+
showing progress, and providing live previews.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
preview_callback: Optional callback for step previews.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
WizardResult with analysis summary and findings.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> wizard = AnalysisWizard(trace)
|
|
202
|
+
>>> result = wizard.run()
|
|
203
|
+
>>> print(result.summary)
|
|
204
|
+
|
|
205
|
+
References:
|
|
206
|
+
DISC-006: Interactive Analysis Wizard
|
|
207
|
+
"""
|
|
208
|
+
from oscura.discovery import (
|
|
209
|
+
assess_data_quality,
|
|
210
|
+
characterize_signal,
|
|
211
|
+
decode_protocol,
|
|
212
|
+
find_anomalies,
|
|
213
|
+
)
|
|
214
|
+
from oscura.guidance import suggest_next_steps
|
|
215
|
+
|
|
216
|
+
# Step 1: Auto-characterization
|
|
217
|
+
step1 = WizardStep(
|
|
218
|
+
number=1,
|
|
219
|
+
id="characterization",
|
|
220
|
+
question="What type of signal are you analyzing?",
|
|
221
|
+
options=[
|
|
222
|
+
"Serial data (UART, SPI, I2C)",
|
|
223
|
+
"PWM / Motor control",
|
|
224
|
+
"Analog sensor output",
|
|
225
|
+
"Not sure - auto-detect",
|
|
226
|
+
],
|
|
227
|
+
default="Not sure - auto-detect",
|
|
228
|
+
skip_if_confident=False,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Always do auto-characterization first
|
|
232
|
+
char_result = characterize_signal(self.trace)
|
|
233
|
+
step1.confidence_before = 0.0
|
|
234
|
+
step1.confidence_after = char_result.confidence
|
|
235
|
+
self._current_state["characterization"] = char_result
|
|
236
|
+
|
|
237
|
+
if self.interactive and char_result.confidence < self.auto_detect_threshold:
|
|
238
|
+
# Ask user to confirm
|
|
239
|
+
step1.user_response = self._predefined_answers.get("signal_type", step1.default)
|
|
240
|
+
self.questions_asked += 1
|
|
241
|
+
else:
|
|
242
|
+
# Auto-detected with high confidence
|
|
243
|
+
signal_type = getattr(char_result, "signal_type", "Unknown")
|
|
244
|
+
step1.user_response = f"Auto-detected: {signal_type}"
|
|
245
|
+
self.questions_skipped += 1
|
|
246
|
+
|
|
247
|
+
self.step_history.append(step1)
|
|
248
|
+
self.steps_completed += 1
|
|
249
|
+
|
|
250
|
+
# Preview callback
|
|
251
|
+
if preview_callback and self.enable_preview:
|
|
252
|
+
preview_callback(char_result)
|
|
253
|
+
|
|
254
|
+
# Step 2: Quality assessment
|
|
255
|
+
step2 = WizardStep(
|
|
256
|
+
number=2,
|
|
257
|
+
id="quality",
|
|
258
|
+
question="Check data quality?",
|
|
259
|
+
options=["Yes", "No"],
|
|
260
|
+
default="Yes",
|
|
261
|
+
skip_if_confident=True,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
quality = assess_data_quality(self.trace)
|
|
265
|
+
step2.confidence_before = char_result.confidence
|
|
266
|
+
step2.confidence_after = quality.confidence
|
|
267
|
+
self._current_state["quality"] = quality
|
|
268
|
+
|
|
269
|
+
if self.interactive and self.questions_asked < self.max_questions:
|
|
270
|
+
step2.user_response = self._predefined_answers.get("check_quality", "Yes")
|
|
271
|
+
if step2.user_response == "Yes":
|
|
272
|
+
self.questions_asked += 1
|
|
273
|
+
else:
|
|
274
|
+
step2.user_response = "Skipped (auto-assessed)"
|
|
275
|
+
self.questions_skipped += 1
|
|
276
|
+
|
|
277
|
+
self.step_history.append(step2)
|
|
278
|
+
self.steps_completed += 1
|
|
279
|
+
|
|
280
|
+
if preview_callback and self.enable_preview:
|
|
281
|
+
preview_callback(quality)
|
|
282
|
+
|
|
283
|
+
# Step 3: Protocol decode (if applicable)
|
|
284
|
+
decode_result = None
|
|
285
|
+
if hasattr(char_result, "signal_type") and char_result.confidence >= 0.7:
|
|
286
|
+
signal_type = char_result.signal_type.lower()
|
|
287
|
+
|
|
288
|
+
if any(proto in signal_type for proto in ["uart", "spi", "i2c", "can"]):
|
|
289
|
+
step3 = WizardStep(
|
|
290
|
+
number=3,
|
|
291
|
+
id="decode",
|
|
292
|
+
question=f"Auto-detected {char_result.signal_type}. Decode data?",
|
|
293
|
+
options=["Yes", "No"],
|
|
294
|
+
default="Yes",
|
|
295
|
+
skip_if_confident=True,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if self.interactive and self.questions_asked < self.max_questions:
|
|
299
|
+
step3.user_response = self._predefined_answers.get("decode_data", "Yes")
|
|
300
|
+
if step3.user_response == "Yes":
|
|
301
|
+
decode_result = decode_protocol(self.trace)
|
|
302
|
+
self._current_state["decode"] = decode_result
|
|
303
|
+
self.questions_asked += 1
|
|
304
|
+
else:
|
|
305
|
+
# Auto-decode
|
|
306
|
+
decode_result = decode_protocol(self.trace)
|
|
307
|
+
self._current_state["decode"] = decode_result
|
|
308
|
+
step3.user_response = "Auto-decoded"
|
|
309
|
+
self.questions_skipped += 1
|
|
310
|
+
|
|
311
|
+
step3.confidence_before = char_result.confidence
|
|
312
|
+
step3.confidence_after = decode_result.overall_confidence if decode_result else 0.0
|
|
313
|
+
|
|
314
|
+
self.step_history.append(step3)
|
|
315
|
+
self.steps_completed += 1
|
|
316
|
+
|
|
317
|
+
if preview_callback and self.enable_preview and decode_result:
|
|
318
|
+
preview_callback(decode_result)
|
|
319
|
+
|
|
320
|
+
# Step 4: Anomaly detection (if quality issues)
|
|
321
|
+
anomalies = None
|
|
322
|
+
if quality.status in ["WARNING", "FAIL"]:
|
|
323
|
+
step4 = WizardStep(
|
|
324
|
+
number=self.steps_completed + 1,
|
|
325
|
+
id="anomalies",
|
|
326
|
+
question="Quality concerns detected. Check for anomalies?",
|
|
327
|
+
options=["Yes", "No"],
|
|
328
|
+
default="Yes",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
if self.interactive and self.questions_asked < self.max_questions:
|
|
332
|
+
step4.user_response = self._predefined_answers.get("check_anomalies", "Yes")
|
|
333
|
+
if step4.user_response == "Yes":
|
|
334
|
+
anomalies = find_anomalies(self.trace)
|
|
335
|
+
self._current_state["anomalies"] = anomalies
|
|
336
|
+
self.questions_asked += 1
|
|
337
|
+
else:
|
|
338
|
+
# Auto-check
|
|
339
|
+
anomalies = find_anomalies(self.trace)
|
|
340
|
+
self._current_state["anomalies"] = anomalies
|
|
341
|
+
step4.user_response = "Auto-checked"
|
|
342
|
+
self.questions_skipped += 1
|
|
343
|
+
|
|
344
|
+
self.step_history.append(step4)
|
|
345
|
+
self.steps_completed += 1
|
|
346
|
+
|
|
347
|
+
if preview_callback and self.enable_preview and anomalies:
|
|
348
|
+
preview_callback(anomalies)
|
|
349
|
+
|
|
350
|
+
# Get recommendations for next steps
|
|
351
|
+
recommendations = suggest_next_steps(
|
|
352
|
+
self.trace,
|
|
353
|
+
current_state=self._current_state,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Build summary
|
|
357
|
+
summary_parts = []
|
|
358
|
+
|
|
359
|
+
if hasattr(char_result, "signal_type") and char_result.signal_type:
|
|
360
|
+
summary_parts.append(f"Signal type: {char_result.signal_type}")
|
|
361
|
+
if hasattr(char_result, "parameters"):
|
|
362
|
+
summary_parts.append(f"Parameters: {_format_params(char_result.parameters)}")
|
|
363
|
+
|
|
364
|
+
if quality.status == "PASS":
|
|
365
|
+
summary_parts.append("Quality: Good")
|
|
366
|
+
elif quality.status == "WARNING":
|
|
367
|
+
summary_parts.append("Quality: Fair (some concerns)")
|
|
368
|
+
else:
|
|
369
|
+
summary_parts.append("Quality: Poor (issues detected)")
|
|
370
|
+
|
|
371
|
+
if decode_result:
|
|
372
|
+
byte_count = len(decode_result.data) if hasattr(decode_result, "data") else 0
|
|
373
|
+
summary_parts.append(f"Decoded: {byte_count} bytes")
|
|
374
|
+
|
|
375
|
+
if anomalies and len(anomalies) > 0:
|
|
376
|
+
critical = sum(1 for a in anomalies if a.severity == "CRITICAL")
|
|
377
|
+
if critical > 0:
|
|
378
|
+
summary_parts.append(f"Anomalies: {critical} critical issues")
|
|
379
|
+
|
|
380
|
+
summary = "\n".join(summary_parts)
|
|
381
|
+
|
|
382
|
+
# Session duration
|
|
383
|
+
self.session_duration_seconds = (datetime.now() - self._start_time).total_seconds()
|
|
384
|
+
|
|
385
|
+
return WizardResult(
|
|
386
|
+
summary=summary,
|
|
387
|
+
signal_type=char_result.signal_type if hasattr(char_result, "signal_type") else None,
|
|
388
|
+
parameters=char_result.parameters if hasattr(char_result, "parameters") else None,
|
|
389
|
+
quality=quality,
|
|
390
|
+
decode=decode_result,
|
|
391
|
+
recommendations=recommendations,
|
|
392
|
+
confidence=char_result.confidence,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
@classmethod
|
|
396
|
+
def from_session(cls, session_file: str) -> AnalysisWizard:
|
|
397
|
+
"""Load wizard from saved session file.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
session_file: Path to session JSON file.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
AnalysisWizard instance configured from session.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
FileNotFoundError: If session file doesn't exist.
|
|
407
|
+
ValueError: If session file is invalid.
|
|
408
|
+
"""
|
|
409
|
+
import json
|
|
410
|
+
from pathlib import Path
|
|
411
|
+
|
|
412
|
+
path = Path(session_file)
|
|
413
|
+
if not path.exists():
|
|
414
|
+
msg = f"Session file not found: {session_file}"
|
|
415
|
+
raise FileNotFoundError(msg)
|
|
416
|
+
|
|
417
|
+
with path.open() as f:
|
|
418
|
+
session_data = json.load(f)
|
|
419
|
+
|
|
420
|
+
# Extract trace path and load
|
|
421
|
+
from oscura import load
|
|
422
|
+
|
|
423
|
+
trace_path = session_data.get("trace_path")
|
|
424
|
+
if not trace_path:
|
|
425
|
+
msg = "Session file missing trace_path"
|
|
426
|
+
raise ValueError(msg)
|
|
427
|
+
|
|
428
|
+
trace = load(trace_path)
|
|
429
|
+
|
|
430
|
+
# Create wizard with saved settings
|
|
431
|
+
wizard = cls(
|
|
432
|
+
trace, # type: ignore[arg-type]
|
|
433
|
+
max_questions=session_data.get("max_questions", 5),
|
|
434
|
+
auto_detect_threshold=session_data.get("auto_detect_threshold", 0.8),
|
|
435
|
+
enable_preview=session_data.get("enable_preview", True),
|
|
436
|
+
allow_backtrack=session_data.get("allow_backtrack", True),
|
|
437
|
+
interactive=session_data.get("interactive", True),
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Set predefined answers
|
|
441
|
+
if "answers" in session_data:
|
|
442
|
+
wizard.set_answers(session_data["answers"])
|
|
443
|
+
|
|
444
|
+
return wizard
|
|
445
|
+
|
|
446
|
+
def save_session(self, output_path: str) -> None:
|
|
447
|
+
"""Save wizard session to JSON file.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
output_path: Path for output JSON file.
|
|
451
|
+
"""
|
|
452
|
+
import json
|
|
453
|
+
from pathlib import Path
|
|
454
|
+
|
|
455
|
+
session_data = {
|
|
456
|
+
"trace_path": str(getattr(self.trace, "path", "")),
|
|
457
|
+
"max_questions": self.max_questions,
|
|
458
|
+
"auto_detect_threshold": self.auto_detect_threshold,
|
|
459
|
+
"enable_preview": self.enable_preview,
|
|
460
|
+
"allow_backtrack": self.allow_backtrack,
|
|
461
|
+
"interactive": self.interactive,
|
|
462
|
+
"steps_completed": self.steps_completed,
|
|
463
|
+
"questions_asked": self.questions_asked,
|
|
464
|
+
"questions_skipped": self.questions_skipped,
|
|
465
|
+
"session_duration_seconds": self.session_duration_seconds,
|
|
466
|
+
"answers": self._predefined_answers,
|
|
467
|
+
"step_history": [
|
|
468
|
+
{
|
|
469
|
+
"number": step.number,
|
|
470
|
+
"id": step.id,
|
|
471
|
+
"question": step.question,
|
|
472
|
+
"user_response": step.user_response,
|
|
473
|
+
"confidence_before": step.confidence_before,
|
|
474
|
+
"confidence_after": step.confidence_after,
|
|
475
|
+
}
|
|
476
|
+
for step in self.step_history
|
|
477
|
+
],
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
path = Path(output_path)
|
|
481
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
482
|
+
|
|
483
|
+
with path.open("w") as f:
|
|
484
|
+
json.dump(session_data, f, indent=2)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _format_params(params: dict) -> str: # type: ignore[type-arg]
|
|
488
|
+
"""Format parameters dictionary for display.
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
params: Parameters dictionary.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
Formatted string.
|
|
495
|
+
"""
|
|
496
|
+
if not params:
|
|
497
|
+
return ""
|
|
498
|
+
|
|
499
|
+
parts = []
|
|
500
|
+
for key, value in params.items():
|
|
501
|
+
if isinstance(value, int | float):
|
|
502
|
+
if key.endswith("_hz") or key.endswith("_freq"):
|
|
503
|
+
parts.append(f"{key}={value / 1e3:.1f}kHz")
|
|
504
|
+
elif key.endswith("_baud") or key.endswith("baud_rate"):
|
|
505
|
+
parts.append(f"{key}={value:.0f}")
|
|
506
|
+
else:
|
|
507
|
+
parts.append(f"{key}={value}")
|
|
508
|
+
else:
|
|
509
|
+
parts.append(f"{key}={value}")
|
|
510
|
+
|
|
511
|
+
return ", ".join(parts[:3]) # Limit to 3 params
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
__all__ = [
|
|
515
|
+
"AnalysisWizard",
|
|
516
|
+
"WizardResult",
|
|
517
|
+
"WizardStep",
|
|
518
|
+
]
|