openprocess 0.10.6__tar.gz → 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. {openprocess-0.10.6/openprocess.egg-info → openprocess-0.11.0}/PKG-INFO +30 -9
  2. {openprocess-0.10.6 → openprocess-0.11.0}/README.md +29 -8
  3. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/__init__.py +1 -1
  4. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/science.py +24 -13
  5. openprocess-0.11.0/openprocess/flow/history.py +113 -0
  6. openprocess-0.11.0/openprocess/flow/provenance.py +110 -0
  7. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/record.py +67 -3
  8. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/runner.py +10 -0
  9. openprocess-0.11.0/openprocess/flow/script.py +87 -0
  10. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/types.py +10 -1
  11. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/workflow.py +18 -1
  12. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/page.py +83 -3
  13. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/viewers.py +82 -6
  14. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/app.py +82 -10
  15. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/connections.py +26 -1
  16. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/definition_view.py +9 -2
  17. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/graph_view.py +9 -0
  18. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/markdown_view.py +3 -1
  19. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/style.py +2 -0
  20. {openprocess-0.10.6 → openprocess-0.11.0/openprocess.egg-info}/PKG-INFO +30 -9
  21. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/SOURCES.txt +4 -0
  22. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_exercises_gui.py +6 -0
  23. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_flow.py +119 -0
  24. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_flow_gui.py +94 -1
  25. openprocess-0.11.0/tests/test_history.py +57 -0
  26. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_studio_gui.py +81 -1
  27. {openprocess-0.10.6 → openprocess-0.11.0}/LICENSE +0 -0
  28. {openprocess-0.10.6 → openprocess-0.11.0}/cpnpy/__init__.py +0 -0
  29. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/analysis/__init__.py +0 -0
  30. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/analysis/state_space.py +0 -0
  31. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/analysis/state_space_process.py +0 -0
  32. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/citation.py +0 -0
  33. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/cli.py +0 -0
  34. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +0 -0
  35. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +0 -0
  36. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +0 -0
  37. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +0 -0
  38. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +0 -0
  39. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +0 -0
  40. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +0 -0
  41. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +0 -0
  42. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +0 -0
  43. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +0 -0
  44. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +0 -0
  45. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +0 -0
  46. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +0 -0
  47. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +0 -0
  48. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +0 -0
  49. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +0 -0
  50. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +0 -0
  51. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +0 -0
  52. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +0 -0
  53. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/exercises/pack.md +0 -0
  54. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/__init__.py +0 -0
  55. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/box.py +0 -0
  56. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/__init__.py +0 -0
  57. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/check.py +0 -0
  58. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/compare.py +0 -0
  59. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/cpn.py +0 -0
  60. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/discover.py +0 -0
  61. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/filter.py +0 -0
  62. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/input.py +0 -0
  63. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/output.py +0 -0
  64. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/predict.py +0 -0
  65. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/boxes/sweeps.py +0 -0
  66. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/convert.py +0 -0
  67. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/datasets.py +0 -0
  68. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/explain.py +0 -0
  69. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/figures.py +0 -0
  70. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/library.py +0 -0
  71. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/report.py +0 -0
  72. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/reproducibility.py +0 -0
  73. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/snapshots.py +0 -0
  74. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/flow/sweep.py +0 -0
  75. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/__init__.py +0 -0
  76. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/app.py +0 -0
  77. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/arc_editing.py +0 -0
  78. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/canvas.py +0 -0
  79. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/__init__.py +0 -0
  80. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/canvas.py +0 -0
  81. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/picker.py +0 -0
  82. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/runs.py +0 -0
  83. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/stepper.py +0 -0
  84. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/summary.py +0 -0
  85. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/flow/templates.py +0 -0
  86. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/items.py +0 -0
  87. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/learn/answer_boxes.py +0 -0
  88. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/learn/concealment.py +0 -0
  89. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/learn/mode.py +0 -0
  90. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/panning.py +0 -0
  91. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/resources/openprocess-icon.png +0 -0
  92. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/__init__.py +0 -0
  93. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/__main__.py +0 -0
  94. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/charts.py +0 -0
  95. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/cite.py +0 -0
  96. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/command_palette.py +0 -0
  97. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/compare_page.py +0 -0
  98. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/cpn_page.py +0 -0
  99. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/derivation_view.py +0 -0
  100. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/documents.py +0 -0
  101. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/dotted_chart.py +0 -0
  102. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/file_dialogs.py +0 -0
  103. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/filter_dialog.py +0 -0
  104. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/graph_builders.py +0 -0
  105. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/instances.py +0 -0
  106. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/log_editor.py +0 -0
  107. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/log_page.py +0 -0
  108. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/mathtext.py +0 -0
  109. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/ml_highlighter.py +0 -0
  110. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/model_page.py +0 -0
  111. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/motion.py +0 -0
  112. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/net_comparison.py +0 -0
  113. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/notes_overlay.py +0 -0
  114. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/petri_page.py +0 -0
  115. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/provenance.py +0 -0
  116. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/regions_view.py +0 -0
  117. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/sidebar.py +0 -0
  118. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/spaces.py +0 -0
  119. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/tool_icons.py +0 -0
  120. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/updates.py +0 -0
  121. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/widgets.py +0 -0
  122. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/workers.py +0 -0
  123. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/studio/workspace.py +0 -0
  124. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/theme.py +0 -0
  125. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/gui/tidy.py +0 -0
  126. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/io/__init__.py +0 -0
  127. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/io/cpn_reader.py +0 -0
  128. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/io/cpn_writer.py +0 -0
  129. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/__init__.py +0 -0
  130. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/answers.py +0 -0
  131. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/checks.py +0 -0
  132. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/computed.py +0 -0
  133. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/context.py +0 -0
  134. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/exam.py +0 -0
  135. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/exercise-packs.md +0 -0
  136. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/importer.py +0 -0
  137. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/notation.py +0 -0
  138. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/pack.py +0 -0
  139. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/learn/sheet.py +0 -0
  140. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/__init__.py +0 -0
  141. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/analysis.py +0 -0
  142. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/columns.py +0 -0
  143. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/compare_nets.py +0 -0
  144. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/__init__.py +0 -0
  145. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/alignments.py +0 -0
  146. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/quality.py +0 -0
  147. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/conformance/token_replay.py +0 -0
  148. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/csv_import.py +0 -0
  149. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/definitions.py +0 -0
  150. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/dfg.py +0 -0
  151. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/__init__.py +0 -0
  152. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/alpha.py +0 -0
  153. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/heuristics.py +0 -0
  154. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/inductive.py +0 -0
  155. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/discovery/state_regions.py +0 -0
  156. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/filtering.py +0 -0
  157. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/footprint.py +0 -0
  158. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/invariants.py +0 -0
  159. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/layout.py +0 -0
  160. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/log.py +0 -0
  161. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/ocel.py +0 -0
  162. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/petrinet.py +0 -0
  163. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/playout.py +0 -0
  164. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/pm4py_bridge.py +0 -0
  165. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/pnml.py +0 -0
  166. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/processtree.py +0 -0
  167. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/regions.py +0 -0
  168. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/stats.py +0 -0
  169. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/structure.py +0 -0
  170. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/transition_system.py +0 -0
  171. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/mining/xes.py +0 -0
  172. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/__init__.py +0 -0
  173. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/ast_nodes.py +0 -0
  174. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/builtins.py +0 -0
  175. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/colorsets.py +0 -0
  176. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/errors.py +0 -0
  177. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/evaluator.py +0 -0
  178. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/lexer.py +0 -0
  179. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/multiset.py +0 -0
  180. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/parser.py +0 -0
  181. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/ml/values.py +0 -0
  182. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/__init__.py +0 -0
  183. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/declarations.py +0 -0
  184. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/examples.py +0 -0
  185. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/net.py +0 -0
  186. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/model/plain.py +0 -0
  187. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/packages.py +0 -0
  188. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/references.py +0 -0
  189. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/__init__.py +0 -0
  190. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/binding.py +0 -0
  191. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/export.py +0 -0
  192. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/sim/simulator.py +0 -0
  193. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/__init__.py +0 -0
  194. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/answers.py +0 -0
  195. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/checks.py +0 -0
  196. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/pack.py +0 -0
  197. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess/teaching/sheet.py +0 -0
  198. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/dependency_links.txt +0 -0
  199. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/entry_points.txt +0 -0
  200. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/requires.txt +0 -0
  201. {openprocess-0.10.6 → openprocess-0.11.0}/openprocess.egg-info/top_level.txt +0 -0
  202. {openprocess-0.10.6 → openprocess-0.11.0}/pyproject.toml +0 -0
  203. {openprocess-0.10.6 → openprocess-0.11.0}/setup.cfg +0 -0
  204. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_arc_editing.py +0 -0
  205. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_citation.py +0 -0
  206. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_colorsets.py +0 -0
  207. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_columns.py +0 -0
  208. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_cpn_course_model.py +0 -0
  209. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_definitions.py +0 -0
  210. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_hierarchy.py +0 -0
  211. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_io.py +0 -0
  212. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_learn.py +0 -0
  213. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_learn_gui.py +0 -0
  214. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_log_editing.py +0 -0
  215. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_mining.py +0 -0
  216. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_ml.py +0 -0
  217. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_multiset.py +0 -0
  218. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_ocel.py +0 -0
  219. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_packages.py +0 -0
  220. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_regions.py +0 -0
  221. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_rename.py +0 -0
  222. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_report.py +0 -0
  223. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_simulation.py +0 -0
  224. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_snapshots.py +0 -0
  225. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_state_space.py +0 -0
  226. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_teaching.py +0 -0
  227. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_tidy.py +0 -0
  228. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_updates.py +0 -0
  229. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_wf_structure.py +0 -0
  230. {openprocess-0.10.6 → openprocess-0.11.0}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openprocess
3
- Version: 0.10.6
3
+ Version: 0.11.0
4
4
  Summary: Process mining, Petri nets and workflows in one open app: every algorithm visible, every result reproducible, every step teachable
5
5
  Author: Sam Struthers
6
6
  License: MIT
@@ -528,17 +528,19 @@ links to the definitions it builds on; **Help ▸ Definitions** lists them all.
528
528
  for bounded nets; for unbounded ones, up to a trace length (and it says
529
529
  so).
530
530
 
531
- | Process map | Discovering a model |
531
+ | Process map | Discover in Mine →: the miner is a box, its result beside the canvas |
532
532
  |---|---|
533
533
  | ![Process map](docs/screenshots/studio-process-map.png) | ![Discover](docs/screenshots/studio-discover.png) |
534
534
 
535
- | α-algorithm result | Conformance (alignments on the model) |
535
+ | α-algorithm result | *How*: the eight steps, from the footprint to α(L) |
536
536
  |---|---|
537
- | ![Alpha](docs/screenshots/studio-alpha.png) | ![Conformance](docs/screenshots/studio-conformance.png) |
537
+ | ![Alpha](docs/screenshots/studio-alpha.png) | ![The eight steps of the α-algorithm](docs/screenshots/studio-alpha-how.png) |
538
538
 
539
- | Comparing two logs | Linked dotted charts of three boarding strategies |
539
+ | Check fit: every score, and how it was computed | Comparing two logs |
540
540
  |---|---|
541
- | ![Comparing two logs](docs/screenshots/compare.png) | ![Linked dotted charts](docs/screenshots/compare-dotted.png) |
541
+ | ![Conformance](docs/screenshots/studio-conformance.png) | ![Comparing two logs](docs/screenshots/compare.png) |
542
+
543
+ ![Linked dotted charts of two logs, one scale and one colour per activity](docs/screenshots/compare-dotted.png)
542
544
 
543
545
  The app follows the system's light or dark appearance:
544
546
 
@@ -555,6 +557,8 @@ sidebar, **Learn ▸ Open Exercise Pack…**, or **Learn ▸ Open Demo Exercises
555
557
  exercises): the window switches to a view made for working through a pack
556
558
  without distractions, and *Exit* brings your folder back exactly as it was.
557
559
 
560
+ ![An exercise in Learn: the worksheet with its answer boxes on the left, the exercise's log, given net and workflow on the right](docs/screenshots/learn-exercise.png)
561
+
558
562
  - **The worksheet** on the left is the question top to bottom, with an
559
563
  answer box wherever one is needed: yes/no, multiple choice, a set
560
564
  (`{a, b}`, sets of sets, or pairs like `({a}, {b,d})`), a number, a short
@@ -742,7 +746,10 @@ generated from `openprocess/mining/definitions.py`; after editing a definition,
742
746
  Likewise [`docs/references.md`](docs/references.md) — every source OpenProcess's
743
747
  notation and algorithms follow, also in the app under **Help ▸ References** —
744
748
  is generated from `openprocess/references.py` with
745
- `python -m openprocess.references > docs/references.md`.
749
+ `python -m openprocess.references > docs/references.md`. The screenshots in
750
+ this README come from the app itself: `python docs/make_screenshots.py`
751
+ renders every page offscreen into `docs/screenshots/`, so run it after a
752
+ change to the app's looks.
746
753
 
747
754
  The engines (`openprocess.mining`, `openprocess.ml`, `openprocess.sim`, `openprocess.analysis`)
748
755
  have **no dependencies**, so they work in a notebook or a script:
@@ -769,6 +776,10 @@ Miner, a fitness check), *Compare discovery* (three miners side by side),
769
776
  confidence* (bootstrap intervals, a test and a plot; needs the `science`
770
777
  extra) and *Predict the next activity*. Or start empty.
771
778
 
779
+ | The canvas: *Compare discovery*, three miners side by side | Summary: every box's key figure, and the process map |
780
+ |---|---|
781
+ | ![The canvas of a Compare discovery analysis, the Compare box's table beside it](docs/screenshots/workflow-canvas.png) | ![The Summary view: a tile per box, the process map under them](docs/screenshots/workflow-summary.png) |
782
+
772
783
  - **The page is the canvas.** Its header has **Canvas | Summary**, then
773
784
  **+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
774
785
  and Save. The sidebar beside it lists the folder's analyses and, folded
@@ -825,8 +836,18 @@ extra) and *Predict the next activity*. Or start empty.
825
836
  `boxes/`, so it is listed under *Yours* and can be used in any workflow.
826
837
  - **Export experiment** (under ⋯) writes a zip with the workflow file and its record,
827
838
  the inputs, your boxes, every result as a file (CSV, PNML, SVG, XES), a
828
- `requirements.lock` and a README that says what was run: supplementary
829
- material for a paper, which `openprocess run --check` can verify.
839
+ `requirements.lock`, a `run.py` that runs the same analysis as a plain
840
+ script, and a README that says what was run: supplementary material for a
841
+ paper, which `openprocess run --check` can verify. Drop such a zip on
842
+ OpenProcess and it is unpacked into a folder of its own and run again.
843
+ - **Export as Python script** (under ⋯) writes the analysis as a `.py` file:
844
+ the same boxes, settings and connections, run with `python analysis.py`,
845
+ the results written beside it.
846
+ - **Every result knows where it came from**: a figure saved as SVG carries
847
+ the analysis, the box, its settings and the result's fingerprint in its
848
+ metadata; a CSV gets a `.provenance.json` beside it.
849
+ - **⌘Z and ⇧⌘Z** undo and redo on the canvas: boxes, connections, moves,
850
+ settings, groups.
830
851
  - **The side panel** resizes by dragging the gap beside it; double-click the
831
852
  gap, or *View ▸ Reset Workflow Layout*, for its default width.
832
853
  - **Connections** (at the bottom of the sidebar, or *View ▸ Connections*)
@@ -489,17 +489,19 @@ links to the definitions it builds on; **Help ▸ Definitions** lists them all.
489
489
  for bounded nets; for unbounded ones, up to a trace length (and it says
490
490
  so).
491
491
 
492
- | Process map | Discovering a model |
492
+ | Process map | Discover in Mine →: the miner is a box, its result beside the canvas |
493
493
  |---|---|
494
494
  | ![Process map](docs/screenshots/studio-process-map.png) | ![Discover](docs/screenshots/studio-discover.png) |
495
495
 
496
- | α-algorithm result | Conformance (alignments on the model) |
496
+ | α-algorithm result | *How*: the eight steps, from the footprint to α(L) |
497
497
  |---|---|
498
- | ![Alpha](docs/screenshots/studio-alpha.png) | ![Conformance](docs/screenshots/studio-conformance.png) |
498
+ | ![Alpha](docs/screenshots/studio-alpha.png) | ![The eight steps of the α-algorithm](docs/screenshots/studio-alpha-how.png) |
499
499
 
500
- | Comparing two logs | Linked dotted charts of three boarding strategies |
500
+ | Check fit: every score, and how it was computed | Comparing two logs |
501
501
  |---|---|
502
- | ![Comparing two logs](docs/screenshots/compare.png) | ![Linked dotted charts](docs/screenshots/compare-dotted.png) |
502
+ | ![Conformance](docs/screenshots/studio-conformance.png) | ![Comparing two logs](docs/screenshots/compare.png) |
503
+
504
+ ![Linked dotted charts of two logs, one scale and one colour per activity](docs/screenshots/compare-dotted.png)
503
505
 
504
506
  The app follows the system's light or dark appearance:
505
507
 
@@ -516,6 +518,8 @@ sidebar, **Learn ▸ Open Exercise Pack…**, or **Learn ▸ Open Demo Exercises
516
518
  exercises): the window switches to a view made for working through a pack
517
519
  without distractions, and *Exit* brings your folder back exactly as it was.
518
520
 
521
+ ![An exercise in Learn: the worksheet with its answer boxes on the left, the exercise's log, given net and workflow on the right](docs/screenshots/learn-exercise.png)
522
+
519
523
  - **The worksheet** on the left is the question top to bottom, with an
520
524
  answer box wherever one is needed: yes/no, multiple choice, a set
521
525
  (`{a, b}`, sets of sets, or pairs like `({a}, {b,d})`), a number, a short
@@ -703,7 +707,10 @@ generated from `openprocess/mining/definitions.py`; after editing a definition,
703
707
  Likewise [`docs/references.md`](docs/references.md) — every source OpenProcess's
704
708
  notation and algorithms follow, also in the app under **Help ▸ References** —
705
709
  is generated from `openprocess/references.py` with
706
- `python -m openprocess.references > docs/references.md`.
710
+ `python -m openprocess.references > docs/references.md`. The screenshots in
711
+ this README come from the app itself: `python docs/make_screenshots.py`
712
+ renders every page offscreen into `docs/screenshots/`, so run it after a
713
+ change to the app's looks.
707
714
 
708
715
  The engines (`openprocess.mining`, `openprocess.ml`, `openprocess.sim`, `openprocess.analysis`)
709
716
  have **no dependencies**, so they work in a notebook or a script:
@@ -730,6 +737,10 @@ Miner, a fitness check), *Compare discovery* (three miners side by side),
730
737
  confidence* (bootstrap intervals, a test and a plot; needs the `science`
731
738
  extra) and *Predict the next activity*. Or start empty.
732
739
 
740
+ | The canvas: *Compare discovery*, three miners side by side | Summary: every box's key figure, and the process map |
741
+ |---|---|
742
+ | ![The canvas of a Compare discovery analysis, the Compare box's table beside it](docs/screenshots/workflow-canvas.png) | ![The Summary view: a tile per box, the process map under them](docs/screenshots/workflow-summary.png) |
743
+
733
744
  - **The page is the canvas.** Its header has **Canvas | Summary**, then
734
745
  **+ Add box**, **Run ▶**, and **⋯** for Re-run, Record, Export experiment
735
746
  and Save. The sidebar beside it lists the folder's analyses and, folded
@@ -786,8 +797,18 @@ extra) and *Predict the next activity*. Or start empty.
786
797
  `boxes/`, so it is listed under *Yours* and can be used in any workflow.
787
798
  - **Export experiment** (under ⋯) writes a zip with the workflow file and its record,
788
799
  the inputs, your boxes, every result as a file (CSV, PNML, SVG, XES), a
789
- `requirements.lock` and a README that says what was run: supplementary
790
- material for a paper, which `openprocess run --check` can verify.
800
+ `requirements.lock`, a `run.py` that runs the same analysis as a plain
801
+ script, and a README that says what was run: supplementary material for a
802
+ paper, which `openprocess run --check` can verify. Drop such a zip on
803
+ OpenProcess and it is unpacked into a folder of its own and run again.
804
+ - **Export as Python script** (under ⋯) writes the analysis as a `.py` file:
805
+ the same boxes, settings and connections, run with `python analysis.py`,
806
+ the results written beside it.
807
+ - **Every result knows where it came from**: a figure saved as SVG carries
808
+ the analysis, the box, its settings and the result's fingerprint in its
809
+ metadata; a CSV gets a `.provenance.json` beside it.
810
+ - **⌘Z and ⇧⌘Z** undo and redo on the canvas: boxes, connections, moves,
811
+ settings, groups.
791
812
  - **The side panel** resizes by dragging the gap beside it; double-click the
792
813
  gap, or *View ▸ Reset Workflow Layout*, for its default width.
793
814
  - **Connections** (at the bottom of the sidebar, or *View ▸ Connections*)
@@ -48,7 +48,7 @@ from .sim.simulator import Simulator
48
48
  #: The one place the version number is written (pyproject.toml, the update
49
49
  #: check and packaging/build.py read it from here). A release is tagged
50
50
  #: "v" + this, e.g. v0.2.0.
51
- __version__ = "0.10.6"
51
+ __version__ = "0.11.0"
52
52
 
53
53
  __all__ = [
54
54
  "Arc", "BindingElement", "CPNet", "Marking", "Multiset", "Page", "Place",
@@ -110,11 +110,13 @@ def correlate(table: Table) -> Table:
110
110
  @box(name="Plot", group="Science", needs="matplotlib")
111
111
  def plot(scores: list[Scores] = (), tables: list[Table] = (),
112
112
  x: Column = "", y: Column = "") -> Figure:
113
- """Bars per model for scores, histograms for bootstrap tables, or a line
114
- of column *y* against column *x* of a table (a sweep). The figure is a
113
+ """Bars per model for scores, histograms for bootstrap tables, or column
114
+ *y* of a table against column *x*: bars when *x* holds names (a model per
115
+ bar), a line when it holds numbers (a sweep). Once *x* and *y* are set
116
+ they decide the figure, whatever else is connected. The figure is a
115
117
  matplotlib figure, converted to SVG; its code is this box's code.
116
118
 
117
- x: a column of the table to put on the x axis (lines only)
119
+ x: a column of the table to put on the x axis
118
120
  y: the column to plot against it
119
121
  """
120
122
  import matplotlib
@@ -122,7 +124,24 @@ def plot(scores: list[Scores] = (), tables: list[Table] = (),
122
124
  import matplotlib.pyplot as plt
123
125
  fig, ax = plt.subplots(figsize=(5.5, 3.2))
124
126
  scores, tables = list(scores), list(tables)
125
- if scores:
127
+ if tables and x and y:
128
+ for table in tables:
129
+ for name in (x, y):
130
+ if name not in table.columns:
131
+ raise ValueError(f"{table.name} has no column {name!r}; it has {', '.join(table.columns)}")
132
+ numeric = all(isinstance(v, (int, float)) and not isinstance(v, bool) for t in tables for v in t.column(x))
133
+ for table in tables:
134
+ values = [float(v) for v in table.column(y)]
135
+ if numeric:
136
+ ax.plot(table.column(x), values, marker="o", label=table.name)
137
+ else:
138
+ ax.bar([str(v) for v in table.column(x)], values, label=table.name)
139
+ ax.set_xlabel(x)
140
+ ax.set_ylabel(y)
141
+ if len(tables) > 1:
142
+ ax.legend(fontsize=8)
143
+ caption = f"{y} against {x}."
144
+ elif scores:
126
145
  metrics = [m for m in dict.fromkeys(k for s in scores for k in s.metrics)
127
146
  if all(isinstance(s.metrics.get(m, 0.0), (int, float)) for s in scores)]
128
147
  width = 0.8 / max(1, len(metrics))
@@ -134,14 +153,6 @@ def plot(scores: list[Scores] = (), tables: list[Table] = (),
134
153
  ax.set_ylim(0, 1.05)
135
154
  ax.legend(fontsize=8)
136
155
  caption = f"{len(scores)} models, {len(metrics)} scores each."
137
- elif tables and x and y:
138
- for table in tables:
139
- ax.plot(table.column(x), table.column(y), marker="o", label=table.name)
140
- ax.set_xlabel(x)
141
- ax.set_ylabel(y)
142
- if len(tables) > 1:
143
- ax.legend(fontsize=8)
144
- caption = f"{y} against {x}."
145
156
  elif tables:
146
157
  for table in tables:
147
158
  if table.samples:
@@ -150,7 +161,7 @@ def plot(scores: list[Scores] = (), tables: list[Table] = (),
150
161
  ax.legend(fontsize=8)
151
162
  caption = f"{len(tables)} bootstrap distribution{'s' if len(tables) != 1 else ''}."
152
163
  else:
153
- raise ValueError("Connect scores, or tables with samples or an x and y column")
164
+ raise ValueError("Connect scores, or tables with samples, or a table and set both x and y")
154
165
  fig.tight_layout()
155
166
  figure = convert(fig, Figure)
156
167
  plt.close(fig)
@@ -0,0 +1,113 @@
1
+ """Undo and redo for a workflow, by snapshots.
2
+
3
+ A workflow is small (its dict form, :meth:`Workflow.to_dict`, is a few
4
+ kilobytes), so every change keeps the whole state before it: adding,
5
+ removing, wiring, moving or grouping boxes, a setting, a rename. Undo puts
6
+ a snapshot back; the page then runs the boxes again, and the cache serves
7
+ the ones that did not change. Snapshots are taken where the page already
8
+ learns of a change, so no edit is instrumented by hand, and a box author
9
+ never sees any of this.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import time
15
+
16
+ #: How many steps back are kept.
17
+ LIMIT = 100
18
+ #: A drag arrives as several moves; those within this many seconds are one step.
19
+ MOVE_SECONDS = 1.5
20
+
21
+
22
+ def describe(before: dict, after: dict) -> str:
23
+ """What changed from ``before`` to ``after``, for "Undo adding a box"."""
24
+ b = {n["id"]: n for n in before.get("boxes", [])}
25
+ a = {n["id"]: n for n in after.get("boxes", [])}
26
+ added, removed = set(a) - set(b), set(b) - set(a)
27
+ if added:
28
+ return "adding a box" if len(added) == 1 else f"adding {len(added)} boxes"
29
+ if removed:
30
+ return "removing a box" if len(removed) == 1 else f"removing {len(removed)} boxes"
31
+ if before.get("connections") != after.get("connections"):
32
+ return "a connection"
33
+ if before.get("groups") != after.get("groups"):
34
+ return "grouping"
35
+ for node_id in a:
36
+ if a[node_id].get("settings") != b[node_id].get("settings"):
37
+ return "a setting"
38
+ if a[node_id].get("title", "") != b[node_id].get("title", ""):
39
+ return "a rename"
40
+ if any(a[i].get("position") != b[i].get("position") for i in a):
41
+ return "a move"
42
+ if any(a[i].get("layout") != b[i].get("layout") for i in a):
43
+ return "tidying a drawing"
44
+ if before.get("name") != after.get("name"):
45
+ return "a rename"
46
+ return "a change"
47
+
48
+
49
+ class History:
50
+ """The states a workflow went through; :attr:`state` is the current one."""
51
+
52
+ def __init__(self, snapshot: dict, limit: int = LIMIT) -> None:
53
+ self.state = snapshot
54
+ self.limit = limit
55
+ self.undo_stack: list[dict] = []
56
+ self.redo_stack: list[dict] = []
57
+ self._last: tuple[str | None, float] = (None, 0.0)
58
+
59
+ def record(self, snapshot: dict, kind: str = "change", now: float | None = None) -> bool:
60
+ """The workflow is now ``snapshot``; the state before it becomes undoable.
61
+ Returns whether anything changed. ``kind`` "move" folds a drag's
62
+ steps into one."""
63
+ if snapshot == self.state:
64
+ return False
65
+ now = time.monotonic() if now is None else now
66
+ last_kind, last_time = self._last
67
+ if not (kind == "move" and last_kind == "move" and now - last_time < MOVE_SECONDS and self.undo_stack):
68
+ self.undo_stack.append(self.state)
69
+ del self.undo_stack[:-self.limit]
70
+ self.redo_stack.clear()
71
+ self.state = snapshot
72
+ self._last = (kind, now)
73
+ return True
74
+
75
+ @property
76
+ def can_undo(self) -> bool:
77
+ return bool(self.undo_stack)
78
+
79
+ @property
80
+ def can_redo(self) -> bool:
81
+ return bool(self.redo_stack)
82
+
83
+ @property
84
+ def undo_label(self) -> str:
85
+ return describe(self.undo_stack[-1], self.state) if self.undo_stack else ""
86
+
87
+ @property
88
+ def redo_label(self) -> str:
89
+ return describe(self.state, self.redo_stack[-1]) if self.redo_stack else ""
90
+
91
+ def undo(self) -> tuple[str, dict] | None:
92
+ """Step back: (what is undone, the state to restore), or None."""
93
+ if not self.undo_stack:
94
+ return None
95
+ before = self.undo_stack.pop()
96
+ label = describe(before, self.state)
97
+ self.redo_stack.append(self.state)
98
+ self.state = before
99
+ self._last = (None, 0.0)
100
+ return label, before
101
+
102
+ def redo(self) -> tuple[str, dict] | None:
103
+ if not self.redo_stack:
104
+ return None
105
+ after = self.redo_stack.pop()
106
+ label = describe(self.state, after)
107
+ self.undo_stack.append(self.state)
108
+ self.state = after
109
+ self._last = (None, 0.0)
110
+ return label, after
111
+
112
+
113
+ __all__ = ["History", "describe", "LIMIT", "MOVE_SECONDS"]
@@ -0,0 +1,110 @@
1
+ """Where a result came from, carried with it and written into the files it becomes.
2
+
3
+ The runner stamps every value a box gives with a :class:`Provenance`: the
4
+ analysis, the box and its settings, and the result's key (the fingerprint of
5
+ the box's code, its settings and its inputs, which is also the cache key).
6
+ A figure saved as SVG carries it in a ``<metadata>`` block; a table saved as
7
+ CSV, or a figure as PNG, gets a ``.provenance.json`` beside it. A figure in
8
+ a paper then points back at the analysis that made it.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import datetime as _dt
14
+ import json
15
+ import re
16
+ from dataclasses import asdict, dataclass
17
+ from pathlib import Path
18
+ from xml.sax.saxutils import escape
19
+
20
+
21
+ @dataclass
22
+ class Provenance:
23
+ workflow: str
24
+ box: str
25
+ title: str
26
+ settings: dict
27
+ key: str
28
+ openprocess: str
29
+ when: str
30
+
31
+ def as_dict(self) -> dict:
32
+ return asdict(self)
33
+
34
+ def json(self) -> str:
35
+ return json.dumps(self.as_dict(), indent=2, ensure_ascii=False, default=str)
36
+
37
+ def line(self) -> str:
38
+ return (f"{self.title} ({self.box}) in “{self.workflow}”, OpenProcess {self.openprocess}, "
39
+ f"{self.when}; key {self.key[:12]}")
40
+
41
+ def svg_metadata(self) -> str:
42
+ """A Dublin Core block for an SVG's ``<metadata>``."""
43
+ return (
44
+ '<metadata id="openprocess-provenance">'
45
+ '<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" '
46
+ 'xmlns:dc="http://purl.org/dc/elements/1.1/"><rdf:Description>'
47
+ f"<dc:creator>OpenProcess {escape(self.openprocess)}</dc:creator>"
48
+ f"<dc:source>{escape(self.line())}</dc:source>"
49
+ f"<dc:date>{escape(self.when)}</dc:date>"
50
+ f"<dc:description>{escape(self.json())}</dc:description>"
51
+ "</rdf:Description></rdf:RDF></metadata>"
52
+ )
53
+
54
+
55
+ def make(workflow, node, spec, settings: dict, key: str) -> Provenance:
56
+ from .. import __version__
57
+ plain = {}
58
+ for name, value in settings.items():
59
+ if hasattr(value, "describe"): # a sweep
60
+ value = value.describe()
61
+ plain[name] = str(value) if isinstance(value, Path) else value
62
+ return Provenance(workflow.name, spec.id, workflow.title(node), plain, key, __version__,
63
+ _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds"))
64
+
65
+
66
+ def stamp(value, provenance: Provenance) -> bool:
67
+ """Attach ``provenance`` to ``value`` when it can carry one (a dataclass
68
+ can; a number or a string cannot). Returns whether it did."""
69
+ if value is None or isinstance(value, (bool, int, float, str, bytes, list, tuple, dict, set, frozenset)):
70
+ return False
71
+ try:
72
+ object.__setattr__(value, "provenance", provenance)
73
+ except (AttributeError, TypeError):
74
+ return False
75
+ return True
76
+
77
+
78
+ def of(value) -> Provenance | None:
79
+ found = getattr(value, "provenance", None)
80
+ return found if isinstance(found, Provenance) else None
81
+
82
+
83
+ _SVG_OPEN = re.compile(r"<svg\b[^>]*>", re.S)
84
+
85
+
86
+ def with_metadata(svg: str, provenance: Provenance | None) -> str:
87
+ """The SVG text with the provenance in a ``<metadata>`` block after the opening tag."""
88
+ if provenance is None:
89
+ return svg
90
+ match = _SVG_OPEN.search(svg)
91
+ if match is None:
92
+ return svg
93
+ return svg[:match.end()] + "\n" + provenance.svg_metadata() + svg[match.end():]
94
+
95
+
96
+ def sidecar_path(path) -> Path:
97
+ """``table.csv`` → ``table.provenance.json`` beside it."""
98
+ path = Path(path)
99
+ return path.with_name(path.stem + ".provenance.json")
100
+
101
+
102
+ def write_sidecar(path, provenance: Provenance | None) -> Path | None:
103
+ if provenance is None:
104
+ return None
105
+ target = sidecar_path(path)
106
+ target.write_text(provenance.json() + "\n", encoding="utf-8")
107
+ return target
108
+
109
+
110
+ __all__ = ["Provenance", "make", "of", "sidecar_path", "stamp", "with_metadata", "write_sidecar"]
@@ -310,7 +310,9 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
310
310
  lines = [f"# {name}", "", f"Exported {record.saved} with OpenProcess {record.versions.get('openprocess', '')}, "
311
311
  f"Python {record.versions.get('python', '')}.", "",
312
312
  "Run it again with `openprocess run " + f"{name}.cpnflow --check`: it fails if any result differs "
313
- "from the recorded one. `requirements.lock` is the environment it ran in.", "", "## Boxes", ""]
313
+ "from the recorded one. `requirements.lock` is the environment it ran in. `run.py` is the same "
314
+ "analysis as a plain Python script (`python run.py`). Or drop this zip on OpenProcess: it unpacks "
315
+ "it into a folder and runs it.", "", "## Boxes", ""]
314
316
  for node in workflow.order():
315
317
  spec = workflow.spec(node)
316
318
  settings = ", ".join(f"{k} = {v}" for k, v in node.settings.items()) or "no settings"
@@ -335,6 +337,8 @@ def export_experiment(workflow: Workflow, run: Run | None, target: str | Path,
335
337
  with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zipped:
336
338
  zipped.writestr(f"{name}.cpnflow", json.dumps(data, indent=2, ensure_ascii=False, default=str))
337
339
  zipped.writestr("requirements.lock", requirements_lock(record))
340
+ from .script import script_text
341
+ zipped.writestr("run.py", script_text(workflow, "run.py"))
338
342
  zipped.writestr("CITATIONS.bib", "\n\n".join(e.bibtex() for e in entries) + "\n")
339
343
  zipped.writestr("README.md", "\n".join(lines) + "\n")
340
344
  for item in record.inputs:
@@ -368,6 +372,62 @@ def _describe(value) -> str:
368
372
  return type(value).__name__ if value is not None else "done"
369
373
 
370
374
 
375
+ def is_experiment(path) -> bool:
376
+ """Whether ``path`` is a zip :func:`export_experiment` wrote: one ``.cpnflow`` at the top."""
377
+ import zipfile
378
+ try:
379
+ with zipfile.ZipFile(path) as zipped:
380
+ return sum(1 for n in zipped.namelist() if n.endswith(".cpnflow") and "/" not in n) == 1
381
+ except (OSError, zipfile.BadZipFile):
382
+ return False
383
+
384
+
385
+ def import_experiment(archive, into) -> Path:
386
+ """Unpack an exported experiment into a new folder under ``into`` (named
387
+ after the zip) and give back its workflow file, ready to open: the file
388
+ settings point at ``inputs/``, the folder's ``boxes/`` holds the custom
389
+ boxes (a box is found by its function name there), and the recorded
390
+ results are kept for the reproducibility check."""
391
+ import zipfile
392
+ archive = Path(archive)
393
+ with zipfile.ZipFile(archive) as zipped:
394
+ names = zipped.namelist()
395
+ flows = [n for n in names if n.endswith(".cpnflow") and "/" not in n]
396
+ if len(flows) != 1:
397
+ raise ValueError(f"{archive.name} is not an exported experiment (one .cpnflow at the top is needed)")
398
+ folder = Path(into) / archive.stem
399
+ counter = 2
400
+ while folder.exists():
401
+ folder = Path(into) / f"{archive.stem} {counter}"
402
+ counter += 1
403
+ folder.mkdir(parents=True)
404
+ for name in names:
405
+ parts = Path(name).parts
406
+ if name.endswith("/") or not parts or ".." in parts or Path(name).is_absolute():
407
+ continue
408
+ target = folder / name
409
+ target.parent.mkdir(parents=True, exist_ok=True)
410
+ target.write_bytes(zipped.read(name))
411
+ flow_file = folder / flows[0]
412
+ data = json.loads(flow_file.read_text(encoding="utf-8"))
413
+ moved: dict[tuple[str, str], str] = {}
414
+ for item in data.get("record", {}).get("inputs", []):
415
+ new = f"inputs/{Path(item['file']).name}"
416
+ if (folder / new).is_file():
417
+ moved[(item["node"], item["setting"])] = new
418
+ item["file"] = new
419
+ for item in data.get("record", {}).get("custom_boxes", []):
420
+ new = f"boxes/{Path(item['file']).name}"
421
+ if (folder / new).is_file():
422
+ item["file"] = new
423
+ for box in data.get("boxes", []):
424
+ for setting in list(box.get("settings", {})):
425
+ if (box["id"], setting) in moved:
426
+ box["settings"][setting] = moved[(box["id"], setting)]
427
+ flow_file.write_text(json.dumps(data, indent=2, ensure_ascii=False, default=str), encoding="utf-8")
428
+ return flow_file
429
+
430
+
371
431
  def _as_file(value, stem: str):
372
432
  """(name, bytes or text) for a result worth a file of its own."""
373
433
  if isinstance(value, Table):
@@ -396,5 +456,9 @@ def _as_file(value, stem: str):
396
456
  return None
397
457
 
398
458
 
399
- __all__ = ["FORMAT", "Record", "check", "differences", "environment", "export_experiment", "fingerprint",
400
- "load", "make_record", "requirements_lock", "save", "sha256_file", "sha256_text", "versions"]
459
+ #: (name, bytes or text) for a result worth a file of its own; the script an experiment carries uses it.
460
+ as_file = _as_file
461
+
462
+ __all__ = ["FORMAT", "Record", "as_file", "check", "differences", "environment", "export_experiment",
463
+ "fingerprint", "import_experiment", "is_experiment", "load", "make_record", "requirements_lock",
464
+ "save", "sha256_file", "sha256_text", "versions"]
@@ -317,6 +317,7 @@ class Runner:
317
317
  result.values, result.explanation = cached
318
318
  result.value = result.values.get("out", next(iter(result.values.values()), None))
319
319
  result.status, result.cached = DONE, True
320
+ self._stamp(result, workflow, node, spec, settings)
320
321
  return result
321
322
  # Run it
322
323
  result.status = RUNNING
@@ -341,8 +342,17 @@ class Runner:
341
342
  result.values, result.value, result.explanation = values, values.get("out", value), explanation
342
343
  result.status = DONE
343
344
  self.cache.put(result.key, values, explanation)
345
+ self._stamp(result, workflow, node, spec, settings)
344
346
  return result
345
347
 
348
+ @staticmethod
349
+ def _stamp(result: Result, workflow: Workflow, node: Node, spec: BoxSpec, settings: dict) -> None:
350
+ """Every value a box gives carries where it came from (:mod:`openprocess.flow.provenance`)."""
351
+ from .provenance import make, stamp
352
+ info = make(workflow, node, spec, settings, result.key)
353
+ for out in result.values.values():
354
+ stamp(out, info)
355
+
346
356
  @staticmethod
347
357
  def _outputs(spec: BoxSpec, value) -> dict[str, Any]:
348
358
  if spec.result_type is not None: