openprocess 0.10.5__tar.gz → 0.10.6__tar.gz

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