openprocess 0.15.0__tar.gz → 0.16.1__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 (274) hide show
  1. {openprocess-0.15.0/openprocess.egg-info → openprocess-0.16.1}/PKG-INFO +18 -13
  2. {openprocess-0.15.0 → openprocess-0.16.1}/README.md +17 -12
  3. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/__init__.py +1 -1
  4. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/cli.py +28 -14
  5. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/pack.md +1 -1
  6. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/__init__.py +1 -1
  7. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/record.py +113 -46
  8. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/report.py +1 -1
  9. openprocess-0.16.1/openprocess/flow/reproducibility.py +110 -0
  10. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/studio.py +2 -2
  11. openprocess-0.16.1/openprocess/flow/versions.py +102 -0
  12. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/workflow.py +2 -2
  13. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/page.py +112 -39
  14. openprocess-0.16.1/openprocess/gui/flow/versions.py +188 -0
  15. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/learn/mode.py +8 -3
  16. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/app.py +31 -10
  17. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/documents.py +2 -1
  18. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/dotted_chart.py +41 -6
  19. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/workspace.py +7 -2
  20. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/checks.py +1 -1
  21. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/computed.py +1 -1
  22. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/exercise-packs.md +5 -5
  23. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/pack.py +2 -2
  24. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/api.py +9 -8
  25. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/connecting.md +1 -1
  26. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/server.py +5 -5
  27. {openprocess-0.15.0 → openprocess-0.16.1/openprocess.egg-info}/PKG-INFO +18 -13
  28. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/SOURCES.txt +2 -1
  29. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_dotted_chart.py +25 -0
  30. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_flow.py +11 -5
  31. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_flow_gui.py +96 -28
  32. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_framework_contract.py +39 -5
  33. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_learn.py +1 -1
  34. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_learn_gui.py +1 -1
  35. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_mcp.py +3 -3
  36. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_parallel_boxes.py +2 -2
  37. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_researcher_api.py +2 -2
  38. openprocess-0.16.1/tests/test_snapshots.py +98 -0
  39. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_studio_gui.py +35 -4
  40. openprocess-0.15.0/openprocess/flow/reproducibility.py +0 -79
  41. openprocess-0.15.0/openprocess/gui/flow/runs.py +0 -130
  42. openprocess-0.15.0/tests/test_snapshots.py +0 -55
  43. {openprocess-0.15.0 → openprocess-0.16.1}/LICENSE +0 -0
  44. {openprocess-0.15.0 → openprocess-0.16.1}/cpnpy/__init__.py +0 -0
  45. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/analysis/__init__.py +0 -0
  46. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/analysis/state_space.py +0 -0
  47. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/analysis/state_space_process.py +0 -0
  48. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/citation.py +0 -0
  49. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  50. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  51. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  52. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  53. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  54. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  55. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  56. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  57. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  58. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  59. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  60. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  61. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  62. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  63. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  64. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  65. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  66. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  67. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  68. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/box.py +0 -0
  69. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/__init__.py +0 -0
  70. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/check.py +0 -0
  71. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/compare.py +0 -0
  72. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/cpn.py +0 -0
  73. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/discover.py +0 -0
  74. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/filter.py +0 -0
  75. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/input.py +0 -0
  76. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/measure.py +0 -0
  77. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/output.py +0 -0
  78. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/predict.py +0 -0
  79. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/science.py +0 -0
  80. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/boxes/sweeps.py +0 -0
  81. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/changes.py +0 -0
  82. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/convert.py +0 -0
  83. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/datasets.py +0 -0
  84. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/derivation.py +0 -0
  85. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/explain.py +0 -0
  86. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/figures.py +0 -0
  87. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/history.py +0 -0
  88. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/library.py +0 -0
  89. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/loading.py +0 -0
  90. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/parallel.py +0 -0
  91. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/provenance.py +0 -0
  92. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/runner.py +0 -0
  93. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/script.py +0 -0
  94. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/snapshots.py +0 -0
  95. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/sweep.py +0 -0
  96. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/flow/types.py +0 -0
  97. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/__init__.py +0 -0
  98. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/app.py +0 -0
  99. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/arc_editing.py +0 -0
  100. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/canvas.py +0 -0
  101. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/__init__.py +0 -0
  102. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/canvas.py +0 -0
  103. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/picker.py +0 -0
  104. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/report.py +0 -0
  105. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/stepper.py +0 -0
  106. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/templates.py +0 -0
  107. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/flow/viewers.py +0 -0
  108. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/items.py +0 -0
  109. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/learn/answer_boxes.py +0 -0
  110. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/learn/concealment.py +0 -0
  111. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/panning.py +0 -0
  112. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/resources/openprocess-icon.png +0 -0
  113. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/__init__.py +0 -0
  114. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/__main__.py +0 -0
  115. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/alignment_view.py +0 -0
  116. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/charts.py +0 -0
  117. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/cite.py +0 -0
  118. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/command_palette.py +0 -0
  119. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/compare_page.py +0 -0
  120. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/connect.py +0 -0
  121. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/connections.py +0 -0
  122. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/cpn_page.py +0 -0
  123. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/definition_view.py +0 -0
  124. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/derivation_view.py +0 -0
  125. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/file_dialogs.py +0 -0
  126. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/filter_dialog.py +0 -0
  127. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/graph_builders.py +0 -0
  128. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/graph_view.py +0 -0
  129. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/instances.py +0 -0
  130. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/loading.py +0 -0
  131. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/log_editor.py +0 -0
  132. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/log_page.py +0 -0
  133. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/markdown_view.py +0 -0
  134. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/mathtext.py +0 -0
  135. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/ml_highlighter.py +0 -0
  136. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/model_page.py +0 -0
  137. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/motion.py +0 -0
  138. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/net_comparison.py +0 -0
  139. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/notes_overlay.py +0 -0
  140. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/petri_page.py +0 -0
  141. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/provenance.py +0 -0
  142. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/regions_view.py +0 -0
  143. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/remote.py +0 -0
  144. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/review.py +0 -0
  145. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/sidebar.py +0 -0
  146. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/spaces.py +0 -0
  147. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/style.py +0 -0
  148. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/tool_icons.py +0 -0
  149. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/updates.py +0 -0
  150. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/widgets.py +0 -0
  151. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/studio/workers.py +0 -0
  152. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/theme.py +0 -0
  153. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/gui/tidy.py +0 -0
  154. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/io/__init__.py +0 -0
  155. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/io/cpn_reader.py +0 -0
  156. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/io/cpn_writer.py +0 -0
  157. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/__init__.py +0 -0
  158. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/answers.py +0 -0
  159. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/context.py +0 -0
  160. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/exam.py +0 -0
  161. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/importer.py +0 -0
  162. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/notation.py +0 -0
  163. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/learn/sheet.py +0 -0
  164. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/__init__.py +0 -0
  165. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/__main__.py +0 -0
  166. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mcp/install.py +0 -0
  167. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/__init__.py +0 -0
  168. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/analysis.py +0 -0
  169. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/bpmn.py +0 -0
  170. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/cases.py +0 -0
  171. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/columns.py +0 -0
  172. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/compare_nets.py +0 -0
  173. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/__init__.py +0 -0
  174. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/alignments.py +0 -0
  175. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/quality.py +0 -0
  176. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/conformance/token_replay.py +0 -0
  177. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/csv_import.py +0 -0
  178. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/definitions.py +0 -0
  179. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/dfg.py +0 -0
  180. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/__init__.py +0 -0
  181. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/alpha.py +0 -0
  182. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/heuristics.py +0 -0
  183. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/inductive.py +0 -0
  184. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/discovery/state_regions.py +0 -0
  185. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/dotted.py +0 -0
  186. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/drawing.py +0 -0
  187. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/filtering.py +0 -0
  188. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/footprint.py +0 -0
  189. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/invariants.py +0 -0
  190. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/layout.py +0 -0
  191. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/log.py +0 -0
  192. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/ocel.py +0 -0
  193. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/parallel.py +0 -0
  194. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/petrinet.py +0 -0
  195. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/playout.py +0 -0
  196. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/pm4py_bridge.py +0 -0
  197. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/pnml.py +0 -0
  198. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/processtree.py +0 -0
  199. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/regions.py +0 -0
  200. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/stats.py +0 -0
  201. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/structure.py +0 -0
  202. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/transition_system.py +0 -0
  203. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/mining/xes.py +0 -0
  204. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/__init__.py +0 -0
  205. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/ast_nodes.py +0 -0
  206. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/builtins.py +0 -0
  207. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/colorsets.py +0 -0
  208. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/errors.py +0 -0
  209. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/evaluator.py +0 -0
  210. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/lexer.py +0 -0
  211. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/multiset.py +0 -0
  212. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/parser.py +0 -0
  213. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/ml/values.py +0 -0
  214. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/__init__.py +0 -0
  215. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/declarations.py +0 -0
  216. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/examples.py +0 -0
  217. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/net.py +0 -0
  218. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/model/plain.py +0 -0
  219. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/packages.py +0 -0
  220. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/palette.py +0 -0
  221. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/parallel.py +0 -0
  222. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/references.py +0 -0
  223. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/__init__.py +0 -0
  224. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/binding.py +0 -0
  225. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/export.py +0 -0
  226. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/performance.py +0 -0
  227. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/sim/simulator.py +0 -0
  228. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/__init__.py +0 -0
  229. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/answers.py +0 -0
  230. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/checks.py +0 -0
  231. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/pack.py +0 -0
  232. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess/teaching/sheet.py +0 -0
  233. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/dependency_links.txt +0 -0
  234. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/entry_points.txt +0 -0
  235. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/requires.txt +0 -0
  236. {openprocess-0.15.0 → openprocess-0.16.1}/openprocess.egg-info/top_level.txt +0 -0
  237. {openprocess-0.15.0 → openprocess-0.16.1}/pyproject.toml +0 -0
  238. {openprocess-0.15.0 → openprocess-0.16.1}/setup.cfg +0 -0
  239. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_arc_editing.py +0 -0
  240. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_bpmn.py +0 -0
  241. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_citation.py +0 -0
  242. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_colorsets.py +0 -0
  243. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_columns.py +0 -0
  244. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_cpn_course_model.py +0 -0
  245. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_definitions.py +0 -0
  246. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_docs.py +0 -0
  247. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_exercises_gui.py +0 -0
  248. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_hierarchy.py +0 -0
  249. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_history.py +0 -0
  250. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_io.py +0 -0
  251. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_large_log_page.py +0 -0
  252. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_large_logs.py +0 -0
  253. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_log_editing.py +0 -0
  254. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_mcp_gui.py +0 -0
  255. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_measure.py +0 -0
  256. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_mining.py +0 -0
  257. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_ml.py +0 -0
  258. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_multiset.py +0 -0
  259. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_ocel.py +0 -0
  260. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_packages.py +0 -0
  261. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_parallel_and_loading.py +0 -0
  262. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_performance.py +0 -0
  263. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_regions.py +0 -0
  264. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_rename.py +0 -0
  265. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_report.py +0 -0
  266. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_review_fixes.py +0 -0
  267. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_simulation.py +0 -0
  268. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_state_space.py +0 -0
  269. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_studio_lazy.py +0 -0
  270. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_teaching.py +0 -0
  271. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_tidy.py +0 -0
  272. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_updates.py +0 -0
  273. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_wf_structure.py +0 -0
  274. {openprocess-0.15.0 → openprocess-0.16.1}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.15.0
3
+ Version: 0.16.1
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
@@ -73,7 +73,7 @@ tools (and, until version 0.7, went by the name CPNpy):
73
73
  | **Petri nets & WF-nets** | Draw nets the way the lectures do. Get a soundness verdict with a counterexample for every violation, and replay it in the token game. Also: behavioural properties, P- and T-invariants, the footprint matrix, the reachability graph, PNML import and export. | WoPeD, ProM, pen and paper |
74
74
  | **Process mining** | Import XES or CSV logs, OCEL 2.0 object-centric logs, or type textbook logs like `[<a,b,c>^3, <a,c>^2]`. Filter them, explore variants, the dotted chart and the process map. Discover models (α-algorithm, Inductive Miner, Heuristics Miner, state-based regions). Check conformance (token replay, alignments, precision…) and compare logs. | ProM, Disco |
75
75
  | **Coloured Petri nets** | Open, edit and save CPN Tools models (`.cpn`), hierarchical ones included. Step through or simulate them, compute the state space, and export a simulation as an event log to mine. | CPN Tools / CPN IDE |
76
- | **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.cpnflow` file with everything needed to get the same numbers back. Your own algorithm is one Python function in a `boxes/` folder. | RapidProM |
76
+ | **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.analysis` file with everything needed to get the same numbers back. Your own algorithm is one Python function in a `boxes/` folder. | RapidProM |
77
77
  | **Learn** | A mode of its own for worksheets, built on the app: answer in boxes on the sheet (sets, markings, matrices, cuts and trees, alignments, a net in the editor or a workflow on the canvas beside it) and press **Check**. Most answers are checked automatically, often against answers worked out from the given log or net. Packs are plain Markdown, can be exams with a clock and points, and a past exam imports as a skeleton pack. Demo exercises included. | Answer sheets, a notes app and guesswork |
78
78
  | **Folders** | Open a folder such as *Week 2*: every log and net in it is listed in the sidebar, with its subfolders. The folder and the app stay in step both ways: new nets and edits are saved into it as you go, and changes made in Finder show up by themselves. | Finder windows and *File ▸ Open* every time |
79
79
 
@@ -598,7 +598,7 @@ without distractions, and *Exit* brings your folder back exactly as it was.
598
598
  writes the marks as CSV. A pack can also give every student a **variant**
599
599
  of its own (a log played out from a net with a seed made from their name).
600
600
  - **Your work is saved as you go**, in the exercise's folder:
601
- `my answers.json`, `my answer.pnml` for a net, `my workflow.cpnflow` for a
601
+ `my answers.json`, `my answer.pnml` for a net, `my workflow.analysis` for a
602
602
  workflow and `my notes.md`.
603
603
 
604
604
  **Writing a pack** (for a course or an exam) is plain Markdown: put an
@@ -796,9 +796,9 @@ extra) and *Predict the next activity*. Or start empty.
796
796
  | ![The canvas of a Compare discovery analysis, the Compare box's table beside it](docs/screenshots/workflow-canvas.png) | ![The Report view: a section per box with its result, a Steps outline beside them](docs/screenshots/workflow-report.png) |
797
797
 
798
798
  - **An analysis opens on its Report.** Its header has **Report | Canvas |
799
- Runs**, then **+ Add box**, **Run ▶**, and **⋯** for Re-run, Record,
800
- Export experiment and Save. The sidebar beside it lists the folder's
801
- analyses and, folded under them, its logs.
799
+ Versions**, then **+ Add box**, **Run ▶**, and **⋯** for Run from scratch,
800
+ Keep as version, Record, Export experiment and Save. The sidebar beside it
801
+ lists the folder's analyses and, folded under them, its logs.
802
802
  - **Report** is every box's result, in the order the boxes run, on one
803
803
  scrolling page: a section per box with the group it is in, its name, the
804
804
  line the canvas shows under it, its status, the result as the side panel
@@ -846,9 +846,14 @@ extra) and *Predict the next activity*. Or start empty.
846
846
  - **Only what changed runs again.** Every box shows a status dot: waiting,
847
847
  running, done, failed (the error is on the Result tab; the rest keeps
848
848
  working), or waiting for your OK.
849
- - **Record** shows the workflow as Python and the file with its record;
850
- **Re-run** says what changed since it was saved (an input file, a box file,
851
- a seed) before running everything again.
849
+ - **Versions** is where the numbers stand against the record and every
850
+ version kept. The first run with no failure is kept as v1; from then on the
851
+ badge beside the title says *v1 · reproducible*, *v1 · 2 changes* or
852
+ *Changed since v1*, and **Keep as version…** records the run as v2 with a
853
+ note. Any version can be compared with now (the boxes whose result differs,
854
+ first) or restored whole. **Record** shows the workflow as Python and the
855
+ file; **Run from scratch** says what changed since the version was kept
856
+ before computing everything again.
852
857
  - **Your own boxes**: a `.py` file in the folder's `boxes/` subfolder (see
853
858
  below). The app asks once per folder before running them, and reloads a
854
859
  box when its file is saved.
@@ -910,7 +915,7 @@ extra) and *Predict the next activity*. Or start empty.
910
915
 
911
916
  Every algorithm is also a **box**: a Python function with type hints that
912
917
  the app (from 0.7) draws on a canvas. A workflow of boxes runs from Python
913
- or from the command line, and its `.cpnflow` file records what is needed
918
+ or from the command line, and its `.analysis` file records what is needed
914
919
  to get the same numbers again: fingerprints of the input files and of the
915
920
  box code, every seed, and the installed packages.
916
921
 
@@ -928,12 +933,12 @@ wf.connect(log, model); wf.connect(model, fit, "model"); wf.connect(log, fit, "l
928
933
  run = Runner().run(wf)
929
934
  print(run.value(fit).metrics) # fitness, precision, generalisation, simplicity
930
935
  print(run.result(model).explanation.steps) # how the Inductive Miner got there
931
- save(wf, "orders.cpnflow", run)
936
+ save(wf, "orders.analysis", run)
932
937
  ```
933
938
 
934
939
  ```bash
935
- openprocess run orders.cpnflow --check # re-run; fails if any result differs from the record
936
- openprocess run orders.cpnflow --sweep noise=0..0.5 step 0.1
940
+ openprocess run orders.analysis --check # re-run; fails if any result differs from the record
941
+ openprocess run orders.analysis --sweep noise=0..0.5 step 0.1
937
942
  openprocess boxes # every box (listed in docs/boxes.md), with a folder's boxes/
938
943
  openprocess datasets list # the BPI Challenge, Sepsis, ... logs by name
939
944
  ```
@@ -31,7 +31,7 @@ tools (and, until version 0.7, went by the name CPNpy):
31
31
  | **Petri nets & WF-nets** | Draw nets the way the lectures do. Get a soundness verdict with a counterexample for every violation, and replay it in the token game. Also: behavioural properties, P- and T-invariants, the footprint matrix, the reachability graph, PNML import and export. | WoPeD, ProM, pen and paper |
32
32
  | **Process mining** | Import XES or CSV logs, OCEL 2.0 object-centric logs, or type textbook logs like `[<a,b,c>^3, <a,c>^2]`. Filter them, explore variants, the dotted chart and the process map. Discover models (α-algorithm, Inductive Miner, Heuristics Miner, state-based regions). Check conformance (token replay, alignments, precision…) and compare logs. | ProM, Disco |
33
33
  | **Coloured Petri nets** | Open, edit and save CPN Tools models (`.cpn`), hierarchical ones included. Step through or simulate them, compute the state space, and export a simulation as an event log to mine. | CPN Tools / CPN IDE |
34
- | **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.cpnflow` file with everything needed to get the same numbers back. Your own algorithm is one Python function in a `boxes/` folder. | RapidProM |
34
+ | **Workflows** | Boxes on a canvas: a log, a miner, a fitness check, a comparison, a sweep over a setting, a prediction pipeline. Click a box to see its result, *how* it got there (the α-algorithm's eight steps, the replay per variant), its code and its settings. Change a setting and only what follows runs again. Saved as a `.analysis` file with everything needed to get the same numbers back. Your own algorithm is one Python function in a `boxes/` folder. | RapidProM |
35
35
  | **Learn** | A mode of its own for worksheets, built on the app: answer in boxes on the sheet (sets, markings, matrices, cuts and trees, alignments, a net in the editor or a workflow on the canvas beside it) and press **Check**. Most answers are checked automatically, often against answers worked out from the given log or net. Packs are plain Markdown, can be exams with a clock and points, and a past exam imports as a skeleton pack. Demo exercises included. | Answer sheets, a notes app and guesswork |
36
36
  | **Folders** | Open a folder such as *Week 2*: every log and net in it is listed in the sidebar, with its subfolders. The folder and the app stay in step both ways: new nets and edits are saved into it as you go, and changes made in Finder show up by themselves. | Finder windows and *File ▸ Open* every time |
37
37
 
@@ -556,7 +556,7 @@ without distractions, and *Exit* brings your folder back exactly as it was.
556
556
  writes the marks as CSV. A pack can also give every student a **variant**
557
557
  of its own (a log played out from a net with a seed made from their name).
558
558
  - **Your work is saved as you go**, in the exercise's folder:
559
- `my answers.json`, `my answer.pnml` for a net, `my workflow.cpnflow` for a
559
+ `my answers.json`, `my answer.pnml` for a net, `my workflow.analysis` for a
560
560
  workflow and `my notes.md`.
561
561
 
562
562
  **Writing a pack** (for a course or an exam) is plain Markdown: put an
@@ -754,9 +754,9 @@ extra) and *Predict the next activity*. Or start empty.
754
754
  | ![The canvas of a Compare discovery analysis, the Compare box's table beside it](docs/screenshots/workflow-canvas.png) | ![The Report view: a section per box with its result, a Steps outline beside them](docs/screenshots/workflow-report.png) |
755
755
 
756
756
  - **An analysis opens on its Report.** Its header has **Report | Canvas |
757
- Runs**, then **+ Add box**, **Run ▶**, and **⋯** for Re-run, Record,
758
- Export experiment and Save. The sidebar beside it lists the folder's
759
- analyses and, folded under them, its logs.
757
+ Versions**, then **+ Add box**, **Run ▶**, and **⋯** for Run from scratch,
758
+ Keep as version, Record, Export experiment and Save. The sidebar beside it
759
+ lists the folder's analyses and, folded under them, its logs.
760
760
  - **Report** is every box's result, in the order the boxes run, on one
761
761
  scrolling page: a section per box with the group it is in, its name, the
762
762
  line the canvas shows under it, its status, the result as the side panel
@@ -804,9 +804,14 @@ extra) and *Predict the next activity*. Or start empty.
804
804
  - **Only what changed runs again.** Every box shows a status dot: waiting,
805
805
  running, done, failed (the error is on the Result tab; the rest keeps
806
806
  working), or waiting for your OK.
807
- - **Record** shows the workflow as Python and the file with its record;
808
- **Re-run** says what changed since it was saved (an input file, a box file,
809
- a seed) before running everything again.
807
+ - **Versions** is where the numbers stand against the record and every
808
+ version kept. The first run with no failure is kept as v1; from then on the
809
+ badge beside the title says *v1 · reproducible*, *v1 · 2 changes* or
810
+ *Changed since v1*, and **Keep as version…** records the run as v2 with a
811
+ note. Any version can be compared with now (the boxes whose result differs,
812
+ first) or restored whole. **Record** shows the workflow as Python and the
813
+ file; **Run from scratch** says what changed since the version was kept
814
+ before computing everything again.
810
815
  - **Your own boxes**: a `.py` file in the folder's `boxes/` subfolder (see
811
816
  below). The app asks once per folder before running them, and reloads a
812
817
  box when its file is saved.
@@ -868,7 +873,7 @@ extra) and *Predict the next activity*. Or start empty.
868
873
 
869
874
  Every algorithm is also a **box**: a Python function with type hints that
870
875
  the app (from 0.7) draws on a canvas. A workflow of boxes runs from Python
871
- or from the command line, and its `.cpnflow` file records what is needed
876
+ or from the command line, and its `.analysis` file records what is needed
872
877
  to get the same numbers again: fingerprints of the input files and of the
873
878
  box code, every seed, and the installed packages.
874
879
 
@@ -886,12 +891,12 @@ wf.connect(log, model); wf.connect(model, fit, "model"); wf.connect(log, fit, "l
886
891
  run = Runner().run(wf)
887
892
  print(run.value(fit).metrics) # fitness, precision, generalisation, simplicity
888
893
  print(run.result(model).explanation.steps) # how the Inductive Miner got there
889
- save(wf, "orders.cpnflow", run)
894
+ save(wf, "orders.analysis", run)
890
895
  ```
891
896
 
892
897
  ```bash
893
- openprocess run orders.cpnflow --check # re-run; fails if any result differs from the record
894
- openprocess run orders.cpnflow --sweep noise=0..0.5 step 0.1
898
+ openprocess run orders.analysis --check # re-run; fails if any result differs from the record
899
+ openprocess run orders.analysis --sweep noise=0..0.5 step 0.1
895
900
  openprocess boxes # every box (listed in docs/boxes.md), with a folder's boxes/
896
901
  openprocess datasets list # the BPI Challenge, Sepsis, ... logs by name
897
902
  ```
@@ -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.15.0"
51
+ __version__ = "0.16.1"
52
52
 
53
53
  __all__ = [
54
54
  "Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
@@ -19,7 +19,7 @@ Commands
19
19
  ``stats``, ``filter``, ``discover``, ``conform``,
20
20
  ``soundness``, ``invariants``
21
21
  ``exercises`` ``check``, ``marks``, ``import``, ``computes``: exercise packs (OpenProcess Learn)
22
- ``run`` run a ``.cpnflow`` workflow headless; ``--check`` re-runs and
22
+ ``run`` run a ``.analysis`` workflow headless; ``--check`` re-runs and
23
23
  compares with the record, ``--sweep`` varies a setting,
24
24
  ``--lock`` writes the requirements the record names,
25
25
  ``--export`` and ``--report`` write a zip and an HTML page
@@ -447,12 +447,19 @@ def command_exercises_computes(arguments: argparse.Namespace) -> int:
447
447
  # ---------------------------------------------------------------------------
448
448
  def command_run(arguments: argparse.Namespace) -> int:
449
449
  """Run a workflow file without the app, and say what every box gave."""
450
- from .flow import Runner, Sweep, check, differences, library_for, load, save
451
- from .flow.record import requirements_lock
450
+ from .flow import Runner, Sweep, check, differences, library_for, save
451
+ from .flow.record import load_all, requirements_lock
452
452
  path = Path(arguments.workflow)
453
453
  folder = path.parent
454
454
  library = library_for(folder)
455
- workflow, record = load(path, library)
455
+ workflow, record, versions = load_all(path, library)
456
+ if arguments.against is not None:
457
+ version = versions.get(arguments.against)
458
+ if version is None:
459
+ print(f"openprocess: {path.name} has no version {arguments.against} "
460
+ f"(it has {', '.join(f'v{v.number}' for v in versions.items) or 'none'})", file=sys.stderr)
461
+ return 2
462
+ record = version.record
456
463
  if arguments.lock:
457
464
  target = folder / "requirements.lock"
458
465
  target.write_text(requirements_lock(record), encoding="utf-8")
@@ -504,6 +511,8 @@ def command_run(arguments: argparse.Namespace) -> int:
504
511
  if arguments.report:
505
512
  from .flow.report import write_report
506
513
  print(f"Wrote {write_report(arguments.report, workflow=workflow, run=run, folder=folder)}")
514
+ against = f"v{arguments.against}" if arguments.against is not None else (
515
+ f"v{versions.latest.number}" if versions.latest else "the record")
507
516
  if arguments.check:
508
517
  if not record.results:
509
518
  # Nothing to compare is not a pass: a CI job would claim a reproduction that never happened.
@@ -513,15 +522,18 @@ def command_run(arguments: argparse.Namespace) -> int:
513
522
  report = check(workflow, record, run, complete=True)
514
523
  if report:
515
524
  for line in report:
516
- print(f"DIFFERS: {line}")
525
+ print(f"DIFFERS from {against}: {line}")
517
526
  return 1
518
- print(f"Every recorded result was reproduced ({len(record.results)} boxes).")
527
+ print(f"Every result recorded in {against} was reproduced ({len(record.results)} boxes).")
519
528
  elif arguments.save:
520
- save(workflow, path, run, folder, previous=record)
521
- if run.failed() and record.results:
522
- print(f"A box failed, so {path.name} keeps the record of its last successful run")
529
+ if run.failed():
530
+ save(workflow, path, run, folder, previous=record, versions=versions)
531
+ print(f"A box failed, so {path.name} keeps " + (f"{against} as its record" if record.results
532
+ else "no record yet"))
523
533
  else:
524
- print(f"Recorded the results in {path.name}")
534
+ save(workflow, path, run, folder, previous=record, versions=versions, keep=arguments.note or "")
535
+ print(f"Kept the results as v{versions.latest.number} in {path.name}"
536
+ + (f": {arguments.note}" if arguments.note else ""))
525
537
  return 0 if run.ok else 1
526
538
 
527
539
 
@@ -713,11 +725,13 @@ def build_parser() -> argparse.ArgumentParser:
713
725
  "computes", help="list every compute: an answer block may use")
714
726
  exercise_computes.set_defaults(handler=command_exercises_computes)
715
727
 
716
- run = subparsers.add_parser("run", help="run a .cpnflow workflow without the app")
717
- run.add_argument("workflow", help="the .cpnflow file")
728
+ run = subparsers.add_parser("run", help="run a .analysis workflow without the app")
729
+ run.add_argument("workflow", help="the .analysis file")
718
730
  run.add_argument("--check", action="store_true",
719
- help="re-run and fail if any result differs from the recorded one")
720
- run.add_argument("--save", action="store_true", help="record the results in the file")
731
+ help="re-run and fail if any result differs from the recorded one (the latest version)")
732
+ run.add_argument("--against", type=int, metavar="N", help="check against version N instead of the latest")
733
+ run.add_argument("--save", action="store_true", help="keep the results in the file as the next version")
734
+ run.add_argument("--note", metavar="TEXT", help="with --save: the version's note (what changed, or why)")
721
735
  run.add_argument("--sweep", action="append", metavar="SETTING=VALUES",
722
736
  help="vary a setting, e.g. noise=0..0.5 step 0.1 or n3.seed=1,2,3")
723
737
  run.add_argument("--lock", action="store_true",
@@ -7,7 +7,7 @@ straight away, and the app never shows you a result you are meant to work out
7
7
  until you ask for it.
8
8
 
9
9
  Your answers are saved in each exercise's folder (*my answers.json*, and
10
- *my answer.pnml* for a net you draw, *my workflow.cpnflow* for a workflow
10
+ *my answer.pnml* for a net you draw, *my workflow.analysis* for a workflow
11
11
  you build), so you can stop and carry on later.
12
12
 
13
13
  To write your own pack, copy this folder and change the `question.md` files:
@@ -24,7 +24,7 @@ Python::
24
24
  wf.connect(log, model); wf.connect(model, fit, "model"); wf.connect(log, fit, "log")
25
25
  run = Runner().run(wf)
26
26
  print(run.value(fit).metrics)
27
- save(wf, "demo.cpnflow", run) # with the record that makes it reproducible
27
+ save(wf, "demo.analysis", run) # with the record that makes it reproducible
28
28
 
29
29
  Work over many independent items (variants, cases, resamples) runs on all
30
30
  cores with :func:`parallel_map`, with exactly the result of a loop.
@@ -1,7 +1,8 @@
1
- """The ``.cpnflow`` file: a workflow, and the record that makes it reproducible.
1
+ """The ``.analysis`` file: a workflow, and the record that makes it reproducible.
2
2
 
3
- A workflow file is JSON: the boxes, their settings and positions, the
4
- connections, the groups, and a **record**:
3
+ An analysis file is JSON: the boxes, their settings and positions, the
4
+ connections, the groups, its **versions** (:mod:`.versions`: the record as
5
+ it was each time the user kept it) and the **record** of the latest version:
5
6
 
6
7
  * the versions of OpenProcess, Python and the optional libraries;
7
8
  * the environment: every installed package and its version (the lock);
@@ -34,7 +35,44 @@ from .runner import DONE, Run
34
35
  from .types import Dataset, Figure, Predictions, Scores, Table, Text
35
36
  from .workflow import Workflow
36
37
 
37
- FORMAT = "cpnflow/1"
38
+ FORMAT = "analysis/1"
39
+
40
+ #: The extension of an analysis file, and the ones it had before (``.cpnflow``,
41
+ #: from when the app was CPNpy: the content is the same, only the name changed).
42
+ EXTENSION = ".analysis"
43
+ OLD_EXTENSIONS = (".cpnflow",)
44
+
45
+
46
+ def is_analysis_file(path) -> bool:
47
+ """Whether ``path`` is an analysis file by its name, under either extension."""
48
+ return str(path).lower().endswith((EXTENSION,) + OLD_EXTENSIONS)
49
+
50
+
51
+ def analysis_name(path) -> Path:
52
+ """``path`` under the current extension (``orders.cpnflow`` → ``orders.analysis``)."""
53
+ path = Path(path)
54
+ return path.with_name(path.stem + EXTENSION) if path.suffix.lower() in OLD_EXTENSIONS else path
55
+
56
+
57
+ def carry_over(folder, depth: int = 3) -> list[Path]:
58
+ """Rename every ``.cpnflow`` under ``folder`` to ``.analysis`` (the content is
59
+ read either way); a file whose new name is taken is left. Returns the new paths."""
60
+ renamed = []
61
+ root = Path(folder)
62
+ for old in OLD_EXTENSIONS:
63
+ for source in sorted(root.rglob(f"*{old}")):
64
+ if len(source.relative_to(root).parts) - 1 > depth or any(
65
+ part.startswith(".") for part in source.relative_to(root).parts):
66
+ continue
67
+ target = analysis_name(source)
68
+ if target.exists():
69
+ continue
70
+ try:
71
+ source.replace(target)
72
+ except OSError:
73
+ continue
74
+ renamed.append(target)
75
+ return renamed
38
76
 
39
77
 
40
78
  # ---------------------------------------------------------------------------
@@ -460,17 +498,45 @@ def differences(record: Record, workflow: Workflow, folder: str | Path | None =
460
498
  # ---------------------------------------------------------------------------
461
499
  def save(workflow: Workflow, path: str | Path, run: Run | None = None,
462
500
  folder: str | Path | None = None, lock: bool = True, history: list[dict] | None = None,
463
- previous: Record | None = None) -> Record:
464
- """Write the workflow and its record to ``path``; returns the record.
465
- ``history``: the analysis's snapshots (see :mod:`.snapshots`), kept in the file.
466
- ``previous``: the file's record so far; see :func:`carried`."""
501
+ previous: Record | None = None, versions: "Versions | None" = None,
502
+ keep: str | None = None) -> Record:
503
+ """Write the workflow, its versions and its record to ``path``; returns the record.
504
+
505
+ ``history``: the analysis's runs (see :mod:`.snapshots`), kept in the file.
506
+ ``previous``: the file's record so far, and ``versions`` the versions it
507
+ holds; see :func:`carried`. ``keep``: a note, to keep this run as a new
508
+ version (``""`` for a version without one): the record becomes this run's.
509
+ Without ``keep`` the record stays as it was, except that the first run in
510
+ which no box fails is kept as version 1 by itself, so a new analysis is
511
+ recorded without a word.
512
+
513
+ ``versions`` is updated in place when a version is kept."""
514
+ from .versions import Versions
467
515
  path = Path(path)
468
516
  folder = folder if folder is not None else path.parent
469
- record = make_record(workflow, run, folder, lock)
470
- if previous is not None:
471
- record = carried(record, previous, workflow, run)
517
+ versions = versions if versions is not None else Versions()
518
+ if keep is not None and (run is None or run.failed()):
519
+ raise ValueError("A version keeps a run in which no box failed")
520
+ if keep is None and previous is not None and previous.results:
521
+ record = carried(previous, workflow)
522
+ elif keep is None and (run is None or run.failed()):
523
+ record = make_record(workflow, None, folder, lock) # nothing to keep yet
524
+ else:
525
+ record = make_record(workflow, run, folder, lock)
526
+ if previous is not None and previous.scheme == record.scheme:
527
+ # A box that could not run here (a library it needs is missing)
528
+ # keeps the fingerprint recorded where it could.
529
+ still = {k: v for k, v in previous.results.items() if k in workflow.nodes}
530
+ record.results = {**still, **record.results}
531
+ for entry in record.inputs:
532
+ if entry["sha256"] == "missing" and previous is not None:
533
+ then = next((i for i in previous.inputs if i["node"] == entry["node"]
534
+ and i["setting"] == entry["setting"] and i["file"] == entry["file"]), None)
535
+ if then is not None:
536
+ entry["sha256"] = then["sha256"]
537
+ versions.keep(workflow, run, record, keep or ("First run" if not versions.items else ""))
472
538
  record.history = list(history or [])
473
- data = {"format": FORMAT, **workflow.to_dict(), "record": record.to_dict()}
539
+ data = {"format": FORMAT, **workflow.to_dict(), "versions": versions.to_list(), "record": record.to_dict()}
474
540
  text = json.dumps(data, indent=2, ensure_ascii=False, default=str)
475
541
  temporary = path.with_name(path.name + ".tmp")
476
542
  temporary.write_text(text, encoding="utf-8")
@@ -478,37 +544,34 @@ def save(workflow: Workflow, path: str | Path, run: Run | None = None,
478
544
  return record
479
545
 
480
546
 
481
- def carried(record: Record, previous: Record, workflow: Workflow, run: Run | None) -> Record:
482
- """The record to write, given the file's ``previous`` one: the record of the
483
- last successful run stays until a successful run replaces it.
484
-
485
- No run, or a run in which a box failed (the input file was not found, say),
486
- leaves the previous record as it was, results of boxes since removed aside.
487
- A run in which no box failed replaces it; a box that could not run here (a
488
- library it needs is missing) keeps its recorded fingerprint if the scheme is
489
- the same, and an input file that is missing now keeps the fingerprint
490
- recorded when it was read."""
547
+ def carried(previous: Record, workflow: Workflow) -> Record:
548
+ """The record to write when no version is kept: the previous one as it
549
+ was, results of boxes since removed aside. Saving as you work never
550
+ replaces the record; keeping a version does (:func:`save` with ``keep``)."""
491
551
  still = {k: v for k, v in previous.results.items() if k in workflow.nodes}
492
- if run is None or run.failed():
493
- return Record(**{**previous.__dict__, "results": still})
494
- if previous.scheme == record.scheme:
495
- record.results = {**still, **record.results}
496
- for entry in record.inputs:
497
- if entry["sha256"] == "missing":
498
- then = next((i for i in previous.inputs if i["node"] == entry["node"]
499
- and i["setting"] == entry["setting"] and i["file"] == entry["file"]), None)
500
- if then is not None:
501
- entry["sha256"] = then["sha256"]
502
- return record
552
+ return Record(**{**previous.__dict__, "results": still})
503
553
 
504
554
 
505
555
  def load(path: str | Path, library=None) -> tuple[Workflow, Record]:
506
- """Read a ``.cpnflow`` file."""
556
+ """Read an analysis file (``.analysis``, or ``.cpnflow`` as it was called)."""
557
+ workflow, record, _versions = load_all(path, library)
558
+ return workflow, record
559
+
560
+
561
+ def load_all(path: str | Path, library=None) -> "tuple[Workflow, Record, Versions]":
562
+ """Read an analysis file: its workflow, the record of its latest version, and
563
+ every version kept. A file from before versions (a record, no list) gives
564
+ its record as version 1."""
565
+ from .versions import Versions
507
566
  data = json.loads(Path(path).read_text(encoding="utf-8"))
508
- if data.get("format", FORMAT).split("/")[0] != "cpnflow":
509
- raise ValueError(f"{path} is not a workflow file")
567
+ if data.get("format", FORMAT).split("/")[0] not in ("analysis", "cpnflow"):
568
+ raise ValueError(f"{path} is not an analysis file")
510
569
  workflow = Workflow.from_dict(data, library)
511
- return workflow, Record.from_dict(data.get("record"))
570
+ record = Record.from_dict(data.get("record"))
571
+ versions = Versions.from_list(data.get("versions"))
572
+ if not versions.items and record.results:
573
+ versions = Versions.first_from(workflow, record)
574
+ return workflow, record, versions
512
575
 
513
576
 
514
577
  def check(workflow: Workflow, record: Record, run: Run, complete: bool = False) -> list[str]:
@@ -578,7 +641,7 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
578
641
  name = workflow.name or "workflow"
579
642
  lines = [f"# {name}", "", f"Exported {record.saved} with OpenProcess {record.versions.get('openprocess', '')}, "
580
643
  f"Python {record.versions.get('python', '')}.", "",
581
- "Run it again with `openprocess run " + f"{name}.cpnflow --check`: it fails if any result differs "
644
+ "Run it again with `openprocess run " + f"{name}{EXTENSION} --check`: it fails if any result differs "
582
645
  "from the recorded one. `requirements.lock` is the environment it ran in. `run.py` is the same "
583
646
  "analysis as a plain Python script (`python run.py`). Or drop this zip on OpenProcess: it unpacks "
584
647
  "it into a folder and runs it.", "", "## Boxes", ""]
@@ -606,7 +669,7 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
606
669
  lines += ["", "## How to cite", "", citation.app_text(), "",
607
670
  "`CITATIONS.bib` has this entry and the papers the algorithms follow, as BibTeX."]
608
671
  with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipped:
609
- zipped.writestr(f"{name}.cpnflow", json.dumps(data, indent=2, ensure_ascii=False, default=str))
672
+ zipped.writestr(f"{name}{EXTENSION}", json.dumps(data, indent=2, ensure_ascii=False, default=str))
610
673
  zipped.writestr("requirements.lock", requirements_lock(record))
611
674
  from .script import script_text
612
675
  zipped.writestr("run.py", script_text(packed_workflow, "run.py"))
@@ -642,11 +705,11 @@ def _describe(value) -> str:
642
705
 
643
706
 
644
707
  def is_experiment(path) -> bool:
645
- """Whether ``path`` is a zip :func:`export_experiment` wrote: one ``.cpnflow`` at the top."""
708
+ """Whether ``path`` is a zip :func:`export_experiment` wrote: one analysis file at the top."""
646
709
  import zipfile
647
710
  try:
648
711
  with zipfile.ZipFile(path) as zipped:
649
- return sum(1 for n in zipped.namelist() if n.endswith(".cpnflow") and "/" not in n) == 1
712
+ return sum(1 for n in zipped.namelist() if is_analysis_file(n) and "/" not in n) == 1
650
713
  except (OSError, zipfile.BadZipFile):
651
714
  return False
652
715
 
@@ -661,9 +724,9 @@ def import_experiment(archive, into) -> Path:
661
724
  archive = Path(archive)
662
725
  with zipfile.ZipFile(archive) as zipped:
663
726
  names = zipped.namelist()
664
- flows = [n for n in names if n.endswith(".cpnflow") and "/" not in n]
727
+ flows = [n for n in names if is_analysis_file(n) and "/" not in n]
665
728
  if len(flows) != 1:
666
- raise ValueError(f"{archive.name} is not an exported experiment (one .cpnflow at the top is needed)")
729
+ raise ValueError(f"{archive.name} is not an exported experiment (one {EXTENSION} file at the top is needed)")
667
730
  folder = Path(into) / archive.stem
668
731
  counter = 2
669
732
  while folder.exists():
@@ -679,6 +742,9 @@ def import_experiment(archive, into) -> Path:
679
742
  target.write_bytes(zipped.read(name))
680
743
  flow_file = folder / flows[0]
681
744
  data = json.loads(flow_file.read_text(encoding="utf-8"))
745
+ if flow_file.suffix.lower() in OLD_EXTENSIONS: # an experiment exported before 0.16
746
+ flow_file.unlink()
747
+ flow_file = analysis_name(flow_file)
682
748
  moved: dict[tuple[str, str], str] = {}
683
749
  for item in data.get("record", {}).get("inputs", []):
684
750
  new = f"inputs/{Path(item['file']).name}"
@@ -728,6 +794,7 @@ def _as_file(value, stem: str):
728
794
  #: (name, bytes or text) for a result worth a file of its own; the script an experiment carries uses it.
729
795
  as_file = _as_file
730
796
 
731
- __all__ = ["FORMAT", "Record", "as_file", "check", "differences", "environment", "export_experiment",
732
- "fingerprint", "import_experiment", "is_experiment", "load", "make_record", "requirements_lock",
733
- "save", "sha256_file", "sha256_text", "versions"]
797
+ __all__ = ["EXTENSION", "FORMAT", "OLD_EXTENSIONS", "Record", "analysis_name", "as_file", "carry_over", "check",
798
+ "differences", "environment", "export_experiment", "fingerprint", "import_experiment",
799
+ "is_analysis_file", "is_experiment", "load", "load_all", "make_record", "requirements_lock", "save",
800
+ "sha256_file", "sha256_text", "versions"]
@@ -9,7 +9,7 @@ external assets, readable in any browser and printable.
9
9
 
10
10
  The page is built from the workflow and its run alone; drawings (SVG) are
11
11
  passed in by the app, which has the canvas to render them. So the same
12
- report can be made without the app (``openprocess run analysis.cpnflow
12
+ report can be made without the app (``openprocess run orders.analysis
13
13
  --report report.html``), minus the pictures.
14
14
  """
15
15
 
@@ -0,0 +1,110 @@
1
+ """Is this analysis reproducible, right now? One status for the header.
2
+
3
+ The record (:mod:`openprocess.flow.record`) is a file; this turns it into a
4
+ visible promise. :func:`status` compares the record (the latest version,
5
+ :mod:`.versions`) with the world now (the inputs' fingerprints, the box
6
+ files, the seeds, the settings, the software versions) and with the latest
7
+ run (every box's result against its recorded fingerprint), and says one of:
8
+
9
+ * **Not recorded yet** -- the analysis has never run without a failure;
10
+ * **v3 · this run reproduces it** -- every result matches the record and
11
+ nothing it depends on has changed;
12
+ * **v3 · n changes since** -- a setting, an input file, a box file, a seed
13
+ or a version differs from the record (a re-run may give other numbers);
14
+ * **Changed since v3** -- those changes, and the results differ: the
15
+ numbers moved for a reason you can see; keep a version when you are happy
16
+ with them;
17
+ * **Differs from v3** -- the same inputs, settings and versions gave another
18
+ result: a reproducibility problem.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass, field
24
+
25
+ from .record import Record, check, differences
26
+ from .runner import BLOCKED, DONE, Run
27
+ from .snapshots import setting_changes, take
28
+ from .versions import Versions
29
+ from .workflow import Workflow
30
+
31
+
32
+ @dataclass
33
+ class Status:
34
+ #: "unrecorded", "reproduces", "unchecked", "changed", "moved" or "differs".
35
+ state: str
36
+ headline: str
37
+ details: list[str] = field(default_factory=list)
38
+ #: The version the record is ("v3"; "" when nothing is recorded).
39
+ version: str = ""
40
+ #: How many of the details are changes since the record (the rest: what could not be compared).
41
+ changes: int = 0
42
+
43
+ @property
44
+ def tone(self) -> str:
45
+ return {"reproduces": "good", "changed": "warning", "moved": "warning",
46
+ "differs": "critical"}.get(self.state, "muted")
47
+
48
+ @property
49
+ def short(self) -> str:
50
+ """Two or three words for a badge; :attr:`headline` is the sentence."""
51
+ v = self.version
52
+ if self.state == "changed":
53
+ return f"{v} · {self.changes} change{'s' if self.changes != 1 else ''}"
54
+ return {"unrecorded": "Not recorded", "reproduces": f"{v} · reproducible", "differs": f"Differs from {v}",
55
+ "moved": f"Changed since {v}", "unchecked": f"{v} · not run"}.get(self.state, self.headline)
56
+
57
+ @property
58
+ def can_keep(self) -> bool:
59
+ """Whether keeping a version now would say something new."""
60
+ return self.state in ("changed", "moved", "differs")
61
+
62
+
63
+ def status(workflow: Workflow, record: Record | None, run: Run | None, folder=None,
64
+ versions: Versions | None = None) -> Status:
65
+ """The analysis's reproducibility, from its record, its versions and its latest run."""
66
+ if record is None or not record.results:
67
+ return Status("unrecorded", "Not recorded yet", [
68
+ "The first run in which no box fails is kept as version 1: the record keeps the inputs' "
69
+ "fingerprints, the versions, the seeds and every result, so the numbers can be checked again later."])
70
+ latest = versions.latest if versions is not None else None
71
+ v = f"v{latest.number}" if latest is not None else "the record"
72
+ name = f"v{latest.number}" if latest is not None else "Recorded" # "v3 · reproducible"
73
+ changed = differences(record, workflow, folder)
74
+ if latest is not None:
75
+ now = take(workflow, None)
76
+ changed += setting_changes(latest.snapshot, now)
77
+ changed += [f"“{now.titles.get(n, n)}” was added." for n in now.settings if n not in latest.snapshot.settings]
78
+ changed += [f"“{latest.snapshot.titles.get(n, n)}” was removed."
79
+ for n in latest.snapshot.settings if n not in now.settings]
80
+ # A box that could not run here (a library is missing) is not a difference
81
+ # in its result: say why it could not be compared instead.
82
+ blocked = {node_id for node_id in record.results
83
+ if run is not None and run.result(node_id, 0) is not None and run.result(node_id, 0).status == BLOCKED}
84
+ uncompared = []
85
+ if blocked:
86
+ record = Record(**{**record.__dict__, "results": {k: v for k, v in record.results.items() if k not in blocked}})
87
+ uncompared = [f"“{workflow.title(n)}” could not run here ({run.result(n, 0).message}), so its "
88
+ "result was not compared." for n in sorted(blocked)]
89
+ changed += uncompared # not a reproduction either: say so, as a change
90
+ mismatches = check(workflow, record, run) if run is not None else []
91
+ compared = [node_id for node_id in record.results
92
+ if run is not None and run.result(node_id, 0) is not None
93
+ and run.result(node_id, 0).status == DONE]
94
+ count = len(changed)
95
+ plural = f"{count} change{'s' if count != 1 else ''}"
96
+ if mismatches and changed:
97
+ return Status("moved", f"The results differ from {v} after {plural}", changed + mismatches, v, count)
98
+ if mismatches:
99
+ return Status("differs", f"This run differs from {v}, with nothing changed", mismatches, v, 0)
100
+ if changed:
101
+ return Status("changed", f"{name} · {plural} since", changed, name, count)
102
+ if not compared:
103
+ return Status("unchecked", f"{name} · not run since", [
104
+ "Run ▶ and every result is checked against its recorded fingerprint."], name, 0)
105
+ return Status("reproduces", f"{name} · this run reproduces it", [
106
+ f"{len(compared)} result{'s' if len(compared) != 1 else ''} match the record's fingerprints, and "
107
+ "nothing the analysis depends on has changed."], name, 0)
108
+
109
+
110
+ __all__ = ["Status", "status"]