openprocess 0.10.6__tar.gz → 0.11.0__tar.gz
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.
- {openprocess-0.10.6/openprocess.egg-info → openprocess-0.11.0}/PKG-INFO +30 -9
- {openprocess-0.10.6 → openprocess-0.11.0}/README.md +29 -8
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/__init__.py +1 -1
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/science.py +24 -13
- openprocess-0.11.0/openprocess/flow/history.py +113 -0
- openprocess-0.11.0/openprocess/flow/provenance.py +110 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/record.py +67 -3
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/runner.py +10 -0
- openprocess-0.11.0/openprocess/flow/script.py +87 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/types.py +10 -1
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/workflow.py +18 -1
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/page.py +83 -3
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/viewers.py +82 -6
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/app.py +82 -10
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/connections.py +26 -1
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/definition_view.py +9 -2
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/graph_view.py +9 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/markdown_view.py +3 -1
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/style.py +2 -0
- {openprocess-0.10.6 → openprocess-0.11.0/openprocess.egg-info}/PKG-INFO +30 -9
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/SOURCES.txt +4 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_exercises_gui.py +6 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_flow.py +119 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_flow_gui.py +94 -1
- openprocess-0.11.0/tests/test_history.py +57 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_studio_gui.py +81 -1
- {openprocess-0.10.6 → openprocess-0.11.0}/LICENSE +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/cpnpy/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/analysis/state_space.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/citation.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/cli.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/pack.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/box.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/check.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/input.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/convert.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/explain.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/figures.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/library.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/report.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/reproducibility.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/snapshots.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/app.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/canvas.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/canvas.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/picker.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/runs.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/stepper.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/summary.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/items.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/learn/answer_boxes.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/learn/mode.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/panning.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/cite.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/command_palette.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/cpn_page.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/derivation_view.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/documents.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/dotted_chart.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/instances.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/model_page.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/motion.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/petri_page.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/provenance.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/sidebar.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/spaces.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/tool_icons.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/updates.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/widgets.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/workspace.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/theme.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/io/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/answers.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/checks.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/computed.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/context.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/exam.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/exercise-packs.md +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/importer.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/notation.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/pack.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/analysis.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/columns.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/alignments.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/token_replay.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/inductive.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/layout.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/log.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/ocel.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/petrinet.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/playout.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/regions.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/stats.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/structure.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/xes.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/errors.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/parser.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/values.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/declarations.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/examples.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/net.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/plain.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/packages.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/references.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/binding.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/export.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/pyproject.toml +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/setup.cfg +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_arc_editing.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_citation.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_colorsets.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_columns.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_definitions.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_hierarchy.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_io.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_learn.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_learn_gui.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_log_editing.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_mining.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_ml.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_multiset.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_ocel.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_packages.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_regions.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_rename.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_report.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_simulation.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_snapshots.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_state_space.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_teaching.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_tidy.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_updates.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_wf_structure.py +0 -0
- {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
|
|
5
5
|
Author: Sam Struthers
|
|
6
6
|
License: MIT
|
|
@@ -528,17 +528,19 @@ links to the definitions it builds on; **Help ▸ Definitions** lists them all.
|
|
|
528
528
|
for bounded nets; for unbounded ones, up to a trace length (and it says
|
|
529
529
|
so).
|
|
530
530
|
|
|
531
|
-
| Process map |
|
|
531
|
+
| Process map | Discover in Mine →: the miner is a box, its result beside the canvas |
|
|
532
532
|
|---|---|
|
|
533
533
|
|  |  |
|
|
534
534
|
|
|
535
|
-
| α-algorithm result |
|
|
535
|
+
| α-algorithm result | *How*: the eight steps, from the footprint to α(L) |
|
|
536
536
|
|---|---|
|
|
537
|
-
|  |  |  |
|
|
538
538
|
|
|
539
|
-
|
|
|
539
|
+
| Check fit: every score, and how it was computed | Comparing two logs |
|
|
540
540
|
|---|---|
|
|
541
|
-
|  |  |
|
|
542
|
+
|
|
543
|
+

|
|
542
544
|
|
|
543
545
|
The app follows the system's light or dark appearance:
|
|
544
546
|
|
|
@@ -555,6 +557,8 @@ sidebar, **Learn ▸ Open Exercise Pack…**, or **Learn ▸ Open Demo Exercises
|
|
|
555
557
|
exercises): the window switches to a view made for working through a pack
|
|
556
558
|
without distractions, and *Exit* brings your folder back exactly as it was.
|
|
557
559
|
|
|
560
|
+

|
|
561
|
+
|
|
558
562
|
- **The worksheet** on the left is the question top to bottom, with an
|
|
559
563
|
answer box wherever one is needed: yes/no, multiple choice, a set
|
|
560
564
|
(`{a, b}`, sets of sets, or pairs like `({a}, {b,d})`), a number, a short
|
|
@@ -742,7 +746,10 @@ generated from `openprocess/mining/definitions.py`; after editing a definition,
|
|
|
742
746
|
Likewise [`docs/references.md`](docs/references.md) — every source OpenProcess's
|
|
743
747
|
notation and algorithms follow, also in the app under **Help ▸ References** —
|
|
744
748
|
is generated from `openprocess/references.py` with
|
|
745
|
-
`python -m openprocess.references > docs/references.md`.
|
|
749
|
+
`python -m openprocess.references > docs/references.md`. The screenshots in
|
|
750
|
+
this README come from the app itself: `python docs/make_screenshots.py`
|
|
751
|
+
renders every page offscreen into `docs/screenshots/`, so run it after a
|
|
752
|
+
change to the app's looks.
|
|
746
753
|
|
|
747
754
|
The engines (`openprocess.mining`, `openprocess.ml`, `openprocess.sim`, `openprocess.analysis`)
|
|
748
755
|
have **no dependencies**, so they work in a notebook or a script:
|
|
@@ -769,6 +776,10 @@ Miner, a fitness check), *Compare discovery* (three miners side by side),
|
|
|
769
776
|
confidence* (bootstrap intervals, a test and a plot; needs the `science`
|
|
770
777
|
extra) and *Predict the next activity*. Or start empty.
|
|
771
778
|
|
|
779
|
+
| The canvas: *Compare discovery*, three miners side by side | Summary: every box's key figure, and the process map |
|
|
780
|
+
|---|---|
|
|
781
|
+
|  |  |
|
|
782
|
+
|
|
772
783
|
- **The page is the canvas.** Its header has **Canvas | Summary**, then
|
|
773
784
|
**+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
|
|
774
785
|
and Save. The sidebar beside it lists the folder's analyses and, folded
|
|
@@ -825,8 +836,18 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
825
836
|
`boxes/`, so it is listed under *Yours* and can be used in any workflow.
|
|
826
837
|
- **Export experiment** (under ⋯) writes a zip with the workflow file and its record,
|
|
827
838
|
the inputs, your boxes, every result as a file (CSV, PNML, SVG, XES), a
|
|
828
|
-
`requirements.lock
|
|
829
|
-
|
|
839
|
+
`requirements.lock`, a `run.py` that runs the same analysis as a plain
|
|
840
|
+
script, and a README that says what was run: supplementary material for a
|
|
841
|
+
paper, which `openprocess run --check` can verify. Drop such a zip on
|
|
842
|
+
OpenProcess and it is unpacked into a folder of its own and run again.
|
|
843
|
+
- **Export as Python script** (under ⋯) writes the analysis as a `.py` file:
|
|
844
|
+
the same boxes, settings and connections, run with `python analysis.py`,
|
|
845
|
+
the results written beside it.
|
|
846
|
+
- **Every result knows where it came from**: a figure saved as SVG carries
|
|
847
|
+
the analysis, the box, its settings and the result's fingerprint in its
|
|
848
|
+
metadata; a CSV gets a `.provenance.json` beside it.
|
|
849
|
+
- **⌘Z and ⇧⌘Z** undo and redo on the canvas: boxes, connections, moves,
|
|
850
|
+
settings, groups.
|
|
830
851
|
- **The side panel** resizes by dragging the gap beside it; double-click the
|
|
831
852
|
gap, or *View ▸ Reset Workflow Layout*, for its default width.
|
|
832
853
|
- **Connections** (at the bottom of the sidebar, or *View ▸ Connections*)
|
|
@@ -489,17 +489,19 @@ links to the definitions it builds on; **Help ▸ Definitions** lists them all.
|
|
|
489
489
|
for bounded nets; for unbounded ones, up to a trace length (and it says
|
|
490
490
|
so).
|
|
491
491
|
|
|
492
|
-
| Process map |
|
|
492
|
+
| Process map | Discover in Mine →: the miner is a box, its result beside the canvas |
|
|
493
493
|
|---|---|
|
|
494
494
|
|  |  |
|
|
495
495
|
|
|
496
|
-
| α-algorithm result |
|
|
496
|
+
| α-algorithm result | *How*: the eight steps, from the footprint to α(L) |
|
|
497
497
|
|---|---|
|
|
498
|
-
|  |  |  |
|
|
499
499
|
|
|
500
|
-
|
|
|
500
|
+
| Check fit: every score, and how it was computed | Comparing two logs |
|
|
501
501
|
|---|---|
|
|
502
|
-
|  |  |
|
|
503
|
+
|
|
504
|
+

|
|
503
505
|
|
|
504
506
|
The app follows the system's light or dark appearance:
|
|
505
507
|
|
|
@@ -516,6 +518,8 @@ sidebar, **Learn ▸ Open Exercise Pack…**, or **Learn ▸ Open Demo Exercises
|
|
|
516
518
|
exercises): the window switches to a view made for working through a pack
|
|
517
519
|
without distractions, and *Exit* brings your folder back exactly as it was.
|
|
518
520
|
|
|
521
|
+

|
|
522
|
+
|
|
519
523
|
- **The worksheet** on the left is the question top to bottom, with an
|
|
520
524
|
answer box wherever one is needed: yes/no, multiple choice, a set
|
|
521
525
|
(`{a, b}`, sets of sets, or pairs like `({a}, {b,d})`), a number, a short
|
|
@@ -703,7 +707,10 @@ generated from `openprocess/mining/definitions.py`; after editing a definition,
|
|
|
703
707
|
Likewise [`docs/references.md`](docs/references.md) — every source OpenProcess's
|
|
704
708
|
notation and algorithms follow, also in the app under **Help ▸ References** —
|
|
705
709
|
is generated from `openprocess/references.py` with
|
|
706
|
-
`python -m openprocess.references > docs/references.md`.
|
|
710
|
+
`python -m openprocess.references > docs/references.md`. The screenshots in
|
|
711
|
+
this README come from the app itself: `python docs/make_screenshots.py`
|
|
712
|
+
renders every page offscreen into `docs/screenshots/`, so run it after a
|
|
713
|
+
change to the app's looks.
|
|
707
714
|
|
|
708
715
|
The engines (`openprocess.mining`, `openprocess.ml`, `openprocess.sim`, `openprocess.analysis`)
|
|
709
716
|
have **no dependencies**, so they work in a notebook or a script:
|
|
@@ -730,6 +737,10 @@ Miner, a fitness check), *Compare discovery* (three miners side by side),
|
|
|
730
737
|
confidence* (bootstrap intervals, a test and a plot; needs the `science`
|
|
731
738
|
extra) and *Predict the next activity*. Or start empty.
|
|
732
739
|
|
|
740
|
+
| The canvas: *Compare discovery*, three miners side by side | Summary: every box's key figure, and the process map |
|
|
741
|
+
|---|---|
|
|
742
|
+
|  |  |
|
|
743
|
+
|
|
733
744
|
- **The page is the canvas.** Its header has **Canvas | Summary**, then
|
|
734
745
|
**+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
|
|
735
746
|
and Save. The sidebar beside it lists the folder's analyses and, folded
|
|
@@ -786,8 +797,18 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
786
797
|
`boxes/`, so it is listed under *Yours* and can be used in any workflow.
|
|
787
798
|
- **Export experiment** (under ⋯) writes a zip with the workflow file and its record,
|
|
788
799
|
the inputs, your boxes, every result as a file (CSV, PNML, SVG, XES), a
|
|
789
|
-
`requirements.lock
|
|
790
|
-
|
|
800
|
+
`requirements.lock`, a `run.py` that runs the same analysis as a plain
|
|
801
|
+
script, and a README that says what was run: supplementary material for a
|
|
802
|
+
paper, which `openprocess run --check` can verify. Drop such a zip on
|
|
803
|
+
OpenProcess and it is unpacked into a folder of its own and run again.
|
|
804
|
+
- **Export as Python script** (under ⋯) writes the analysis as a `.py` file:
|
|
805
|
+
the same boxes, settings and connections, run with `python analysis.py`,
|
|
806
|
+
the results written beside it.
|
|
807
|
+
- **Every result knows where it came from**: a figure saved as SVG carries
|
|
808
|
+
the analysis, the box, its settings and the result's fingerprint in its
|
|
809
|
+
metadata; a CSV gets a `.provenance.json` beside it.
|
|
810
|
+
- **⌘Z and ⇧⌘Z** undo and redo on the canvas: boxes, connections, moves,
|
|
811
|
+
settings, groups.
|
|
791
812
|
- **The side panel** resizes by dragging the gap beside it; double-click the
|
|
792
813
|
gap, or *View ▸ Reset Workflow Layout*, for its default width.
|
|
793
814
|
- **Connections** (at the bottom of the sidebar, or *View ▸ Connections*)
|
|
@@ -48,7 +48,7 @@ from .sim.simulator import Simulator
|
|
|
48
48
|
#: The one place the version number is written (pyproject.toml, the update
|
|
49
49
|
#: check and packaging/build.py read it from here). A release is tagged
|
|
50
50
|
#: "v" + this, e.g. v0.2.0.
|
|
51
|
-
__version__ = "0.
|
|
51
|
+
__version__ = "0.11.0"
|
|
52
52
|
|
|
53
53
|
__all__ = [
|
|
54
54
|
"Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
|
|
@@ -110,11 +110,13 @@ def correlate(table: Table) -> Table:
|
|
|
110
110
|
@box(name="Plot", group="Science", needs="matplotlib")
|
|
111
111
|
def plot(scores: list[Scores] = (), tables: list[Table] = (),
|
|
112
112
|
x: Column = "", y: Column = "") -> Figure:
|
|
113
|
-
"""Bars per model for scores, histograms for bootstrap tables, or
|
|
114
|
-
|
|
113
|
+
"""Bars per model for scores, histograms for bootstrap tables, or column
|
|
114
|
+
*y* of a table against column *x*: bars when *x* holds names (a model per
|
|
115
|
+
bar), a line when it holds numbers (a sweep). Once *x* and *y* are set
|
|
116
|
+
they decide the figure, whatever else is connected. The figure is a
|
|
115
117
|
matplotlib figure, converted to SVG; its code is this box's code.
|
|
116
118
|
|
|
117
|
-
x: a column of the table to put on the x axis
|
|
119
|
+
x: a column of the table to put on the x axis
|
|
118
120
|
y: the column to plot against it
|
|
119
121
|
"""
|
|
120
122
|
import matplotlib
|
|
@@ -122,7 +124,24 @@ def plot(scores: list[Scores] = (), tables: list[Table] = (),
|
|
|
122
124
|
import matplotlib.pyplot as plt
|
|
123
125
|
fig, ax = plt.subplots(figsize=(5.5, 3.2))
|
|
124
126
|
scores, tables = list(scores), list(tables)
|
|
125
|
-
if
|
|
127
|
+
if tables and x and y:
|
|
128
|
+
for table in tables:
|
|
129
|
+
for name in (x, y):
|
|
130
|
+
if name not in table.columns:
|
|
131
|
+
raise ValueError(f"{table.name} has no column {name!r}; it has {', '.join(table.columns)}")
|
|
132
|
+
numeric = all(isinstance(v, (int, float)) and not isinstance(v, bool) for t in tables for v in t.column(x))
|
|
133
|
+
for table in tables:
|
|
134
|
+
values = [float(v) for v in table.column(y)]
|
|
135
|
+
if numeric:
|
|
136
|
+
ax.plot(table.column(x), values, marker="o", label=table.name)
|
|
137
|
+
else:
|
|
138
|
+
ax.bar([str(v) for v in table.column(x)], values, label=table.name)
|
|
139
|
+
ax.set_xlabel(x)
|
|
140
|
+
ax.set_ylabel(y)
|
|
141
|
+
if len(tables) > 1:
|
|
142
|
+
ax.legend(fontsize=8)
|
|
143
|
+
caption = f"{y} against {x}."
|
|
144
|
+
elif scores:
|
|
126
145
|
metrics = [m for m in dict.fromkeys(k for s in scores for k in s.metrics)
|
|
127
146
|
if all(isinstance(s.metrics.get(m, 0.0), (int, float)) for s in scores)]
|
|
128
147
|
width = 0.8 / max(1, len(metrics))
|
|
@@ -134,14 +153,6 @@ def plot(scores: list[Scores] = (), tables: list[Table] = (),
|
|
|
134
153
|
ax.set_ylim(0, 1.05)
|
|
135
154
|
ax.legend(fontsize=8)
|
|
136
155
|
caption = f"{len(scores)} models, {len(metrics)} scores each."
|
|
137
|
-
elif tables and x and y:
|
|
138
|
-
for table in tables:
|
|
139
|
-
ax.plot(table.column(x), table.column(y), marker="o", label=table.name)
|
|
140
|
-
ax.set_xlabel(x)
|
|
141
|
-
ax.set_ylabel(y)
|
|
142
|
-
if len(tables) > 1:
|
|
143
|
-
ax.legend(fontsize=8)
|
|
144
|
-
caption = f"{y} against {x}."
|
|
145
156
|
elif tables:
|
|
146
157
|
for table in tables:
|
|
147
158
|
if table.samples:
|
|
@@ -150,7 +161,7 @@ def plot(scores: list[Scores] = (), tables: list[Table] = (),
|
|
|
150
161
|
ax.legend(fontsize=8)
|
|
151
162
|
caption = f"{len(tables)} bootstrap distribution{'s' if len(tables) != 1 else ''}."
|
|
152
163
|
else:
|
|
153
|
-
raise ValueError("Connect scores, or tables with samples or
|
|
164
|
+
raise ValueError("Connect scores, or tables with samples, or a table and set both x and y")
|
|
154
165
|
fig.tight_layout()
|
|
155
166
|
figure = convert(fig, Figure)
|
|
156
167
|
plt.close(fig)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Undo and redo for a workflow, by snapshots.
|
|
2
|
+
|
|
3
|
+
A workflow is small (its dict form, :meth:`Workflow.to_dict`, is a few
|
|
4
|
+
kilobytes), so every change keeps the whole state before it: adding,
|
|
5
|
+
removing, wiring, moving or grouping boxes, a setting, a rename. Undo puts
|
|
6
|
+
a snapshot back; the page then runs the boxes again, and the cache serves
|
|
7
|
+
the ones that did not change. Snapshots are taken where the page already
|
|
8
|
+
learns of a change, so no edit is instrumented by hand, and a box author
|
|
9
|
+
never sees any of this.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
#: How many steps back are kept.
|
|
17
|
+
LIMIT = 100
|
|
18
|
+
#: A drag arrives as several moves; those within this many seconds are one step.
|
|
19
|
+
MOVE_SECONDS = 1.5
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def describe(before: dict, after: dict) -> str:
|
|
23
|
+
"""What changed from ``before`` to ``after``, for "Undo adding a box"."""
|
|
24
|
+
b = {n["id"]: n for n in before.get("boxes", [])}
|
|
25
|
+
a = {n["id"]: n for n in after.get("boxes", [])}
|
|
26
|
+
added, removed = set(a) - set(b), set(b) - set(a)
|
|
27
|
+
if added:
|
|
28
|
+
return "adding a box" if len(added) == 1 else f"adding {len(added)} boxes"
|
|
29
|
+
if removed:
|
|
30
|
+
return "removing a box" if len(removed) == 1 else f"removing {len(removed)} boxes"
|
|
31
|
+
if before.get("connections") != after.get("connections"):
|
|
32
|
+
return "a connection"
|
|
33
|
+
if before.get("groups") != after.get("groups"):
|
|
34
|
+
return "grouping"
|
|
35
|
+
for node_id in a:
|
|
36
|
+
if a[node_id].get("settings") != b[node_id].get("settings"):
|
|
37
|
+
return "a setting"
|
|
38
|
+
if a[node_id].get("title", "") != b[node_id].get("title", ""):
|
|
39
|
+
return "a rename"
|
|
40
|
+
if any(a[i].get("position") != b[i].get("position") for i in a):
|
|
41
|
+
return "a move"
|
|
42
|
+
if any(a[i].get("layout") != b[i].get("layout") for i in a):
|
|
43
|
+
return "tidying a drawing"
|
|
44
|
+
if before.get("name") != after.get("name"):
|
|
45
|
+
return "a rename"
|
|
46
|
+
return "a change"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class History:
|
|
50
|
+
"""The states a workflow went through; :attr:`state` is the current one."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, snapshot: dict, limit: int = LIMIT) -> None:
|
|
53
|
+
self.state = snapshot
|
|
54
|
+
self.limit = limit
|
|
55
|
+
self.undo_stack: list[dict] = []
|
|
56
|
+
self.redo_stack: list[dict] = []
|
|
57
|
+
self._last: tuple[str | None, float] = (None, 0.0)
|
|
58
|
+
|
|
59
|
+
def record(self, snapshot: dict, kind: str = "change", now: float | None = None) -> bool:
|
|
60
|
+
"""The workflow is now ``snapshot``; the state before it becomes undoable.
|
|
61
|
+
Returns whether anything changed. ``kind`` "move" folds a drag's
|
|
62
|
+
steps into one."""
|
|
63
|
+
if snapshot == self.state:
|
|
64
|
+
return False
|
|
65
|
+
now = time.monotonic() if now is None else now
|
|
66
|
+
last_kind, last_time = self._last
|
|
67
|
+
if not (kind == "move" and last_kind == "move" and now - last_time < MOVE_SECONDS and self.undo_stack):
|
|
68
|
+
self.undo_stack.append(self.state)
|
|
69
|
+
del self.undo_stack[:-self.limit]
|
|
70
|
+
self.redo_stack.clear()
|
|
71
|
+
self.state = snapshot
|
|
72
|
+
self._last = (kind, now)
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def can_undo(self) -> bool:
|
|
77
|
+
return bool(self.undo_stack)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def can_redo(self) -> bool:
|
|
81
|
+
return bool(self.redo_stack)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def undo_label(self) -> str:
|
|
85
|
+
return describe(self.undo_stack[-1], self.state) if self.undo_stack else ""
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def redo_label(self) -> str:
|
|
89
|
+
return describe(self.state, self.redo_stack[-1]) if self.redo_stack else ""
|
|
90
|
+
|
|
91
|
+
def undo(self) -> tuple[str, dict] | None:
|
|
92
|
+
"""Step back: (what is undone, the state to restore), or None."""
|
|
93
|
+
if not self.undo_stack:
|
|
94
|
+
return None
|
|
95
|
+
before = self.undo_stack.pop()
|
|
96
|
+
label = describe(before, self.state)
|
|
97
|
+
self.redo_stack.append(self.state)
|
|
98
|
+
self.state = before
|
|
99
|
+
self._last = (None, 0.0)
|
|
100
|
+
return label, before
|
|
101
|
+
|
|
102
|
+
def redo(self) -> tuple[str, dict] | None:
|
|
103
|
+
if not self.redo_stack:
|
|
104
|
+
return None
|
|
105
|
+
after = self.redo_stack.pop()
|
|
106
|
+
label = describe(self.state, after)
|
|
107
|
+
self.undo_stack.append(self.state)
|
|
108
|
+
self.state = after
|
|
109
|
+
self._last = (None, 0.0)
|
|
110
|
+
return label, after
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
__all__ = ["History", "describe", "LIMIT", "MOVE_SECONDS"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Where a result came from, carried with it and written into the files it becomes.
|
|
2
|
+
|
|
3
|
+
The runner stamps every value a box gives with a :class:`Provenance`: the
|
|
4
|
+
analysis, the box and its settings, and the result's key (the fingerprint of
|
|
5
|
+
the box's code, its settings and its inputs, which is also the cache key).
|
|
6
|
+
A figure saved as SVG carries it in a ``<metadata>`` block; a table saved as
|
|
7
|
+
CSV, or a figure as PNG, gets a ``.provenance.json`` beside it. A figure in
|
|
8
|
+
a paper then points back at the analysis that made it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import datetime as _dt
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import asdict, dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from xml.sax.saxutils import escape
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Provenance:
|
|
23
|
+
workflow: str
|
|
24
|
+
box: str
|
|
25
|
+
title: str
|
|
26
|
+
settings: dict
|
|
27
|
+
key: str
|
|
28
|
+
openprocess: str
|
|
29
|
+
when: str
|
|
30
|
+
|
|
31
|
+
def as_dict(self) -> dict:
|
|
32
|
+
return asdict(self)
|
|
33
|
+
|
|
34
|
+
def json(self) -> str:
|
|
35
|
+
return json.dumps(self.as_dict(), indent=2, ensure_ascii=False, default=str)
|
|
36
|
+
|
|
37
|
+
def line(self) -> str:
|
|
38
|
+
return (f"{self.title} ({self.box}) in “{self.workflow}”, OpenProcess {self.openprocess}, "
|
|
39
|
+
f"{self.when}; key {self.key[:12]}")
|
|
40
|
+
|
|
41
|
+
def svg_metadata(self) -> str:
|
|
42
|
+
"""A Dublin Core block for an SVG's ``<metadata>``."""
|
|
43
|
+
return (
|
|
44
|
+
'<metadata id="openprocess-provenance">'
|
|
45
|
+
'<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" '
|
|
46
|
+
'xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description>'
|
|
47
|
+
f"<dc:creator>OpenProcess {escape(self.openprocess)}</dc:creator>"
|
|
48
|
+
f"<dc:source>{escape(self.line())}</dc:source>"
|
|
49
|
+
f"<dc:date>{escape(self.when)}</dc:date>"
|
|
50
|
+
f"<dc:description>{escape(self.json())}</dc:description>"
|
|
51
|
+
"</rdf:Description></rdf:RDF></metadata>"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def make(workflow, node, spec, settings: dict, key: str) -> Provenance:
|
|
56
|
+
from .. import __version__
|
|
57
|
+
plain = {}
|
|
58
|
+
for name, value in settings.items():
|
|
59
|
+
if hasattr(value, "describe"): # a sweep
|
|
60
|
+
value = value.describe()
|
|
61
|
+
plain[name] = str(value) if isinstance(value, Path) else value
|
|
62
|
+
return Provenance(workflow.name, spec.id, workflow.title(node), plain, key, __version__,
|
|
63
|
+
_dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def stamp(value, provenance: Provenance) -> bool:
|
|
67
|
+
"""Attach ``provenance`` to ``value`` when it can carry one (a dataclass
|
|
68
|
+
can; a number or a string cannot). Returns whether it did."""
|
|
69
|
+
if value is None or isinstance(value, (bool, int, float, str, bytes, list, tuple, dict, set, frozenset)):
|
|
70
|
+
return False
|
|
71
|
+
try:
|
|
72
|
+
object.__setattr__(value, "provenance", provenance)
|
|
73
|
+
except (AttributeError, TypeError):
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def of(value) -> Provenance | None:
|
|
79
|
+
found = getattr(value, "provenance", None)
|
|
80
|
+
return found if isinstance(found, Provenance) else None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
_SVG_OPEN = re.compile(r"<svg\b[^>]*>", re.S)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def with_metadata(svg: str, provenance: Provenance | None) -> str:
|
|
87
|
+
"""The SVG text with the provenance in a ``<metadata>`` block after the opening tag."""
|
|
88
|
+
if provenance is None:
|
|
89
|
+
return svg
|
|
90
|
+
match = _SVG_OPEN.search(svg)
|
|
91
|
+
if match is None:
|
|
92
|
+
return svg
|
|
93
|
+
return svg[:match.end()] + "\n" + provenance.svg_metadata() + svg[match.end():]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def sidecar_path(path) -> Path:
|
|
97
|
+
"""``table.csv`` → ``table.provenance.json`` beside it."""
|
|
98
|
+
path = Path(path)
|
|
99
|
+
return path.with_name(path.stem + ".provenance.json")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_sidecar(path, provenance: Provenance | None) -> Path | None:
|
|
103
|
+
if provenance is None:
|
|
104
|
+
return None
|
|
105
|
+
target = sidecar_path(path)
|
|
106
|
+
target.write_text(provenance.json() + "\n", encoding="utf-8")
|
|
107
|
+
return target
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
__all__ = ["Provenance", "make", "of", "sidecar_path", "stamp", "with_metadata", "write_sidecar"]
|
|
@@ -310,7 +310,9 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
|
|
|
310
310
|
lines = [f"# {name}", "", f"Exported {record.saved} with OpenProcess {record.versions.get('openprocess', '')}, "
|
|
311
311
|
f"Python {record.versions.get('python', '')}.", "",
|
|
312
312
|
"Run it again with `openprocess run " + f"{name}.cpnflow --check`: it fails if any result differs "
|
|
313
|
-
"from the recorded one. `requirements.lock` is the environment it ran in.
|
|
313
|
+
"from the recorded one. `requirements.lock` is the environment it ran in. `run.py` is the same "
|
|
314
|
+
"analysis as a plain Python script (`python run.py`). Or drop this zip on OpenProcess: it unpacks "
|
|
315
|
+
"it into a folder and runs it.", "", "## Boxes", ""]
|
|
314
316
|
for node in workflow.order():
|
|
315
317
|
spec = workflow.spec(node)
|
|
316
318
|
settings = ", ".join(f"{k} = {v}" for k, v in node.settings.items()) or "no settings"
|
|
@@ -335,6 +337,8 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
|
|
|
335
337
|
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipped:
|
|
336
338
|
zipped.writestr(f"{name}.cpnflow", json.dumps(data, indent=2, ensure_ascii=False, default=str))
|
|
337
339
|
zipped.writestr("requirements.lock", requirements_lock(record))
|
|
340
|
+
from .script import script_text
|
|
341
|
+
zipped.writestr("run.py", script_text(workflow, "run.py"))
|
|
338
342
|
zipped.writestr("CITATIONS.bib", "\n\n".join(e.bibtex() for e in entries) + "\n")
|
|
339
343
|
zipped.writestr("README.md", "\n".join(lines) + "\n")
|
|
340
344
|
for item in record.inputs:
|
|
@@ -368,6 +372,62 @@ def _describe(value) -> str:
|
|
|
368
372
|
return type(value).__name__ if value is not None else "done"
|
|
369
373
|
|
|
370
374
|
|
|
375
|
+
def is_experiment(path) -> bool:
|
|
376
|
+
"""Whether ``path`` is a zip :func:`export_experiment` wrote: one ``.cpnflow`` at the top."""
|
|
377
|
+
import zipfile
|
|
378
|
+
try:
|
|
379
|
+
with zipfile.ZipFile(path) as zipped:
|
|
380
|
+
return sum(1 for n in zipped.namelist() if n.endswith(".cpnflow") and "/" not in n) == 1
|
|
381
|
+
except (OSError, zipfile.BadZipFile):
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def import_experiment(archive, into) -> Path:
|
|
386
|
+
"""Unpack an exported experiment into a new folder under ``into`` (named
|
|
387
|
+
after the zip) and give back its workflow file, ready to open: the file
|
|
388
|
+
settings point at ``inputs/``, the folder's ``boxes/`` holds the custom
|
|
389
|
+
boxes (a box is found by its function name there), and the recorded
|
|
390
|
+
results are kept for the reproducibility check."""
|
|
391
|
+
import zipfile
|
|
392
|
+
archive = Path(archive)
|
|
393
|
+
with zipfile.ZipFile(archive) as zipped:
|
|
394
|
+
names = zipped.namelist()
|
|
395
|
+
flows = [n for n in names if n.endswith(".cpnflow") and "/" not in n]
|
|
396
|
+
if len(flows) != 1:
|
|
397
|
+
raise ValueError(f"{archive.name} is not an exported experiment (one .cpnflow at the top is needed)")
|
|
398
|
+
folder = Path(into) / archive.stem
|
|
399
|
+
counter = 2
|
|
400
|
+
while folder.exists():
|
|
401
|
+
folder = Path(into) / f"{archive.stem} {counter}"
|
|
402
|
+
counter += 1
|
|
403
|
+
folder.mkdir(parents=True)
|
|
404
|
+
for name in names:
|
|
405
|
+
parts = Path(name).parts
|
|
406
|
+
if name.endswith("/") or not parts or ".." in parts or Path(name).is_absolute():
|
|
407
|
+
continue
|
|
408
|
+
target = folder / name
|
|
409
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
410
|
+
target.write_bytes(zipped.read(name))
|
|
411
|
+
flow_file = folder / flows[0]
|
|
412
|
+
data = json.loads(flow_file.read_text(encoding="utf-8"))
|
|
413
|
+
moved: dict[tuple[str, str], str] = {}
|
|
414
|
+
for item in data.get("record", {}).get("inputs", []):
|
|
415
|
+
new = f"inputs/{Path(item['file']).name}"
|
|
416
|
+
if (folder / new).is_file():
|
|
417
|
+
moved[(item["node"], item["setting"])] = new
|
|
418
|
+
item["file"] = new
|
|
419
|
+
for item in data.get("record", {}).get("custom_boxes", []):
|
|
420
|
+
new = f"boxes/{Path(item['file']).name}"
|
|
421
|
+
if (folder / new).is_file():
|
|
422
|
+
item["file"] = new
|
|
423
|
+
for box in data.get("boxes", []):
|
|
424
|
+
for setting in list(box.get("settings", {})):
|
|
425
|
+
if (box["id"], setting) in moved:
|
|
426
|
+
box["settings"][setting] = moved[(box["id"], setting)]
|
|
427
|
+
flow_file.write_text(json.dumps(data, indent=2, ensure_ascii=False, default=str), encoding="utf-8")
|
|
428
|
+
return flow_file
|
|
429
|
+
|
|
430
|
+
|
|
371
431
|
def _as_file(value, stem: str):
|
|
372
432
|
"""(name, bytes or text) for a result worth a file of its own."""
|
|
373
433
|
if isinstance(value, Table):
|
|
@@ -396,5 +456,9 @@ def _as_file(value, stem: str):
|
|
|
396
456
|
return None
|
|
397
457
|
|
|
398
458
|
|
|
399
|
-
|
|
400
|
-
|
|
459
|
+
#: (name, bytes or text) for a result worth a file of its own; the script an experiment carries uses it.
|
|
460
|
+
as_file = _as_file
|
|
461
|
+
|
|
462
|
+
__all__ = ["FORMAT", "Record", "as_file", "check", "differences", "environment", "export_experiment",
|
|
463
|
+
"fingerprint", "import_experiment", "is_experiment", "load", "make_record", "requirements_lock",
|
|
464
|
+
"save", "sha256_file", "sha256_text", "versions"]
|
|
@@ -317,6 +317,7 @@ class Runner:
|
|
|
317
317
|
result.values, result.explanation = cached
|
|
318
318
|
result.value = result.values.get("out", next(iter(result.values.values()), None))
|
|
319
319
|
result.status, result.cached = DONE, True
|
|
320
|
+
self._stamp(result, workflow, node, spec, settings)
|
|
320
321
|
return result
|
|
321
322
|
# Run it
|
|
322
323
|
result.status = RUNNING
|
|
@@ -341,8 +342,17 @@ class Runner:
|
|
|
341
342
|
result.values, result.value, result.explanation = values, values.get("out", value), explanation
|
|
342
343
|
result.status = DONE
|
|
343
344
|
self.cache.put(result.key, values, explanation)
|
|
345
|
+
self._stamp(result, workflow, node, spec, settings)
|
|
344
346
|
return result
|
|
345
347
|
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _stamp(result: Result, workflow: Workflow, node: Node, spec: BoxSpec, settings: dict) -> None:
|
|
350
|
+
"""Every value a box gives carries where it came from (:mod:`openprocess.flow.provenance`)."""
|
|
351
|
+
from .provenance import make, stamp
|
|
352
|
+
info = make(workflow, node, spec, settings, result.key)
|
|
353
|
+
for out in result.values.values():
|
|
354
|
+
stamp(out, info)
|
|
355
|
+
|
|
346
356
|
@staticmethod
|
|
347
357
|
def _outputs(spec: BoxSpec, value) -> dict[str, Any]:
|
|
348
358
|
if spec.result_type is not None:
|