openprocess 0.10.5__tar.gz → 0.10.6__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.5/openprocess.egg-info → openprocess-0.10.6}/PKG-INFO +16 -2
- {openprocess-0.10.5 → openprocess-0.10.6}/README.md +15 -1
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/__init__.py +1 -1
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/citation.py +37 -10
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/__init__.py +2 -1
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/box.py +6 -3
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/science.py +2 -2
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/reproducibility.py +9 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/runner.py +28 -1
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/types.py +12 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/page.py +5 -3
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/viewers.py +40 -1
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/learn/answer_boxes.py +3 -5
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/app.py +42 -5
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/connections.py +23 -12
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/style.py +3 -9
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/widgets.py +102 -10
- openprocess-0.10.6/openprocess/packages.py +139 -0
- {openprocess-0.10.5 → openprocess-0.10.6/openprocess.egg-info}/PKG-INFO +16 -2
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess.egg-info/SOURCES.txt +2 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_citation.py +27 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_flow.py +51 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_flow_gui.py +91 -3
- openprocess-0.10.6/tests/test_packages.py +69 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_studio_gui.py +19 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/LICENSE +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/cpnpy/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/analysis/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/analysis/state_space.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/analysis/state_space_process.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/cli.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/exercises/pack.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/check.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/compare.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/cpn.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/discover.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/filter.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/input.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/output.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/predict.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/boxes/sweeps.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/convert.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/datasets.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/explain.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/figures.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/library.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/record.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/report.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/snapshots.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/sweep.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/flow/workflow.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/app.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/arc_editing.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/canvas.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/canvas.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/picker.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/runs.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/stepper.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/summary.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/flow/templates.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/items.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/learn/concealment.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/learn/mode.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/panning.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/resources/openprocess-icon.png +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/__main__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/charts.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/cite.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/command_palette.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/compare_page.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/cpn_page.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/definition_view.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/derivation_view.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/documents.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/dotted_chart.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/file_dialogs.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/filter_dialog.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/graph_builders.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/graph_view.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/instances.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/log_editor.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/log_page.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/markdown_view.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/mathtext.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/ml_highlighter.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/model_page.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/motion.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/net_comparison.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/notes_overlay.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/petri_page.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/provenance.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/regions_view.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/sidebar.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/spaces.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/tool_icons.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/updates.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/workers.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/studio/workspace.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/theme.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/gui/tidy.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/io/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/io/cpn_reader.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/io/cpn_writer.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/answers.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/checks.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/computed.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/context.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/exam.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/exercise-packs.md +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/importer.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/notation.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/pack.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/learn/sheet.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/analysis.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/columns.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/compare_nets.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/conformance/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/conformance/alignments.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/conformance/quality.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/conformance/token_replay.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/csv_import.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/definitions.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/dfg.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/discovery/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/discovery/alpha.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/discovery/heuristics.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/discovery/inductive.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/discovery/state_regions.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/filtering.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/footprint.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/invariants.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/layout.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/log.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/ocel.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/petrinet.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/playout.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/pm4py_bridge.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/pnml.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/processtree.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/regions.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/stats.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/structure.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/transition_system.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/mining/xes.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/ast_nodes.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/builtins.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/colorsets.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/errors.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/evaluator.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/lexer.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/multiset.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/parser.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/ml/values.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/model/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/model/declarations.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/model/examples.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/model/net.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/model/plain.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/references.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/sim/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/sim/binding.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/sim/export.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/sim/simulator.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/teaching/__init__.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/teaching/answers.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/teaching/checks.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/teaching/pack.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess/teaching/sheet.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess.egg-info/dependency_links.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess.egg-info/entry_points.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess.egg-info/requires.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/openprocess.egg-info/top_level.txt +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/pyproject.toml +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/setup.cfg +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_arc_editing.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_colorsets.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_columns.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_cpn_course_model.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_definitions.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_exercises_gui.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_hierarchy.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_io.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_learn.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_learn_gui.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_log_editing.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_mining.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_ml.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_multiset.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_ocel.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_regions.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_rename.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_report.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_simulation.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_snapshots.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_state_space.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_teaching.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_tidy.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_updates.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_wf_structure.py +0 -0
- {openprocess-0.10.5 → openprocess-0.10.6}/tests/test_workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openprocess
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.6
|
|
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
|
|
@@ -833,7 +833,11 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
833
833
|
draws what flows in, OpenProcess and its boxes, what flows out, and the
|
|
834
834
|
optional tools (pandas, numpy, scipy, matplotlib, …) with whether each is
|
|
835
835
|
installed, how to install it and the boxes it brings. All of it is read
|
|
836
|
-
from the box library.
|
|
836
|
+
from the box library. *Install…* works in the downloaded app too: it carries
|
|
837
|
+
pip and installs into a folder of your own (on a Mac,
|
|
838
|
+
`~/Library/Application Support/OpenProcess/packages`), and the boxes that
|
|
839
|
+
need the package come alive without a restart. PM4Py needs a Python
|
|
840
|
+
install (`pip install pm4py`), as parts of it are built from source.
|
|
837
841
|
- **Motion** is kept short: a space switch, the side panel and Canvas ↔
|
|
838
842
|
Summary settle in a fifth of a second, and a box pulses once when it
|
|
839
843
|
finishes. `OPENPROCESS_NO_MOTION=1` turns it off.
|
|
@@ -969,6 +973,16 @@ Plainly stated:
|
|
|
969
973
|
Next up: several instances of a subpage, CPN monitors, and signed apps that
|
|
970
974
|
open without a security prompt.
|
|
971
975
|
|
|
976
|
+
## Cite
|
|
977
|
+
|
|
978
|
+
If OpenProcess helped with a course, a thesis or a paper, cite it. *Help ▸
|
|
979
|
+
Cite OpenProcess…* in the app gives BibTeX and a one-line citation, and
|
|
980
|
+
each algorithm's Code tab has *Cite* for the paper it follows. GitHub's
|
|
981
|
+
*Cite this repository* reads [CITATION.cff](CITATION.cff). Releases are
|
|
982
|
+
archived on Zenodo, which gives each version a DOI; the all-versions DOI
|
|
983
|
+
goes into `CITATION.cff` (`doi:`) once the first release is archived, and
|
|
984
|
+
the app's citations pick it up from there.
|
|
985
|
+
|
|
972
986
|
## Licence
|
|
973
987
|
|
|
974
988
|
MIT. See [LICENSE](LICENSE).
|
|
@@ -794,7 +794,11 @@ extra) and *Predict the next activity*. Or start empty.
|
|
|
794
794
|
draws what flows in, OpenProcess and its boxes, what flows out, and the
|
|
795
795
|
optional tools (pandas, numpy, scipy, matplotlib, …) with whether each is
|
|
796
796
|
installed, how to install it and the boxes it brings. All of it is read
|
|
797
|
-
from the box library.
|
|
797
|
+
from the box library. *Install…* works in the downloaded app too: it carries
|
|
798
|
+
pip and installs into a folder of your own (on a Mac,
|
|
799
|
+
`~/Library/Application Support/OpenProcess/packages`), and the boxes that
|
|
800
|
+
need the package come alive without a restart. PM4Py needs a Python
|
|
801
|
+
install (`pip install pm4py`), as parts of it are built from source.
|
|
798
802
|
- **Motion** is kept short: a space switch, the side panel and Canvas ↔
|
|
799
803
|
Summary settle in a fifth of a second, and a box pulses once when it
|
|
800
804
|
finishes. `OPENPROCESS_NO_MOTION=1` turns it off.
|
|
@@ -930,6 +934,16 @@ Plainly stated:
|
|
|
930
934
|
Next up: several instances of a subpage, CPN monitors, and signed apps that
|
|
931
935
|
open without a security prompt.
|
|
932
936
|
|
|
937
|
+
## Cite
|
|
938
|
+
|
|
939
|
+
If OpenProcess helped with a course, a thesis or a paper, cite it. *Help ▸
|
|
940
|
+
Cite OpenProcess…* in the app gives BibTeX and a one-line citation, and
|
|
941
|
+
each algorithm's Code tab has *Cite* for the paper it follows. GitHub's
|
|
942
|
+
*Cite this repository* reads [CITATION.cff](CITATION.cff). Releases are
|
|
943
|
+
archived on Zenodo, which gives each version a DOI; the all-versions DOI
|
|
944
|
+
goes into `CITATION.cff` (`doi:`) once the first release is archived, and
|
|
945
|
+
the app's citations pick it up from there.
|
|
946
|
+
|
|
933
947
|
## Licence
|
|
934
948
|
|
|
935
949
|
MIT. See [LICENSE](LICENSE).
|
|
@@ -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.10.
|
|
51
|
+
__version__ = "0.10.6"
|
|
52
52
|
|
|
53
53
|
__all__ = [
|
|
54
54
|
"Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
|
|
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import datetime as _dt
|
|
15
15
|
import re
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
17
18
|
|
|
18
19
|
from . import references as _references
|
|
19
20
|
|
|
@@ -27,6 +28,23 @@ def app_version() -> str:
|
|
|
27
28
|
return __version__
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
def concept_doi() -> str:
|
|
32
|
+
"""The DOI Zenodo gives the software (all versions), from ``CITATION.cff``
|
|
33
|
+
at the top of the repository or installed beside the package; "" until
|
|
34
|
+
the first release is archived."""
|
|
35
|
+
import re
|
|
36
|
+
for candidate in (Path(__file__).resolve().parents[1] / "CITATION.cff",
|
|
37
|
+
Path(__file__).resolve().parent / "CITATION.cff"):
|
|
38
|
+
try:
|
|
39
|
+
text = candidate.read_text(encoding="utf-8")
|
|
40
|
+
except OSError:
|
|
41
|
+
continue
|
|
42
|
+
found = re.search(r"^doi:\s*(\S+)\s*$", text, re.M)
|
|
43
|
+
if found:
|
|
44
|
+
return found.group(1).strip("'\"")
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
|
|
30
48
|
# ---------------------------------------------------------------------------
|
|
31
49
|
# One entry
|
|
32
50
|
# ---------------------------------------------------------------------------
|
|
@@ -137,45 +155,54 @@ def entries_for_module(module: str) -> list[Entry]:
|
|
|
137
155
|
# ---------------------------------------------------------------------------
|
|
138
156
|
# The app itself
|
|
139
157
|
# ---------------------------------------------------------------------------
|
|
140
|
-
def app_entry(version: str | None = None, year: int | None = None) -> Entry:
|
|
158
|
+
def app_entry(version: str | None = None, year: int | None = None, doi: str | None = None) -> Entry:
|
|
141
159
|
version = version or app_version()
|
|
142
160
|
year = year or _dt.date.today().year
|
|
161
|
+
doi = concept_doi() if doi is None else doi
|
|
143
162
|
return Entry("software", "openprocess", {
|
|
144
163
|
"author": " and ".join(f"{last}, {first}" for last, first in APP_AUTHORS),
|
|
145
164
|
"title": APP_TITLE,
|
|
146
165
|
"version": version,
|
|
147
166
|
"year": str(year),
|
|
167
|
+
"doi": doi,
|
|
148
168
|
"url": APP_URL,
|
|
149
169
|
"note": "Every algorithm shows its code and the paper it follows; every analysis keeps a record "
|
|
150
170
|
"that runs again",
|
|
151
171
|
})
|
|
152
172
|
|
|
153
173
|
|
|
154
|
-
def app_bibtex(version: str | None = None, year: int | None = None) -> str:
|
|
155
|
-
return app_entry(version, year).bibtex()
|
|
174
|
+
def app_bibtex(version: str | None = None, year: int | None = None, doi: str | None = None) -> str:
|
|
175
|
+
return app_entry(version, year, doi).bibtex()
|
|
156
176
|
|
|
157
177
|
|
|
158
|
-
def app_text(version: str | None = None, year: int | None = None) -> str:
|
|
178
|
+
def app_text(version: str | None = None, year: int | None = None, doi: str | None = None) -> str:
|
|
159
179
|
"""The citation in one line, as a reference list would have it."""
|
|
160
180
|
version = version or app_version()
|
|
161
181
|
year = year or _dt.date.today().year
|
|
182
|
+
doi = concept_doi() if doi is None else doi
|
|
162
183
|
authors = ", ".join(f"{first} {last}" for last, first in APP_AUTHORS)
|
|
163
|
-
|
|
184
|
+
where = f"https://doi.org/{doi}" if doi else APP_URL
|
|
185
|
+
return f"{authors}. {APP_TITLE} (version {version}) [software], {year}. {where}"
|
|
164
186
|
|
|
165
187
|
|
|
166
|
-
def cff_text(version: str | None = None, released: _dt.date | None = None) -> str:
|
|
167
|
-
"""The contents of ``CITATION.cff`` (Citation File Format 1.2.0) for
|
|
188
|
+
def cff_text(version: str | None = None, released: _dt.date | None = None, doi: str = "") -> str:
|
|
189
|
+
"""The contents of ``CITATION.cff`` (Citation File Format 1.2.0) for
|
|
190
|
+
``version``; ``doi`` is Zenodo's concept DOI once the first release is archived."""
|
|
168
191
|
version = version or app_version()
|
|
169
192
|
released = released or _dt.date.today()
|
|
170
193
|
authors = "\n".join(f" - family-names: {last}\n given-names: {first}" for last, first in APP_AUTHORS)
|
|
194
|
+
def quoted(text: str) -> str: # a YAML string: a colon in the title would otherwise break it
|
|
195
|
+
return '"' + text.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
|
196
|
+
|
|
171
197
|
return (
|
|
172
198
|
"cff-version: 1.2.0\n"
|
|
173
|
-
"message: If you use OpenProcess in teaching or research, please cite it as below
|
|
199
|
+
f"message: {quoted('If you use OpenProcess in teaching or research, please cite it as below.')}\n"
|
|
174
200
|
"type: software\n"
|
|
175
|
-
f"title: {APP_TITLE}\n"
|
|
201
|
+
f"title: {quoted(APP_TITLE)}\n"
|
|
176
202
|
f"version: {version}\n"
|
|
177
203
|
f"date-released: {released.isoformat()}\n"
|
|
178
|
-
f"
|
|
204
|
+
+ (f"doi: {doi}\n" if doi else "")
|
|
205
|
+
+ f"repository-code: {APP_URL}\n"
|
|
179
206
|
f"url: {APP_URL}\n"
|
|
180
207
|
"license: MIT\n"
|
|
181
208
|
"keywords:\n - process mining\n - Petri nets\n - workflow nets\n - conformance checking\n"
|
|
@@ -35,12 +35,13 @@ from .library import Library, library_for, standard_library
|
|
|
35
35
|
from .record import check, differences, load, save
|
|
36
36
|
from .runner import Cache, Result, Run, Runner
|
|
37
37
|
from .sweep import Sweep
|
|
38
|
-
from .types import (AlignmentResult, Any, CPNet, DFG, Dataset, EventLog, Figure, Footprint, Marking,
|
|
38
|
+
from .types import (Column, AlignmentResult, Any, CPNet, DFG, Dataset, EventLog, Figure, Footprint, Marking,
|
|
39
39
|
PetriNet, Predictions, Predictor, ProcessTree, Regions, ReplayResult, Scores,
|
|
40
40
|
SimpleLog, Table, Text, TransitionSystem, register_type)
|
|
41
41
|
from .workflow import Edge, Group, Node, Workflow, WorkflowError, to_python, workflow
|
|
42
42
|
|
|
43
43
|
__all__ = [
|
|
44
|
+
"Column",
|
|
44
45
|
"AlignmentResult", "Any", "Box", "BoxError", "BoxSpec", "CPNet", "Cache", "DFG", "Dataset", "Edge",
|
|
45
46
|
"EventLog", "Explanation", "Figure", "Footprint", "Group", "Library", "Marking", "Node", "PetriNet",
|
|
46
47
|
"Port", "Predictions", "Predictor", "ProcessTree", "Regions", "ReplayResult", "Result", "Run",
|
|
@@ -18,6 +18,7 @@ parameters whose type is a OpenProcess type input connection points,
|
|
|
18
18
|
the return type the output; a dataclass of typed fields gives several
|
|
19
19
|
``int``, ``float``, ``bool``, ``str`` with a default a setting, with the default as its value
|
|
20
20
|
``Literal["a", "b"]`` a setting that is a choice
|
|
21
|
+
``Column`` a setting that is a column of a connected table
|
|
21
22
|
``Path`` a setting that is a file in the folder
|
|
22
23
|
the docstring the help text
|
|
23
24
|
``inspect.getsource`` the Code tab
|
|
@@ -44,7 +45,7 @@ from dataclasses import dataclass, field
|
|
|
44
45
|
from pathlib import Path
|
|
45
46
|
from typing import Any, Callable, Literal, Union, get_args, get_origin, get_type_hints
|
|
46
47
|
|
|
47
|
-
from .types import TypeInfo, is_known, type_info
|
|
48
|
+
from .types import Column, TypeInfo, is_known, type_info
|
|
48
49
|
|
|
49
50
|
SETTING_TYPES = (int, float, bool, str)
|
|
50
51
|
|
|
@@ -106,7 +107,7 @@ class Setting:
|
|
|
106
107
|
"""A parameter that is not an input: shown as a control in the side panel."""
|
|
107
108
|
|
|
108
109
|
name: str
|
|
109
|
-
kind: str # "int", "float", "bool", "str", "choice", "path"
|
|
110
|
+
kind: str # "int", "float", "bool", "str", "choice", "path", "column"
|
|
110
111
|
default: Any
|
|
111
112
|
choices: tuple = ()
|
|
112
113
|
help: str = ""
|
|
@@ -273,12 +274,14 @@ def _setting_from(name: str, annotation, default, has_default: bool, doc_hints:
|
|
|
273
274
|
kind = "int"
|
|
274
275
|
elif annotation is float:
|
|
275
276
|
kind = "float"
|
|
277
|
+
elif annotation is Column:
|
|
278
|
+
kind = "column"
|
|
276
279
|
elif annotation is str:
|
|
277
280
|
kind = "str"
|
|
278
281
|
else:
|
|
279
282
|
raise BoxError(
|
|
280
283
|
f"parameter {name!r} has type {_type_text(annotation)}, which is neither a OpenProcess "
|
|
281
|
-
f"type (an input) nor int, float, bool, str, Literal[...] or Path (a setting)")
|
|
284
|
+
f"type (an input) nor int, float, bool, str, Literal[...], Column or Path (a setting)")
|
|
282
285
|
if not has_default:
|
|
283
286
|
raise BoxError(f"setting {name!r} needs a default value (e.g. {name}: {kind} = ...)")
|
|
284
287
|
if kind == "float" and isinstance(default, int) and not isinstance(default, bool):
|
|
@@ -17,7 +17,7 @@ from ...mining.log import EventLog
|
|
|
17
17
|
from ...mining.petrinet import PetriNet
|
|
18
18
|
from ..box import box
|
|
19
19
|
from ..convert import convert
|
|
20
|
-
from ..types import Figure, Scores, Table
|
|
20
|
+
from ..types import Column, Figure, Scores, Table
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@box(name="Describe log", group="Science", needs="pandas")
|
|
@@ -109,7 +109,7 @@ def correlate(table: Table) -> Table:
|
|
|
109
109
|
|
|
110
110
|
@box(name="Plot", group="Science", needs="matplotlib")
|
|
111
111
|
def plot(scores: list[Scores] = (), tables: list[Table] = (),
|
|
112
|
-
x:
|
|
112
|
+
x: Column = "", y: Column = "") -> Figure:
|
|
113
113
|
"""Bars per model for scores, histograms for bootstrap tables, or a line
|
|
114
114
|
of column *y* against column *x* of a table (a sweep). The figure is a
|
|
115
115
|
matplotlib figure, converted to SVG; its code is this box's code.
|
|
@@ -34,6 +34,15 @@ class Status:
|
|
|
34
34
|
def tone(self) -> str:
|
|
35
35
|
return {"reproduces": "good", "changed": "warning", "differs": "critical"}.get(self.state, "muted")
|
|
36
36
|
|
|
37
|
+
@property
|
|
38
|
+
def short(self) -> str:
|
|
39
|
+
"""Two or three words for a badge; :attr:`headline` is the sentence."""
|
|
40
|
+
if self.state == "changed":
|
|
41
|
+
count = len(self.details)
|
|
42
|
+
return f"{count} change{'s' if count != 1 else ''} since"
|
|
43
|
+
return {"unrecorded": "Not recorded", "reproduces": "Reproducible", "differs": "Differs from the record",
|
|
44
|
+
"unchecked": "Recorded, not run"}.get(self.state, self.headline)
|
|
45
|
+
|
|
37
46
|
|
|
38
47
|
def status(workflow: Workflow, record: Record | None, run: Run | None, folder=None) -> Status:
|
|
39
48
|
"""The analysis's reproducibility, from its record and its latest run."""
|
|
@@ -41,6 +41,25 @@ WAITING, RUNNING, DONE, FAILED, IDLE, BLOCKED, SKIPPED = (
|
|
|
41
41
|
"waiting", "running", "done", "failed", "idle", "blocked", "skipped")
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def columns_connected(workflow: Workflow, run: "Run | None", node: Node) -> list[str]:
|
|
45
|
+
"""The columns of the tables connected to ``node``, as of ``run``: what a
|
|
46
|
+
``Column`` setting lists. Empty until the boxes before it have run."""
|
|
47
|
+
if run is None:
|
|
48
|
+
return []
|
|
49
|
+
from .types import Table
|
|
50
|
+
out: list[str] = []
|
|
51
|
+
for sources in workflow.inputs_of(node).values():
|
|
52
|
+
for source_id, output in sources:
|
|
53
|
+
result = run.result(source_id)
|
|
54
|
+
if result is None or result.status != DONE:
|
|
55
|
+
continue
|
|
56
|
+
value = result.values.get(output, result.value)
|
|
57
|
+
for item in (value if isinstance(value, (list, tuple)) else [value]):
|
|
58
|
+
if isinstance(item, Table):
|
|
59
|
+
out.extend(column for column in item.columns if column not in out)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
44
63
|
@dataclass
|
|
45
64
|
class Result:
|
|
46
65
|
"""What one box produced in one run (one per sweep variant)."""
|
|
@@ -270,6 +289,14 @@ class Runner:
|
|
|
270
289
|
values.append(self._fit(value, port))
|
|
271
290
|
keys.append(upstream.key + ":" + output)
|
|
272
291
|
inputs[port.name] = values if port.many else values[0]
|
|
292
|
+
# Nothing plugged in at all (every input optional, as Plot's are): the box
|
|
293
|
+
# would run on nothing and fail. Wait instead, and say what to connect.
|
|
294
|
+
if spec.inputs and not any(fed[port.name] for port in spec.inputs):
|
|
295
|
+
labels = [port.label or port.name for port in spec.inputs]
|
|
296
|
+
result.status = IDLE
|
|
297
|
+
result.message = "Connect " + (labels[0] if len(labels) == 1 else
|
|
298
|
+
", ".join(labels[:-1]) + " or " + labels[-1])
|
|
299
|
+
return result
|
|
273
300
|
# A file not chosen yet (a box just added from the list): wait and say
|
|
274
301
|
# so, rather than run and fail with a traceback about None.
|
|
275
302
|
for setting in spec.settings:
|
|
@@ -376,5 +403,5 @@ def run_in_process(spec: BoxSpec, inputs: dict, settings: dict, timeout: float |
|
|
|
376
403
|
return outcome[1], outcome[2]
|
|
377
404
|
|
|
378
405
|
|
|
379
|
-
__all__ = ["BLOCKED", "Cache", "DONE", "FAILED", "IDLE", "RUNNING", "Result", "Run", "Runner",
|
|
406
|
+
__all__ = ["columns_connected", "BLOCKED", "Cache", "DONE", "FAILED", "IDLE", "RUNNING", "Result", "Run", "Runner",
|
|
380
407
|
"WAITING", "run_in_process", "settings_key"]
|
|
@@ -112,6 +112,17 @@ class Table:
|
|
|
112
112
|
writer.writerows(self.rows)
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
|
|
116
|
+
class Column(str):
|
|
117
|
+
"""The name of a column of a table connected to the box.
|
|
118
|
+
|
|
119
|
+
A setting typed ``Column`` (``x: Column = ""``) is a plain string to the
|
|
120
|
+
function. In the app it is a list of the connected table's columns to
|
|
121
|
+
pick from, once the box before it has run; a name can still be typed.
|
|
122
|
+
It is a setting, not an input: the table is the input.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
|
|
115
126
|
@dataclass
|
|
116
127
|
class Figure:
|
|
117
128
|
"""A picture: SVG text, or PNG bytes, as a box made it."""
|
|
@@ -286,6 +297,7 @@ for _cls, _key, _name, _colour in (
|
|
|
286
297
|
register_type(_cls, _key, _name, _colour)
|
|
287
298
|
|
|
288
299
|
__all__ = [
|
|
300
|
+
"Column",
|
|
289
301
|
"AlignmentResult", "Any", "CPNet", "DFG", "Dataset", "EventLog", "Figure", "Footprint", "OCDFG", "OCEL",
|
|
290
302
|
"Marking", "PetriNet", "Predictions", "Predictor", "ProcessTree", "Regions", "ReplayResult",
|
|
291
303
|
"Scores", "SimpleLog", "Table", "Text", "TransitionSystem", "TypeInfo", "TYPES",
|
|
@@ -36,7 +36,8 @@ from PySide6.QtWidgets import (
|
|
|
36
36
|
from ...flow import record as records
|
|
37
37
|
from ...flow import reproducibility, snapshots
|
|
38
38
|
from ...flow.library import Library
|
|
39
|
-
from ...flow.runner import BLOCKED, DONE, FAILED, IDLE, RUNNING, WAITING, Cache, Result, Run, Runner
|
|
39
|
+
from ...flow.runner import (BLOCKED, DONE, FAILED, IDLE, RUNNING, WAITING, Cache, Result, Run, Runner,
|
|
40
|
+
columns_connected)
|
|
40
41
|
from ...flow.types import EventLog, Figure, PetriNet, Scores, Table
|
|
41
42
|
from ...flow.workflow import Edge, Workflow, group_to_python, to_python
|
|
42
43
|
from .. import theme
|
|
@@ -341,7 +342,7 @@ class WorkflowPage(QWidget):
|
|
|
341
342
|
def refresh_title(self) -> None:
|
|
342
343
|
self.header.set_text(self.workflow.name, self._subtitle())
|
|
343
344
|
status = self._repro
|
|
344
|
-
self.header.set_badge(status.
|
|
345
|
+
self.header.set_badge(status.short, status.tone,
|
|
345
346
|
"\n".join([status.headline] + ["· " + d for d in status.details]
|
|
346
347
|
+ ["", "Click for the record."]),
|
|
347
348
|
self.show_record)
|
|
@@ -879,7 +880,8 @@ class WorkflowPage(QWidget):
|
|
|
879
880
|
return how_widget(result.explanation, self, result.value)
|
|
880
881
|
if index == 2:
|
|
881
882
|
return code_widget(spec, self)
|
|
882
|
-
settings = SettingsWidget(spec, node.settings, self.folder
|
|
883
|
+
settings = SettingsWidget(spec, node.settings, self.folder,
|
|
884
|
+
columns=columns_connected(self.workflow, self.run, node))
|
|
883
885
|
settings.changed.connect(lambda name, value, nid=node.id: self._setting_changed(nid, name, value))
|
|
884
886
|
return settings
|
|
885
887
|
|
|
@@ -658,9 +658,11 @@ class SettingsWidget(QWidget):
|
|
|
658
658
|
|
|
659
659
|
changed = Signal(str, object)
|
|
660
660
|
|
|
661
|
-
def __init__(self, spec: BoxSpec, settings: dict, folder: Path | None = None, parent=None
|
|
661
|
+
def __init__(self, spec: BoxSpec, settings: dict, folder: Path | None = None, parent=None,
|
|
662
|
+
columns: list[str] | None = None) -> None:
|
|
662
663
|
super().__init__(parent)
|
|
663
664
|
self.spec, self.folder = spec, folder
|
|
665
|
+
self.columns = list(columns or []) # of the tables connected to the box, for Column settings
|
|
664
666
|
layout = QVBoxLayout(self)
|
|
665
667
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
666
668
|
layout.setSpacing(10)
|
|
@@ -706,6 +708,8 @@ class SettingsWidget(QWidget):
|
|
|
706
708
|
control.valueChanged.connect(lambda v, s=setting: self.changed.emit(s.name, v))
|
|
707
709
|
elif setting.kind == "path":
|
|
708
710
|
control = self._path_control(setting, value)
|
|
711
|
+
elif setting.kind == "column":
|
|
712
|
+
control = self._column_control(setting, value)
|
|
709
713
|
else:
|
|
710
714
|
control = QLineEdit(str(value) if not swept else "")
|
|
711
715
|
control.editingFinished.connect(lambda s=setting, c=control: self.changed.emit(s.name, c.text()))
|
|
@@ -757,6 +761,41 @@ class SettingsWidget(QWidget):
|
|
|
757
761
|
edit.setStyleSheet("")
|
|
758
762
|
self.changed.emit(setting.name, sweep)
|
|
759
763
|
|
|
764
|
+
def _column_control(self, setting: Setting, value) -> QComboBox:
|
|
765
|
+
"""A column of a connected table: the table's columns to pick from, once
|
|
766
|
+
the box before this one has run, or a name typed in. The first entry
|
|
767
|
+
is no column at all (Plot without x and y draws bars, not a line)."""
|
|
768
|
+
control = QComboBox()
|
|
769
|
+
control.setEditable(True)
|
|
770
|
+
control.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLengthWithIcon)
|
|
771
|
+
control.setMinimumContentsLength(12)
|
|
772
|
+
control.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
|
773
|
+
control.setMaxVisibleItems(14)
|
|
774
|
+
control.lineEdit().setPlaceholderText(
|
|
775
|
+
"Pick a column…" if self.columns else "Connect a table and run it; its columns are listed here")
|
|
776
|
+
control.addItem("—", "")
|
|
777
|
+
for name in self.columns:
|
|
778
|
+
control.addItem(name, name)
|
|
779
|
+
control.setCurrentText("" if value in (None, "") else str(value))
|
|
780
|
+
last = {"text": control.currentText()}
|
|
781
|
+
|
|
782
|
+
def report(text: str, s=setting):
|
|
783
|
+
if text == last["text"]: # only a real change: see _path_control
|
|
784
|
+
return
|
|
785
|
+
last["text"] = text
|
|
786
|
+
self.changed.emit(s.name, text)
|
|
787
|
+
|
|
788
|
+
def picked(index: int, c=control):
|
|
789
|
+
text = c.itemData(index) if index >= 0 else c.currentText()
|
|
790
|
+
text = "" if text is None else str(text)
|
|
791
|
+
if c.currentText() != text:
|
|
792
|
+
c.setCurrentText(text)
|
|
793
|
+
report(text)
|
|
794
|
+
|
|
795
|
+
control.currentIndexChanged.connect(picked)
|
|
796
|
+
control.lineEdit().editingFinished.connect(lambda c=control: report(c.currentText().strip()))
|
|
797
|
+
return control
|
|
798
|
+
|
|
760
799
|
def _path_control(self, setting: Setting, value) -> QComboBox:
|
|
761
800
|
"""The file field: a list of the folder's files to pick from, or a
|
|
762
801
|
path typed or pasted (quotes and shell escapes are tolerated)."""
|
|
@@ -27,7 +27,7 @@ from PySide6.QtWidgets import (
|
|
|
27
27
|
from ...learn import answers, notation
|
|
28
28
|
from ...learn.checks import CORRECT, INCORRECT, PARTIAL, UNKNOWN
|
|
29
29
|
from ..studio.markdown_view import MarkdownLabel
|
|
30
|
-
from ..studio.widgets import button, hbox, label
|
|
30
|
+
from ..studio.widgets import Chip, button, hbox, label
|
|
31
31
|
|
|
32
32
|
#: What the card's caption says above each kind of box.
|
|
33
33
|
CAPTIONS = {
|
|
@@ -781,7 +781,7 @@ class TaskCard(QFrame):
|
|
|
781
781
|
if task.points != 1:
|
|
782
782
|
caption += f" · {task.points:g} points"
|
|
783
783
|
self.caption = label(caption.upper(), "taskCaption")
|
|
784
|
-
self.chip =
|
|
784
|
+
self.chip = Chip()
|
|
785
785
|
self.chip.setHidden(True)
|
|
786
786
|
layout.addLayout(hbox(self.caption, None, self.chip))
|
|
787
787
|
layout.addWidget(editor)
|
|
@@ -852,9 +852,7 @@ class TaskCard(QFrame):
|
|
|
852
852
|
if shown is not None:
|
|
853
853
|
text, colour = shown
|
|
854
854
|
self.chip.setText(("✓ " if colour == "good" else "") + text)
|
|
855
|
-
self.chip.
|
|
856
|
-
self.chip.style().unpolish(self.chip)
|
|
857
|
-
self.chip.style().polish(self.chip)
|
|
855
|
+
self.chip.set_state(colour)
|
|
858
856
|
self.setProperty("state", (shown or ("", ""))[1])
|
|
859
857
|
self.style().unpolish(self)
|
|
860
858
|
self.style().polish(self)
|
|
@@ -883,11 +883,12 @@ class StudioWindow(QMainWindow):
|
|
|
883
883
|
"""*Install…* on a Connections card: pip installs the package into this
|
|
884
884
|
app's Python, in the background; the boxes that need it come alive."""
|
|
885
885
|
import importlib
|
|
886
|
+
from ... import packages
|
|
886
887
|
from .connections import package_for
|
|
887
888
|
package = package_for(name)
|
|
888
|
-
command =
|
|
889
|
+
command = packages.install_command(package)
|
|
889
890
|
answer = QMessageBox.question(
|
|
890
|
-
self, "Install a tool", f"Install {package}
|
|
891
|
+
self, "Install a tool", f"Install {package} {packages.where_text()}?\n\nThis runs:\n"
|
|
891
892
|
f"{' '.join(command)}\n\nThe app stays usable meanwhile.",
|
|
892
893
|
QMessageBox.Yes | QMessageBox.Cancel, QMessageBox.Yes)
|
|
893
894
|
if answer != QMessageBox.Yes:
|
|
@@ -899,6 +900,7 @@ class StudioWindow(QMainWindow):
|
|
|
899
900
|
def done(result) -> None:
|
|
900
901
|
code, output = result
|
|
901
902
|
self.connections_page.installing.discard(name)
|
|
903
|
+
packages.activate() # the downloaded app: the new folder, on the path
|
|
902
904
|
importlib.invalidate_caches()
|
|
903
905
|
self.connections_page.refresh(self.library(), self.workspace.folder if self.workspace else None)
|
|
904
906
|
if code == 0:
|
|
@@ -1465,7 +1467,9 @@ class StudioWindow(QMainWindow):
|
|
|
1465
1467
|
if options:
|
|
1466
1468
|
self.open_options[document.id] = options
|
|
1467
1469
|
self._make_page(document)
|
|
1468
|
-
|
|
1470
|
+
# A view of a box's result waits for Keep in folder (a model has its own Keep,
|
|
1471
|
+
# which _materialise shows); everything else becomes a file now.
|
|
1472
|
+
if not keep_unsaved and (not self.is_view(document) or isinstance(document, ModelDocument)):
|
|
1469
1473
|
self._materialise(document, near)
|
|
1470
1474
|
if document.path:
|
|
1471
1475
|
key = self._key(document.path)
|
|
@@ -1567,6 +1571,12 @@ class StudioWindow(QMainWindow):
|
|
|
1567
1571
|
origin = getattr(document, "origin", "") if isinstance(document, CpnDocument) else ""
|
|
1568
1572
|
text = f"‹ A copy, {origin}" if origin else f"‹ Back to {source.name}"
|
|
1569
1573
|
page.header.set_back(text, lambda _checked=False, s=source: self.back_to(s))
|
|
1574
|
+
if self.is_view(document) and self.workspace is not None and not hasattr(page, "keep_button"):
|
|
1575
|
+
# A view is not a file of the folder until you say so.
|
|
1576
|
+
page.keep_view_button = button("Keep in folder", lambda _checked=False, d=document: self.keep_view(d),
|
|
1577
|
+
kind="primary", tooltip="Save this as a file in the folder; until "
|
|
1578
|
+
"then it is only a view of the box's result")
|
|
1579
|
+
page.header.actions.insertWidget(0, page.keep_view_button)
|
|
1570
1580
|
self.pages[document.id] = page
|
|
1571
1581
|
# The page sits in a scroll area: if the window is made smaller than
|
|
1572
1582
|
# the page's minimum size, scroll bars appear instead of the window
|
|
@@ -1581,6 +1591,30 @@ class StudioWindow(QMainWindow):
|
|
|
1581
1591
|
self.content.addWidget(holder)
|
|
1582
1592
|
return page
|
|
1583
1593
|
|
|
1594
|
+
@staticmethod
|
|
1595
|
+
def is_view(document) -> bool:
|
|
1596
|
+
"""A page opened from a box's result (Open as log ›): a view of something
|
|
1597
|
+
the workflow can make again. Not saved to the folder unless kept, and
|
|
1598
|
+
closed without a question."""
|
|
1599
|
+
return getattr(document, "opened_from", None) is not None and not document.path and \
|
|
1600
|
+
isinstance(document, (LogDocument, TransitionSystemDocument, ModelDocument))
|
|
1601
|
+
|
|
1602
|
+
def keep_view(self, document) -> None:
|
|
1603
|
+
"""A view's *Keep in folder*: now it is a file of the folder like any other."""
|
|
1604
|
+
source = getattr(document, "opened_from", None)
|
|
1605
|
+
near = getattr(source, "path", None)
|
|
1606
|
+
self._materialise(document, near)
|
|
1607
|
+
if document.path:
|
|
1608
|
+
page = self.pages.get(document.id)
|
|
1609
|
+
keep = getattr(page, "keep_view_button", None)
|
|
1610
|
+
if keep is not None:
|
|
1611
|
+
keep.setVisible(False)
|
|
1612
|
+
self._remember_identity(document)
|
|
1613
|
+
self._update_autosave(document)
|
|
1614
|
+
self._rebuild_sidebar()
|
|
1615
|
+
self._select_document(document)
|
|
1616
|
+
self.statusBar().showMessage(f"Kept as {Path(document.path).name}", 6000)
|
|
1617
|
+
|
|
1584
1618
|
def back_to(self, source) -> None:
|
|
1585
1619
|
"""A page's Back link: the analysis it was opened from, if it is still open."""
|
|
1586
1620
|
if source in self.documents:
|
|
@@ -1769,8 +1803,9 @@ class StudioWindow(QMainWindow):
|
|
|
1769
1803
|
if isinstance(other, ComparisonDocument) and other not in documents and \
|
|
1770
1804
|
any(log.id in removed_logs for log in other.logs):
|
|
1771
1805
|
documents.append(other)
|
|
1772
|
-
# A comparison is recreated in a moment,
|
|
1773
|
-
|
|
1806
|
+
# A comparison is recreated in a moment, and a view of a box's result
|
|
1807
|
+
# can be opened again from the box, so neither needs a confirmation.
|
|
1808
|
+
unsaved = [d for d in documents if not isinstance(d, ComparisonDocument) and not self.is_view(d) and
|
|
1774
1809
|
(not d.path or d.missing or getattr(d, "dirty", False))]
|
|
1775
1810
|
if unsaved and confirm:
|
|
1776
1811
|
names = "\n".join(f"• {d.name}" for d in unsaved[:8])
|
|
@@ -4459,6 +4494,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
4459
4494
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("OpenProcess.Studio")
|
|
4460
4495
|
except (AttributeError, OSError):
|
|
4461
4496
|
pass
|
|
4497
|
+
from ... import packages
|
|
4498
|
+
packages.activate() # packages installed from Connections
|
|
4462
4499
|
application = QApplication.instance() or QApplication(argv)
|
|
4463
4500
|
application.setApplicationName(APPLICATION_NAME)
|
|
4464
4501
|
application.setWindowIcon(app_icon())
|
|
@@ -18,7 +18,7 @@ from PySide6.QtCore import Signal
|
|
|
18
18
|
from PySide6.QtWidgets import QScrollArea, QVBoxLayout, QWidget
|
|
19
19
|
|
|
20
20
|
from ...flow.library import Library
|
|
21
|
-
from .widgets import Card, PageHeader, button, flow, hbox, label
|
|
21
|
+
from .widgets import Card, Chip, PageHeader, button, flow, hbox, label
|
|
22
22
|
|
|
23
23
|
#: (title, what it is, the input boxes' function names)
|
|
24
24
|
INPUTS = [
|
|
@@ -50,9 +50,17 @@ def package_for(tool: str) -> str:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
def can_install() -> bool:
|
|
53
|
-
"""Whether
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
"""Whether Install… can work here (see :mod:`openprocess.packages`: from a
|
|
54
|
+
Python install pip runs in that Python; the downloaded app carries pip and
|
|
55
|
+
installs into a folder of the user's own)."""
|
|
56
|
+
from ... import packages
|
|
57
|
+
return packages.can_install()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def installable(tool: str) -> bool:
|
|
61
|
+
"""Whether Install… works for this tool here (pip, and a package this build can install)."""
|
|
62
|
+
from ... import packages
|
|
63
|
+
return can_install() and packages.installable(package_for(tool))
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
def _by_function(library: Library) -> dict[str, object]:
|
|
@@ -117,9 +125,7 @@ class ConnectionsPage(QWidget):
|
|
|
117
125
|
item.setMinimumWidth(200)
|
|
118
126
|
item.setMaximumWidth(300)
|
|
119
127
|
if status:
|
|
120
|
-
|
|
121
|
-
chip.setStyleSheet(f"background: {_tone(tone)}; color: white;")
|
|
122
|
-
item.header.addWidget(chip)
|
|
128
|
+
item.header.addWidget(Chip(status, fill=_tone(tone), ink="white"))
|
|
123
129
|
if boxes:
|
|
124
130
|
item.add(label("Boxes: " + ", ".join(boxes), "muted", wrap=True))
|
|
125
131
|
self.cards[title] = item
|
|
@@ -152,14 +158,19 @@ class ConnectionsPage(QWidget):
|
|
|
152
158
|
text, status, tone = "Installed.", "ready", "good"
|
|
153
159
|
elif name in self.installing:
|
|
154
160
|
text, status, tone = "Installing… (a minute or two; the app stays usable)", "installing", "muted"
|
|
161
|
+
elif installable(name):
|
|
162
|
+
from ... import packages
|
|
163
|
+
text, status, tone = (f"Not installed. Install… puts it {packages.where_text()}; the boxes "
|
|
164
|
+
"that need it come alive without a restart."), "not installed", "warning"
|
|
155
165
|
elif can_install():
|
|
156
|
-
text, status, tone = f"Not installed.
|
|
157
|
-
|
|
166
|
+
text, status, tone = (f"Not installed. The downloaded app installs only packages that come "
|
|
167
|
+
f"ready-built, and this one needs building: in a Python install, run "
|
|
168
|
+
f"{tool['install']}."), "not installed", "warning"
|
|
158
169
|
else:
|
|
159
|
-
text, status, tone = (f"Not installed
|
|
160
|
-
f"
|
|
170
|
+
text, status, tone = (f"Not installed, and this build has no pip: in a Python install, run "
|
|
171
|
+
f"{tool['install']}."), "not installed", "warning"
|
|
161
172
|
item = card(name, text, tool["boxes"], status, tone)
|
|
162
|
-
if not tool["installed"] and
|
|
173
|
+
if not tool["installed"] and installable(name) and name not in self.installing:
|
|
163
174
|
install = button("Install…", lambda _checked=False, n=name: self.install_requested.emit(n),
|
|
164
175
|
kind="primary", tooltip=f"Runs {tool['install']} in the background")
|
|
165
176
|
self.install_buttons[name] = install
|