openprocess 0.12.0__tar.gz → 0.12.2__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 (240) hide show
  1. {openprocess-0.12.0/openprocess.egg-info → openprocess-0.12.2}/PKG-INFO +6 -2
  2. {openprocess-0.12.0 → openprocess-0.12.2}/README.md +5 -1
  3. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/__init__.py +1 -1
  4. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/measure.py +72 -3
  5. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/viewers.py +78 -10
  6. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/app.py +102 -8
  7. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/dotted_chart.py +69 -313
  8. openprocess-0.12.2/openprocess/gui/studio/loading.py +166 -0
  9. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/style.py +3 -6
  10. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/analysis.py +22 -0
  11. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/definitions.py +11 -1
  12. openprocess-0.12.2/openprocess/mining/dotted.py +481 -0
  13. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/xes.py +46 -6
  14. openprocess-0.12.2/openprocess/palette.py +14 -0
  15. {openprocess-0.12.0 → openprocess-0.12.2/openprocess.egg-info}/PKG-INFO +6 -2
  16. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess.egg-info/SOURCES.txt +3 -0
  17. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_flow_gui.py +9 -0
  18. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_large_logs.py +25 -0
  19. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_measure.py +27 -1
  20. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_mining.py +19 -0
  21. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_studio_gui.py +96 -0
  22. {openprocess-0.12.0 → openprocess-0.12.2}/LICENSE +0 -0
  23. {openprocess-0.12.0 → openprocess-0.12.2}/cpnpy/__init__.py +0 -0
  24. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/analysis/__init__.py +0 -0
  25. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/analysis/state_space.py +0 -0
  26. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/analysis/state_space_process.py +0 -0
  27. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/citation.py +0 -0
  28. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/cli.py +0 -0
  29. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  30. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  31. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  32. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  33. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  34. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  35. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  36. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  37. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  38. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  39. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  40. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  41. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  42. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  43. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  44. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  45. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  46. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  47. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  48. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/exercises/pack.md +0 -0
  49. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/__init__.py +0 -0
  50. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/box.py +0 -0
  51. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/__init__.py +0 -0
  52. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/check.py +0 -0
  53. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/compare.py +0 -0
  54. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/cpn.py +0 -0
  55. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/discover.py +0 -0
  56. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/filter.py +0 -0
  57. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/input.py +0 -0
  58. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/output.py +0 -0
  59. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/predict.py +0 -0
  60. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/science.py +0 -0
  61. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/boxes/sweeps.py +0 -0
  62. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/convert.py +0 -0
  63. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/datasets.py +0 -0
  64. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/explain.py +0 -0
  65. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/figures.py +0 -0
  66. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/history.py +0 -0
  67. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/library.py +0 -0
  68. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/provenance.py +0 -0
  69. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/record.py +0 -0
  70. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/report.py +0 -0
  71. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/reproducibility.py +0 -0
  72. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/runner.py +0 -0
  73. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/script.py +0 -0
  74. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/snapshots.py +0 -0
  75. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/sweep.py +0 -0
  76. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/types.py +0 -0
  77. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/flow/workflow.py +0 -0
  78. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/__init__.py +0 -0
  79. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/app.py +0 -0
  80. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/arc_editing.py +0 -0
  81. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/canvas.py +0 -0
  82. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/__init__.py +0 -0
  83. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/canvas.py +0 -0
  84. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/page.py +0 -0
  85. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/picker.py +0 -0
  86. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/runs.py +0 -0
  87. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/stepper.py +0 -0
  88. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/summary.py +0 -0
  89. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/flow/templates.py +0 -0
  90. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/items.py +0 -0
  91. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/learn/answer_boxes.py +0 -0
  92. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/learn/concealment.py +0 -0
  93. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/learn/mode.py +0 -0
  94. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/panning.py +0 -0
  95. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/resources/openprocess-icon.png +0 -0
  96. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/__init__.py +0 -0
  97. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/__main__.py +0 -0
  98. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/alignment_view.py +0 -0
  99. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/charts.py +0 -0
  100. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/cite.py +0 -0
  101. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/command_palette.py +0 -0
  102. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/compare_page.py +0 -0
  103. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/connections.py +0 -0
  104. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/cpn_page.py +0 -0
  105. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/definition_view.py +0 -0
  106. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/derivation_view.py +0 -0
  107. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/documents.py +0 -0
  108. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/file_dialogs.py +0 -0
  109. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/filter_dialog.py +0 -0
  110. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/graph_builders.py +0 -0
  111. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/graph_view.py +0 -0
  112. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/instances.py +0 -0
  113. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/log_editor.py +0 -0
  114. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/log_page.py +0 -0
  115. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/markdown_view.py +0 -0
  116. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/mathtext.py +0 -0
  117. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/ml_highlighter.py +0 -0
  118. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/model_page.py +0 -0
  119. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/motion.py +0 -0
  120. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/net_comparison.py +0 -0
  121. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/notes_overlay.py +0 -0
  122. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/petri_page.py +0 -0
  123. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/provenance.py +0 -0
  124. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/regions_view.py +0 -0
  125. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/sidebar.py +0 -0
  126. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/spaces.py +0 -0
  127. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/tool_icons.py +0 -0
  128. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/updates.py +0 -0
  129. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/widgets.py +0 -0
  130. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/workers.py +0 -0
  131. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/studio/workspace.py +0 -0
  132. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/theme.py +0 -0
  133. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/gui/tidy.py +0 -0
  134. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/io/__init__.py +0 -0
  135. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/io/cpn_reader.py +0 -0
  136. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/io/cpn_writer.py +0 -0
  137. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/__init__.py +0 -0
  138. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/answers.py +0 -0
  139. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/checks.py +0 -0
  140. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/computed.py +0 -0
  141. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/context.py +0 -0
  142. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/exam.py +0 -0
  143. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/exercise-packs.md +0 -0
  144. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/importer.py +0 -0
  145. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/notation.py +0 -0
  146. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/pack.py +0 -0
  147. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/learn/sheet.py +0 -0
  148. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/__init__.py +0 -0
  149. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/cases.py +0 -0
  150. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/columns.py +0 -0
  151. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/compare_nets.py +0 -0
  152. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/conformance/__init__.py +0 -0
  153. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/conformance/alignments.py +0 -0
  154. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/conformance/quality.py +0 -0
  155. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/conformance/token_replay.py +0 -0
  156. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/csv_import.py +0 -0
  157. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/dfg.py +0 -0
  158. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/discovery/__init__.py +0 -0
  159. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/discovery/alpha.py +0 -0
  160. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/discovery/heuristics.py +0 -0
  161. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/discovery/inductive.py +0 -0
  162. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/discovery/state_regions.py +0 -0
  163. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/filtering.py +0 -0
  164. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/footprint.py +0 -0
  165. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/invariants.py +0 -0
  166. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/layout.py +0 -0
  167. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/log.py +0 -0
  168. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/ocel.py +0 -0
  169. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/petrinet.py +0 -0
  170. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/playout.py +0 -0
  171. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/pm4py_bridge.py +0 -0
  172. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/pnml.py +0 -0
  173. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/processtree.py +0 -0
  174. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/regions.py +0 -0
  175. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/stats.py +0 -0
  176. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/structure.py +0 -0
  177. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/mining/transition_system.py +0 -0
  178. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/__init__.py +0 -0
  179. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/ast_nodes.py +0 -0
  180. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/builtins.py +0 -0
  181. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/colorsets.py +0 -0
  182. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/errors.py +0 -0
  183. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/evaluator.py +0 -0
  184. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/lexer.py +0 -0
  185. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/multiset.py +0 -0
  186. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/parser.py +0 -0
  187. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/ml/values.py +0 -0
  188. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/model/__init__.py +0 -0
  189. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/model/declarations.py +0 -0
  190. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/model/examples.py +0 -0
  191. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/model/net.py +0 -0
  192. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/model/plain.py +0 -0
  193. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/packages.py +0 -0
  194. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/references.py +0 -0
  195. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/sim/__init__.py +0 -0
  196. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/sim/binding.py +0 -0
  197. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/sim/export.py +0 -0
  198. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/sim/simulator.py +0 -0
  199. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/teaching/__init__.py +0 -0
  200. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/teaching/answers.py +0 -0
  201. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/teaching/checks.py +0 -0
  202. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/teaching/pack.py +0 -0
  203. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess/teaching/sheet.py +0 -0
  204. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess.egg-info/dependency_links.txt +0 -0
  205. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess.egg-info/entry_points.txt +0 -0
  206. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess.egg-info/requires.txt +0 -0
  207. {openprocess-0.12.0 → openprocess-0.12.2}/openprocess.egg-info/top_level.txt +0 -0
  208. {openprocess-0.12.0 → openprocess-0.12.2}/pyproject.toml +0 -0
  209. {openprocess-0.12.0 → openprocess-0.12.2}/setup.cfg +0 -0
  210. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_arc_editing.py +0 -0
  211. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_citation.py +0 -0
  212. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_colorsets.py +0 -0
  213. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_columns.py +0 -0
  214. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_cpn_course_model.py +0 -0
  215. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_definitions.py +0 -0
  216. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_docs.py +0 -0
  217. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_exercises_gui.py +0 -0
  218. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_flow.py +0 -0
  219. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_framework_contract.py +0 -0
  220. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_hierarchy.py +0 -0
  221. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_history.py +0 -0
  222. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_io.py +0 -0
  223. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_learn.py +0 -0
  224. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_learn_gui.py +0 -0
  225. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_log_editing.py +0 -0
  226. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_ml.py +0 -0
  227. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_multiset.py +0 -0
  228. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_ocel.py +0 -0
  229. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_packages.py +0 -0
  230. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_regions.py +0 -0
  231. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_rename.py +0 -0
  232. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_report.py +0 -0
  233. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_simulation.py +0 -0
  234. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_snapshots.py +0 -0
  235. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_state_space.py +0 -0
  236. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_teaching.py +0 -0
  237. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_tidy.py +0 -0
  238. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_updates.py +0 -0
  239. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_wf_structure.py +0 -0
  240. {openprocess-0.12.0 → openprocess-0.12.2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.12.0
3
+ Version: 0.12.2
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
@@ -873,7 +873,11 @@ extra) and *Predict the next activity*. Or start empty.
873
873
  and cleared an invoice?) and *Compliance by attribute* (per vendor: cases,
874
874
  share that fits, value), with *Case attribute* to keep one category and
875
875
  *Bubble chart* to draw it. [examples/bpi2019](examples/bpi2019/README.md)
876
- uses them on the BPI Challenge 2019.
876
+ uses them on the BPI Challenge 2019. *Dotted chart* saves a dotted chart
877
+ as a figure (the same dots as the log page's tab), and *Who does what*
878
+ counts each activity per resource;
879
+ [examples/energy_contracts](examples/energy_contracts/README.md) uses them
880
+ with the course's miners and checks on the 2AMI10 exam log.
877
881
  - **Connections** (at the bottom of the sidebar, or *View ▸ Connections*)
878
882
  draws what flows in, OpenProcess and its boxes, what flows out, and the
879
883
  optional tools (pandas, numpy, scipy, matplotlib, …) with whether each is
@@ -834,7 +834,11 @@ extra) and *Predict the next activity*. Or start empty.
834
834
  and cleared an invoice?) and *Compliance by attribute* (per vendor: cases,
835
835
  share that fits, value), with *Case attribute* to keep one category and
836
836
  *Bubble chart* to draw it. [examples/bpi2019](examples/bpi2019/README.md)
837
- uses them on the BPI Challenge 2019.
837
+ uses them on the BPI Challenge 2019. *Dotted chart* saves a dotted chart
838
+ as a figure (the same dots as the log page's tab), and *Who does what*
839
+ counts each activity per resource;
840
+ [examples/energy_contracts](examples/energy_contracts/README.md) uses them
841
+ with the course's miners and checks on the 2AMI10 exam log.
838
842
  - **Connections** (at the bottom of the sidebar, or *View ▸ Connections*)
839
843
  draws what flows in, OpenProcess and its boxes, what flows out, and the
840
844
  optional tools (pandas, numpy, scipy, matplotlib, …) with whether each is
@@ -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.12.0"
51
+ __version__ = "0.12.2"
52
52
 
53
53
  __all__ = [
54
54
  "Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
@@ -5,7 +5,9 @@ the goods receipt? Did one user record an invoice and also clear it? How
5
5
  compliant is each vendor, and with how much money? Each box answers one such
6
6
  question for every case, in a table (and, where it helps, the cases
7
7
  themselves, to analyse further), and works on a large log without building
8
- its events (:func:`openprocess.mining.cases.cases`). The BPI Challenge 2019
8
+ its events (:func:`openprocess.mining.cases.cases`). The dotted chart and
9
+ *Who does what* are the first look at a log: when things happen, and who
10
+ does them. The BPI Challenge 2019
9
11
  analysis in ``examples/bpi2019`` is built from them.
10
12
  """
11
13
 
@@ -18,10 +20,11 @@ from typing import Literal
18
20
 
19
21
  from ... import flow
20
22
  from ...mining.cases import case_value, cases
23
+ from ...mining.dotted import SORT_MODES, X_MODES, DotSettings, Dots, how_to_read, svg
21
24
  from ...mining.log import EventLog
22
25
  from ...mining.petrinet import PetriNet
23
26
  from ..box import box
24
- from ..types import Table
27
+ from ..types import Figure, Table
25
28
 
26
29
  UNITS = {"hours": 3600.0, "days": 86400.0}
27
30
 
@@ -192,4 +195,70 @@ def compliance_by(model: PetriNet, log: EventLog, attribute: str = "Vendor",
192
195
  return Table(f"Compliance by {attribute}", columns, rows)
193
196
 
194
197
 
195
- __all__ = ["RuleResult", "compliance_by", "order_rule", "segregation_of_duties", "time_between"]
198
+ @box(name="Dotted chart", group="Measure")
199
+ def dotted_chart(log: EventLog,
200
+ x: Literal["Actual time", "Time since case start", "Relative to case duration (%)",
201
+ "Logical: order in log", "Logical: position in case"] = "Actual time",
202
+ rows: str = "Case", colour: str = "Activity",
203
+ sort: Literal["First event", "Last event", "Duration", "Number of events", "Name"] = "First event",
204
+ unit: Literal["Auto", "Seconds", "Minutes", "Hours", "Days", "Weeks"] = "Auto") -> Figure:
205
+ """Every event as a dot (Song & van der Aalst, 2007): *x* says when, the
206
+ row what it belongs to, the colour one more attribute; the same dots as
207
+ the dotted chart in Mine, saved as a figure. Patterns to look for: the
208
+ arrival rate (the slope of the first dots), batches (vertical lines),
209
+ waiting (long gaps in a row), and in Time since case start, how long
210
+ each step takes after the case begins.
211
+
212
+ x: when: actual time, time since the case started, the share of the case's duration, or a logical position
213
+ rows: Case, Activity, Resource, Lifecycle or any event attribute
214
+ colour: Activity, Resource, Lifecycle, Case, None or any event attribute
215
+ sort: the order of the rows
216
+ unit: the time unit of the x axis (Auto: dates and durations)
217
+ """
218
+ settings = DotSettings(x_mode=X_MODES.index(x), rows=rows, colour_by=colour, sort=SORT_MODES.index(sort))
219
+ dots = Dots(log, log.default_classifier(), settings)
220
+ if not len(dots):
221
+ raise ValueError(f"{log.name} has no events to draw")
222
+ if dots.mode != settings.x_mode:
223
+ flow.note(f"The log has no timestamps, so x is {X_MODES[dots.mode].lower()}")
224
+ flow.note(f"{len(dots):,} events in {len(dots.row_values):,} rows ({rows}), "
225
+ f"{len(dots.colour_values):,} colours ({colour})")
226
+ if dots.mode == 0 and dots.origin is not None:
227
+ from datetime import timedelta
228
+ flow.note(f"From {dots.origin:%d %b %Y %H:%M} to "
229
+ f"{dots.origin + timedelta(seconds=dots.x_max - dots.x_min):%d %b %Y %H:%M}")
230
+ title = f"{log.name}: {rows.lower()} by {X_MODES[dots.mode].lower()}, colour by {colour.lower()}"
231
+ return Figure("Dotted chart", svg=svg(dots, title, unit),
232
+ caption=f"{how_to_read(settings, dots.mode)} Colour: {colour}.")
233
+
234
+
235
+ @box(name="Who does what", group="Measure")
236
+ def who_does_what(log: EventLog, by: str = "org:resource") -> Table:
237
+ """How often each activity is done by each value of the event attribute
238
+ *by* (each resource, say): one row per activity, one column per value,
239
+ most frequent first. It shows the roles in a process: who only does one
240
+ step, who does several, and which steps nobody else does.
241
+
242
+ by: the event attribute to count by (org:resource, or any other)
243
+ """
244
+ counts: dict[str, Counter] = {}
245
+ totals: Counter = Counter()
246
+ for case in cases(log, (by,)):
247
+ for activity, value in zip(case.activities, case.columns[by]):
248
+ name = "–" if value is None else str(value)
249
+ counts.setdefault(activity, Counter())[name] += 1
250
+ totals[name] += 1
251
+ if not totals or set(totals) == {"–"}:
252
+ raise ValueError(f"No event of {log.name} has the attribute {by!r}")
253
+ values = [v for v, _ in totals.most_common()]
254
+ activities = sorted(counts, key=lambda a: -sum(counts[a].values()))
255
+ rows = [[a, *[counts[a][v] for v in values], sum(counts[a].values())] for a in activities]
256
+ for value in values:
257
+ done = [a for a in activities if counts[a][value]]
258
+ if len(done) == 1:
259
+ flow.note(f"{value} only does {done[0]}")
260
+ return Table(f"Who does what ({by})", ["activity", *values, "events"], rows)
261
+
262
+
263
+ __all__ = ["RuleResult", "compliance_by", "dotted_chart", "order_rule", "segregation_of_duties",
264
+ "time_between", "who_does_what"]
@@ -398,17 +398,10 @@ def result_widget(value, page=None, node=None) -> QWidget:
398
398
  if value.note:
399
399
  add(label(value.note, "muted", wrap=True))
400
400
  elif isinstance(value, Figure):
401
- shown = False
401
+ shown, svg = False, None
402
402
  if value.svg:
403
403
  try:
404
- from PySide6.QtSvgWidgets import QSvgWidget
405
- svg = QSvgWidget()
406
- svg.load(value.svg.encode("utf-8"))
407
- svg.setMinimumHeight(260)
408
- svg.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
409
- size = svg.renderer().defaultSize()
410
- if size.width() > 0:
411
- svg.setMaximumHeight(int(min(600, max(260, size.height() * 1.4))))
404
+ svg = SvgFigure(value.svg)
412
405
  add(svg)
413
406
  shown = True
414
407
  except ImportError:
@@ -425,8 +418,11 @@ def result_widget(value, page=None, node=None) -> QWidget:
425
418
  add(label("The figure has no SVG or PNG form.", "muted"))
426
419
  if value.caption:
427
420
  add(label(value.caption, "muted", wrap=True))
421
+ buttons = [button("Open larger", lambda: open_figure(value, svg))] if value.svg and shown else []
428
422
  if page is not None:
429
- add(hbox(button("Save as SVG…", lambda: page.save_figure(value)), None))
423
+ buttons.append(button("Save as SVG…", lambda: page.save_figure(value)))
424
+ if buttons:
425
+ add(hbox(*buttons, None))
430
426
  elif isinstance(value, Text):
431
427
  text = QPlainTextEdit(value.text)
432
428
  text.setReadOnly(True)
@@ -1047,3 +1043,75 @@ def status_text(result: Result | None) -> tuple[str, str]:
1047
1043
  if result.status == "blocked":
1048
1044
  return result.message or "Not allowed to run yet", "warning"
1049
1045
  return result.message or "Waiting", "muted"
1046
+
1047
+
1048
+ class SvgFigure(QWidget):
1049
+ """An SVG figure drawn at the width it is given and in its own
1050
+ proportions, never stretched: text stays readable and nothing is cut
1051
+ off. Click it to see it at full size."""
1052
+
1053
+ def __init__(self, svg: str, parent=None) -> None:
1054
+ from PySide6.QtSvg import QSvgRenderer
1055
+ super().__init__(parent)
1056
+ self.renderer = QSvgRenderer(svg.encode("utf-8"), self)
1057
+ size = self.renderer.defaultSize()
1058
+ self.ratio = size.height() / size.width() if size.width() > 0 else 0.6
1059
+ self.svg = svg
1060
+ policy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
1061
+ policy.setHeightForWidth(True)
1062
+ self.setSizePolicy(policy)
1063
+ self.setMinimumWidth(200)
1064
+ self.setCursor(Qt.PointingHandCursor)
1065
+ self.setToolTip("Click to see the figure at full size")
1066
+
1067
+ def hasHeightForWidth(self) -> bool:
1068
+ return True
1069
+
1070
+ def heightForWidth(self, width: int) -> int:
1071
+ return max(1, round(width * self.ratio))
1072
+
1073
+ def sizeHint(self):
1074
+ from PySide6.QtCore import QSize
1075
+ size = self.renderer.defaultSize()
1076
+ return QSize(size.width(), size.height()) if size.width() > 0 else QSize(560, 336)
1077
+
1078
+ def paintEvent(self, event) -> None:
1079
+ from PySide6.QtCore import QRectF
1080
+ from PySide6.QtGui import QPainter
1081
+ painter = QPainter(self)
1082
+ painter.setRenderHint(QPainter.Antialiasing)
1083
+ width = min(self.width(), round(self.height() / self.ratio)) if self.ratio else self.width()
1084
+ height = round(width * self.ratio)
1085
+ self.renderer.render(painter, QRectF((self.width() - width) / 2, 0, width, height))
1086
+ painter.end()
1087
+
1088
+ def mouseReleaseEvent(self, event) -> None:
1089
+ if event.button() == Qt.LeftButton:
1090
+ open_figure(None, self)
1091
+ super().mouseReleaseEvent(event)
1092
+
1093
+
1094
+ def open_figure(figure, widget: "SvgFigure | None") -> None:
1095
+ """The figure in a window of its own, as large as the screen allows;
1096
+ resizing the window resizes the figure, in proportion."""
1097
+ if widget is None:
1098
+ return
1099
+ window = QDialog(widget.window())
1100
+ window.setWindowTitle(getattr(figure, "name", None) or "Figure")
1101
+ full = SvgFigure(widget.svg)
1102
+ full.setCursor(Qt.ArrowCursor)
1103
+ full.setToolTip("")
1104
+ full.mouseReleaseEvent = lambda event: None
1105
+ full.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
1106
+ layout = QVBoxLayout(window)
1107
+ layout.setContentsMargins(12, 12, 12, 12)
1108
+ layout.addWidget(full)
1109
+ size = full.sizeHint()
1110
+ width, height = size.width() + 24, size.height() + 24
1111
+ screen = widget.screen().availableGeometry() if widget.screen() else None
1112
+ if screen is not None:
1113
+ scale = min(1.0, (screen.width() - 80) / width, (screen.height() - 80) / height)
1114
+ width, height = round(width * scale), round(height * scale)
1115
+ window.resize(width, height)
1116
+ window.setAttribute(Qt.WA_DeleteOnClose)
1117
+ window.show()
@@ -101,6 +101,7 @@ from .workspace import (
101
101
  exercise_files, file_kind, file_stem, file_suffix,
102
102
  made_by_openprocess, safe_file_name, unique_path,
103
103
  )
104
+ from .loading import Loading, LoadingPage
104
105
  from .workers import run_in_background
105
106
 
106
107
  APPLICATION_NAME = "OpenProcess Studio"
@@ -181,6 +182,17 @@ def _same_file(a: Path, b: Path) -> bool:
181
182
  return False
182
183
 
183
184
 
185
+ #: How long a read may take before the loading page shows (a small file never flashes it).
186
+ LOADING_DELAY_MS = 350
187
+
188
+
189
+ def _file_size(path: str) -> int:
190
+ try:
191
+ return Path(path).stat().st_size
192
+ except OSError:
193
+ return 0
194
+
195
+
184
196
  def _signature(path: str | Path) -> tuple[int, int] | None:
185
197
  """(modification time, size) of a file, or None when it is not there."""
186
198
  try:
@@ -581,6 +593,12 @@ class StudioWindow(QMainWindow):
581
593
  self.connections_page = ConnectionsPage()
582
594
  self.connections_page.install_requested.connect(self.install_tool)
583
595
  self.content.addWidget(self.connections_page)
596
+ #: What the main area shows while a file you opened is read (see _start_loading).
597
+ self.loading_page = LoadingPage()
598
+ self.loading_page.cancel_requested.connect(self._cancel_loading)
599
+ self.content.addWidget(self.loading_page)
600
+ self._loads: dict[str, Loading] = {}
601
+ self._before_loading = None
584
602
  # Above the pages: "a new version is out" (see check_automatically).
585
603
  from .updates import UpdateBar
586
604
  self.update_bar = UpdateBar()
@@ -1572,8 +1590,7 @@ class StudioWindow(QMainWindow):
1572
1590
  page = ComparePage(document)
1573
1591
  page.tabs.changed.connect(lambda i: self.remembered.__setitem__("compare_tab", i))
1574
1592
  elif isinstance(document, WorkflowDocument):
1575
- page = WorkflowPage(document, self.library(),
1576
- self.workspace.folder if self.workspace is not None else None)
1593
+ page = WorkflowPage(document, self.library(), self._workflow_folder(document))
1577
1594
  page.open_document.connect(lambda doc, source=document: self.add_document(doc, near=source.path))
1578
1595
  page.edit_requested.connect(lambda net, origin, doc=document: self.edit_petri_net(net, origin, doc))
1579
1596
  page.keep_requested.connect(lambda doc=document: self.keep_workflow(doc))
@@ -2527,14 +2544,24 @@ class StudioWindow(QMainWindow):
2527
2544
  self._rebuild_sidebar()
2528
2545
  return
2529
2546
  mapping = dialog.mapping()
2530
- log = read_csv(path, mapping)
2531
- self.add_document(LogDocument(log, path=path),
2532
- options={"csv_mapping": asdict(mapping)})
2547
+ self._start_loading(path, "Reading the event log")
2548
+ folder = self._folder_switches
2549
+
2550
+ def csv_loaded(log, mapping=mapping) -> None:
2551
+ self._finish_loading(path)
2552
+ if folder != self._folder_switches:
2553
+ self._opening.discard(self._key(path))
2554
+ return
2555
+ self.add_document(LogDocument(log, path=path), options={"csv_mapping": asdict(mapping)})
2556
+
2557
+ run_in_background(lambda: read_csv(path, mapping), csv_loaded,
2558
+ lambda message: self._load_failed(path, message, size))
2533
2559
  elif lower.endswith((".xes", ".xes.gz", ".gz")):
2534
- self.statusBar().showMessage(f"Reading {name}…")
2560
+ loading = self._start_loading(path, "Reading the event log")
2535
2561
  folder = self._folder_switches
2536
2562
 
2537
2563
  def loaded(log) -> None:
2564
+ self._finish_loading(path)
2538
2565
  if folder != self._folder_switches:
2539
2566
  # The folder was switched or closed while this was
2540
2567
  # loading: it belongs to the folder that was left.
@@ -2543,8 +2570,8 @@ class StudioWindow(QMainWindow):
2543
2570
  self.add_document(LogDocument(log, path=path))
2544
2571
  self.statusBar().showMessage(f"Opened {name}: {len(log):,} cases", 6000)
2545
2572
 
2546
- run_in_background(lambda: read_xes(path), loaded,
2547
- lambda message: self._could_not_open(path, message, size))
2573
+ run_in_background(lambda: read_xes(path, progress=loading.report), loaded,
2574
+ lambda message: self._load_failed(path, message, size))
2548
2575
  else:
2549
2576
  QMessageBox.information(self, "Unsupported file",
2550
2577
  f"{name}: open .xes, .csv, .pnml or .cpn files (or a "
@@ -2554,6 +2581,61 @@ class StudioWindow(QMainWindow):
2554
2581
  except Exception as error: # noqa: BLE001 - surface any import problem
2555
2582
  self._could_not_open(path, str(error), size)
2556
2583
 
2584
+ # -- reading a large file: the loading page --------------------------------------------------
2585
+ def _start_loading(self, path: str, title: str) -> Loading:
2586
+ """Start reading ``path`` on a worker. For a file the user opened (not
2587
+ one reopened with its folder), the main area shows the loading page
2588
+ if the read takes more than a moment, so a large log never leaves the
2589
+ window looking stuck; the window keeps working meanwhile."""
2590
+ key = self._key(path)
2591
+ loading = Loading(path, title, size=_file_size(path))
2592
+ self._loads[key] = loading
2593
+ self.statusBar().showMessage(f"Reading {Path(path).name}…")
2594
+ if key not in self._restore_paths:
2595
+ origin = self.content.currentWidget()
2596
+ QTimer.singleShot(LOADING_DELAY_MS, lambda: self._show_loading(key, origin))
2597
+ return loading
2598
+
2599
+ def _show_loading(self, key: str, origin) -> None:
2600
+ loading = self._loads.get(key)
2601
+ if loading is None or self.in_learn:
2602
+ return # done already, or Learn has the window
2603
+ if self.content.currentWidget() not in (origin, self.loading_page):
2604
+ return # you went to another page meanwhile: stay there
2605
+ if self.content.currentWidget() is not self.loading_page:
2606
+ self._before_loading = self.content.currentWidget()
2607
+ self.loading_page.show_loading(loading)
2608
+ self.content.setCurrentWidget(self.loading_page)
2609
+
2610
+ def _finish_loading(self, path: str) -> None:
2611
+ """The read ended (done, failed or stopped): leave the loading page if it shows this file."""
2612
+ loading = self._loads.pop(self._key(path), None)
2613
+ if loading is None or self.loading_page.loading is not loading:
2614
+ return
2615
+ self.loading_page.stop()
2616
+ self.loading_page.loading = None
2617
+ waiting = next(iter(self._loads.values()), None)
2618
+ if waiting is not None and self.content.currentWidget() is self.loading_page:
2619
+ self.loading_page.show_loading(waiting) # another file you opened is still coming
2620
+ elif self.content.currentWidget() is self.loading_page:
2621
+ back = self._before_loading
2622
+ self.content.setCurrentWidget(back if back is not None and self.content.indexOf(back) >= 0
2623
+ else self.content.widget(0))
2624
+
2625
+ def _cancel_loading(self, path: str) -> None:
2626
+ self.statusBar().showMessage(f"Stopping {Path(path).name}…")
2627
+
2628
+ def _load_failed(self, path: str, message: str, before: tuple | None) -> None:
2629
+ loading = self._loads.get(self._key(path))
2630
+ cancelled = loading is not None and loading.cancelled
2631
+ self._finish_loading(path)
2632
+ if cancelled:
2633
+ self._opening.discard(self._key(path))
2634
+ self._rebuild_sidebar()
2635
+ self.statusBar().showMessage(f"Stopped reading {Path(path).name}", 6000)
2636
+ return
2637
+ self._could_not_open(path, message, before)
2638
+
2557
2639
  def _could_not_open(self, path: str, message: str, before: tuple | None) -> None:
2558
2640
  self._opening.discard(self._key(path))
2559
2641
  self._rebuild_sidebar()
@@ -2762,6 +2844,18 @@ class StudioWindow(QMainWindow):
2762
2844
  self.statusBar().showMessage(f"{document.name} against {Path(path).name}: click Check fit for the "
2763
2845
  "scores and how they were computed", 10000)
2764
2846
 
2847
+ def _workflow_folder(self, document) -> str | None:
2848
+ """The folder a workflow's files are relative to: the open folder, or
2849
+ the workflow file's own folder when it was opened from somewhere else
2850
+ (an example double-clicked, a file from a download), as
2851
+ ``openprocess run`` reads it."""
2852
+ folder = self.workspace.folder if self.workspace is not None else None
2853
+ if document.path:
2854
+ own = Path(document.path).resolve().parent
2855
+ if folder is None or not own.is_relative_to(Path(folder).resolve()):
2856
+ return str(own)
2857
+ return folder
2858
+
2765
2859
  def library(self):
2766
2860
  """The boxes available in this window (the folder's boxes/ included)."""
2767
2861
  from ...flow.library import library_for