appose 0.4.0__py3-none-any.whl → 0.6.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.
- appose/python_worker.py +118 -108
- appose/service.py +32 -23
- {appose-0.4.0.dist-info → appose-0.6.0.dist-info}/METADATA +1 -1
- appose-0.6.0.dist-info/RECORD +11 -0
- appose-0.4.0.dist-info/RECORD +0 -11
- {appose-0.4.0.dist-info → appose-0.6.0.dist-info}/WHEEL +0 -0
- {appose-0.4.0.dist-info → appose-0.6.0.dist-info}/licenses/LICENSE.txt +0 -0
- {appose-0.4.0.dist-info → appose-0.6.0.dist-info}/top_level.txt +0 -0
appose/python_worker.py
CHANGED
@@ -43,7 +43,7 @@ import sys
|
|
43
43
|
import traceback
|
44
44
|
from threading import Thread
|
45
45
|
from time import sleep
|
46
|
-
from typing import Any
|
46
|
+
from typing import Any
|
47
47
|
|
48
48
|
# NB: Avoid relative imports so that this script can be run standalone.
|
49
49
|
from appose.service import RequestType, ResponseType
|
@@ -51,19 +51,23 @@ from appose.types import Args, _set_worker, decode, encode
|
|
51
51
|
|
52
52
|
|
53
53
|
class Task:
|
54
|
-
def __init__(self, uuid: str) -> None:
|
55
|
-
self.
|
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.
|
56
62
|
self.outputs = {}
|
57
|
-
self.finished = False
|
58
63
|
self.cancel_requested = False
|
59
|
-
self.thread = None # Initialize thread attribute
|
60
64
|
|
61
65
|
def update(
|
62
66
|
self,
|
63
|
-
message:
|
64
|
-
current:
|
65
|
-
maximum:
|
66
|
-
info:
|
67
|
+
message: str | None = None,
|
68
|
+
current: int | None = None,
|
69
|
+
maximum: int | None = None,
|
70
|
+
info: dict[str, Any] | None = None,
|
67
71
|
) -> None:
|
68
72
|
args = {}
|
69
73
|
if message is not None:
|
@@ -84,16 +88,16 @@ class Task:
|
|
84
88
|
def cancel(self) -> None:
|
85
89
|
self._respond(ResponseType.CANCELATION, None)
|
86
90
|
|
87
|
-
def fail(self, error:
|
91
|
+
def fail(self, error: str | None = None) -> None:
|
88
92
|
args = None if error is None else {"error": error}
|
89
93
|
self._respond(ResponseType.FAILURE, args)
|
90
94
|
|
91
|
-
def
|
92
|
-
|
95
|
+
def _run(self) -> None:
|
96
|
+
try:
|
93
97
|
# Populate script bindings.
|
94
98
|
binding = {"task": self}
|
95
|
-
if
|
96
|
-
binding.update(
|
99
|
+
if self._inputs is not None:
|
100
|
+
binding.update(self._inputs)
|
97
101
|
|
98
102
|
# Inform the calling process that the script is launching.
|
99
103
|
self._report_launch()
|
@@ -101,37 +105,32 @@ class Task:
|
|
101
105
|
# Execute the script.
|
102
106
|
# result = exec(script, locals=binding)
|
103
107
|
result = None
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
compile(last, "<string>", mode="eval"), _globals, binding
|
131
|
-
)
|
132
|
-
except Exception:
|
133
|
-
self.fail(traceback.format_exc())
|
134
|
-
return
|
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)
|
135
134
|
|
136
135
|
# Report the results to the Appose calling process.
|
137
136
|
if isinstance(result, dict):
|
@@ -140,27 +139,9 @@ class Task:
|
|
140
139
|
elif result is not None:
|
141
140
|
# Script produced a non-dict; add it alone to the outputs.
|
142
141
|
self.outputs["result"] = result
|
143
|
-
|
144
142
|
self._report_completion()
|
145
|
-
|
146
|
-
|
147
|
-
# as a whole on its own Thread. Why? Because on Windows, some imports
|
148
|
-
# (e.g. numpy) may lead to hangs if loaded from a separate thread.
|
149
|
-
# See https://github.com/apposed/appose/issues/13.
|
150
|
-
block = ast.parse(script, mode="exec")
|
151
|
-
import_nodes = [
|
152
|
-
node
|
153
|
-
for node in block.body
|
154
|
-
if isinstance(node, (ast.Import, ast.ImportFrom))
|
155
|
-
]
|
156
|
-
import_block = ast.Module(body=import_nodes, type_ignores=[])
|
157
|
-
compiled_imports = compile(import_block, filename="<imports>", mode="exec")
|
158
|
-
exec(compiled_imports, globals())
|
159
|
-
|
160
|
-
# Create a thread and save a reference to it, in case its script
|
161
|
-
# ends up killing the thread. This happens e.g. if it calls sys.exit.
|
162
|
-
self.thread = Thread(target=execute_script, name=f"Appose-{self.uuid}")
|
163
|
-
self.thread.start()
|
143
|
+
except Exception:
|
144
|
+
self.fail(traceback.format_exc())
|
164
145
|
|
165
146
|
def _report_launch(self) -> None:
|
166
147
|
self._respond(ResponseType.LAUNCH, None)
|
@@ -169,20 +150,20 @@ class Task:
|
|
169
150
|
args = None if self.outputs is None else {"outputs": self.outputs}
|
170
151
|
self._respond(ResponseType.COMPLETION, args)
|
171
152
|
|
172
|
-
def _respond(self, response_type: ResponseType, args:
|
153
|
+
def _respond(self, response_type: ResponseType, args: Args | None) -> None:
|
173
154
|
already_terminated = False
|
174
155
|
if response_type.is_terminal():
|
175
|
-
if self.
|
156
|
+
if self._finished:
|
176
157
|
# This is not the first terminal response. Let's
|
177
158
|
# remember, in case an exception is generated below,
|
178
159
|
# so that we can avoid infinite recursion loops.
|
179
160
|
already_terminated = True
|
180
|
-
self.
|
161
|
+
self._finished = True
|
181
162
|
|
182
163
|
response = {}
|
183
164
|
if args is not None:
|
184
165
|
response.update(args)
|
185
|
-
response.update({"task": self.
|
166
|
+
response.update({"task": self._uuid, "responseType": response_type.value})
|
186
167
|
# NB: Flush is necessary to ensure service receives the data!
|
187
168
|
try:
|
188
169
|
print(encode(response), flush=True)
|
@@ -198,57 +179,86 @@ class Task:
|
|
198
179
|
self.fail(traceback.format_exc())
|
199
180
|
|
200
181
|
|
201
|
-
|
202
|
-
_set_worker(True)
|
182
|
+
class Worker:
|
203
183
|
|
204
|
-
|
205
|
-
|
184
|
+
def __init__(self):
|
185
|
+
self.tasks = {}
|
186
|
+
self.queue: list[Task] = []
|
187
|
+
self.running = True
|
206
188
|
|
207
|
-
|
208
|
-
|
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:
|
209
246
|
sleep(0.05)
|
210
247
|
dead = {
|
211
248
|
uuid: task
|
212
|
-
for uuid, task in tasks.items()
|
213
|
-
if task.
|
249
|
+
for uuid, task in self.tasks.items()
|
250
|
+
if task._thread is not None and not task._thread.is_alive()
|
214
251
|
}
|
215
252
|
for uuid, task in dead.items():
|
216
|
-
tasks.pop(uuid)
|
217
|
-
if not task.
|
253
|
+
self.tasks.pop(uuid)
|
254
|
+
if not task._finished:
|
218
255
|
# The task died before reporting a terminal status.
|
219
256
|
# We report this situation as failure by thread death.
|
220
257
|
task.fail("thread death")
|
221
258
|
|
222
|
-
Thread(target=cleanup_threads, name="Appose-Janitor").start()
|
223
259
|
|
224
|
-
|
225
|
-
|
226
|
-
line = input().strip()
|
227
|
-
except EOFError:
|
228
|
-
break
|
229
|
-
if not line:
|
230
|
-
break
|
231
|
-
|
232
|
-
request = decode(line)
|
233
|
-
uuid = request.get("task")
|
234
|
-
request_type = request.get("requestType")
|
235
|
-
|
236
|
-
match RequestType(request_type):
|
237
|
-
case RequestType.EXECUTE:
|
238
|
-
script = request.get("script")
|
239
|
-
inputs = request.get("inputs")
|
240
|
-
task = Task(uuid)
|
241
|
-
tasks[uuid] = task
|
242
|
-
task._start(script, inputs)
|
243
|
-
|
244
|
-
case RequestType.CANCEL:
|
245
|
-
task = tasks.get(uuid)
|
246
|
-
if task is None:
|
247
|
-
print(f"No such task: {uuid}", file=sys.stderr)
|
248
|
-
continue
|
249
|
-
task.cancel_requested = True
|
250
|
-
|
251
|
-
running = False
|
260
|
+
def main() -> None:
|
261
|
+
Worker().run()
|
252
262
|
|
253
263
|
|
254
264
|
if __name__ == "__main__":
|
appose/service.py
CHANGED
@@ -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
|
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:
|
55
|
+
def __init__(self, cwd: str | Path, args: list[str]) -> None:
|
56
56
|
self._cwd = cwd
|
57
57
|
self._args = args[:]
|
58
|
-
self._tasks:
|
58
|
+
self._tasks: dict[str, "Task"] = {}
|
59
59
|
self._service_id = Service._service_count
|
60
60
|
Service._service_count += 1
|
61
|
-
self._process:
|
62
|
-
self._stdout_thread:
|
63
|
-
self._stderr_thread:
|
64
|
-
self._monitor_thread:
|
65
|
-
self._debug_callback:
|
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(
|
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
|
"""
|
@@ -274,17 +278,17 @@ class TaskEvent:
|
|
274
278
|
self,
|
275
279
|
task: "Task",
|
276
280
|
response_type: ResponseType,
|
277
|
-
message:
|
278
|
-
current:
|
279
|
-
maximum:
|
280
|
-
info:
|
281
|
+
message: str | None = None,
|
282
|
+
current: int | None = None,
|
283
|
+
maximum: int | None = None,
|
284
|
+
info: dict[str, Any] | None = None,
|
281
285
|
) -> None:
|
282
286
|
self.task: "Task" = task
|
283
287
|
self.response_type: ResponseType = response_type
|
284
|
-
self.message:
|
285
|
-
self.current:
|
286
|
-
self.maximum:
|
287
|
-
self.info:
|
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
|
288
292
|
|
289
293
|
def __str__(self):
|
290
294
|
return f"[{self.response_type}] {self.task}"
|
@@ -298,21 +302,26 @@ class Task:
|
|
298
302
|
"""
|
299
303
|
|
300
304
|
def __init__(
|
301
|
-
self,
|
305
|
+
self,
|
306
|
+
service: Service,
|
307
|
+
script: str,
|
308
|
+
inputs: Args | None = None,
|
309
|
+
queue: str | None = None,
|
302
310
|
) -> None:
|
303
311
|
self.uuid = uuid4().hex
|
304
312
|
self.service = service
|
305
313
|
self.script = script
|
306
314
|
self.inputs: Args = {}
|
315
|
+
self.queue: str | None = queue
|
307
316
|
if inputs is not None:
|
308
317
|
self.inputs.update(inputs)
|
309
318
|
self.outputs: Args = {}
|
310
319
|
self.status: TaskStatus = TaskStatus.INITIAL
|
311
|
-
self.message:
|
320
|
+
self.message: str | None = None
|
312
321
|
self.current: int = 0
|
313
322
|
self.maximum: int = 1
|
314
|
-
self.error:
|
315
|
-
self.listeners:
|
323
|
+
self.error: str | None = None
|
324
|
+
self.listeners: list[Callable[["TaskEvent"], None]] = []
|
316
325
|
self.cv = threading.Condition()
|
317
326
|
self.service._tasks[self.uuid] = self
|
318
327
|
|
@@ -323,7 +332,7 @@ class Task:
|
|
323
332
|
|
324
333
|
self.status = TaskStatus.QUEUED
|
325
334
|
|
326
|
-
args = {"script": self.script, "inputs": self.inputs}
|
335
|
+
args = {"script": self.script, "inputs": self.inputs, "queue": self.queue}
|
327
336
|
self._request(RequestType.EXECUTE, args)
|
328
337
|
|
329
338
|
return self
|
@@ -0,0 +1,11 @@
|
|
1
|
+
appose/__init__.py,sha256=SRXmNEqFIAajVlwVCSAdhHQytRKZJiWI5Y5-xwioW0w,7544
|
2
|
+
appose/environment.py,sha256=Kg-MfAcmVPDltwqvmrcvus-pP01SNUdn4bE2kdkIVuk,7597
|
3
|
+
appose/paths.py,sha256=WgBHwnZdS3Ot8_hssFqUiTGnunolYRrHLf9rAfEU5r4,2182
|
4
|
+
appose/python_worker.py,sha256=rkssHh3YTBueGTQ9qAuRVqq7bXncYulscG3kbM0D2TQ,10061
|
5
|
+
appose/service.py,sha256=jdtGOOoWjut9eZj441fL8t_P_ln2mfgilBvJdH0bDus,14750
|
6
|
+
appose/types.py,sha256=uidbt2JwWJ2D3Mbtc4Vl2sRP6WNgKjmfcHjJ9QLWkcs,10557
|
7
|
+
appose-0.6.0.dist-info/licenses/LICENSE.txt,sha256=ZedidA6NyZclNlsx-vl9IVBWRAQSqXdNsCWgtFcwzIE,1313
|
8
|
+
appose-0.6.0.dist-info/METADATA,sha256=F900U1qP-pPiPgklzrnjg7og3LuY5h8pXYDdadVtfdY,5598
|
9
|
+
appose-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
+
appose-0.6.0.dist-info/top_level.txt,sha256=oqHhw2QGlaFfH3jMR9H5Cd6XYHdEv5DvK1am0iEFPW0,7
|
11
|
+
appose-0.6.0.dist-info/RECORD,,
|
appose-0.4.0.dist-info/RECORD
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
appose/__init__.py,sha256=SRXmNEqFIAajVlwVCSAdhHQytRKZJiWI5Y5-xwioW0w,7544
|
2
|
-
appose/environment.py,sha256=Kg-MfAcmVPDltwqvmrcvus-pP01SNUdn4bE2kdkIVuk,7597
|
3
|
-
appose/paths.py,sha256=WgBHwnZdS3Ot8_hssFqUiTGnunolYRrHLf9rAfEU5r4,2182
|
4
|
-
appose/python_worker.py,sha256=iQoz2t0glbXlR1lgynN-yXPIgtlAbCFtMYYpev-LZyU,9844
|
5
|
-
appose/service.py,sha256=UQn1aoL3uQ8gPKS6nf5oYg0Ea1ZRsaQ3TfLCKeCZLPQ,14581
|
6
|
-
appose/types.py,sha256=uidbt2JwWJ2D3Mbtc4Vl2sRP6WNgKjmfcHjJ9QLWkcs,10557
|
7
|
-
appose-0.4.0.dist-info/licenses/LICENSE.txt,sha256=ZedidA6NyZclNlsx-vl9IVBWRAQSqXdNsCWgtFcwzIE,1313
|
8
|
-
appose-0.4.0.dist-info/METADATA,sha256=cI_E4xetwcHlusk2UzSPOk6SWrNr_vBrGjOC_0H4CFo,5598
|
9
|
-
appose-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
10
|
-
appose-0.4.0.dist-info/top_level.txt,sha256=oqHhw2QGlaFfH3jMR9H5Cd6XYHdEv5DvK1am0iEFPW0,7
|
11
|
-
appose-0.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|