appose 0.2.0__py3-none-any.whl → 0.4.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 - 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
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 - 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:
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 - 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:
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 - 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,7 +42,8 @@ import ast
42
42
  import sys
43
43
  import traceback
44
44
  from threading import Thread
45
- from typing import Optional
45
+ from time import sleep
46
+ from typing import Any, Dict, Optional
46
47
 
47
48
  # NB: Avoid relative imports so that this script can be run standalone.
48
49
  from appose.service import RequestType, ResponseType
@@ -53,13 +54,16 @@ class Task:
53
54
  def __init__(self, uuid: str) -> None:
54
55
  self.uuid = uuid
55
56
  self.outputs = {}
57
+ self.finished = False
56
58
  self.cancel_requested = False
59
+ self.thread = None # Initialize thread attribute
57
60
 
58
61
  def update(
59
62
  self,
60
63
  message: Optional[str] = None,
61
64
  current: Optional[int] = None,
62
65
  maximum: Optional[int] = None,
66
+ info: Optional[Dict[str, Any]] = None,
63
67
  ) -> None:
64
68
  args = {}
65
69
  if message is not None:
@@ -74,6 +78,7 @@ class Task:
74
78
  args["maximum"] = int(maximum)
75
79
  except ValueError:
76
80
  pass
81
+ args["info"] = info
77
82
  self._respond(ResponseType.UPDATE, args)
78
83
 
79
84
  def cancel(self) -> None:
@@ -111,7 +116,14 @@ class Task:
111
116
  # Last statement of the script looks like an expression. Evaluate!
112
117
  last = ast.Expression(block.body.pop().value)
113
118
 
114
- _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
115
127
  exec(compile(block, "<string>", mode="exec"), _globals, binding)
116
128
  if last is not None:
117
129
  result = eval(
@@ -131,10 +143,24 @@ class Task:
131
143
 
132
144
  self._report_completion()
133
145
 
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()
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()
138
164
 
139
165
  def _report_launch(self) -> None:
140
166
  self._respond(ResponseType.LAUNCH, None)
@@ -144,20 +170,31 @@ class Task:
144
170
  self._respond(ResponseType.COMPLETION, args)
145
171
 
146
172
  def _respond(self, response_type: ResponseType, args: Optional[Args]) -> None:
147
- 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 = {}
148
183
  if args is not None:
149
184
  response.update(args)
185
+ response.update({"task": self.uuid, "responseType": response_type.value})
150
186
  # NB: Flush is necessary to ensure service receives the data!
151
187
  try:
152
188
  print(encode(response), flush=True)
153
189
  except Exception:
154
- # Encoding can fail due to unsupported types, when the response
155
- # or its elements are not supported by JSON encoding.
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.
156
197
  # 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
198
  self.fail(traceback.format_exc())
162
199
 
163
200
 
@@ -165,6 +202,24 @@ def main() -> None:
165
202
  _set_worker(True)
166
203
 
167
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()
168
223
 
169
224
  while True:
170
225
  try:
@@ -193,6 +248,8 @@ def main() -> None:
193
248
  continue
194
249
  task.cancel_requested = True
195
250
 
251
+ running = False
252
+
196
253
 
197
254
  if __name__ == "__main__":
198
255
  main()
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 - 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:
@@ -256,11 +256,35 @@ class ResponseType(Enum):
256
256
  FAILURE = "FAILURE"
257
257
  CRASH = "CRASH"
258
258
 
259
+ """
260
+ True iff response type is COMPLETE, CANCELED, FAILED, or CRASHED.
261
+ """
262
+
263
+ def is_terminal(self):
264
+ return self in (
265
+ ResponseType.COMPLETION,
266
+ ResponseType.CANCELATION,
267
+ ResponseType.FAILURE,
268
+ ResponseType.CRASH,
269
+ )
270
+
259
271
 
260
272
  class TaskEvent:
261
- def __init__(self, task: "Task", response_type: ResponseType) -> None:
273
+ def __init__(
274
+ self,
275
+ task: "Task",
276
+ response_type: ResponseType,
277
+ message: Optional[str] = None,
278
+ current: Optional[int] = None,
279
+ maximum: Optional[int] = None,
280
+ info: Optional[Dict[str, Any]] = None,
281
+ ) -> None:
262
282
  self.task: "Task" = task
263
283
  self.response_type: ResponseType = response_type
284
+ self.message: Optional[str] = message
285
+ self.current: Optional[int] = current
286
+ self.maximum: Optional[int] = maximum
287
+ self.info: Optional[Dict[str, Any]] = info
264
288
 
265
289
  def __str__(self):
266
290
  return f"[{self.response_type}] {self.task}"
@@ -354,13 +378,8 @@ class Task:
354
378
  case ResponseType.LAUNCH:
355
379
  self.status = TaskStatus.RUNNING
356
380
  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)
381
+ # No extra action needed.
382
+ pass
364
383
  case ResponseType.COMPLETION:
365
384
  self.service._tasks.pop(self.uuid, None)
366
385
  self.status = TaskStatus.COMPLETE
@@ -380,7 +399,11 @@ class Task:
380
399
  )
381
400
  return
382
401
 
383
- event = TaskEvent(self, response_type)
402
+ message = response.get("message")
403
+ current = response.get("current")
404
+ maximum = response.get("maximum")
405
+ info = response.get("info")
406
+ event = TaskEvent(self, response_type, message, current, maximum, info)
384
407
  for listener in self.listeners:
385
408
  listener(event)
386
409
 
@@ -397,7 +420,4 @@ class Task:
397
420
  self.cv.notify_all()
398
421
 
399
422
  def __str__(self):
400
- return (
401
- f"{self.uuid=}, {self.status=}, {self.message=}, "
402
- f"{self.current=}, {self.maximum=}, {self.error=}"
403
- )
423
+ return f"{self.uuid=}, {self.status=}, {self.error=}"
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 - 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.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
@@ -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
@@ -29,17 +29,18 @@ Requires-Python: >=3.10
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE.txt
31
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'
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
43
44
 
44
45
  # Appose Python
45
46
 
@@ -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=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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,11 +0,0 @@
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,,