appose 0.2.0__tar.gz → 0.6.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.
@@ -1,4 +1,4 @@
1
- Copyright (c) 2023 - 2024, Appose developers.
1
+ Copyright (c) 2023 - 2025, Appose developers.
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without modification,
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: appose
3
- Version: 0.2.0
3
+ Version: 0.6.0
4
4
  Summary: Appose: multi-language interprocess cooperation with shared memory.
5
5
  Author: Appose developers
6
- License: Simplified BSD License
6
+ License-Expression: BSD-2-Clause
7
7
  Project-URL: homepage, https://github.com/apposed/appose-python
8
8
  Project-URL: documentation, https://github.com/apposed/appose-python/blob/main/README.md
9
9
  Project-URL: source, https://github.com/apposed/appose-python
@@ -17,7 +17,7 @@ Classifier: Intended Audience :: Science/Research
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
- Classifier: License :: OSI Approved :: BSD License
20
+ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Operating System :: Microsoft :: Windows
22
22
  Classifier: Operating System :: Unix
23
23
  Classifier: Operating System :: MacOS
@@ -40,6 +40,7 @@ Requires-Dist: pytest; extra == "dev"
40
40
  Requires-Dist: numpy; extra == "dev"
41
41
  Requires-Dist: toml; extra == "dev"
42
42
  Requires-Dist: validate-pyproject[all]; extra == "dev"
43
+ Dynamic: license-file
43
44
 
44
45
  # Appose Python
45
46
 
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "appose"
7
- version = "0.2.0"
7
+ version = "0.6.0"
8
8
  description = "Appose: multi-language interprocess cooperation with shared memory."
9
- license = {text = "Simplified BSD License"}
9
+ license = "BSD-2-Clause"
10
10
  authors = [{name = "Appose developers"}]
11
11
  readme = "README.md"
12
12
  keywords = ["java", "javascript", "python", "cross-language", "interprocess"]
@@ -18,7 +18,7 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3 :: Only",
19
19
  "Programming Language :: Python :: 3.10",
20
20
  "Programming Language :: Python :: 3.11",
21
- "License :: OSI Approved :: BSD License",
21
+ "Programming Language :: Python :: 3.12",
22
22
  "Operating System :: Microsoft :: Windows",
23
23
  "Operating System :: Unix",
24
24
  "Operating System :: MacOS",
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -42,8 +42,6 @@ The steps for using Appose are:
42
42
 
43
43
  ## Examples
44
44
 
45
- * TODO - move the below code somewhere linkable, for succinctness here.
46
-
47
45
  Here is a very simple example written in Python:
48
46
 
49
47
  import appose
@@ -84,7 +82,7 @@ And here is an example using a few more of Appose's features:
84
82
  def task_listener(event):
85
83
  match event.responseType:
86
84
  case UPDATE:
87
- print(f"Progress {task.current}/{task.maximum}")
85
+ print(f"Progress {event.current}/{event.maximum}")
88
86
  case COMPLETION:
89
87
  numer = task.outputs["numer"]
90
88
  denom = task.outputs["denom"]
@@ -127,8 +125,94 @@ But Appose is compatible with any program that abides by the
127
125
  2. The worker must issue responses in Appose's response format on its
128
126
  standard output (stdout) stream.
129
127
 
130
- TODO - write up the request and response formats in detail here!
131
- JSON, one line per request/response.
128
+ ### Requests to worker from service
129
+
130
+ A *request* is a single line of JSON sent to the worker process via
131
+ its standard input stream. It has a `task` key taking the form of a
132
+ UUID, and a `requestType` key with one of the following values:
133
+
134
+ #### EXECUTE
135
+
136
+ Asynchronously execute a script within the worker process. E.g.:
137
+
138
+ {
139
+ "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
140
+ "requestType" : "EXECUTE",
141
+ "script" : "task.outputs[\"result\"] = computeResult(gamma)\n",
142
+ "inputs" : {"gamma": 2.2}
143
+ }
144
+
145
+ #### CANCEL
146
+
147
+ Cancel a running script. E.g.:
148
+
149
+ {
150
+ "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
151
+ "requestType" : "CANCEL"
152
+ }
153
+
154
+ ### Responses from worker to service
155
+
156
+ A *response* is a single line of JSON with a `task` key taking the form
157
+ of a UUID, and a `responseType` key with one of the following values:
158
+
159
+ #### LAUNCH
160
+
161
+ A LAUNCH response is issued to confirm the success of an EXECUTE
162
+ request.
163
+
164
+ {
165
+ "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
166
+ "responseType" : "LAUNCH"
167
+ }
168
+
169
+ #### UPDATE
170
+
171
+ An UPDATE response is issued to convey that a task has somehow made
172
+ progress. The UPDATE response typically comes bundled with a
173
+ `message` string indicating what has changed, `current` and/or
174
+ `maximum` progress indicators conveying the step the task has
175
+ reached, or both.
176
+
177
+ {
178
+ "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
179
+ "responseType" : "UPDATE",
180
+ "message" : "Processing step 0 of 91",
181
+ "current" : 0,
182
+ "maximum" : 91
183
+ }
184
+
185
+ #### COMPLETION
186
+
187
+ A COMPLETION response is issued to convey that a task has successfully
188
+ completed execution, as well as report the values of any task outputs.
189
+
190
+ {
191
+ "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
192
+ "responseType" : "COMPLETION",
193
+ "outputs" : {"result" : 91}
194
+ }
195
+
196
+ #### CANCELATION
197
+
198
+ A CANCELATION response is issued to confirm the success of a CANCEL
199
+ request.
200
+
201
+ {
202
+ "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
203
+ "responseType" : "CANCELATION"
204
+ }
205
+
206
+ #### FAILURE
207
+
208
+ A FAILURE response is issued to convey that a task did not completely
209
+ and successfully execute, such as an exception being raised.
210
+
211
+ {
212
+ "task" : "87427f91-d193-4b25-8d35-e1292a34b5c4",
213
+ "responseType" : "FAILURE",
214
+ "error", "Invalid gamma value"
215
+ }
132
216
  '''
133
217
 
134
218
  from pathlib import Path
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -0,0 +1,265 @@
1
+ ###
2
+ # #%L
3
+ # Appose: multi-language interprocess cooperation with shared memory.
4
+ # %%
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
+ # %%
7
+ # Redistribution and use in source and binary forms, with or without
8
+ # modification, are permitted provided that the following conditions are met:
9
+ #
10
+ # 1. Redistributions of source code must retain the above copyright notice,
11
+ # this list of conditions and the following disclaimer.
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
+ # POSSIBILITY OF SUCH DAMAGE.
27
+ # #L%
28
+ ###
29
+
30
+ """
31
+ The Appose worker for running Python scripts.
32
+
33
+ Like all Appose workers, this program conforms to the Appose worker process
34
+ contract, meaning it accepts requests on stdin and produces responses on
35
+ stdout, both formatted according to Appose's assumptions.
36
+
37
+ For details, see the Appose README:
38
+ https://github.com/apposed/appose/blob/-/README.md#workers
39
+ """
40
+
41
+ import ast
42
+ import sys
43
+ import traceback
44
+ from threading import Thread
45
+ from time import sleep
46
+ from typing import Any
47
+
48
+ # NB: Avoid relative imports so that this script can be run standalone.
49
+ from appose.service import RequestType, ResponseType
50
+ from appose.types import Args, _set_worker, decode, encode
51
+
52
+
53
+ class Task:
54
+ def __init__(self, uuid: str, script: str, inputs: Args | None = None) -> None:
55
+ self._uuid = uuid
56
+ self._script = script
57
+ self._inputs = inputs
58
+ self._finished = False
59
+ self._thread = None
60
+
61
+ # Public-facing fields for use within the task script.
62
+ self.outputs = {}
63
+ self.cancel_requested = False
64
+
65
+ def update(
66
+ self,
67
+ message: str | None = None,
68
+ current: int | None = None,
69
+ maximum: int | None = None,
70
+ info: dict[str, Any] | None = None,
71
+ ) -> None:
72
+ args = {}
73
+ if message is not None:
74
+ args["message"] = str(message)
75
+ if current is not None:
76
+ try:
77
+ args["current"] = int(current)
78
+ except ValueError:
79
+ pass
80
+ if maximum is not None:
81
+ try:
82
+ args["maximum"] = int(maximum)
83
+ except ValueError:
84
+ pass
85
+ args["info"] = info
86
+ self._respond(ResponseType.UPDATE, args)
87
+
88
+ def cancel(self) -> None:
89
+ self._respond(ResponseType.CANCELATION, None)
90
+
91
+ def fail(self, error: str | None = None) -> None:
92
+ args = None if error is None else {"error": error}
93
+ self._respond(ResponseType.FAILURE, args)
94
+
95
+ def _run(self) -> None:
96
+ try:
97
+ # Populate script bindings.
98
+ binding = {"task": self}
99
+ if self._inputs is not None:
100
+ binding.update(self._inputs)
101
+
102
+ # Inform the calling process that the script is launching.
103
+ self._report_launch()
104
+
105
+ # Execute the script.
106
+ # result = exec(script, locals=binding)
107
+ result = None
108
+
109
+ # NB: Execute the block, except for the last statement,
110
+ # which we evaluate instead to get its return value.
111
+ # Credit: https://stackoverflow.com/a/39381428/1207769
112
+
113
+ block = ast.parse(self._script, mode="exec")
114
+ last = None
115
+ if (
116
+ len(block.body) > 0
117
+ and hasattr(block.body[-1], "value")
118
+ and not isinstance(block.body[-1], ast.Assign)
119
+ ):
120
+ # Last statement of the script looks like an expression. Evaluate!
121
+ last = ast.Expression(block.body.pop().value)
122
+
123
+ # NB: When `exec` gets two separate objects as *globals* and
124
+ # *locals*, the code will be executed as if it were embedded in
125
+ # a class definition. This means functions and classes defined
126
+ # in the executed code will not be able to access variables
127
+ # assigned at the top level, because the "top level" variables
128
+ # are treated as class variables in a class definition.
129
+ # See: https://docs.python.org/3/library/functions.html#exec
130
+ _globals = binding
131
+ exec(compile(block, "<string>", mode="exec"), _globals, binding)
132
+ if last is not None:
133
+ result = eval(compile(last, "<string>", mode="eval"), _globals, binding)
134
+
135
+ # Report the results to the Appose calling process.
136
+ if isinstance(result, dict):
137
+ # Script produced a dict; add all entries to the outputs.
138
+ self.outputs.update(result)
139
+ elif result is not None:
140
+ # Script produced a non-dict; add it alone to the outputs.
141
+ self.outputs["result"] = result
142
+ self._report_completion()
143
+ except Exception:
144
+ self.fail(traceback.format_exc())
145
+
146
+ def _report_launch(self) -> None:
147
+ self._respond(ResponseType.LAUNCH, None)
148
+
149
+ def _report_completion(self) -> None:
150
+ args = None if self.outputs is None else {"outputs": self.outputs}
151
+ self._respond(ResponseType.COMPLETION, args)
152
+
153
+ def _respond(self, response_type: ResponseType, args: Args | None) -> None:
154
+ already_terminated = False
155
+ if response_type.is_terminal():
156
+ if self._finished:
157
+ # This is not the first terminal response. Let's
158
+ # remember, in case an exception is generated below,
159
+ # so that we can avoid infinite recursion loops.
160
+ already_terminated = True
161
+ self._finished = True
162
+
163
+ response = {}
164
+ if args is not None:
165
+ response.update(args)
166
+ response.update({"task": self._uuid, "responseType": response_type.value})
167
+ # NB: Flush is necessary to ensure service receives the data!
168
+ try:
169
+ print(encode(response), flush=True)
170
+ except Exception:
171
+ if already_terminated:
172
+ # An exception triggered a failure response which
173
+ # then triggered another exception. Let's stop here
174
+ # to avoid the risk of infinite recursion loops.
175
+ return
176
+ # Encoding can fail due to unsupported types, when the
177
+ # response or its elements are not supported by JSON encoding.
178
+ # No matter what goes wrong, we want to tell the caller.
179
+ self.fail(traceback.format_exc())
180
+
181
+
182
+ class Worker:
183
+
184
+ def __init__(self):
185
+ self.tasks = {}
186
+ self.queue: list[Task] = []
187
+ self.running = True
188
+
189
+ # Flag this process as a worker, not a service.
190
+ _set_worker(True)
191
+
192
+ Thread(target=self._process_input, name="Appose-Receiver").start()
193
+ Thread(target=self._cleanup_threads, name="Appose-Janitor").start()
194
+
195
+ def run(self) -> None:
196
+ """
197
+ Processes tasks from the task queue.
198
+ """
199
+ while self.running:
200
+ if len(self.queue) == 0:
201
+ # Nothing queued, so wait a bit.
202
+ sleep(0.05)
203
+ continue
204
+ task = self.queue.pop()
205
+ task._run()
206
+
207
+ def _process_input(self) -> None:
208
+ while True:
209
+ try:
210
+ line = input().strip()
211
+ except EOFError:
212
+ line = None
213
+ if not line:
214
+ self.running = False
215
+ return
216
+
217
+ request = decode(line)
218
+ uuid = request.get("task")
219
+ request_type = request.get("requestType")
220
+
221
+ match RequestType(request_type):
222
+ case RequestType.EXECUTE:
223
+ script = request.get("script")
224
+ inputs = request.get("inputs")
225
+ queue = request.get("queue")
226
+ task = Task(uuid, script, inputs)
227
+ self.tasks[uuid] = task
228
+ if queue == "main":
229
+ # Add the task to the main thread queue.
230
+ self.queue.append(task)
231
+ else:
232
+ # Create a thread and save a reference to it, in case its script
233
+ # kills the thread. This happens e.g. if it calls sys.exit.
234
+ task._thread = Thread(target=task._run, name=f"Appose-{uuid}")
235
+ task._thread.start()
236
+
237
+ case RequestType.CANCEL:
238
+ task = self.tasks.get(uuid)
239
+ if task is None:
240
+ print(f"No such task: {uuid}", file=sys.stderr)
241
+ continue
242
+ task.cancel_requested = True
243
+
244
+ def _cleanup_threads(self) -> None:
245
+ while self.running:
246
+ sleep(0.05)
247
+ dead = {
248
+ uuid: task
249
+ for uuid, task in self.tasks.items()
250
+ if task._thread is not None and not task._thread.is_alive()
251
+ }
252
+ for uuid, task in dead.items():
253
+ self.tasks.pop(uuid)
254
+ if not task._finished:
255
+ # The task died before reporting a terminal status.
256
+ # We report this situation as failure by thread death.
257
+ task.fail("thread death")
258
+
259
+
260
+ def main() -> None:
261
+ Worker().run()
262
+
263
+
264
+ if __name__ == "__main__":
265
+ main()
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -36,7 +36,7 @@ import threading
36
36
  from enum import Enum
37
37
  from pathlib import Path
38
38
  from traceback import format_exc
39
- from typing import Any, Callable, Dict, List, Optional, Sequence, Union
39
+ from typing import Any, Callable
40
40
  from uuid import uuid4
41
41
 
42
42
  from .types import Args, decode, encode
@@ -52,17 +52,17 @@ class Service:
52
52
 
53
53
  _service_count = 0
54
54
 
55
- def __init__(self, cwd: Union[str, Path], args: Sequence[str]) -> None:
55
+ def __init__(self, cwd: str | Path, args: list[str]) -> None:
56
56
  self._cwd = cwd
57
57
  self._args = args[:]
58
- self._tasks: Dict[str, "Task"] = {}
58
+ self._tasks: dict[str, "Task"] = {}
59
59
  self._service_id = Service._service_count
60
60
  Service._service_count += 1
61
- self._process: Optional[subprocess.Popen] = None
62
- self._stdout_thread: Optional[threading.Thread] = None
63
- self._stderr_thread: Optional[threading.Thread] = None
64
- self._monitor_thread: Optional[threading.Thread] = None
65
- self._debug_callback: Optional[Callable[[Any], Any]] = None
61
+ self._process: subprocess.Popen | None = None
62
+ self._stdout_thread: threading.Thread | None = None
63
+ self._stderr_thread: threading.Thread | None = None
64
+ self._monitor_thread: threading.Thread | None = None
65
+ self._debug_callback: Callable[[Any], Any] | None = None
66
66
 
67
67
  def debug(self, debug_callback: Callable[[Any], Any]) -> None:
68
68
  """
@@ -109,16 +109,20 @@ class Service:
109
109
  self._stderr_thread.start()
110
110
  self._monitor_thread.start()
111
111
 
112
- def task(self, script: str, inputs: Optional[Args] = None) -> "Task":
112
+ def task(
113
+ self, script: str, inputs: Args | None = None, queue: str | None = None
114
+ ) -> "Task":
113
115
  """
114
116
  Create a new task, passing the given script to the worker for execution.
115
117
  :param script:
116
118
  The script for the worker to execute in its environment.
117
119
  :param inputs:
118
120
  Optional list of key/value pairs to feed into the script as inputs.
121
+ :param queue:
122
+ Optional queue target. Pass "main" to queue to worker's main thread.
119
123
  """
120
124
  self.start()
121
- return Task(self, script, inputs)
125
+ return Task(self, script, inputs, queue)
122
126
 
123
127
  def close(self) -> None:
124
128
  """
@@ -256,11 +260,35 @@ class ResponseType(Enum):
256
260
  FAILURE = "FAILURE"
257
261
  CRASH = "CRASH"
258
262
 
263
+ """
264
+ True iff response type is COMPLETE, CANCELED, FAILED, or CRASHED.
265
+ """
266
+
267
+ def is_terminal(self):
268
+ return self in (
269
+ ResponseType.COMPLETION,
270
+ ResponseType.CANCELATION,
271
+ ResponseType.FAILURE,
272
+ ResponseType.CRASH,
273
+ )
274
+
259
275
 
260
276
  class TaskEvent:
261
- def __init__(self, task: "Task", response_type: ResponseType) -> None:
277
+ def __init__(
278
+ self,
279
+ task: "Task",
280
+ response_type: ResponseType,
281
+ message: str | None = None,
282
+ current: int | None = None,
283
+ maximum: int | None = None,
284
+ info: dict[str, Any] | None = None,
285
+ ) -> None:
262
286
  self.task: "Task" = task
263
287
  self.response_type: ResponseType = response_type
288
+ self.message: str | None = message
289
+ self.current: int | None = current
290
+ self.maximum: int | None = maximum
291
+ self.info: dict[str, Any] | None = info
264
292
 
265
293
  def __str__(self):
266
294
  return f"[{self.response_type}] {self.task}"
@@ -274,21 +302,26 @@ class Task:
274
302
  """
275
303
 
276
304
  def __init__(
277
- self, service: Service, script: str, inputs: Optional[Args] = None
305
+ self,
306
+ service: Service,
307
+ script: str,
308
+ inputs: Args | None = None,
309
+ queue: str | None = None,
278
310
  ) -> None:
279
311
  self.uuid = uuid4().hex
280
312
  self.service = service
281
313
  self.script = script
282
314
  self.inputs: Args = {}
315
+ self.queue: str | None = queue
283
316
  if inputs is not None:
284
317
  self.inputs.update(inputs)
285
318
  self.outputs: Args = {}
286
319
  self.status: TaskStatus = TaskStatus.INITIAL
287
- self.message: Optional[str] = None
320
+ self.message: str | None = None
288
321
  self.current: int = 0
289
322
  self.maximum: int = 1
290
- self.error: Optional[str] = None
291
- self.listeners: List[Callable[["TaskEvent"], None]] = []
323
+ self.error: str | None = None
324
+ self.listeners: list[Callable[["TaskEvent"], None]] = []
292
325
  self.cv = threading.Condition()
293
326
  self.service._tasks[self.uuid] = self
294
327
 
@@ -299,7 +332,7 @@ class Task:
299
332
 
300
333
  self.status = TaskStatus.QUEUED
301
334
 
302
- args = {"script": self.script, "inputs": self.inputs}
335
+ args = {"script": self.script, "inputs": self.inputs, "queue": self.queue}
303
336
  self._request(RequestType.EXECUTE, args)
304
337
 
305
338
  return self
@@ -354,13 +387,8 @@ class Task:
354
387
  case ResponseType.LAUNCH:
355
388
  self.status = TaskStatus.RUNNING
356
389
  case ResponseType.UPDATE:
357
- self.message = response.get("message")
358
- current = response.get("current")
359
- maximum = response.get("maximum")
360
- if current is not None:
361
- self.current = int(current)
362
- if maximum is not None:
363
- self.maximum = int(maximum)
390
+ # No extra action needed.
391
+ pass
364
392
  case ResponseType.COMPLETION:
365
393
  self.service._tasks.pop(self.uuid, None)
366
394
  self.status = TaskStatus.COMPLETE
@@ -380,7 +408,11 @@ class Task:
380
408
  )
381
409
  return
382
410
 
383
- event = TaskEvent(self, response_type)
411
+ message = response.get("message")
412
+ current = response.get("current")
413
+ maximum = response.get("maximum")
414
+ info = response.get("info")
415
+ event = TaskEvent(self, response_type, message, current, maximum, info)
384
416
  for listener in self.listeners:
385
417
  listener(event)
386
418
 
@@ -397,7 +429,4 @@ class Task:
397
429
  self.cv.notify_all()
398
430
 
399
431
  def __str__(self):
400
- return (
401
- f"{self.uuid=}, {self.status=}, {self.message=}, "
402
- f"{self.current=}, {self.maximum=}, {self.error=}"
403
- )
432
+ return f"{self.uuid=}, {self.status=}, {self.error=}"
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -45,13 +45,29 @@ class SharedMemory(shared_memory.SharedMemory):
45
45
  `unlink_on_dispose` flag.
46
46
  """
47
47
 
48
- def __init__(self, name: str = None, create: bool = False, size: int = 0):
49
- super().__init__(name=name, create=create, size=size)
48
+ def __init__(self, name: str = None, create: bool = False, rsize: int = 0):
49
+ """
50
+ Create a new shared memory block, or attach to an existing one.
51
+
52
+ :param name:
53
+ The unique name for the requested shared memory, specified as a
54
+ string. If create is True (i.e. a new shared memory block) and
55
+ no name is given, a novel name will be generated.
56
+ :param create:
57
+ Whether a new shared memory block is created (True)
58
+ or an existing one is attached to (False).
59
+ :param rsize:
60
+ Requested size in bytes. The true allocated size will be at least
61
+ this much, but may be rounded up to the next block size multiple,
62
+ depending on the running platform.
63
+ """
64
+ super().__init__(name=name, create=create, size=rsize)
65
+ self.rsize = rsize
50
66
  self._unlink_on_dispose = create
51
67
  if _is_worker:
52
68
  # HACK: Remove this shared memory block from the resource_tracker,
53
- # which wants to clean up shared memory blocks after all known
54
- # references are done using them.
69
+ # which would otherwise want to clean up shared memory blocks
70
+ # after all known references are done using them.
55
71
  #
56
72
  # There is one resource_tracker per Python process, and they will
57
73
  # each try to delete shared memory blocks known to them when they
@@ -60,7 +76,37 @@ class SharedMemory(shared_memory.SharedMemory):
60
76
  # As such, the rule Appose follows is: let the service process
61
77
  # always handle cleanup of shared memory blocks, regardless of
62
78
  # which process initially allocated it.
63
- resource_tracker.unregister(self._name, "shared_memory")
79
+ try:
80
+ resource_tracker.unregister(self._name, "shared_memory")
81
+ except ModuleNotFoundError:
82
+ # Unfortunately, on (some?) Windows systems, we see the error:
83
+ #
84
+ # Traceback (most recent call last): # noqa: E501
85
+ # File "...\site-packages\appose\types.py", line 97, in decode # noqa: E501
86
+ # return json.loads(the_json, object_hook=_appose_object_hook) # noqa: E501
87
+ # File "...\lib\json\__init__.py", line 359, in loads # noqa: E501
88
+ # return cls(**kw).decode(s) # noqa: E501
89
+ # File "...\lib\json\decoder.py", line 337, in decode # noqa: E501
90
+ # obj, end = self.raw_decode(s, idx=_w(s, 0).end()) # noqa: E501
91
+ # File "...\lib\json\decoder.py", line 353, in raw_decode # noqa: E501
92
+ # obj, end = self.scan_once(s, idx) # noqa: E501
93
+ # File "...\site-packages\appose\types.py", line 177, in _appose_object_hook # noqa: E501
94
+ # return SharedMemory(name=(obj["name"]), size=(obj["size"])) # noqa: E501
95
+ # File "...\site-packages\appose\types.py", line 63, in __init__ # noqa: E501
96
+ # resource_tracker.unregister(self._name, "shared_memory") # noqa: E501
97
+ # File "...\lib\multiprocessing\resource_tracker.py", line 159, in unregister # noqa: E501
98
+ # self._send('UNREGISTER', name, rtype) # noqa: E501
99
+ # File "...\lib\multiprocessing\resource_tracker.py", line 162, in _send # noqa: E501
100
+ # self.ensure_running() # noqa: E501
101
+ # File "...\lib\multiprocessing\resource_tracker.py", line 129, in ensure_running # noqa: E501
102
+ # pid = util.spawnv_passfds(exe, args, fds_to_pass) # noqa: E501
103
+ # File "...\lib\multiprocessing\util.py", line 448, in spawnv_passfds # noqa: E501
104
+ # import _posixsubprocess # noqa: E501
105
+ # ModuleNotFoundError: No module named '_posixsubprocess' # noqa: E501
106
+ #
107
+ # A bug in Python? Regardless: we guard against it here.
108
+ # See also: https://github.com/imglib/imglib2-appose/issues/1
109
+ pass
64
110
 
65
111
  def unlink_on_dispose(self, value: bool) -> None:
66
112
  """
@@ -116,7 +162,7 @@ class NDArray:
116
162
  self.shape = shape
117
163
  self.shm = (
118
164
  SharedMemory(
119
- create=True, size=ceil(prod(shape) * _bytes_per_element(dtype))
165
+ create=True, rsize=ceil(prod(shape) * _bytes_per_element(dtype))
120
166
  )
121
167
  if shm is None
122
168
  else shm
@@ -127,7 +173,7 @@ class NDArray:
127
173
  f"NDArray("
128
174
  f"dtype='{self.dtype}', "
129
175
  f"shape={self.shape}, "
130
- f"shm='{self.shm.name}' ({self.shm.size}))"
176
+ f"shm='{self.shm.name}' ({self.shm.rsize}))"
131
177
  )
132
178
 
133
179
  def ndarray(self):
@@ -158,7 +204,7 @@ class _ApposeJSONEncoder(json.JSONEncoder):
158
204
  return {
159
205
  "appose_type": "shm",
160
206
  "name": obj.name,
161
- "size": obj.size,
207
+ "rsize": obj.rsize,
162
208
  }
163
209
  if isinstance(obj, NDArray):
164
210
  return {
@@ -174,7 +220,7 @@ def _appose_object_hook(obj: Dict):
174
220
  atype = obj.get("appose_type")
175
221
  if atype == "shm":
176
222
  # Attach to existing shared memory block.
177
- return SharedMemory(name=(obj["name"]), size=(obj["size"]))
223
+ return SharedMemory(name=(obj["name"]), rsize=(obj["rsize"]))
178
224
  elif atype == "ndarray":
179
225
  return NDArray(obj["dtype"], obj["shape"], obj["shm"])
180
226
  else:
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: appose
3
- Version: 0.2.0
3
+ Version: 0.6.0
4
4
  Summary: Appose: multi-language interprocess cooperation with shared memory.
5
5
  Author: Appose developers
6
- License: Simplified BSD License
6
+ License-Expression: BSD-2-Clause
7
7
  Project-URL: homepage, https://github.com/apposed/appose-python
8
8
  Project-URL: documentation, https://github.com/apposed/appose-python/blob/main/README.md
9
9
  Project-URL: source, https://github.com/apposed/appose-python
@@ -17,7 +17,7 @@ Classifier: Intended Audience :: Science/Research
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Classifier: Programming Language :: Python :: 3.10
19
19
  Classifier: Programming Language :: Python :: 3.11
20
- Classifier: License :: OSI Approved :: BSD License
20
+ Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Operating System :: Microsoft :: Windows
22
22
  Classifier: Operating System :: Unix
23
23
  Classifier: Operating System :: MacOS
@@ -40,6 +40,7 @@ Requires-Dist: pytest; extra == "dev"
40
40
  Requires-Dist: numpy; extra == "dev"
41
41
  Requires-Dist: toml; extra == "dev"
42
42
  Requires-Dist: validate-pyproject[all]; extra == "dev"
43
+ Dynamic: license-file
43
44
 
44
45
  # Appose Python
45
46
 
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -27,6 +27,8 @@
27
27
  # #L%
28
28
  ###
29
29
 
30
+ import os
31
+
30
32
  import appose
31
33
  from appose.service import ResponseType, Service, TaskStatus
32
34
 
@@ -55,18 +57,36 @@ while v != 1:
55
57
  task.outputs["result"] = time
56
58
  """
57
59
 
60
+ sqrt_import = """
61
+ from math import sqrt
62
+ def sqrt_age(age):
63
+ return sqrt(age)
64
+ task.outputs["result"] = sqrt_age(age)
65
+ """
66
+
67
+ main_thread_check_groovy = """
68
+ task.outputs["thread"] = Thread.currentThread().getName()
69
+ """
70
+
71
+ main_thread_check_python = """
72
+ import threading
73
+ task.outputs["thread"] = threading.current_thread().name
74
+ """
75
+
58
76
 
59
77
  def test_groovy():
60
78
  env = appose.system()
61
79
  # NB: For now, use bin/test.sh to copy the needed JARs.
62
80
  class_path = ["target/dependency/*"]
63
81
  with env.groovy(class_path=class_path) as service:
82
+ maybe_debug(service)
64
83
  execute_and_assert(service, collatz_groovy)
65
84
 
66
85
 
67
86
  def test_python():
68
87
  env = appose.system()
69
88
  with env.python() as service:
89
+ maybe_debug(service)
70
90
  execute_and_assert(service, collatz_python)
71
91
 
72
92
 
@@ -83,6 +103,49 @@ def test_service_startup_failure():
83
103
  ) == str(e)
84
104
 
85
105
 
106
+ def test_scope():
107
+ env = appose.system()
108
+ with env.python() as service:
109
+ maybe_debug(service)
110
+ task = service.task(sqrt_import, {"age": 100})
111
+ task.start()
112
+ task.wait_for()
113
+ result = round(task.outputs.get("result"))
114
+ assert result == 10
115
+
116
+
117
+ def test_main_thread_queue_groovy():
118
+ env = appose.system()
119
+ # NB: For now, use bin/test.sh to copy the needed JARs.
120
+ class_path = ["target/dependency/*"]
121
+ with env.groovy(class_path=class_path) as service:
122
+ maybe_debug(service)
123
+
124
+ task = service.task(main_thread_check_groovy, queue="main")
125
+ task.wait_for()
126
+ thread = task.outputs.get("thread")
127
+ assert thread == "main"
128
+
129
+ task = service.task(main_thread_check_groovy)
130
+ task.wait_for()
131
+ thread = task.outputs.get("thread")
132
+ assert thread != "main"
133
+
134
+
135
+ def test_main_thread_queue_python():
136
+ env = appose.system()
137
+ with env.python() as service:
138
+ task = service.task(main_thread_check_python, queue="main")
139
+ task.wait_for()
140
+ thread = task.outputs.get("thread")
141
+ assert thread == "MainThread"
142
+
143
+ task = service.task(main_thread_check_python)
144
+ task.wait_for()
145
+ thread = task.outputs.get("thread")
146
+ assert thread != "MainThread"
147
+
148
+
86
149
  def execute_and_assert(service: Service, script: str):
87
150
  task = service.task(script)
88
151
 
@@ -91,10 +154,10 @@ def execute_and_assert(service: Service, script: str):
91
154
  class TaskState:
92
155
  def __init__(self, event):
93
156
  self.response_type = event.response_type
157
+ self.message = event.message
158
+ self.current = event.current
159
+ self.maximum = event.maximum
94
160
  self.status = event.task.status
95
- self.message = event.task.message
96
- self.current = event.task.current
97
- self.maximum = event.task.maximum
98
161
  self.error = event.task.error
99
162
 
100
163
  events = []
@@ -116,8 +179,8 @@ def execute_and_assert(service: Service, script: str):
116
179
  assert ResponseType.LAUNCH == launch.response_type
117
180
  assert TaskStatus.RUNNING == launch.status
118
181
  assert launch.message is None
119
- assert 0 == launch.current
120
- assert 1 == launch.maximum
182
+ assert launch.current is None
183
+ assert launch.maximum is None
121
184
  assert launch.error is None
122
185
 
123
186
  v = 9999
@@ -128,13 +191,19 @@ def execute_and_assert(service: Service, script: str):
128
191
  assert TaskStatus.RUNNING == update.status
129
192
  assert f"[{i}] -> {v}" == update.message
130
193
  assert i == update.current
131
- assert 1 == update.maximum
194
+ assert update.maximum is None
132
195
  assert update.error is None
133
196
 
134
197
  completion = events[92]
135
198
  assert ResponseType.COMPLETION == completion.response_type
136
199
  assert TaskStatus.COMPLETE == completion.status
137
- assert "[90] -> 1" == completion.message
138
- assert 90 == completion.current
139
- assert 1 == completion.maximum
200
+ assert completion.message is None # no message from non-UPDATE response
201
+ assert completion.current is None # no current from non-UPDATE response
202
+ assert completion.maximum is None # no maximum from non-UPDATE response
140
203
  assert completion.error is None
204
+
205
+
206
+ def maybe_debug(service):
207
+ debug = os.getenv("DEBUG")
208
+ if debug:
209
+ service.debug(print)
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -31,6 +31,7 @@ import appose
31
31
  from appose.service import TaskStatus
32
32
 
33
33
  ndarray_inspect = """
34
+ task.outputs["rsize"] = data.shm.rsize
34
35
  task.outputs["size"] = data.shm.size
35
36
  task.outputs["dtype"] = data.dtype
36
37
  task.outputs["shape"] = data.shape
@@ -41,7 +42,7 @@ task.outputs["sum"] = sum(v for v in data.shm.buf)
41
42
  def test_ndarray():
42
43
  env = appose.system()
43
44
  with env.python() as service:
44
- with appose.SharedMemory(create=True, size=2 * 2 * 20 * 25) as shm:
45
+ with appose.SharedMemory(create=True, rsize=2 * 2 * 20 * 25) as shm:
45
46
  # Construct the data.
46
47
  shm.buf[0] = 123
47
48
  shm.buf[456] = 78
@@ -54,7 +55,10 @@ def test_ndarray():
54
55
 
55
56
  # Validate the execution result.
56
57
  assert TaskStatus.COMPLETE == task.status
57
- assert 2 * 20 * 25 * 2 == task.outputs["size"]
58
+ # The requested size is 2*20*25*2=2000, but actual allocated
59
+ # shm size varies by platform; e.g. on macOS it is 16384.
60
+ assert 2 * 20 * 25 * 2 == task.outputs["rsize"]
61
+ assert task.outputs["size"] >= task.outputs["rsize"]
58
62
  assert "uint16" == task.outputs["dtype"]
59
63
  assert [2, 20, 25] == task.outputs["shape"]
60
64
  assert 123 + 78 + 210 == task.outputs["sum"]
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
5
+ # Copyright (C) 2023 - 2025 Appose developers.
6
6
  # %%
7
7
  # Redistribution and use in source and binary forms, with or without
8
8
  # modification, are permitted provided that the following conditions are met:
@@ -56,7 +56,7 @@ class TypesTest(unittest.TestCase):
56
56
  '"shm":{' # noqa: E131
57
57
  '"appose_type":"shm",' # noqa: E131
58
58
  '"name":"SHM_NAME",' # noqa: E131
59
- '"size":4000' # noqa: E131
59
+ '"rsize":4000' # noqa: E131
60
60
  "}" # noqa: E131
61
61
  "}"
62
62
  # fmt: on
@@ -103,7 +103,7 @@ class TypesTest(unittest.TestCase):
103
103
  self.assertEqual(expected, json_str)
104
104
 
105
105
  def test_decode(self):
106
- with appose.SharedMemory(create=True, size=4000) as shm:
106
+ with appose.SharedMemory(create=True, rsize=4000) as shm:
107
107
  shm_name = shm.name
108
108
  data = appose.types.decode(self.JSON.replace("SHM_NAME", shm_name))
109
109
  self.assertIsNotNone(data)
@@ -1,198 +0,0 @@
1
- ###
2
- # #%L
3
- # Appose: multi-language interprocess cooperation with shared memory.
4
- # %%
5
- # Copyright (C) 2023 - 2024 Appose developers.
6
- # %%
7
- # Redistribution and use in source and binary forms, with or without
8
- # modification, are permitted provided that the following conditions are met:
9
- #
10
- # 1. Redistributions of source code must retain the above copyright notice,
11
- # this list of conditions and the following disclaimer.
12
- # 2. Redistributions in binary form must reproduce the above copyright notice,
13
- # this list of conditions and the following disclaimer in the documentation
14
- # and/or other materials provided with the distribution.
15
- #
16
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26
- # POSSIBILITY OF SUCH DAMAGE.
27
- # #L%
28
- ###
29
-
30
- """
31
- The Appose worker for running Python scripts.
32
-
33
- Like all Appose workers, this program conforms to the Appose worker process
34
- contract, meaning it accepts requests on stdin and produces responses on
35
- stdout, both formatted according to Appose's assumptions.
36
-
37
- For details, see the Appose README:
38
- https://github.com/apposed/appose/blob/-/README.md#workers
39
- """
40
-
41
- import ast
42
- import sys
43
- import traceback
44
- from threading import Thread
45
- from typing import Optional
46
-
47
- # NB: Avoid relative imports so that this script can be run standalone.
48
- from appose.service import RequestType, ResponseType
49
- from appose.types import Args, _set_worker, decode, encode
50
-
51
-
52
- class Task:
53
- def __init__(self, uuid: str) -> None:
54
- self.uuid = uuid
55
- self.outputs = {}
56
- self.cancel_requested = False
57
-
58
- def update(
59
- self,
60
- message: Optional[str] = None,
61
- current: Optional[int] = None,
62
- maximum: Optional[int] = None,
63
- ) -> None:
64
- args = {}
65
- if message is not None:
66
- args["message"] = str(message)
67
- if current is not None:
68
- try:
69
- args["current"] = int(current)
70
- except ValueError:
71
- pass
72
- if maximum is not None:
73
- try:
74
- args["maximum"] = int(maximum)
75
- except ValueError:
76
- pass
77
- self._respond(ResponseType.UPDATE, args)
78
-
79
- def cancel(self) -> None:
80
- self._respond(ResponseType.CANCELATION, None)
81
-
82
- def fail(self, error: Optional[str] = None) -> None:
83
- args = None if error is None else {"error": error}
84
- self._respond(ResponseType.FAILURE, args)
85
-
86
- def _start(self, script: str, inputs: Optional[Args]) -> None:
87
- def execute_script():
88
- # Populate script bindings.
89
- binding = {"task": self}
90
- if inputs is not None:
91
- binding.update(inputs)
92
-
93
- # Inform the calling process that the script is launching.
94
- self._report_launch()
95
-
96
- # Execute the script.
97
- # result = exec(script, locals=binding)
98
- result = None
99
- try:
100
- # NB: Execute the block, except for the last statement,
101
- # which we evaluate instead to get its return value.
102
- # Credit: https://stackoverflow.com/a/39381428/1207769
103
-
104
- block = ast.parse(script, mode="exec")
105
- last = None
106
- if (
107
- len(block.body) > 0
108
- and hasattr(block.body[-1], "value")
109
- and not isinstance(block.body[-1], ast.Assign)
110
- ):
111
- # Last statement of the script looks like an expression. Evaluate!
112
- last = ast.Expression(block.body.pop().value)
113
-
114
- _globals = {}
115
- exec(compile(block, "<string>", mode="exec"), _globals, binding)
116
- if last is not None:
117
- result = eval(
118
- compile(last, "<string>", mode="eval"), _globals, binding
119
- )
120
- except Exception:
121
- self.fail(traceback.format_exc())
122
- return
123
-
124
- # Report the results to the Appose calling process.
125
- if isinstance(result, dict):
126
- # Script produced a dict; add all entries to the outputs.
127
- self.outputs.update(result)
128
- elif result is not None:
129
- # Script produced a non-dict; add it alone to the outputs.
130
- self.outputs["result"] = result
131
-
132
- self._report_completion()
133
-
134
- # TODO: Consider whether to retain a reference to this Thread, and
135
- # expose a "force" option for cancelation that kills it forcibly; see:
136
- # https://www.geeksforgeeks.org/python-different-ways-to-kill-a-thread/
137
- Thread(target=execute_script, name=f"Appose-{self.uuid}").start()
138
-
139
- def _report_launch(self) -> None:
140
- self._respond(ResponseType.LAUNCH, None)
141
-
142
- def _report_completion(self) -> None:
143
- args = None if self.outputs is None else {"outputs": self.outputs}
144
- self._respond(ResponseType.COMPLETION, args)
145
-
146
- def _respond(self, response_type: ResponseType, args: Optional[Args]) -> None:
147
- response = {"task": self.uuid, "responseType": response_type.value}
148
- if args is not None:
149
- response.update(args)
150
- # NB: Flush is necessary to ensure service receives the data!
151
- try:
152
- print(encode(response), flush=True)
153
- except Exception:
154
- # Encoding can fail due to unsupported types, when the response
155
- # or its elements are not supported by JSON encoding.
156
- # No matter what goes wrong, we want to tell the caller.
157
- if response_type is ResponseType.FAILURE:
158
- # TODO: How to address this hypothetical case
159
- # of a failure message triggering another failure?
160
- raise
161
- self.fail(traceback.format_exc())
162
-
163
-
164
- def main() -> None:
165
- _set_worker(True)
166
-
167
- tasks = {}
168
-
169
- while True:
170
- try:
171
- line = input().strip()
172
- except EOFError:
173
- break
174
- if not line:
175
- break
176
-
177
- request = decode(line)
178
- uuid = request.get("task")
179
- request_type = request.get("requestType")
180
-
181
- match RequestType(request_type):
182
- case RequestType.EXECUTE:
183
- script = request.get("script")
184
- inputs = request.get("inputs")
185
- task = Task(uuid)
186
- tasks[uuid] = task
187
- task._start(script, inputs)
188
-
189
- case RequestType.CANCEL:
190
- task = tasks.get(uuid)
191
- if task is None:
192
- print(f"No such task: {uuid}", file=sys.stderr)
193
- continue
194
- task.cancel_requested = True
195
-
196
-
197
- if __name__ == "__main__":
198
- main()
File without changes
File without changes