appose 0.1.0__tar.gz → 0.4.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, 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,14 +1,14 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: appose
3
- Version: 0.1.0
3
+ Version: 0.4.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
10
10
  Project-URL: download, https://pypi.org/project/appose-python
11
- Project-URL: tracker, https://github.com/apposed/appose-python/issues
11
+ Project-URL: tracker, https://github.com/apposed/appose/issues
12
12
  Keywords: java,javascript,python,cross-language,interprocess
13
13
  Classifier: Development Status :: 2 - Pre-Alpha
14
14
  Classifier: Intended Audience :: Developers
@@ -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
@@ -27,8 +27,20 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
27
  Classifier: Topic :: Utilities
28
28
  Requires-Python: >=3.10
29
29
  Description-Content-Type: text/markdown
30
- Provides-Extra: dev
31
30
  License-File: LICENSE.txt
31
+ Provides-Extra: dev
32
+ Requires-Dist: autopep8; extra == "dev"
33
+ Requires-Dist: black; extra == "dev"
34
+ Requires-Dist: build; extra == "dev"
35
+ Requires-Dist: flake8; extra == "dev"
36
+ Requires-Dist: flake8-pyproject; extra == "dev"
37
+ Requires-Dist: flake8-typing-imports; extra == "dev"
38
+ Requires-Dist: isort; extra == "dev"
39
+ Requires-Dist: pytest; extra == "dev"
40
+ Requires-Dist: numpy; extra == "dev"
41
+ Requires-Dist: toml; extra == "dev"
42
+ Requires-Dist: validate-pyproject[all]; extra == "dev"
43
+ Dynamic: license-file
32
44
 
33
45
  # Appose Python
34
46
 
@@ -60,16 +72,6 @@ This is the **Python implementation of Appose**.
60
72
 
61
73
  The name of the package is `appose`.
62
74
 
63
- ### Conda/Mamba
64
-
65
- To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
66
- add `appose` to your `environment.yml`'s `dependencies` section:
67
-
68
- ```yaml
69
- dependencies:
70
- - appose
71
- ```
72
-
73
75
  ### PyPI/Pip
74
76
 
75
77
  To use [the PyPI package](https://pypi.org/project/appose),
@@ -87,6 +89,16 @@ dependencies = [
87
89
  ]
88
90
  ```
89
91
 
92
+ ### Conda/Mamba
93
+
94
+ To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
95
+ add `appose` to your `environment.yml`'s `dependencies` section:
96
+
97
+ ```yaml
98
+ dependencies:
99
+ - appose
100
+ ```
101
+
90
102
  ## Examples
91
103
 
92
104
  Here is a minimal example for calling into Java from Python:
@@ -96,12 +108,12 @@ import appose
96
108
  env = appose.java(vendor="zulu", version="17").build()
97
109
  with env.groovy() as groovy:
98
110
  task = groovy.task("5 + 6")
99
- task.waitFor()
100
- result = task.outputs.get("result")
111
+ task.wait_for()
112
+ result = task.outputs["result"]
101
113
  assert 11 == result
102
114
  ```
103
115
 
104
- *Note: The `Appose.java` builder is planned, but not yet implemented.*
116
+ *Note: The `appose.java` builder is planned, but not yet implemented.*
105
117
 
106
118
  Here is an example using a few more of Appose's features:
107
119
 
@@ -158,3 +170,9 @@ with env.groovy() as groovy:
158
170
 
159
171
  Of course, the above examples could have been done all in one language. But
160
172
  hopefully they hint at the possibilities of easy cross-language integration.
173
+
174
+ ## Issue tracker
175
+
176
+ All implementations of Appose use the same issue tracker:
177
+
178
+ https://github.com/apposed/appose/issues
@@ -28,16 +28,6 @@ This is the **Python implementation of Appose**.
28
28
 
29
29
  The name of the package is `appose`.
30
30
 
31
- ### Conda/Mamba
32
-
33
- To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
34
- add `appose` to your `environment.yml`'s `dependencies` section:
35
-
36
- ```yaml
37
- dependencies:
38
- - appose
39
- ```
40
-
41
31
  ### PyPI/Pip
42
32
 
43
33
  To use [the PyPI package](https://pypi.org/project/appose),
@@ -55,6 +45,16 @@ dependencies = [
55
45
  ]
56
46
  ```
57
47
 
48
+ ### Conda/Mamba
49
+
50
+ To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
51
+ add `appose` to your `environment.yml`'s `dependencies` section:
52
+
53
+ ```yaml
54
+ dependencies:
55
+ - appose
56
+ ```
57
+
58
58
  ## Examples
59
59
 
60
60
  Here is a minimal example for calling into Java from Python:
@@ -64,12 +64,12 @@ import appose
64
64
  env = appose.java(vendor="zulu", version="17").build()
65
65
  with env.groovy() as groovy:
66
66
  task = groovy.task("5 + 6")
67
- task.waitFor()
68
- result = task.outputs.get("result")
67
+ task.wait_for()
68
+ result = task.outputs["result"]
69
69
  assert 11 == result
70
70
  ```
71
71
 
72
- *Note: The `Appose.java` builder is planned, but not yet implemented.*
72
+ *Note: The `appose.java` builder is planned, but not yet implemented.*
73
73
 
74
74
  Here is an example using a few more of Appose's features:
75
75
 
@@ -126,3 +126,9 @@ with env.groovy() as groovy:
126
126
 
127
127
  Of course, the above examples could have been done all in one language. But
128
128
  hopefully they hint at the possibilities of easy cross-language integration.
129
+
130
+ ## Issue tracker
131
+
132
+ All implementations of Appose use the same issue tracker:
133
+
134
+ https://github.com/apposed/appose/issues
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "appose"
7
- version = "0.1.0"
7
+ version = "0.4.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",
@@ -54,7 +54,7 @@ homepage = "https://github.com/apposed/appose-python"
54
54
  documentation = "https://github.com/apposed/appose-python/blob/main/README.md"
55
55
  source = "https://github.com/apposed/appose-python"
56
56
  download = "https://pypi.org/project/appose-python"
57
- tracker = "https://github.com/apposed/appose-python/issues"
57
+ tracker = "https://github.com/apposed/appose/issues"
58
58
 
59
59
  [tool.setuptools]
60
60
  package-dir = {"" = "src"}
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 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
@@ -52,7 +50,7 @@ Here is a very simple example written in Python:
52
50
  Task task = groovy.task("""
53
51
  5 + 6
54
52
  """)
55
- task.waitFor()
53
+ task.wait_for()
56
54
  result = task.outputs.get("result")
57
55
  assert 11 == result
58
56
 
@@ -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"]
@@ -103,7 +101,7 @@ And here is an example using a few more of Appose's features:
103
101
  # Task is taking too long; request a cancelation.
104
102
  task.cancel()
105
103
 
106
- task.waitFor()
104
+ task.wait_for()
107
105
 
108
106
  Of course, the above examples could have been done all in Python. But
109
107
  hopefully they hint at the possibilities of easy cross-language integration.
@@ -127,13 +125,100 @@ 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
135
219
 
136
220
  from .environment import Builder, Environment
221
+ from .types import NDArray, SharedMemory # noqa: F401
137
222
 
138
223
 
139
224
  def base(directory: Path) -> Builder:
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 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:
@@ -58,6 +58,7 @@ class Environment:
58
58
  """
59
59
  python_exes = [
60
60
  "python",
61
+ "python3",
61
62
  "python.exe",
62
63
  "bin/python",
63
64
  "bin/python.exe",
@@ -109,8 +110,9 @@ class Environment:
109
110
  # TODO: Ensure that the classpath includes Appose and its dependencies.
110
111
 
111
112
  # Append any explicitly requested classpath elements.
112
- for element in class_path:
113
- cp[element] = None
113
+ if class_path is not None:
114
+ for element in class_path:
115
+ cp[element] = None
114
116
 
115
117
  # Build up the service arguments.
116
118
  args = [
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 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:
@@ -51,6 +51,6 @@ def find_exe(dirs: Sequence[str], exes: Sequence[str]) -> Optional[Path]:
51
51
  # Candidate is a relative path; check beneath each given directory.
52
52
  for d in dirs:
53
53
  f = Path(d) / exe
54
- if can_execute(f):
54
+ if can_execute(f) and not f.is_dir():
55
55
  return f
56
56
  return None
@@ -2,7 +2,7 @@
2
2
  # #%L
3
3
  # Appose: multi-language interprocess cooperation with shared memory.
4
4
  # %%
5
- # Copyright (C) 2023 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:
@@ -28,39 +28,57 @@
28
28
  ###
29
29
 
30
30
  """
31
- TODO
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
32
39
  """
33
40
 
34
41
  import ast
35
42
  import sys
36
43
  import traceback
37
44
  from threading import Thread
38
- from typing import Optional
45
+ from time import sleep
46
+ from typing import Any, Dict, Optional
39
47
 
40
48
  # NB: Avoid relative imports so that this script can be run standalone.
41
49
  from appose.service import RequestType, ResponseType
42
- from appose.types import Args, decode, encode
50
+ from appose.types import Args, _set_worker, decode, encode
43
51
 
44
52
 
45
53
  class Task:
46
54
  def __init__(self, uuid: str) -> None:
47
55
  self.uuid = uuid
48
56
  self.outputs = {}
57
+ self.finished = False
49
58
  self.cancel_requested = False
59
+ self.thread = None # Initialize thread attribute
50
60
 
51
61
  def update(
52
62
  self,
53
63
  message: Optional[str] = None,
54
64
  current: Optional[int] = None,
55
65
  maximum: Optional[int] = None,
66
+ info: Optional[Dict[str, Any]] = None,
56
67
  ) -> None:
57
68
  args = {}
58
69
  if message is not None:
59
- args["message"] = message
70
+ args["message"] = str(message)
60
71
  if current is not None:
61
- args["current"] = current
72
+ try:
73
+ args["current"] = int(current)
74
+ except ValueError:
75
+ pass
62
76
  if maximum is not None:
63
- args["maximum"] = maximum
77
+ try:
78
+ args["maximum"] = int(maximum)
79
+ except ValueError:
80
+ pass
81
+ args["info"] = info
64
82
  self._respond(ResponseType.UPDATE, args)
65
83
 
66
84
  def cancel(self) -> None:
@@ -74,7 +92,6 @@ class Task:
74
92
  def execute_script():
75
93
  # Populate script bindings.
76
94
  binding = {"task": self}
77
- # TODO: Magically convert shared memory image inputs.
78
95
  if inputs is not None:
79
96
  binding.update(inputs)
80
97
 
@@ -99,7 +116,14 @@ class Task:
99
116
  # Last statement of the script looks like an expression. Evaluate!
100
117
  last = ast.Expression(block.body.pop().value)
101
118
 
102
- _globals = {}
119
+ # NB: When `exec` gets two separate objects as *globals* and
120
+ # *locals*, the code will be executed as if it were embedded in
121
+ # a class definition. This means functions and classes defined
122
+ # in the executed code will not be able to access variables
123
+ # assigned at the top level, because the "top level" variables
124
+ # are treated as class variables in a class definition.
125
+ # See: https://docs.python.org/3/library/functions.html#exec
126
+ _globals = binding
103
127
  exec(compile(block, "<string>", mode="exec"), _globals, binding)
104
128
  if last is not None:
105
129
  result = eval(
@@ -119,10 +143,24 @@ class Task:
119
143
 
120
144
  self._report_completion()
121
145
 
122
- # TODO: Consider whether to retain a reference to this Thread, and
123
- # expose a "force" option for cancelation that kills it forcibly; see:
124
- # https://www.geeksforgeeks.org/python-different-ways-to-kill-a-thread/
125
- Thread(target=execute_script, name=f"Appose-{self.uuid}").start()
146
+ # HACK: Pre-load toplevel import statements before running the script
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()
126
164
 
127
165
  def _report_launch(self) -> None:
128
166
  self._respond(ResponseType.LAUNCH, None)
@@ -132,15 +170,56 @@ class Task:
132
170
  self._respond(ResponseType.COMPLETION, args)
133
171
 
134
172
  def _respond(self, response_type: ResponseType, args: Optional[Args]) -> None:
135
- response = {"task": self.uuid, "responseType": response_type.value}
173
+ already_terminated = False
174
+ if response_type.is_terminal():
175
+ if self.finished:
176
+ # This is not the first terminal response. Let's
177
+ # remember, in case an exception is generated below,
178
+ # so that we can avoid infinite recursion loops.
179
+ already_terminated = True
180
+ self.finished = True
181
+
182
+ response = {}
136
183
  if args is not None:
137
184
  response.update(args)
185
+ response.update({"task": self.uuid, "responseType": response_type.value})
138
186
  # NB: Flush is necessary to ensure service receives the data!
139
- print(encode(response), flush=True)
187
+ try:
188
+ print(encode(response), flush=True)
189
+ except Exception:
190
+ if already_terminated:
191
+ # An exception triggered a failure response which
192
+ # then triggered another exception. Let's stop here
193
+ # to avoid the risk of infinite recursion loops.
194
+ return
195
+ # Encoding can fail due to unsupported types, when the
196
+ # response or its elements are not supported by JSON encoding.
197
+ # No matter what goes wrong, we want to tell the caller.
198
+ self.fail(traceback.format_exc())
140
199
 
141
200
 
142
201
  def main() -> None:
202
+ _set_worker(True)
203
+
143
204
  tasks = {}
205
+ running = True
206
+
207
+ def cleanup_threads():
208
+ while running:
209
+ sleep(0.05)
210
+ dead = {
211
+ uuid: task
212
+ for uuid, task in tasks.items()
213
+ if task.thread is not None and not task.thread.is_alive()
214
+ }
215
+ for uuid, task in dead.items():
216
+ tasks.pop(uuid)
217
+ if not task.finished:
218
+ # The task died before reporting a terminal status.
219
+ # We report this situation as failure by thread death.
220
+ task.fail("thread death")
221
+
222
+ Thread(target=cleanup_threads, name="Appose-Janitor").start()
144
223
 
145
224
  while True:
146
225
  try:
@@ -165,12 +244,12 @@ def main() -> None:
165
244
  case RequestType.CANCEL:
166
245
  task = tasks.get(uuid)
167
246
  if task is None:
168
- # TODO: proper logging
169
- # Maybe should stdout the error back to Appose calling process.
170
247
  print(f"No such task: {uuid}", file=sys.stderr)
171
248
  continue
172
249
  task.cancel_requested = True
173
250
 
251
+ running = False
252
+
174
253
 
175
254
  if __name__ == "__main__":
176
255
  main()