appose 0.1.0__py3-none-any.whl → 0.2.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/__init__.py CHANGED
@@ -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 - 2024 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:
@@ -52,7 +52,7 @@ Here is a very simple example written in Python:
52
52
  Task task = groovy.task("""
53
53
  5 + 6
54
54
  """)
55
- task.waitFor()
55
+ task.wait_for()
56
56
  result = task.outputs.get("result")
57
57
  assert 11 == result
58
58
 
@@ -103,7 +103,7 @@ And here is an example using a few more of Appose's features:
103
103
  # Task is taking too long; request a cancelation.
104
104
  task.cancel()
105
105
 
106
- task.waitFor()
106
+ task.wait_for()
107
107
 
108
108
  Of course, the above examples could have been done all in Python. But
109
109
  hopefully they hint at the possibilities of easy cross-language integration.
@@ -134,6 +134,7 @@ JSON, one line per request/response.
134
134
  from pathlib import Path
135
135
 
136
136
  from .environment import Builder, Environment
137
+ from .types import NDArray, SharedMemory # noqa: F401
137
138
 
138
139
 
139
140
  def base(directory: Path) -> Builder:
appose/environment.py CHANGED
@@ -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 - 2024 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 = [
appose/paths.py CHANGED
@@ -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 - 2024 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
appose/python_worker.py CHANGED
@@ -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 - 2024 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,7 +28,14 @@
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
@@ -39,7 +46,7 @@ from typing import Optional
39
46
 
40
47
  # NB: Avoid relative imports so that this script can be run standalone.
41
48
  from appose.service import RequestType, ResponseType
42
- from appose.types import Args, decode, encode
49
+ from appose.types import Args, _set_worker, decode, encode
43
50
 
44
51
 
45
52
  class Task:
@@ -56,11 +63,17 @@ class Task:
56
63
  ) -> None:
57
64
  args = {}
58
65
  if message is not None:
59
- args["message"] = message
66
+ args["message"] = str(message)
60
67
  if current is not None:
61
- args["current"] = current
68
+ try:
69
+ args["current"] = int(current)
70
+ except ValueError:
71
+ pass
62
72
  if maximum is not None:
63
- args["maximum"] = maximum
73
+ try:
74
+ args["maximum"] = int(maximum)
75
+ except ValueError:
76
+ pass
64
77
  self._respond(ResponseType.UPDATE, args)
65
78
 
66
79
  def cancel(self) -> None:
@@ -74,7 +87,6 @@ class Task:
74
87
  def execute_script():
75
88
  # Populate script bindings.
76
89
  binding = {"task": self}
77
- # TODO: Magically convert shared memory image inputs.
78
90
  if inputs is not None:
79
91
  binding.update(inputs)
80
92
 
@@ -136,10 +148,22 @@ class Task:
136
148
  if args is not None:
137
149
  response.update(args)
138
150
  # NB: Flush is necessary to ensure service receives the data!
139
- print(encode(response), flush=True)
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())
140
162
 
141
163
 
142
164
  def main() -> None:
165
+ _set_worker(True)
166
+
143
167
  tasks = {}
144
168
 
145
169
  while True:
@@ -165,8 +189,6 @@ def main() -> None:
165
189
  case RequestType.CANCEL:
166
190
  task = tasks.get(uuid)
167
191
  if task is None:
168
- # TODO: proper logging
169
- # Maybe should stdout the error back to Appose calling process.
170
192
  print(f"No such task: {uuid}", file=sys.stderr)
171
193
  continue
172
194
  task.cancel_requested = True
appose/service.py CHANGED
@@ -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 - 2024 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:
@@ -61,6 +61,7 @@ class Service:
61
61
  self._process: Optional[subprocess.Popen] = None
62
62
  self._stdout_thread: Optional[threading.Thread] = None
63
63
  self._stderr_thread: Optional[threading.Thread] = None
64
+ self._monitor_thread: Optional[threading.Thread] = None
64
65
  self._debug_callback: Optional[Callable[[Any], Any]] = None
65
66
 
66
67
  def debug(self, debug_callback: Callable[[Any], Any]) -> None:
@@ -101,8 +102,12 @@ class Service:
101
102
  self._stderr_thread = threading.Thread(
102
103
  target=self._stderr_loop, name=f"{prefix}-Stderr"
103
104
  )
105
+ self._monitor_thread = threading.Thread(
106
+ target=self._monitor_loop, name=f"{prefix}-Monitor"
107
+ )
104
108
  self._stdout_thread.start()
105
109
  self._stderr_thread.start()
110
+ self._monitor_thread.start()
106
111
 
107
112
  def task(self, script: str, inputs: Optional[Args] = None) -> "Task":
108
113
  """
@@ -131,15 +136,24 @@ class Service:
131
136
  """
132
137
  Input loop processing lines from the worker's stdout stream.
133
138
  """
134
- # noinspection PyBroadException
135
- try:
136
- while True:
137
- line = self._process.stdout.readline()
138
- self._debug_service("<worker stdout closed>" if line is None else line)
139
+ while True:
140
+ stdout = self._process.stdout
141
+ # noinspection PyBroadException
142
+ try:
143
+ line = None if stdout is None else stdout.readline()
144
+ except Exception:
145
+ # Something went wrong reading the line. Panic!
146
+ self._debug_service(format_exc())
147
+ break
148
+
149
+ if not line: # readline returns empty string upon EOF
150
+ self._debug_service("<worker stdout closed>")
151
+ return
139
152
 
140
- if line is None:
141
- return # pipe closed
153
+ # noinspection PyBroadException
154
+ try:
142
155
  response = decode(line)
156
+ self._debug_service(line) # Echo the line to the debug listener.
143
157
  uuid = response.get("task")
144
158
  if uuid is None:
145
159
  self._debug_service("Invalid service message: {line}")
@@ -150,8 +164,10 @@ class Service:
150
164
  continue
151
165
  # noinspection PyProtectedMember
152
166
  task._handle(response)
153
- except Exception:
154
- self._debug_service(format_exc())
167
+ except Exception:
168
+ # Something went wrong decoding the line of JSON.
169
+ # Skip it and keep going, but log it first.
170
+ self._debug_service(f"<INVALID> {line}")
155
171
 
156
172
  def _stderr_loop(self) -> None:
157
173
  """
@@ -160,14 +176,37 @@ class Service:
160
176
  # noinspection PyBroadException
161
177
  try:
162
178
  while True:
163
- line = self._process.stderr.readline()
164
- if line is None:
179
+ stderr = self._process.stderr
180
+ line = None if stderr is None else stderr.readline()
181
+ if not line: # readline returns empty string upon EOF
165
182
  self._debug_service("<worker stderr closed>")
166
183
  return
167
184
  self._debug_worker(line)
168
185
  except Exception:
169
186
  self._debug_service(format_exc())
170
187
 
188
+ def _monitor_loop(self) -> None:
189
+ # Wait until the worker process terminates.
190
+ self._process.wait()
191
+
192
+ # Do some sanity checks.
193
+ exit_code = self._process.returncode
194
+ if exit_code != 0:
195
+ self._debug_service(
196
+ f"<worker process terminated with exit code {exit_code}>"
197
+ )
198
+ task_count = len(self._tasks)
199
+ if task_count > 0:
200
+ self._debug_service(
201
+ f"<worker process terminated with {task_count} pending tasks>"
202
+ )
203
+
204
+ # Notify any remaining tasks about the process crash.
205
+ for task in self._tasks.values():
206
+ task._crash()
207
+
208
+ self._tasks.clear()
209
+
171
210
  def _debug_service(self, message: str) -> None:
172
211
  self._debug("SERVICE", message)
173
212
 
@@ -190,15 +229,17 @@ class TaskStatus(Enum):
190
229
  COMPLETE = "COMPLETE"
191
230
  CANCELED = "CANCELED"
192
231
  FAILED = "FAILED"
232
+ CRASHED = "CRASHED"
193
233
 
194
234
  def is_finished(self):
195
235
  """
196
- True iff status is COMPLETE, CANCELED, or FAILED.
236
+ True iff status is COMPLETE, CANCELED, FAILED, or CRASHED.
197
237
  """
198
238
  return self in (
199
239
  TaskStatus.COMPLETE,
200
240
  TaskStatus.CANCELED,
201
241
  TaskStatus.FAILED,
242
+ TaskStatus.CRASHED,
202
243
  )
203
244
 
204
245
 
@@ -213,6 +254,7 @@ class ResponseType(Enum):
213
254
  COMPLETION = "COMPLETION"
214
255
  CANCELATION = "CANCELATION"
215
256
  FAILURE = "FAILURE"
257
+ CRASH = "CRASH"
216
258
 
217
259
 
218
260
  class TaskEvent:
@@ -220,6 +262,9 @@ class TaskEvent:
220
262
  self.task: "Task" = task
221
263
  self.response_type: ResponseType = response_type
222
264
 
265
+ def __str__(self):
266
+ return f"[{self.response_type}] {self.task}"
267
+
223
268
 
224
269
  # noinspection PyProtectedMember
225
270
  class Task:
@@ -342,3 +387,17 @@ class Task:
342
387
  if self.status.is_finished():
343
388
  with self.cv:
344
389
  self.cv.notify_all()
390
+
391
+ def _crash(self):
392
+ event = TaskEvent(self, ResponseType.CRASH)
393
+ self.status = TaskStatus.CRASHED
394
+ for listener in self.listeners:
395
+ listener(event)
396
+ with self.cv:
397
+ self.cv.notify_all()
398
+
399
+ def __str__(self):
400
+ return (
401
+ f"{self.uuid=}, {self.status=}, {self.message=}, "
402
+ f"{self.current=}, {self.maximum=}, {self.error=}"
403
+ )
appose/types.py CHANGED
@@ -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 - 2024 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,14 +28,170 @@
28
28
  ###
29
29
 
30
30
  import json
31
- from typing import Any, Dict
31
+ import re
32
+ from math import ceil, prod
33
+ from multiprocessing import resource_tracker, shared_memory
34
+ from typing import Any, Dict, Sequence, Union
32
35
 
33
36
  Args = Dict[str, Any]
34
37
 
35
38
 
39
+ class SharedMemory(shared_memory.SharedMemory):
40
+ """
41
+ An enhanced version of Python's multiprocessing.shared_memory.SharedMemory
42
+ class which can be used with a `with` statement. When the program flow
43
+ exits the `with` block, this class's `dispose()` method will be invoked,
44
+ which might call `close()` or `unlink()` depending on the value of its
45
+ `unlink_on_dispose` flag.
46
+ """
47
+
48
+ def __init__(self, name: str = None, create: bool = False, size: int = 0):
49
+ super().__init__(name=name, create=create, size=size)
50
+ self._unlink_on_dispose = create
51
+ if _is_worker:
52
+ # 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.
55
+ #
56
+ # There is one resource_tracker per Python process, and they will
57
+ # each try to delete shared memory blocks known to them when they
58
+ # are shutting down, even when other processes still need them.
59
+ #
60
+ # As such, the rule Appose follows is: let the service process
61
+ # always handle cleanup of shared memory blocks, regardless of
62
+ # which process initially allocated it.
63
+ resource_tracker.unregister(self._name, "shared_memory")
64
+
65
+ def unlink_on_dispose(self, value: bool) -> None:
66
+ """
67
+ Set whether the `unlink()` method should be invoked to destroy
68
+ the shared memory block when the `dispose()` method is called.
69
+
70
+ Note: dispose() is the method called when exiting a `with` block.
71
+
72
+ By default, shared memory objects constructed with `create=True`
73
+ will behave this way, whereas shared memory objects constructed
74
+ with `create=False` will not. But this method allows to override
75
+ the behavior.
76
+ """
77
+ self._unlink_on_dispose = value
78
+
79
+ def dispose(self) -> None:
80
+ if self._unlink_on_dispose:
81
+ self.unlink()
82
+ else:
83
+ self.close()
84
+
85
+ def __enter__(self) -> "SharedMemory":
86
+ return self
87
+
88
+ def __exit__(self, exc_type, exc_value, exc_tb) -> None:
89
+ self.dispose()
90
+
91
+
36
92
  def encode(data: Args) -> str:
37
- return json.dumps(data)
93
+ return json.dumps(data, cls=_ApposeJSONEncoder, separators=(",", ":"))
38
94
 
39
95
 
40
96
  def decode(the_json: str) -> Args:
41
- return json.loads(the_json)
97
+ return json.loads(the_json, object_hook=_appose_object_hook)
98
+
99
+
100
+ class NDArray:
101
+ """
102
+ Data structure for a multi-dimensional array.
103
+ The array contains elements of a data type, arranged in
104
+ a particular shape, and flattened into SharedMemory.
105
+ """
106
+
107
+ def __init__(self, dtype: str, shape: Sequence[int], shm: SharedMemory = None):
108
+ """
109
+ Create an NDArray.
110
+ :param dtype: The type of the data elements; e.g. int8, uint8, float32, float64.
111
+ :param shape: The dimensional extents; e.g. a stack of 7 image planes
112
+ with resolution 512x512 would have shape [7, 512, 512].
113
+ :param shm: The SharedMemory containing the array data, or None to create it.
114
+ """
115
+ self.dtype = dtype
116
+ self.shape = shape
117
+ self.shm = (
118
+ SharedMemory(
119
+ create=True, size=ceil(prod(shape) * _bytes_per_element(dtype))
120
+ )
121
+ if shm is None
122
+ else shm
123
+ )
124
+
125
+ def __str__(self):
126
+ return (
127
+ f"NDArray("
128
+ f"dtype='{self.dtype}', "
129
+ f"shape={self.shape}, "
130
+ f"shm='{self.shm.name}' ({self.shm.size}))"
131
+ )
132
+
133
+ def ndarray(self):
134
+ """
135
+ Create a NumPy ndarray object for working with the array data.
136
+ No array data is copied; the NumPy array wraps the same SharedMemory.
137
+ Requires the numpy package to be installed.
138
+ """
139
+ try:
140
+ import numpy
141
+
142
+ return numpy.ndarray(
143
+ prod(self.shape), dtype=self.dtype, buffer=self.shm.buf
144
+ ).reshape(self.shape)
145
+ except ModuleNotFoundError:
146
+ raise ImportError("NumPy is not available.")
147
+
148
+ def __enter__(self) -> "NDArray":
149
+ return self
150
+
151
+ def __exit__(self, exc_type, exc_value, exc_tb) -> None:
152
+ self.shm.dispose()
153
+
154
+
155
+ class _ApposeJSONEncoder(json.JSONEncoder):
156
+ def default(self, obj):
157
+ if isinstance(obj, SharedMemory):
158
+ return {
159
+ "appose_type": "shm",
160
+ "name": obj.name,
161
+ "size": obj.size,
162
+ }
163
+ if isinstance(obj, NDArray):
164
+ return {
165
+ "appose_type": "ndarray",
166
+ "dtype": obj.dtype,
167
+ "shape": obj.shape,
168
+ "shm": obj.shm,
169
+ }
170
+ return super().default(obj)
171
+
172
+
173
+ def _appose_object_hook(obj: Dict):
174
+ atype = obj.get("appose_type")
175
+ if atype == "shm":
176
+ # Attach to existing shared memory block.
177
+ return SharedMemory(name=(obj["name"]), size=(obj["size"]))
178
+ elif atype == "ndarray":
179
+ return NDArray(obj["dtype"], obj["shape"], obj["shm"])
180
+ else:
181
+ return obj
182
+
183
+
184
+ def _bytes_per_element(dtype: str) -> Union[int, float]:
185
+ try:
186
+ bits = int(re.sub("[^0-9]", "", dtype))
187
+ except ValueError:
188
+ raise ValueError(f"Invalid dtype: {dtype}")
189
+ return bits / 8
190
+
191
+
192
+ _is_worker = False
193
+
194
+
195
+ def _set_worker(value: bool) -> None:
196
+ global _is_worker
197
+ _is_worker = value
@@ -1,4 +1,4 @@
1
- Copyright (c) 2023, Appose developers.
1
+ Copyright (c) 2023 - 2024, Appose developers.
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without modification,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appose
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Appose: multi-language interprocess cooperation with shared memory.
5
5
  Author: Appose developers
6
6
  License: Simplified BSD License
@@ -8,7 +8,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
@@ -71,16 +71,6 @@ This is the **Python implementation of Appose**.
71
71
 
72
72
  The name of the package is `appose`.
73
73
 
74
- ### Conda/Mamba
75
-
76
- To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
77
- add `appose` to your `environment.yml`'s `dependencies` section:
78
-
79
- ```yaml
80
- dependencies:
81
- - appose
82
- ```
83
-
84
74
  ### PyPI/Pip
85
75
 
86
76
  To use [the PyPI package](https://pypi.org/project/appose),
@@ -98,6 +88,16 @@ dependencies = [
98
88
  ]
99
89
  ```
100
90
 
91
+ ### Conda/Mamba
92
+
93
+ To use [the conda-forge package](https://anaconda.org/conda-forge/appose),
94
+ add `appose` to your `environment.yml`'s `dependencies` section:
95
+
96
+ ```yaml
97
+ dependencies:
98
+ - appose
99
+ ```
100
+
101
101
  ## Examples
102
102
 
103
103
  Here is a minimal example for calling into Java from Python:
@@ -107,12 +107,12 @@ import appose
107
107
  env = appose.java(vendor="zulu", version="17").build()
108
108
  with env.groovy() as groovy:
109
109
  task = groovy.task("5 + 6")
110
- task.waitFor()
111
- result = task.outputs.get("result")
110
+ task.wait_for()
111
+ result = task.outputs["result"]
112
112
  assert 11 == result
113
113
  ```
114
114
 
115
- *Note: The `Appose.java` builder is planned, but not yet implemented.*
115
+ *Note: The `appose.java` builder is planned, but not yet implemented.*
116
116
 
117
117
  Here is an example using a few more of Appose's features:
118
118
 
@@ -169,3 +169,9 @@ with env.groovy() as groovy:
169
169
 
170
170
  Of course, the above examples could have been done all in one language. But
171
171
  hopefully they hint at the possibilities of easy cross-language integration.
172
+
173
+ ## Issue tracker
174
+
175
+ All implementations of Appose use the same issue tracker:
176
+
177
+ https://github.com/apposed/appose/issues
@@ -0,0 +1,11 @@
1
+ appose/__init__.py,sha256=sGkVQv1eZQp0BOoySsVOGcVMr47rE3PSyqxsk8_EOn4,5404
2
+ appose/environment.py,sha256=8BBvRIzr6ZIjBeZMQYOHG6Va1gSxMkYc3ieJ9HafeXA,7597
3
+ appose/paths.py,sha256=dcXD54rOUiCWfM58vqttRY0Eu6YUFMD65ZO_2V4hhEk,2182
4
+ appose/python_worker.py,sha256=jhe_iFnCS7vJ_N1EycDTt6oq09JhYIOnvvhyqnLG40U,7335
5
+ appose/service.py,sha256=v5u_HAliJlsoghwC6QIn3CZV2YpnPU8V3GdBcjgA62w,14070
6
+ appose/types.py,sha256=m5mX5BhdR7REnkSCuP5fGnnQWc_o7qQDyOFHWND8Vvs,6975
7
+ appose-0.2.0.dist-info/LICENSE.txt,sha256=LcTntIsPpu7JOZfEBAVslv-PVV4vnD9tfvXrS4cROe8,1313
8
+ appose-0.2.0.dist-info/METADATA,sha256=lqL66Xs9gTSJLr_ISb7ge8lK80xSQh04_Amfsf1jQWg,5586
9
+ appose-0.2.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
10
+ appose-0.2.0.dist-info/top_level.txt,sha256=oqHhw2QGlaFfH3jMR9H5Cd6XYHdEv5DvK1am0iEFPW0,7
11
+ appose-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.40.0)
2
+ Generator: setuptools (72.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,11 +0,0 @@
1
- appose/__init__.py,sha256=IwFrDvIa6QOgV_RVgbIUfwyzswWB4RckLrzA9cts6IE,5340
2
- appose/environment.py,sha256=m1a7166dJq69XPWX53d63KoITqmH4fKMNPKbEGSxeC0,7524
3
- appose/paths.py,sha256=qBEWWf0LB0Cpk-l7u5uoPx9DTley6HltMTg0vJ2iFEQ,2156
4
- appose/python_worker.py,sha256=ZR6AtWXXk_Xcp0nSoWehbl7LYc6z18ZTTvo4QjO_MwU,6474
5
- appose/service.py,sha256=LqchMIxQXV6YF_l8sAGEyJapc2lSL6KBttcbLzdABFw,11966
6
- appose/types.py,sha256=uuVNLt21eT3JEPtRQCMQ_IcFnp2yLgrJTSxzAnxkKFY,1618
7
- appose-0.1.0.dist-info/LICENSE.txt,sha256=BOQExkI6YKvnUVMUqDbPuSxsY7XXL1XRUTTbnfEWII0,1306
8
- appose-0.1.0.dist-info/METADATA,sha256=6ohYqhKiVnCvof0UX4HAnMmarKKIyi7hRScNtEO1T9E,5477
9
- appose-0.1.0.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
10
- appose-0.1.0.dist-info/top_level.txt,sha256=oqHhw2QGlaFfH3jMR9H5Cd6XYHdEv5DvK1am0iEFPW0,7
11
- appose-0.1.0.dist-info/RECORD,,