openprocess 0.10.6__tar.gz → 0.11.3__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.
Files changed (230) hide show
  1. {openprocess-0.10.6/openprocess.egg-info → openprocess-0.11.3}/PKG-INFO +46 -20
  2. {openprocess-0.10.6 → openprocess-0.11.3}/README.md +45 -19
  3. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/__init__.py +1 -1
  4. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/cli.py +2 -0
  5. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/check.py +6 -1
  6. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/science.py +24 -13
  7. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/convert.py +76 -8
  8. openprocess-0.11.3/openprocess/flow/history.py +113 -0
  9. openprocess-0.11.3/openprocess/flow/provenance.py +110 -0
  10. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/record.py +67 -3
  11. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/runner.py +10 -0
  12. openprocess-0.11.3/openprocess/flow/script.py +87 -0
  13. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/types.py +10 -1
  14. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/workflow.py +18 -1
  15. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/canvas.py +10 -5
  16. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/canvas.py +2 -0
  17. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/page.py +83 -3
  18. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/picker.py +113 -88
  19. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/viewers.py +87 -8
  20. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/app.py +156 -33
  21. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/connections.py +26 -1
  22. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/cpn_page.py +55 -25
  23. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/definition_view.py +9 -2
  24. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/derivation_view.py +3 -1
  25. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/graph_view.py +55 -0
  26. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/markdown_view.py +3 -1
  27. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/model_page.py +21 -4
  28. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/petri_page.py +5 -0
  29. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/sidebar.py +6 -4
  30. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/style.py +2 -0
  31. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/tool_icons.py +27 -20
  32. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/conformance/alignments.py +86 -7
  33. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/discovery/inductive.py +133 -24
  34. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/references.py +5 -2
  35. {openprocess-0.10.6 → openprocess-0.11.3/openprocess.egg-info}/PKG-INFO +46 -20
  36. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess.egg-info/SOURCES.txt +4 -0
  37. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_exercises_gui.py +6 -0
  38. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_flow.py +139 -0
  39. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_flow_gui.py +141 -9
  40. openprocess-0.11.3/tests/test_history.py +57 -0
  41. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_mining.py +71 -0
  42. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_studio_gui.py +175 -4
  43. {openprocess-0.10.6 → openprocess-0.11.3}/LICENSE +0 -0
  44. {openprocess-0.10.6 → openprocess-0.11.3}/cpnpy/__init__.py +0 -0
  45. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/analysis/__init__.py +0 -0
  46. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/analysis/state_space.py +0 -0
  47. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/analysis/state_space_process.py +0 -0
  48. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/citation.py +0 -0
  49. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  50. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  51. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  52. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  53. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  54. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  55. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  56. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  57. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  58. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  59. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  60. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  61. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  62. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  63. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  64. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  65. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  66. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  67. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  68. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/exercises/pack.md +0 -0
  69. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/__init__.py +0 -0
  70. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/box.py +0 -0
  71. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/__init__.py +0 -0
  72. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/compare.py +0 -0
  73. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/cpn.py +0 -0
  74. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/discover.py +0 -0
  75. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/filter.py +0 -0
  76. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/input.py +0 -0
  77. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/output.py +0 -0
  78. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/predict.py +0 -0
  79. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/boxes/sweeps.py +0 -0
  80. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/datasets.py +0 -0
  81. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/explain.py +0 -0
  82. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/figures.py +0 -0
  83. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/library.py +0 -0
  84. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/report.py +0 -0
  85. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/reproducibility.py +0 -0
  86. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/snapshots.py +0 -0
  87. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/flow/sweep.py +0 -0
  88. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/__init__.py +0 -0
  89. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/app.py +0 -0
  90. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/arc_editing.py +0 -0
  91. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/__init__.py +0 -0
  92. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/runs.py +0 -0
  93. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/stepper.py +0 -0
  94. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/summary.py +0 -0
  95. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/flow/templates.py +0 -0
  96. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/items.py +0 -0
  97. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/learn/answer_boxes.py +0 -0
  98. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/learn/concealment.py +0 -0
  99. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/learn/mode.py +0 -0
  100. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/panning.py +0 -0
  101. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/resources/openprocess-icon.png +0 -0
  102. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/__init__.py +0 -0
  103. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/__main__.py +0 -0
  104. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/charts.py +0 -0
  105. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/cite.py +0 -0
  106. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/command_palette.py +0 -0
  107. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/compare_page.py +0 -0
  108. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/documents.py +0 -0
  109. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/dotted_chart.py +0 -0
  110. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/file_dialogs.py +0 -0
  111. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/filter_dialog.py +0 -0
  112. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/graph_builders.py +0 -0
  113. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/instances.py +0 -0
  114. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/log_editor.py +0 -0
  115. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/log_page.py +0 -0
  116. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/mathtext.py +0 -0
  117. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/ml_highlighter.py +0 -0
  118. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/motion.py +0 -0
  119. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/net_comparison.py +0 -0
  120. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/notes_overlay.py +0 -0
  121. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/provenance.py +0 -0
  122. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/regions_view.py +0 -0
  123. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/spaces.py +0 -0
  124. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/updates.py +0 -0
  125. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/widgets.py +0 -0
  126. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/workers.py +0 -0
  127. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/studio/workspace.py +0 -0
  128. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/theme.py +0 -0
  129. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/gui/tidy.py +0 -0
  130. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/io/__init__.py +0 -0
  131. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/io/cpn_reader.py +0 -0
  132. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/io/cpn_writer.py +0 -0
  133. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/__init__.py +0 -0
  134. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/answers.py +0 -0
  135. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/checks.py +0 -0
  136. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/computed.py +0 -0
  137. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/context.py +0 -0
  138. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/exam.py +0 -0
  139. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/exercise-packs.md +0 -0
  140. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/importer.py +0 -0
  141. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/notation.py +0 -0
  142. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/pack.py +0 -0
  143. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/learn/sheet.py +0 -0
  144. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/__init__.py +0 -0
  145. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/analysis.py +0 -0
  146. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/columns.py +0 -0
  147. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/compare_nets.py +0 -0
  148. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/conformance/__init__.py +0 -0
  149. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/conformance/quality.py +0 -0
  150. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/conformance/token_replay.py +0 -0
  151. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/csv_import.py +0 -0
  152. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/definitions.py +0 -0
  153. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/dfg.py +0 -0
  154. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/discovery/__init__.py +0 -0
  155. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/discovery/alpha.py +0 -0
  156. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/discovery/heuristics.py +0 -0
  157. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/discovery/state_regions.py +0 -0
  158. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/filtering.py +0 -0
  159. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/footprint.py +0 -0
  160. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/invariants.py +0 -0
  161. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/layout.py +0 -0
  162. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/log.py +0 -0
  163. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/ocel.py +0 -0
  164. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/petrinet.py +0 -0
  165. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/playout.py +0 -0
  166. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/pm4py_bridge.py +0 -0
  167. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/pnml.py +0 -0
  168. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/processtree.py +0 -0
  169. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/regions.py +0 -0
  170. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/stats.py +0 -0
  171. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/structure.py +0 -0
  172. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/transition_system.py +0 -0
  173. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/mining/xes.py +0 -0
  174. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/__init__.py +0 -0
  175. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/ast_nodes.py +0 -0
  176. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/builtins.py +0 -0
  177. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/colorsets.py +0 -0
  178. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/errors.py +0 -0
  179. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/evaluator.py +0 -0
  180. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/lexer.py +0 -0
  181. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/multiset.py +0 -0
  182. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/parser.py +0 -0
  183. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/ml/values.py +0 -0
  184. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/model/__init__.py +0 -0
  185. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/model/declarations.py +0 -0
  186. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/model/examples.py +0 -0
  187. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/model/net.py +0 -0
  188. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/model/plain.py +0 -0
  189. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/packages.py +0 -0
  190. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/sim/__init__.py +0 -0
  191. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/sim/binding.py +0 -0
  192. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/sim/export.py +0 -0
  193. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/sim/simulator.py +0 -0
  194. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/teaching/__init__.py +0 -0
  195. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/teaching/answers.py +0 -0
  196. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/teaching/checks.py +0 -0
  197. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/teaching/pack.py +0 -0
  198. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess/teaching/sheet.py +0 -0
  199. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess.egg-info/dependency_links.txt +0 -0
  200. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess.egg-info/entry_points.txt +0 -0
  201. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess.egg-info/requires.txt +0 -0
  202. {openprocess-0.10.6 → openprocess-0.11.3}/openprocess.egg-info/top_level.txt +0 -0
  203. {openprocess-0.10.6 → openprocess-0.11.3}/pyproject.toml +0 -0
  204. {openprocess-0.10.6 → openprocess-0.11.3}/setup.cfg +0 -0
  205. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_arc_editing.py +0 -0
  206. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_citation.py +0 -0
  207. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_colorsets.py +0 -0
  208. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_columns.py +0 -0
  209. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_cpn_course_model.py +0 -0
  210. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_definitions.py +0 -0
  211. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_hierarchy.py +0 -0
  212. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_io.py +0 -0
  213. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_learn.py +0 -0
  214. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_learn_gui.py +0 -0
  215. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_log_editing.py +0 -0
  216. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_ml.py +0 -0
  217. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_multiset.py +0 -0
  218. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_ocel.py +0 -0
  219. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_packages.py +0 -0
  220. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_regions.py +0 -0
  221. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_rename.py +0 -0
  222. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_report.py +0 -0
  223. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_simulation.py +0 -0
  224. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_snapshots.py +0 -0
  225. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_state_space.py +0 -0
  226. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_teaching.py +0 -0
  227. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_tidy.py +0 -0
  228. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_updates.py +0 -0
  229. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_wf_structure.py +0 -0
  230. {openprocess-0.10.6 → openprocess-0.11.3}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.10.6
3
+ Version: 0.11.3
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
@@ -281,8 +281,9 @@ which space you were in.
281
281
  state; it stays in the folder.
282
282
  - **Flat lists, the same in both spaces.** The folder's own files first,
283
283
  then each subfolder's under a caption naming it ("Week 5 / Part 1").
284
- Nothing to unfold except LOGS. An empty folder shows in both spaces until
285
- it has files.
284
+ Click a caption to fold its files away or bring them back; the app
285
+ remembers which are folded, per folder. An empty folder shows in both
286
+ spaces until it has files.
286
287
  - **Organise from the app**: right-click for **New Folder…**, **Rename…**,
287
288
  **Show in Finder** and **Move to Bin** (recoverable from the Bin; never a
288
289
  hard delete). **Drag files onto a subfolder** to move them on disk. An open
@@ -373,7 +374,9 @@ Nets drawn as in the lectures and the book:
373
374
  - **Places** are circles with black tokens inside, drawn as dots up to 5 and
374
375
  as a number above that.
375
376
  - **Transitions** are boxes with their name inside. A **silent (τ)**
376
- transition is a black bar.
377
+ transition is a black bar: the **τ** tool (toolbar or palette) adds one
378
+ with a click, and the Element tab's *Silent* box turns an ordinary
379
+ transition into τ and back.
377
380
  - **Arc weights** above 1 are written on the arc.
378
381
  - **Names** go inside the places and transitions, which grow to fit them.
379
382
  Tick **Names outside** in the toolbar to put them underneath instead; you
@@ -528,17 +531,19 @@ links to the definitions it builds on; **Help ▸ Definitions** lists them all.
528
531
  for bounded nets; for unbounded ones, up to a trace length (and it says
529
532
  so).
530
533
 
531
- | Process map | Discovering a model |
534
+ | Process map | Discover in Mine →: the miner is a box, its result beside the canvas |
532
535
  |---|---|
533
536
  | ![Process map](docs/screenshots/studio-process-map.png) | ![Discover](docs/screenshots/studio-discover.png) |
534
537
 
535
- | α-algorithm result | Conformance (alignments on the model) |
538
+ | α-algorithm result | *How*: the eight steps, from the footprint to α(L) |
536
539
  |---|---|
537
- | ![Alpha](docs/screenshots/studio-alpha.png) | ![Conformance](docs/screenshots/studio-conformance.png) |
540
+ | ![Alpha](docs/screenshots/studio-alpha.png) | ![The eight steps of the α-algorithm](docs/screenshots/studio-alpha-how.png) |
538
541
 
539
- | Comparing two logs | Linked dotted charts of three boarding strategies |
542
+ | Check fit: every score, and how it was computed | Comparing two logs |
540
543
  |---|---|
541
- | ![Comparing two logs](docs/screenshots/compare.png) | ![Linked dotted charts](docs/screenshots/compare-dotted.png) |
544
+ | ![Conformance](docs/screenshots/studio-conformance.png) | ![Comparing two logs](docs/screenshots/compare.png) |
545
+
546
+ ![Linked dotted charts of two logs, one scale and one colour per activity](docs/screenshots/compare-dotted.png)
542
547
 
543
548
  The app follows the system's light or dark appearance:
544
549
 
@@ -555,6 +560,8 @@ sidebar, **Learn ▸ Open Exercise Pack…**, or **Learn ▸ Open Demo Exercises
555
560
  exercises): the window switches to a view made for working through a pack
556
561
  without distractions, and *Exit* brings your folder back exactly as it was.
557
562
 
563
+ ![An exercise in Learn: the worksheet with its answer boxes on the left, the exercise's log, given net and workflow on the right](docs/screenshots/learn-exercise.png)
564
+
558
565
  - **The worksheet** on the left is the question top to bottom, with an
559
566
  answer box wherever one is needed: yes/no, multiple choice, a set
560
567
  (`{a, b}`, sets of sets, or pairs like `({a}, {b,d})`), a number, a short
@@ -742,7 +749,10 @@ generated from `openprocess/mining/definitions.py`; after editing a definition,
742
749
  Likewise [`docs/references.md`](docs/references.md) — every source OpenProcess's
743
750
  notation and algorithms follow, also in the app under **Help ▸ References** —
744
751
  is generated from `openprocess/references.py` with
745
- `python -m openprocess.references > docs/references.md`.
752
+ `python -m openprocess.references > docs/references.md`. The screenshots in
753
+ this README come from the app itself: `python docs/make_screenshots.py`
754
+ renders every page offscreen into `docs/screenshots/`, so run it after a
755
+ change to the app's looks.
746
756
 
747
757
  The engines (`openprocess.mining`, `openprocess.ml`, `openprocess.sim`, `openprocess.analysis`)
748
758
  have **no dependencies**, so they work in a notebook or a script:
@@ -758,7 +768,10 @@ print(report.sound, report.findings)
758
768
 
759
769
  [**docs/how-it-works.md**](docs/how-it-works.md) covers the architecture, the
760
770
  CPN ML subset, binding search, timed nets, the state space, the file format
761
- and the project layout.
771
+ and the project layout. [**docs/design.md**](docs/design.md) is the *why*:
772
+ the problem, the claim, the three ideas and the decisions that follow from
773
+ them. `tests/cross_check_inductive_miner.py` compares the built-in Inductive
774
+ Miner with PM4Py's on hundreds of logs and reports where they differ.
762
775
 
763
776
  ## Workflows
764
777
 
@@ -769,17 +782,20 @@ Miner, a fitness check), *Compare discovery* (three miners side by side),
769
782
  confidence* (bootstrap intervals, a test and a plot; needs the `science`
770
783
  extra) and *Predict the next activity*. Or start empty.
771
784
 
785
+ | The canvas: *Compare discovery*, three miners side by side | Summary: every box's key figure, and the process map |
786
+ |---|---|
787
+ | ![The canvas of a Compare discovery analysis, the Compare box's table beside it](docs/screenshots/workflow-canvas.png) | ![The Summary view: a tile per box, the process map under them](docs/screenshots/workflow-summary.png) |
788
+
772
789
  - **The page is the canvas.** Its header has **Canvas | Summary**, then
773
790
  **+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
774
791
  and Save. The sidebar beside it lists the folder's analyses and, folded
775
792
  under them, its logs.
776
- - **+ Add box** (or a double-click on the canvas) opens every box, by group:
777
- the six core groups side by side (Input, Filter, Discover, Check, Compare,
778
- Output) and one line for the rest (Science, Predict, Coloured nets, Sweep,
779
- your own boxes) with *Show all*. Under *Input*, **the folder's own files**:
780
- one click adds the box that reads the file, already set. Type to search
781
- them all: "alpha" finds the α-algorithm, "boarding" the log; Enter adds the
782
- first match.
793
+ - **+ Add box** (or a double-click on the canvas) opens every box, one
794
+ section per group (Input, Filter, Discover, Check, Compare, Output, then
795
+ Science, Predict, Coloured nets, Sweep and your own boxes), in a list that
796
+ scrolls. Under *Input*, **the folder's own files**: one click adds the box
797
+ that reads the file, already set. Type to search them all: "alpha" finds
798
+ the α-algorithm, "boarding" the log; Enter adds the first match.
783
799
  - **Files flow into the canvas.** Drop a log, an OCEL 2.0 object-centric log
784
800
  (`.jsonocel`), a PNML file, a .cpn or a ts.txt on the canvas and it becomes
785
801
  its input box at that spot; an input
@@ -825,8 +841,18 @@ extra) and *Predict the next activity*. Or start empty.
825
841
  `boxes/`, so it is listed under *Yours* and can be used in any workflow.
826
842
  - **Export experiment** (under ⋯) writes a zip with the workflow file and its record,
827
843
  the inputs, your boxes, every result as a file (CSV, PNML, SVG, XES), a
828
- `requirements.lock` and a README that says what was run: supplementary
829
- material for a paper, which `openprocess run --check` can verify.
844
+ `requirements.lock`, a `run.py` that runs the same analysis as a plain
845
+ script, and a README that says what was run: supplementary material for a
846
+ paper, which `openprocess run --check` can verify. Drop such a zip on
847
+ OpenProcess and it is unpacked into a folder of its own and run again.
848
+ - **Export as Python script** (under ⋯) writes the analysis as a `.py` file:
849
+ the same boxes, settings and connections, run with `python analysis.py`,
850
+ the results written beside it.
851
+ - **Every result knows where it came from**: a figure saved as SVG carries
852
+ the analysis, the box, its settings and the result's fingerprint in its
853
+ metadata; a CSV gets a `.provenance.json` beside it.
854
+ - **⌘Z and ⇧⌘Z** undo and redo on the canvas: boxes, connections, moves,
855
+ settings, groups.
830
856
  - **The side panel** resizes by dragging the gap beside it; double-click the
831
857
  gap, or *View ▸ Reset Workflow Layout*, for its default width.
832
858
  - **Connections** (at the bottom of the sidebar, or *View ▸ Connections*)
@@ -242,8 +242,9 @@ which space you were in.
242
242
  state; it stays in the folder.
243
243
  - **Flat lists, the same in both spaces.** The folder's own files first,
244
244
  then each subfolder's under a caption naming it ("Week 5 / Part 1").
245
- Nothing to unfold except LOGS. An empty folder shows in both spaces until
246
- it has files.
245
+ Click a caption to fold its files away or bring them back; the app
246
+ remembers which are folded, per folder. An empty folder shows in both
247
+ spaces until it has files.
247
248
  - **Organise from the app**: right-click for **New Folder…**, **Rename…**,
248
249
  **Show in Finder** and **Move to Bin** (recoverable from the Bin; never a
249
250
  hard delete). **Drag files onto a subfolder** to move them on disk. An open
@@ -334,7 +335,9 @@ Nets drawn as in the lectures and the book:
334
335
  - **Places** are circles with black tokens inside, drawn as dots up to 5 and
335
336
  as a number above that.
336
337
  - **Transitions** are boxes with their name inside. A **silent (τ)**
337
- transition is a black bar.
338
+ transition is a black bar: the **τ** tool (toolbar or palette) adds one
339
+ with a click, and the Element tab's *Silent* box turns an ordinary
340
+ transition into τ and back.
338
341
  - **Arc weights** above 1 are written on the arc.
339
342
  - **Names** go inside the places and transitions, which grow to fit them.
340
343
  Tick **Names outside** in the toolbar to put them underneath instead; you
@@ -489,17 +492,19 @@ links to the definitions it builds on; **Help ▸ Definitions** lists them all.
489
492
  for bounded nets; for unbounded ones, up to a trace length (and it says
490
493
  so).
491
494
 
492
- | Process map | Discovering a model |
495
+ | Process map | Discover in Mine →: the miner is a box, its result beside the canvas |
493
496
  |---|---|
494
497
  | ![Process map](docs/screenshots/studio-process-map.png) | ![Discover](docs/screenshots/studio-discover.png) |
495
498
 
496
- | α-algorithm result | Conformance (alignments on the model) |
499
+ | α-algorithm result | *How*: the eight steps, from the footprint to α(L) |
497
500
  |---|---|
498
- | ![Alpha](docs/screenshots/studio-alpha.png) | ![Conformance](docs/screenshots/studio-conformance.png) |
501
+ | ![Alpha](docs/screenshots/studio-alpha.png) | ![The eight steps of the α-algorithm](docs/screenshots/studio-alpha-how.png) |
499
502
 
500
- | Comparing two logs | Linked dotted charts of three boarding strategies |
503
+ | Check fit: every score, and how it was computed | Comparing two logs |
501
504
  |---|---|
502
- | ![Comparing two logs](docs/screenshots/compare.png) | ![Linked dotted charts](docs/screenshots/compare-dotted.png) |
505
+ | ![Conformance](docs/screenshots/studio-conformance.png) | ![Comparing two logs](docs/screenshots/compare.png) |
506
+
507
+ ![Linked dotted charts of two logs, one scale and one colour per activity](docs/screenshots/compare-dotted.png)
503
508
 
504
509
  The app follows the system's light or dark appearance:
505
510
 
@@ -516,6 +521,8 @@ sidebar, **Learn ▸ Open Exercise Pack…**, or **Learn ▸ Open Demo Exercises
516
521
  exercises): the window switches to a view made for working through a pack
517
522
  without distractions, and *Exit* brings your folder back exactly as it was.
518
523
 
524
+ ![An exercise in Learn: the worksheet with its answer boxes on the left, the exercise's log, given net and workflow on the right](docs/screenshots/learn-exercise.png)
525
+
519
526
  - **The worksheet** on the left is the question top to bottom, with an
520
527
  answer box wherever one is needed: yes/no, multiple choice, a set
521
528
  (`{a, b}`, sets of sets, or pairs like `({a}, {b,d})`), a number, a short
@@ -703,7 +710,10 @@ generated from `openprocess/mining/definitions.py`; after editing a definition,
703
710
  Likewise [`docs/references.md`](docs/references.md) — every source OpenProcess's
704
711
  notation and algorithms follow, also in the app under **Help ▸ References** —
705
712
  is generated from `openprocess/references.py` with
706
- `python -m openprocess.references > docs/references.md`.
713
+ `python -m openprocess.references > docs/references.md`. The screenshots in
714
+ this README come from the app itself: `python docs/make_screenshots.py`
715
+ renders every page offscreen into `docs/screenshots/`, so run it after a
716
+ change to the app's looks.
707
717
 
708
718
  The engines (`openprocess.mining`, `openprocess.ml`, `openprocess.sim`, `openprocess.analysis`)
709
719
  have **no dependencies**, so they work in a notebook or a script:
@@ -719,7 +729,10 @@ print(report.sound, report.findings)
719
729
 
720
730
  [**docs/how-it-works.md**](docs/how-it-works.md) covers the architecture, the
721
731
  CPN ML subset, binding search, timed nets, the state space, the file format
722
- and the project layout.
732
+ and the project layout. [**docs/design.md**](docs/design.md) is the *why*:
733
+ the problem, the claim, the three ideas and the decisions that follow from
734
+ them. `tests/cross_check_inductive_miner.py` compares the built-in Inductive
735
+ Miner with PM4Py's on hundreds of logs and reports where they differ.
723
736
 
724
737
  ## Workflows
725
738
 
@@ -730,17 +743,20 @@ Miner, a fitness check), *Compare discovery* (three miners side by side),
730
743
  confidence* (bootstrap intervals, a test and a plot; needs the `science`
731
744
  extra) and *Predict the next activity*. Or start empty.
732
745
 
746
+ | The canvas: *Compare discovery*, three miners side by side | Summary: every box's key figure, and the process map |
747
+ |---|---|
748
+ | ![The canvas of a Compare discovery analysis, the Compare box's table beside it](docs/screenshots/workflow-canvas.png) | ![The Summary view: a tile per box, the process map under them](docs/screenshots/workflow-summary.png) |
749
+
733
750
  - **The page is the canvas.** Its header has **Canvas | Summary**, then
734
751
  **+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
735
752
  and Save. The sidebar beside it lists the folder's analyses and, folded
736
753
  under them, its logs.
737
- - **+ Add box** (or a double-click on the canvas) opens every box, by group:
738
- the six core groups side by side (Input, Filter, Discover, Check, Compare,
739
- Output) and one line for the rest (Science, Predict, Coloured nets, Sweep,
740
- your own boxes) with *Show all*. Under *Input*, **the folder's own files**:
741
- one click adds the box that reads the file, already set. Type to search
742
- them all: "alpha" finds the α-algorithm, "boarding" the log; Enter adds the
743
- first match.
754
+ - **+ Add box** (or a double-click on the canvas) opens every box, one
755
+ section per group (Input, Filter, Discover, Check, Compare, Output, then
756
+ Science, Predict, Coloured nets, Sweep and your own boxes), in a list that
757
+ scrolls. Under *Input*, **the folder's own files**: one click adds the box
758
+ that reads the file, already set. Type to search them all: "alpha" finds
759
+ the α-algorithm, "boarding" the log; Enter adds the first match.
744
760
  - **Files flow into the canvas.** Drop a log, an OCEL 2.0 object-centric log
745
761
  (`.jsonocel`), a PNML file, a .cpn or a ts.txt on the canvas and it becomes
746
762
  its input box at that spot; an input
@@ -786,8 +802,18 @@ extra) and *Predict the next activity*. Or start empty.
786
802
  `boxes/`, so it is listed under *Yours* and can be used in any workflow.
787
803
  - **Export experiment** (under ⋯) writes a zip with the workflow file and its record,
788
804
  the inputs, your boxes, every result as a file (CSV, PNML, SVG, XES), a
789
- `requirements.lock` and a README that says what was run: supplementary
790
- material for a paper, which `openprocess run --check` can verify.
805
+ `requirements.lock`, a `run.py` that runs the same analysis as a plain
806
+ script, and a README that says what was run: supplementary material for a
807
+ paper, which `openprocess run --check` can verify. Drop such a zip on
808
+ OpenProcess and it is unpacked into a folder of its own and run again.
809
+ - **Export as Python script** (under ⋯) writes the analysis as a `.py` file:
810
+ the same boxes, settings and connections, run with `python analysis.py`,
811
+ the results written beside it.
812
+ - **Every result knows where it came from**: a figure saved as SVG carries
813
+ the analysis, the box, its settings and the result's fingerprint in its
814
+ metadata; a CSV gets a `.provenance.json` beside it.
815
+ - **⌘Z and ⇧⌘Z** undo and redo on the canvas: boxes, connections, moves,
816
+ settings, groups.
791
817
  - **The side panel** resizes by dragging the gap beside it; double-click the
792
818
  gap, or *View ▸ Reset Workflow Layout*, for its default width.
793
819
  - **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.10.6"
51
+ __version__ = "0.11.3"
52
52
 
53
53
  __all__ = [
54
54
  "Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
@@ -249,6 +249,8 @@ def command_mine_conform(arguments: argparse.Namespace) -> int:
249
249
  print(f"Generalisation {generalisation(replay):.4f}")
250
250
  print(f"Simplicity {simplicity(net):.4f}")
251
251
  print(f"Fitting cases {alignments.fitting_traces} / {alignments.trace_count}")
252
+ for note in alignments.notes:
253
+ print(f"Note: {note}")
252
254
  return 0
253
255
 
254
256
 
@@ -40,6 +40,8 @@ def check_fit(model: PetriNet, log: EventLog,
40
40
  alignments = align_log(model, simple)
41
41
  fitness = alignments.average_fitness
42
42
  flow.note(f"{alignments.fitting_traces} of {alignments.trace_count} cases align without a deviation")
43
+ for note in alignments.notes:
44
+ flow.note(note)
43
45
  else:
44
46
  fitness = replay.fitness
45
47
  flow.note(f"{replay.fitting_traces} of {replay.trace_count} cases replay without missing or remaining tokens")
@@ -63,8 +65,11 @@ def alignments(model: PetriNet, log: EventLog) -> AlignmentResult:
63
65
  """Optimal alignments of every variant with the model (A* over the
64
66
  synchronous product, standard cost function)."""
65
67
  result = align_log(model, log.simple_log())
66
- rows = [["⟨" + ", ".join(a.trace) + "⟩", a.count, a.cost, round(a.fitness, 4)] for a in result.alignments]
68
+ rows = [["⟨" + ", ".join(a.trace) + "⟩", a.count, a.cost if a.complete else "?", round(a.fitness, 4)]
69
+ for a in result.alignments]
67
70
  flow.show(Table("Alignments", ["variant", "cases", "cost", "fitness"], rows), "Alignment per variant")
71
+ for note in result.notes:
72
+ flow.note(note)
68
73
  return result
69
74
 
70
75
 
@@ -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 a line
114
- of column *y* against column *x* of a table (a sweep). The figure is a
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 (lines only)
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 scores:
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 an x and y column")
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)
@@ -15,6 +15,7 @@ class and the target (a class, or a dotted name).
15
15
  from __future__ import annotations
16
16
 
17
17
  import importlib
18
+ import importlib.util
18
19
  from typing import Any, Callable
19
20
 
20
21
  from ..mining.log import KEY_NAME, KEY_RESOURCE, KEY_TIME, EventLog, SimpleLog
@@ -25,11 +26,49 @@ _converters: dict[tuple[str, str], Callable] = {}
25
26
 
26
27
 
27
28
  def _name(target) -> str:
29
+ """The name a type is registered under: ``module.qualname`` for a class,
30
+ a dotted string as given. Nothing is imported here, so registering a
31
+ converter for ``"pandas.core.frame.DataFrame"`` costs nothing when pandas
32
+ is absent, and importing this module never imports pandas."""
28
33
  if isinstance(target, str):
29
34
  return target
30
35
  return f"{target.__module__}.{target.__qualname__}"
31
36
 
32
37
 
38
+ def _resolve(dotted: str):
39
+ """The class a dotted name points at, or None when it cannot be found.
40
+
41
+ Tries the longest module prefix first (``a.b.c.D`` as module ``a.b.c``,
42
+ attribute ``D``), then shorter ones, so both ``pandas.DataFrame`` and
43
+ ``pandas.core.frame.DataFrame`` resolve to the same class. A name whose
44
+ top-level package is not installed resolves to None without importing
45
+ anything.
46
+ """
47
+ parts = dotted.split(".")
48
+ if len(parts) < 2 or importlib.util.find_spec(parts[0]) is None:
49
+ return None
50
+ for split in range(len(parts) - 1, 0, -1):
51
+ try:
52
+ obj = importlib.import_module(".".join(parts[:split]))
53
+ except ImportError:
54
+ continue
55
+ for attribute in parts[split:]:
56
+ obj = getattr(obj, attribute, None)
57
+ if obj is None:
58
+ break
59
+ else:
60
+ if isinstance(obj, type):
61
+ return obj
62
+ return None
63
+
64
+
65
+ def _canonical(name: str) -> str:
66
+ """``name`` as the class it points at is defined (``module.qualname``),
67
+ or ``name`` itself when it points at nothing importable."""
68
+ resolved = _resolve(name)
69
+ return name if resolved is None else _name(resolved)
70
+
71
+
33
72
  def register(source, target, function: Callable) -> None:
34
73
  """``function(value) -> converted`` for a value of class ``source``."""
35
74
  _converters[(_name(source), _name(target))] = function
@@ -39,20 +78,49 @@ def _class_names(value) -> list[str]:
39
78
  return [f"{cls.__module__}.{cls.__qualname__}" for cls in type(value).__mro__]
40
79
 
41
80
 
81
+ def _find(value: Any, target) -> Callable | None:
82
+ """The registered converter from ``value``'s class (or a base class) to
83
+ ``target``, or None.
84
+
85
+ A class may be named by where it is imported from (``"pandas.DataFrame"``)
86
+ or where it is defined (``"pandas.core.frame.DataFrame"``, which pandas
87
+ 2 reports as the class's module, while pandas 3 reports ``"pandas"``).
88
+ Both the source and the target are matched by resolving the names to the
89
+ class. Only registered names in the same top-level package as the value
90
+ or the target are resolved, so looking up a pandas conversion imports
91
+ pandas and nothing else, and only once a conversion is asked for.
92
+ """
93
+ wanted = _name(target)
94
+ wanted_names = {wanted, _canonical(wanted)}
95
+ target_package = wanted.split(".")[0]
96
+ value_names = set(_class_names(value))
97
+ value_package = type(value).__module__.split(".")[0]
98
+
99
+ def matches(registered: str, names: set[str], package: str) -> bool:
100
+ if registered in names:
101
+ return True
102
+ return registered.split(".")[0] == package and _canonical(registered) in names
103
+
104
+ for (source, registered), function in _converters.items():
105
+ if matches(source, value_names, value_package) and \
106
+ matches(registered, wanted_names, target_package):
107
+ return function
108
+ return None
109
+
110
+
42
111
  def can_convert(value: Any, target) -> bool:
43
- return any((name, _name(target)) in _converters for name in _class_names(value))
112
+ return _name(target) in _class_names(value) or _find(value, target) is not None
44
113
 
45
114
 
46
115
  def convert(value: Any, target):
47
116
  """Convert ``value`` to ``target`` (a class or a dotted name)."""
48
117
  wanted = _name(target)
49
- for name in _class_names(value):
50
- if name == wanted:
51
- return value
52
- function = _converters.get((name, wanted))
53
- if function is not None:
54
- return function(value)
55
- raise TypeError(f"No converter from {type(value).__name__} to {wanted}")
118
+ if wanted in _class_names(value):
119
+ return value
120
+ function = _find(value, target)
121
+ if function is None:
122
+ raise TypeError(f"No converter from {type(value).__name__} to {wanted}")
123
+ return function(value)
56
124
 
57
125
 
58
126
  def converters() -> list[tuple[str, str]]:
@@ -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"]