openprocess 0.7.0__py3-none-any.whl
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.
- cpnpy/__init__.py +46 -0
- openprocess/__init__.py +57 -0
- openprocess/analysis/__init__.py +0 -0
- openprocess/analysis/state_space.py +521 -0
- openprocess/analysis/state_space_process.py +251 -0
- openprocess/cli.py +742 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/answer.pnml +31 -0
- openprocess/exercises/1 Petri nets/Exercise 1.1 Order handling/question.md +38 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/answer.pnml +27 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/net.pnml +29 -0
- openprocess/exercises/2 Soundness/Exercise 2.1 Spot the flaw/question.md +65 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/log.txt +1 -0
- openprocess/exercises/3 Discovery/Exercise 3.1 The alpha-algorithm/question.md +67 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/question.md +75 -0
- openprocess/exercises/4 Regions/Exercise 4.1 Regions of a transition system/ts.txt +5 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/net.pnml +27 -0
- openprocess/exercises/5 Markings/Exercise 5.1 Markings and matrices/question.md +65 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/log.txt +1 -0
- openprocess/exercises/6 Inductive Miner/Exercise 6.1 Cuts and trees/question.md +62 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/log.txt +1 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m1.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m2.pnml +28 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/m3.pnml +30 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/net.pnml +36 -0
- openprocess/exercises/7 Conformance/Exercise 7.1 Replay, alignments and workflows/question.md +69 -0
- openprocess/exercises/pack.md +14 -0
- openprocess/flow/__init__.py +50 -0
- openprocess/flow/box.py +466 -0
- openprocess/flow/boxes/__init__.py +7 -0
- openprocess/flow/boxes/check.py +119 -0
- openprocess/flow/boxes/compare.py +16 -0
- openprocess/flow/boxes/cpn.py +53 -0
- openprocess/flow/boxes/discover.py +124 -0
- openprocess/flow/boxes/filter.py +80 -0
- openprocess/flow/boxes/input.py +124 -0
- openprocess/flow/boxes/output.py +52 -0
- openprocess/flow/boxes/predict.py +186 -0
- openprocess/flow/boxes/science.py +159 -0
- openprocess/flow/boxes/sweeps.py +18 -0
- openprocess/flow/convert.py +187 -0
- openprocess/flow/datasets.py +198 -0
- openprocess/flow/explain.py +115 -0
- openprocess/flow/library.py +222 -0
- openprocess/flow/record.py +385 -0
- openprocess/flow/runner.py +357 -0
- openprocess/flow/sweep.py +92 -0
- openprocess/flow/types.py +290 -0
- openprocess/flow/workflow.py +628 -0
- openprocess/gui/__init__.py +0 -0
- openprocess/gui/app.py +90 -0
- openprocess/gui/arc_editing.py +295 -0
- openprocess/gui/canvas.py +1414 -0
- openprocess/gui/flow/__init__.py +8 -0
- openprocess/gui/flow/canvas.py +854 -0
- openprocess/gui/flow/page.py +972 -0
- openprocess/gui/flow/templates.py +131 -0
- openprocess/gui/flow/viewers.py +665 -0
- openprocess/gui/items.py +1275 -0
- openprocess/gui/learn/answer_boxes.py +978 -0
- openprocess/gui/learn/concealment.py +91 -0
- openprocess/gui/learn/mode.py +1181 -0
- openprocess/gui/panning.py +241 -0
- openprocess/gui/resources/openprocess-icon.png +0 -0
- openprocess/gui/studio/__init__.py +1 -0
- openprocess/gui/studio/__main__.py +3 -0
- openprocess/gui/studio/app.py +4031 -0
- openprocess/gui/studio/charts.py +115 -0
- openprocess/gui/studio/compare_page.py +487 -0
- openprocess/gui/studio/cpn_page.py +1858 -0
- openprocess/gui/studio/definition_view.py +284 -0
- openprocess/gui/studio/derivation_view.py +421 -0
- openprocess/gui/studio/documents.py +152 -0
- openprocess/gui/studio/dotted_chart.py +1401 -0
- openprocess/gui/studio/file_dialogs.py +143 -0
- openprocess/gui/studio/filter_dialog.py +247 -0
- openprocess/gui/studio/graph_builders.py +176 -0
- openprocess/gui/studio/graph_view.py +682 -0
- openprocess/gui/studio/instances.py +413 -0
- openprocess/gui/studio/log_editor.py +675 -0
- openprocess/gui/studio/log_page.py +800 -0
- openprocess/gui/studio/markdown_view.py +127 -0
- openprocess/gui/studio/mathtext.py +260 -0
- openprocess/gui/studio/ml_highlighter.py +75 -0
- openprocess/gui/studio/model_page.py +760 -0
- openprocess/gui/studio/net_comparison.py +124 -0
- openprocess/gui/studio/notes_overlay.py +275 -0
- openprocess/gui/studio/petri_page.py +844 -0
- openprocess/gui/studio/regions_view.py +502 -0
- openprocess/gui/studio/sidebar.py +149 -0
- openprocess/gui/studio/style.py +503 -0
- openprocess/gui/studio/tool_icons.py +134 -0
- openprocess/gui/studio/updates.py +439 -0
- openprocess/gui/studio/widgets.py +899 -0
- openprocess/gui/studio/workers.py +60 -0
- openprocess/gui/studio/workspace.py +447 -0
- openprocess/gui/theme.py +394 -0
- openprocess/gui/tidy.py +86 -0
- openprocess/io/__init__.py +0 -0
- openprocess/io/cpn_reader.py +389 -0
- openprocess/io/cpn_writer.py +357 -0
- openprocess/learn/__init__.py +23 -0
- openprocess/learn/answers.py +188 -0
- openprocess/learn/checks.py +953 -0
- openprocess/learn/computed.py +1180 -0
- openprocess/learn/context.py +145 -0
- openprocess/learn/exam.py +169 -0
- openprocess/learn/exercise-packs.md +325 -0
- openprocess/learn/importer.py +216 -0
- openprocess/learn/notation.py +474 -0
- openprocess/learn/pack.py +511 -0
- openprocess/learn/sheet.py +296 -0
- openprocess/mining/__init__.py +73 -0
- openprocess/mining/analysis.py +689 -0
- openprocess/mining/columns.py +282 -0
- openprocess/mining/compare_nets.py +246 -0
- openprocess/mining/conformance/__init__.py +0 -0
- openprocess/mining/conformance/alignments.py +263 -0
- openprocess/mining/conformance/quality.py +145 -0
- openprocess/mining/conformance/token_replay.py +252 -0
- openprocess/mining/csv_import.py +222 -0
- openprocess/mining/definitions.py +584 -0
- openprocess/mining/dfg.py +187 -0
- openprocess/mining/discovery/__init__.py +0 -0
- openprocess/mining/discovery/alpha.py +168 -0
- openprocess/mining/discovery/heuristics.py +332 -0
- openprocess/mining/discovery/inductive.py +477 -0
- openprocess/mining/discovery/state_regions.py +62 -0
- openprocess/mining/filtering.py +237 -0
- openprocess/mining/footprint.py +183 -0
- openprocess/mining/invariants.py +191 -0
- openprocess/mining/layout.py +279 -0
- openprocess/mining/log.py +364 -0
- openprocess/mining/petrinet.py +354 -0
- openprocess/mining/playout.py +75 -0
- openprocess/mining/pm4py_bridge.py +82 -0
- openprocess/mining/pnml.py +223 -0
- openprocess/mining/processtree.py +216 -0
- openprocess/mining/regions.py +476 -0
- openprocess/mining/stats.py +160 -0
- openprocess/mining/structure.py +374 -0
- openprocess/mining/transition_system.py +409 -0
- openprocess/mining/xes.py +399 -0
- openprocess/ml/__init__.py +0 -0
- openprocess/ml/ast_nodes.py +332 -0
- openprocess/ml/builtins.py +364 -0
- openprocess/ml/colorsets.py +522 -0
- openprocess/ml/errors.py +60 -0
- openprocess/ml/evaluator.py +754 -0
- openprocess/ml/lexer.py +277 -0
- openprocess/ml/multiset.py +417 -0
- openprocess/ml/parser.py +737 -0
- openprocess/ml/values.py +319 -0
- openprocess/model/__init__.py +0 -0
- openprocess/model/declarations.py +617 -0
- openprocess/model/examples.py +98 -0
- openprocess/model/net.py +701 -0
- openprocess/model/plain.py +192 -0
- openprocess/references.py +280 -0
- openprocess/sim/__init__.py +0 -0
- openprocess/sim/binding.py +620 -0
- openprocess/sim/export.py +66 -0
- openprocess/sim/simulator.py +315 -0
- openprocess/teaching/__init__.py +4 -0
- openprocess/teaching/answers.py +4 -0
- openprocess/teaching/checks.py +5 -0
- openprocess/teaching/pack.py +4 -0
- openprocess/teaching/sheet.py +4 -0
- openprocess-0.7.0.dist-info/METADATA +927 -0
- openprocess-0.7.0.dist-info/RECORD +173 -0
- openprocess-0.7.0.dist-info/WHEEL +5 -0
- openprocess-0.7.0.dist-info/entry_points.txt +6 -0
- openprocess-0.7.0.dist-info/licenses/LICENSE +21 -0
- openprocess-0.7.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Compute a state space in a separate process.
|
|
2
|
+
|
|
3
|
+
Why a process and not a thread
|
|
4
|
+
------------------------------
|
|
5
|
+
State space exploration is pure Python. In a thread it competes with the
|
|
6
|
+
window for Python's global interpreter lock: every paint and every click
|
|
7
|
+
handler has to wait for the lock, so the whole app stutters for as long as
|
|
8
|
+
the exploration runs. A separate process has its own interpreter, so the
|
|
9
|
+
window stays fully responsive -- and it can be stopped at any moment.
|
|
10
|
+
|
|
11
|
+
The model travels to the worker as a ``.cpn`` file (the same format the app
|
|
12
|
+
saves), and the results come back as plain data (numbers, lists, strings),
|
|
13
|
+
which is everything the GUI shows.
|
|
14
|
+
|
|
15
|
+
The worker is ``python -m openprocess.analysis.state_space_process`` started with
|
|
16
|
+
the same interpreter, rather than :mod:`multiprocessing`, which would
|
|
17
|
+
re-import the GUI's main script in the child.
|
|
18
|
+
|
|
19
|
+
Inside a standalone app (built with PyInstaller, see ``packaging/``) there is
|
|
20
|
+
no separate Python to run: ``sys.executable`` is the app itself. The app is
|
|
21
|
+
then started again with :data:`WORKER_FLAG` as its first argument, and its
|
|
22
|
+
entry script hands those runs to :func:`run_worker` instead of opening a
|
|
23
|
+
window (:func:`worker_command` picks the right form).
|
|
24
|
+
|
|
25
|
+
Usage::
|
|
26
|
+
|
|
27
|
+
job = StateSpaceJob(net, max_nodes=20_000)
|
|
28
|
+
job.start()
|
|
29
|
+
... # poll job.progress() / job.result() from a timer
|
|
30
|
+
job.cancel() # ask it to stop; it returns what it has found
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
import subprocess
|
|
37
|
+
import sys
|
|
38
|
+
import tempfile
|
|
39
|
+
import threading
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def summarise(space, graph_limit: int = 200) -> dict[str, Any]:
|
|
45
|
+
"""Everything the GUI displays, as picklable data."""
|
|
46
|
+
net = space.net
|
|
47
|
+
places = list(net.all_places())
|
|
48
|
+
partial = space.partial
|
|
49
|
+
bounds = space.integer_bounds()
|
|
50
|
+
result: dict[str, Any] = {
|
|
51
|
+
"nodes": space.node_count,
|
|
52
|
+
"arcs": space.arc_count,
|
|
53
|
+
"partial": partial,
|
|
54
|
+
"cancelled": space.cancelled,
|
|
55
|
+
"unexplored": space.unexplored_count,
|
|
56
|
+
"sccs": len(space.strongly_connected_components()),
|
|
57
|
+
"dead": space.dead_markings(),
|
|
58
|
+
"home": None if partial else space.home_markings(),
|
|
59
|
+
"dead_transitions": [t.name for t in space.dead_transitions()],
|
|
60
|
+
"live": None if partial else [t.name for t in space.live_transitions()],
|
|
61
|
+
"bounds": [(p.id, p.name, *bounds[p.id]) for p in places],
|
|
62
|
+
"multiset": {k: str(v) for k, v in space.upper_multiset_bounds().items()},
|
|
63
|
+
"report": space.report(),
|
|
64
|
+
"graph": None,
|
|
65
|
+
}
|
|
66
|
+
if space.node_count <= graph_limit:
|
|
67
|
+
dead = set(result["dead"])
|
|
68
|
+
nodes = [(index, space.describe_state(index), index in dead)
|
|
69
|
+
for index in range(space.node_count)]
|
|
70
|
+
arcs = [(arc.source, arc.target, arc.binding.describe(net))
|
|
71
|
+
for arcs in space.arcs.values() for arc in arcs]
|
|
72
|
+
result["graph"] = (nodes, arcs)
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
#: First argument that makes a standalone app run as a state space worker.
|
|
77
|
+
WORKER_FLAG = "--openprocess-state-space-worker"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def worker_command(*arguments: str) -> list[str]:
|
|
81
|
+
"""The command line that starts a worker with ``arguments``.
|
|
82
|
+
|
|
83
|
+
From source (or a pip/conda install) that is this module run by the same
|
|
84
|
+
Python. In a frozen app ``sys.executable`` is the app's own program, so
|
|
85
|
+
the app is re-launched with :data:`WORKER_FLAG` instead.
|
|
86
|
+
"""
|
|
87
|
+
if getattr(sys, "frozen", False):
|
|
88
|
+
return [sys.executable, WORKER_FLAG, *arguments]
|
|
89
|
+
return [sys.executable, "-m", "openprocess.analysis.state_space_process", *arguments]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run_worker(arguments: list[str]) -> int:
|
|
93
|
+
"""Run the worker on ``[MODEL, MAX_NODES, OUT]`` (the arguments after the flag)."""
|
|
94
|
+
model_path, max_nodes, output_path = arguments
|
|
95
|
+
return _child(model_path, int(max_nodes), output_path)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _child(model_path: str, max_nodes: int, output_path: str) -> int:
|
|
99
|
+
"""The worker: ``python -m openprocess.analysis.state_space_process MODEL N OUT``.
|
|
100
|
+
|
|
101
|
+
Talks to the parent through its standard streams: it prints
|
|
102
|
+
``progress <nodes> <arcs>`` lines and ``phase <name>`` while working, and
|
|
103
|
+
stops early when the parent writes ``stop``. The result is pickled to
|
|
104
|
+
``OUT``, then ``done`` (or ``error <message>``) is printed.
|
|
105
|
+
"""
|
|
106
|
+
import pickle
|
|
107
|
+
import sys
|
|
108
|
+
import threading
|
|
109
|
+
import time
|
|
110
|
+
|
|
111
|
+
stop = threading.Event()
|
|
112
|
+
# The parent reads UTF-8; Windows pipes would otherwise use the ANSI code page.
|
|
113
|
+
for stream in (sys.stdout, sys.stdin):
|
|
114
|
+
if hasattr(stream, "reconfigure"):
|
|
115
|
+
stream.reconfigure(encoding="utf-8")
|
|
116
|
+
|
|
117
|
+
def listen() -> None:
|
|
118
|
+
for line in sys.stdin:
|
|
119
|
+
if line.strip() == "stop":
|
|
120
|
+
stop.set()
|
|
121
|
+
stop.set() # the parent went away: stop too
|
|
122
|
+
|
|
123
|
+
threading.Thread(target=listen, daemon=True).start()
|
|
124
|
+
|
|
125
|
+
def say(text: str) -> None:
|
|
126
|
+
print(text, flush=True)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
from ..io.cpn_reader import read_cpn
|
|
130
|
+
from .state_space import StateSpace
|
|
131
|
+
net = read_cpn(model_path)
|
|
132
|
+
problems = net.compile()
|
|
133
|
+
if problems:
|
|
134
|
+
say("error " + "; ".join(str(p) for p in problems[:3]).replace("\n", " "))
|
|
135
|
+
return 1
|
|
136
|
+
last = [0.0]
|
|
137
|
+
|
|
138
|
+
def progress(node_count: int, arc_count: int) -> None:
|
|
139
|
+
now = time.monotonic()
|
|
140
|
+
if now - last[0] > 0.15:
|
|
141
|
+
last[0] = now
|
|
142
|
+
say(f"progress {node_count} {arc_count}")
|
|
143
|
+
|
|
144
|
+
space = StateSpace(net).generate(max_nodes=max_nodes, should_stop=stop.is_set,
|
|
145
|
+
progress=progress)
|
|
146
|
+
say(f"progress {space.node_count} {space.arc_count}")
|
|
147
|
+
say("phase Analysing")
|
|
148
|
+
with open(output_path, "wb") as handle:
|
|
149
|
+
pickle.dump(summarise(space), handle)
|
|
150
|
+
say("done")
|
|
151
|
+
return 0
|
|
152
|
+
except Exception as error: # noqa: BLE001 - report anything to the GUI
|
|
153
|
+
say(f"error {type(error).__name__}: {error}".replace("\n", " "))
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class StateSpaceJob:
|
|
158
|
+
"""One state space computation in a child process."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, net, max_nodes: int = 20_000) -> None:
|
|
161
|
+
from ..io.cpn_writer import write_cpn
|
|
162
|
+
directory = tempfile.mkdtemp(prefix="openprocess-space-")
|
|
163
|
+
self.model_path = os.path.join(directory, "model.cpn")
|
|
164
|
+
self.output_path = os.path.join(directory, "result.pickle")
|
|
165
|
+
self.directory = directory
|
|
166
|
+
write_cpn(net, Path(self.model_path))
|
|
167
|
+
self.max_nodes = max_nodes
|
|
168
|
+
self.process: subprocess.Popen | None = None
|
|
169
|
+
self.nodes = self.arcs = 0
|
|
170
|
+
self.phase = "Starting"
|
|
171
|
+
self.outcome: tuple[str, Any] | None = None
|
|
172
|
+
self._message: str | None = None
|
|
173
|
+
|
|
174
|
+
def start(self) -> None:
|
|
175
|
+
# A fresh interpreter running this module: nothing of the GUI's state
|
|
176
|
+
# (Qt, open windows) is inherited.
|
|
177
|
+
# From source, run from the project folder so ``-m openprocess...`` resolves
|
|
178
|
+
# even without an install; a frozen app finds its code by itself.
|
|
179
|
+
cwd = None if getattr(sys, "frozen", False) else str(Path(__file__).resolve().parents[2])
|
|
180
|
+
self.process = subprocess.Popen(
|
|
181
|
+
worker_command(self.model_path, str(self.max_nodes), self.output_path),
|
|
182
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
183
|
+
text=True, encoding="utf-8", bufsize=1, cwd=cwd,
|
|
184
|
+
# Windows: don't flash a console window for the worker.
|
|
185
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
|
|
186
|
+
self.phase = "Exploring"
|
|
187
|
+
threading.Thread(target=self._read, daemon=True).start()
|
|
188
|
+
|
|
189
|
+
def _read(self) -> None:
|
|
190
|
+
"""Reader thread: parse the child's messages (blocks on I/O only)."""
|
|
191
|
+
assert self.process is not None and self.process.stdout is not None
|
|
192
|
+
for line in self.process.stdout:
|
|
193
|
+
word, _, rest = line.strip().partition(" ")
|
|
194
|
+
if word == "progress":
|
|
195
|
+
parts = rest.split()
|
|
196
|
+
if len(parts) == 2:
|
|
197
|
+
self.nodes, self.arcs = int(parts[0]), int(parts[1])
|
|
198
|
+
elif word == "phase":
|
|
199
|
+
self.phase = rest
|
|
200
|
+
elif word in ("done", "error"):
|
|
201
|
+
self._message = word + (" " + rest if rest else "")
|
|
202
|
+
|
|
203
|
+
def progress(self) -> tuple[int, int, str]:
|
|
204
|
+
return self.nodes, self.arcs, self.phase
|
|
205
|
+
|
|
206
|
+
def poll(self) -> tuple[str, Any] | None:
|
|
207
|
+
"""``("done", summary)``, ``("error", message)``, or ``None`` while running."""
|
|
208
|
+
if self.outcome is not None or self.process is None:
|
|
209
|
+
return self.outcome
|
|
210
|
+
if self.process.poll() is None:
|
|
211
|
+
return None
|
|
212
|
+
# The child has exited; make sure its last lines have been read.
|
|
213
|
+
for _ in range(50):
|
|
214
|
+
if self._message is not None:
|
|
215
|
+
break
|
|
216
|
+
import time
|
|
217
|
+
time.sleep(0.01)
|
|
218
|
+
message = self._message or ""
|
|
219
|
+
if message == "done":
|
|
220
|
+
import pickle
|
|
221
|
+
with open(self.output_path, "rb") as handle:
|
|
222
|
+
self.outcome = ("done", pickle.load(handle))
|
|
223
|
+
elif message.startswith("error"):
|
|
224
|
+
self.outcome = ("error", message[6:] or "unknown error")
|
|
225
|
+
else:
|
|
226
|
+
self.outcome = ("error", "stopped")
|
|
227
|
+
self._cleanup()
|
|
228
|
+
return self.outcome
|
|
229
|
+
|
|
230
|
+
def cancel(self) -> None:
|
|
231
|
+
"""Ask the worker to stop; it then reports what it has explored."""
|
|
232
|
+
if self.process is not None and self.process.poll() is None:
|
|
233
|
+
try:
|
|
234
|
+
self.process.stdin.write("stop\n")
|
|
235
|
+
self.process.stdin.flush()
|
|
236
|
+
except (OSError, ValueError):
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
def kill(self) -> None:
|
|
240
|
+
"""Stop at once, without results (closing the page, a stuck worker)."""
|
|
241
|
+
if self.process is not None and self.process.poll() is None:
|
|
242
|
+
self.process.kill()
|
|
243
|
+
self._cleanup()
|
|
244
|
+
|
|
245
|
+
def _cleanup(self) -> None:
|
|
246
|
+
import shutil
|
|
247
|
+
shutil.rmtree(self.directory, ignore_errors=True)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__": # the child process
|
|
251
|
+
raise SystemExit(run_worker(sys.argv[1:4]))
|