wool 0.1rc9__py3-none-any.whl → 0.1rc10__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.

Potentially problematic release.


This version of wool might be problematic. Click here for more details.

Files changed (44) hide show
  1. wool/__init__.py +71 -50
  2. wool/_protobuf/__init__.py +12 -5
  3. wool/_protobuf/exception.py +3 -0
  4. wool/_protobuf/task.py +11 -0
  5. wool/_protobuf/task_pb2.py +42 -0
  6. wool/_protobuf/task_pb2.pyi +43 -0
  7. wool/_protobuf/{mempool/metadata_pb2_grpc.py → task_pb2_grpc.py} +2 -2
  8. wool/_protobuf/worker.py +24 -0
  9. wool/_protobuf/worker_pb2.py +47 -0
  10. wool/_protobuf/worker_pb2.pyi +39 -0
  11. wool/_protobuf/worker_pb2_grpc.py +141 -0
  12. wool/_resource_pool.py +376 -0
  13. wool/_typing.py +0 -10
  14. wool/_work.py +553 -0
  15. wool/_worker.py +843 -169
  16. wool/_worker_discovery.py +1223 -0
  17. wool/_worker_pool.py +331 -0
  18. wool/_worker_proxy.py +515 -0
  19. {wool-0.1rc9.dist-info → wool-0.1rc10.dist-info}/METADATA +7 -7
  20. wool-0.1rc10.dist-info/RECORD +22 -0
  21. wool-0.1rc10.dist-info/entry_points.txt +2 -0
  22. wool/_cli.py +0 -262
  23. wool/_event.py +0 -109
  24. wool/_future.py +0 -171
  25. wool/_logging.py +0 -44
  26. wool/_manager.py +0 -181
  27. wool/_mempool/__init__.py +0 -4
  28. wool/_mempool/_client.py +0 -167
  29. wool/_mempool/_mempool.py +0 -311
  30. wool/_mempool/_metadata.py +0 -35
  31. wool/_mempool/_service.py +0 -227
  32. wool/_pool.py +0 -524
  33. wool/_protobuf/mempool/metadata_pb2.py +0 -36
  34. wool/_protobuf/mempool/metadata_pb2.pyi +0 -17
  35. wool/_protobuf/mempool/service_pb2.py +0 -66
  36. wool/_protobuf/mempool/service_pb2.pyi +0 -108
  37. wool/_protobuf/mempool/service_pb2_grpc.py +0 -355
  38. wool/_queue.py +0 -32
  39. wool/_session.py +0 -429
  40. wool/_task.py +0 -366
  41. wool/_utils.py +0 -63
  42. wool-0.1rc9.dist-info/RECORD +0 -29
  43. wool-0.1rc9.dist-info/entry_points.txt +0 -2
  44. {wool-0.1rc9.dist-info → wool-0.1rc10.dist-info}/WHEEL +0 -0
wool/_manager.py DELETED
@@ -1,181 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from functools import partial
5
- from multiprocessing.managers import BaseManager
6
- from multiprocessing.managers import DictProxy
7
- from queue import Empty
8
- from threading import Event
9
- from threading import Lock
10
- from typing import TYPE_CHECKING
11
- from typing import Any
12
- from typing import Callable
13
- from typing import TypeVar
14
- from typing import overload
15
- from uuid import UUID
16
- from weakref import WeakValueDictionary
17
-
18
- from wool._event import TaskEvent
19
- from wool._future import Future
20
- from wool._queue import TaskQueue
21
- from wool._typing import PassthroughDecorator
22
-
23
- if TYPE_CHECKING:
24
- from wool._task import Task
25
-
26
-
27
- C = TypeVar("C", bound=Callable[..., Any])
28
-
29
- _manager_registry = {}
30
-
31
-
32
- def _register(fn, /, **kwargs):
33
- assert fn.__name__ not in _manager_registry, (
34
- f"Function '{fn.__name__}' already registered"
35
- )
36
- _manager_registry[fn.__name__] = (fn, kwargs)
37
- return fn
38
-
39
-
40
- @overload
41
- def register(
42
- fn: C,
43
- /,
44
- *,
45
- exposed: tuple[str, ...] | None = None,
46
- proxytype: None = None,
47
- method_to_typeid: None = None,
48
- ) -> C: ...
49
-
50
-
51
- @overload
52
- def register(
53
- fn: None = None,
54
- /,
55
- *,
56
- exposed: tuple[str, ...] | None = None,
57
- proxytype: type | None = None,
58
- method_to_typeid: dict[str, str] | None = None,
59
- ) -> PassthroughDecorator[C]: ...
60
-
61
-
62
- def register(
63
- fn: C | None = None,
64
- /,
65
- *,
66
- exposed: tuple[str, ...] | None = None,
67
- proxytype: type | None = None,
68
- method_to_typeid: dict[str, str] | None = None,
69
- ) -> PassthroughDecorator[C] | C:
70
- kwargs = {}
71
- if exposed is not None:
72
- kwargs["exposed"] = exposed
73
- if proxytype is not None:
74
- kwargs["proxytype"] = proxytype
75
- if method_to_typeid is not None:
76
- kwargs["method_to_typeid"] = method_to_typeid
77
- if fn:
78
- return _register(fn, **kwargs)
79
- else:
80
- return partial(_register, **kwargs)
81
-
82
-
83
- class FuturesProxy(DictProxy):
84
- assert (method_to_typeid := getattr(DictProxy, "_method_to_typeid_"))
85
- _method_to_typeid_ = {
86
- **method_to_typeid,
87
- "__getitem__": "proxify",
88
- "setdefault": "proxify",
89
- }
90
-
91
-
92
- _task_queue: TaskQueue[Task] = TaskQueue(100000, None)
93
-
94
- _task_queue_lock = Lock()
95
-
96
- _task_futures: WeakValueDictionary[UUID, Future] = WeakValueDictionary()
97
-
98
-
99
- @register
100
- def put(task: Task) -> Future:
101
- try:
102
- with queue_lock():
103
- queue().put(task, block=False)
104
- future = futures()[task.id] = Future()
105
- TaskEvent("task-queued", task=task).emit()
106
- return future
107
- except Exception as e:
108
- logging.exception(e)
109
- raise
110
-
111
-
112
- @register
113
- def get() -> Task | Empty | None:
114
- try:
115
- return queue().get(block=False)
116
- except Empty as e:
117
- return e
118
-
119
-
120
- @register
121
- def proxify(value):
122
- return value
123
-
124
-
125
- @register(proxytype=FuturesProxy)
126
- def futures():
127
- global _task_futures
128
- if not _task_futures:
129
- _task_futures = WeakValueDictionary()
130
- return _task_futures
131
-
132
-
133
- @register
134
- def queue() -> TaskQueue:
135
- global _task_queue
136
- if not _task_queue:
137
- _task_queue = TaskQueue(1000, None)
138
- return _task_queue
139
-
140
-
141
- @register
142
- def queue_lock() -> Lock:
143
- global _task_queue_lock
144
- if not _task_queue_lock:
145
- _task_queue_lock = Lock()
146
- return _task_queue_lock
147
-
148
-
149
- _stop_event = Event()
150
- _wait_event = Event()
151
-
152
-
153
- @register(exposed=("is_set", "set", "clear", "wait"))
154
- def stopping() -> Event:
155
- return _stop_event
156
-
157
-
158
- @register(exposed=("is_set", "set", "clear", "wait"))
159
- def waiting() -> Event:
160
- return _wait_event
161
-
162
-
163
- class ManagerMeta(type):
164
- def __new__(mcs, name, bases, attrs):
165
- cls = super().__new__(mcs, name, bases, attrs)
166
- assert (register := getattr(cls, "register"))
167
- for name, (fn, kwargs) in _manager_registry.items():
168
- register(name, callable=fn, **kwargs)
169
- return cls
170
-
171
-
172
- class Manager(BaseManager, metaclass=ManagerMeta):
173
- if TYPE_CHECKING:
174
- put = staticmethod(put)
175
- get = staticmethod(get)
176
- proxify = staticmethod(proxify)
177
- futures = staticmethod(futures)
178
- queue = staticmethod(queue)
179
- queue_lock = staticmethod(queue_lock)
180
- stopping = staticmethod(stopping)
181
- waiting = staticmethod(waiting)
wool/_mempool/__init__.py DELETED
@@ -1,4 +0,0 @@
1
- from wool._mempool._mempool import MemoryPool
2
- from wool._mempool._service import MemoryPoolService
3
-
4
- __all__ = ["MemoryPool", "MemoryPoolService"]
wool/_mempool/_client.py DELETED
@@ -1,167 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- from typing import Final
5
- from typing import Protocol
6
-
7
- try:
8
- from typing import Self
9
- except ImportError:
10
- from typing_extensions import Self
11
-
12
- import grpc
13
-
14
- try:
15
- from wool._protobuf.mempool import service_pb2 as pb
16
- from wool._protobuf.mempool import service_pb2_grpc as rpc
17
- except ImportError as e:
18
- from wool._protobuf import ProtobufImportError
19
-
20
- raise ProtobufImportError(e) from e
21
-
22
-
23
- class EventHandler(Protocol):
24
- @staticmethod
25
- def __call__(client: MemoryPoolClient, response: pb.Event) -> None: ...
26
-
27
-
28
- class MemoryPoolClient:
29
- _event_stream: Final[grpc.aio.UnaryStreamCall]
30
- _listener: asyncio.Task
31
- _session: pb.Session | None
32
-
33
- def __init__(self, channel: grpc.aio.Channel, event_handler: EventHandler):
34
- self._stub = rpc.MemoryPoolStub(channel)
35
- self._event_stream = self._stub.session(pb.SessionRequest())
36
- self._event_handler = event_handler
37
- self._session = None
38
-
39
- async def __aenter__(self) -> Self:
40
- if self._session:
41
- raise RuntimeError(
42
- "Client session may not be re-entered. "
43
- "Use 'async with MemoryPoolClient(...)' only once."
44
- )
45
- response = await self._event_stream.read()
46
- assert isinstance(response, pb.SessionResponse)
47
- self._session = response.session
48
- self._listener = asyncio.create_task(self._listen())
49
- return self
50
-
51
- async def __aexit__(self, *_):
52
- if not self._session:
53
- raise RuntimeError(
54
- "Client session has not been entered. "
55
- "Use 'async with MemoryPoolClient(...)'."
56
- )
57
- assert self._listener
58
- self._listener.cancel()
59
- try:
60
- await self._listener
61
- except asyncio.CancelledError:
62
- pass
63
-
64
- def __del__(self):
65
- try:
66
- self._event_stream.cancel()
67
- except Exception:
68
- pass
69
-
70
- @property
71
- def id(self) -> str:
72
- if not self._session:
73
- raise RuntimeError(
74
- "Client session has not been entered. "
75
- "Use 'async with MemoryPoolClient(...)'."
76
- )
77
- return self._session.id
78
-
79
- async def map(self, ref: str):
80
- if not self._session:
81
- raise RuntimeError(
82
- "Client session has not been entered. "
83
- "Use 'async with MemoryPoolClient(...)'."
84
- )
85
- request = pb.AcquireRequest(
86
- session=self._session,
87
- reference=pb.Reference(id=ref),
88
- )
89
- await self._stub.map(request)
90
-
91
- async def get(self, ref: str) -> bytes:
92
- if not self._session:
93
- raise RuntimeError(
94
- "Client session has not been entered. "
95
- "Use 'async with MemoryPoolClient(...)'."
96
- )
97
- request = pb.GetRequest(
98
- session=self._session,
99
- reference=pb.Reference(id=ref),
100
- )
101
- response: pb.GetResponse = await self._stub.get(request)
102
- return response.dump
103
-
104
- async def put(self, dump: bytes, *, mutable: bool = False) -> str:
105
- if not self._session:
106
- raise RuntimeError(
107
- "Client session has not been entered. "
108
- "Use 'async with MemoryPoolClient(...)'."
109
- )
110
- request = pb.PutRequest(
111
- session=self._session,
112
- dump=dump,
113
- mutable=mutable,
114
- )
115
- response: pb.PutResponse = await self._stub.put(request)
116
- return response.reference.id
117
-
118
- async def post(self, ref: str, dump: bytes) -> bool:
119
- if not self._session:
120
- raise RuntimeError(
121
- "Client session has not been entered. "
122
- "Use 'async with MemoryPoolClient(...)'."
123
- )
124
- request = pb.PostRequest(
125
- session=self._session,
126
- reference=pb.Reference(id=ref),
127
- dump=dump,
128
- )
129
- response: pb.PostResponse = await self._stub.post(request)
130
- return response.updated
131
-
132
- async def acquire(self, ref: str):
133
- if not self._session:
134
- raise RuntimeError(
135
- "Client session has not been entered. "
136
- "Use 'async with MemoryPoolClient(...)'."
137
- )
138
- request = pb.AcquireRequest(
139
- session=self._session,
140
- reference=pb.Reference(id=ref),
141
- )
142
- await self._stub.acquire(request)
143
-
144
- async def release(self, ref: str):
145
- if not self._session:
146
- raise RuntimeError(
147
- "Client session has not been entered. "
148
- "Use 'async with MemoryPoolClient(...)'."
149
- )
150
- request = pb.ReleaseRequest(
151
- session=self._session,
152
- reference=pb.Reference(id=ref),
153
- )
154
- await self._stub.release(request)
155
-
156
- async def _listen(self):
157
- assert self._event_stream
158
- try:
159
- while True:
160
- if (response := await self._event_stream.read()) is None:
161
- break
162
- assert isinstance(response, pb.SessionResponse), (
163
- f"Unexpected event type: {type(response)}"
164
- )
165
- self._event_handler(self, response.event)
166
- except asyncio.CancelledError:
167
- self._event_stream.cancel()
wool/_mempool/_mempool.py DELETED
@@ -1,311 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import asyncio
4
- import hashlib
5
- import os
6
- import pathlib
7
- import shutil
8
- from contextlib import asynccontextmanager
9
- from mmap import mmap
10
- from typing import BinaryIO
11
-
12
- try:
13
- from typing import Self
14
- except ImportError:
15
- from typing_extensions import Self
16
-
17
- import shortuuid
18
-
19
- from wool._mempool._metadata import MetadataMessage
20
-
21
-
22
- class SharedObject:
23
- _id: str
24
- _mempool: MemoryPool
25
- _file: BinaryIO
26
- _mmap: mmap
27
- _size: int
28
- _md5: bytes
29
-
30
- def __init__(self, id: str, *, mempool: MemoryPool):
31
- self._id = id
32
- self._mempool = mempool
33
- self._file = open(self._path / "dump", "r+b")
34
- self._mmap = mmap(self._file.fileno(), 0)
35
- self._size = self.metadata.size
36
- self._md5 = self.metadata.md5
37
-
38
- def __del__(self):
39
- try:
40
- self.close()
41
- except Exception:
42
- pass
43
-
44
- @property
45
- def id(self) -> str:
46
- return self._id
47
-
48
- @property
49
- def metadata(self) -> SharedObjectMetadata:
50
- return SharedObjectMetadata(self.id, mempool=self._mempool)
51
-
52
- @property
53
- def mmap(self) -> mmap:
54
- return self._mmap
55
-
56
- @property
57
- def _path(self) -> pathlib.Path:
58
- return pathlib.Path(self._mempool.path, self.id)
59
-
60
- def close(self):
61
- self.metadata.close()
62
- self._mmap.close()
63
- self._file.close()
64
-
65
- def refresh(self) -> Self:
66
- if self._size != self.metadata.size or self._md5 != self.metadata.md5:
67
- self._mmap.close()
68
- self._file.close()
69
- self._file = open(self._path / "dump", "r+b")
70
- self._mmap = mmap(self._file.fileno(), 0)
71
- self._size = self.metadata.size
72
- self._md5 = self.metadata.md5
73
- return self
74
-
75
-
76
- class SharedObjectMetadata:
77
- _id: str
78
- _mempool: MemoryPool
79
- _file: BinaryIO
80
- _mmap: mmap
81
- _instances: dict[str, SharedObjectMetadata] = {}
82
-
83
- def __new__(cls, id: str, *, mempool: MemoryPool):
84
- if id in cls._instances:
85
- return cls._instances[id]
86
- return super().__new__(cls)
87
-
88
- def __init__(self, id: str, mempool: MemoryPool):
89
- self._id = id
90
- self._mempool = mempool
91
- self._file = open(self._path / "meta", "r+b")
92
- self._mmap = mmap(self._file.fileno(), 0)
93
- self._instances[id] = self
94
-
95
- def __del__(self):
96
- try:
97
- self.close()
98
- except Exception:
99
- pass
100
-
101
- @property
102
- def id(self) -> str:
103
- return self._id
104
-
105
- @property
106
- def mutable(self) -> bool:
107
- return self._metadata.mutable
108
-
109
- @property
110
- def size(self) -> int:
111
- return self._metadata.size
112
-
113
- @property
114
- def md5(self) -> bytes:
115
- return self._metadata.md5
116
-
117
- @property
118
- def mmap(self) -> mmap:
119
- return self._mmap
120
-
121
- @property
122
- def _path(self) -> pathlib.Path:
123
- return pathlib.Path(self._mempool.path, self.id)
124
-
125
- @property
126
- def _metadata(self) -> MetadataMessage:
127
- return MetadataMessage.loads(bytes(self._mmap))
128
-
129
- def close(self):
130
- self._mmap.close()
131
- self._file.close()
132
- del self._instances[self.id]
133
-
134
-
135
- class MemoryPool:
136
- _objects: dict[str, SharedObject]
137
- _path: pathlib.Path
138
-
139
- def __init__(self, path: str | pathlib.Path = pathlib.Path(".mempool")):
140
- if isinstance(path, str):
141
- self._path = pathlib.Path(path)
142
- else:
143
- self._path = path
144
- self._lockdir = self._path / "locks"
145
- os.makedirs(self._lockdir, exist_ok=True)
146
- self._acquire(f"pid-{os.getpid()}")
147
- self._objects = dict()
148
-
149
- def __contains__(self, ref: str) -> bool:
150
- return ref in self._objects
151
-
152
- def __del__(self):
153
- self._release(f"pid-{os.getpid()}")
154
-
155
- @property
156
- def path(self) -> pathlib.Path:
157
- return self._path
158
-
159
- async def map(self, ref: str | None = None):
160
- if ref is not None and ref not in self._objects:
161
- if self._locked(f"delete-{ref}"):
162
- raise RuntimeError(
163
- f"Reference {ref} is currently locked for deletion"
164
- )
165
- async with self._reference_lock(ref):
166
- self._map(ref)
167
- else:
168
- for entry in os.scandir(self._path):
169
- if entry.is_dir() and (ref := entry.name) != "locks":
170
- if not self._locked(f"delete-{ref}"):
171
- async with self._reference_lock(ref):
172
- self._map(ref)
173
-
174
- async def put(
175
- self, dump: bytes, *, mutable: bool = False, ref: str | None = None
176
- ) -> str:
177
- ref = ref or str(shortuuid.uuid())
178
- async with self._reference_lock(ref):
179
- self._put(ref, dump, mutable=mutable, exist_ok=False)
180
- return ref
181
-
182
- async def post(self, ref: str, dump: bytes) -> bool:
183
- if self._locked(f"delete-{ref}"):
184
- raise RuntimeError(
185
- f"Reference {ref} is currently locked for deletion"
186
- )
187
- async with self._reference_lock(ref):
188
- if ref not in self._objects:
189
- self._map(ref)
190
- obj = self._objects[ref]
191
- if not obj.metadata.mutable:
192
- raise ValueError("Cannot modify an immutable reference")
193
- if (size := len(dump)) != obj.metadata.size:
194
- try:
195
- obj.mmap.resize(size)
196
- self._post(ref, obj, dump)
197
- except SystemError:
198
- self._put(ref, dump, mutable=True, exist_ok=True)
199
- return True
200
- elif hashlib.md5(dump).digest() != obj.metadata.md5:
201
- self._post(ref, obj, dump)
202
- return True
203
- else:
204
- return False
205
-
206
- async def get(self, ref: str) -> bytes:
207
- if self._locked(f"delete-{ref}"):
208
- raise RuntimeError(
209
- f"Reference {ref} is currently locked for deletion"
210
- )
211
- async with self._reference_lock(ref):
212
- if ref not in self._objects:
213
- self._map(ref)
214
- return bytes(self._objects[ref].refresh().mmap)
215
-
216
- async def delete(self, ref: str):
217
- async with self._delete_lock(ref):
218
- async with self._reference_lock(ref):
219
- if ref not in self._objects:
220
- self._map(ref)
221
- self._objects.pop(ref).close()
222
- try:
223
- shutil.rmtree(self.path / ref)
224
- except FileNotFoundError:
225
- pass
226
-
227
- def _put(
228
- self,
229
- ref: str,
230
- dump: bytes,
231
- *,
232
- mutable: bool = False,
233
- exist_ok: bool = False,
234
- ):
235
- metadata = MetadataMessage(
236
- ref=ref,
237
- mutable=mutable,
238
- size=len(dump),
239
- md5=hashlib.md5(dump).digest(),
240
- )
241
-
242
- refpath = pathlib.Path(self._path, f"{ref}")
243
- os.makedirs(refpath, exist_ok=exist_ok)
244
-
245
- with open(refpath / "meta", "wb") as metafile:
246
- metafile.write(metadata.dumps())
247
-
248
- with open(refpath / "dump", "wb") as dumpfile:
249
- dumpfile.write(dump)
250
-
251
- self._map(ref)
252
-
253
- def _post(self, ref: str, obj: SharedObject, dump: bytes):
254
- if not obj.metadata.mutable:
255
- raise ValueError("Cannot modify an immutable reference")
256
- metadata = MetadataMessage(
257
- ref=ref,
258
- mutable=True,
259
- size=len(dump),
260
- md5=hashlib.md5(dump).digest(),
261
- )
262
- obj.metadata.mmap[:] = metadata.dumps()
263
- obj.metadata.mmap.flush()
264
- obj.mmap.seek(0)
265
- obj.mmap.write(dump)
266
-
267
- def _map(self, ref: str):
268
- obj = self._objects.pop(ref, None)
269
- if obj:
270
- obj.close()
271
- self._objects[ref] = SharedObject(id=ref, mempool=self)
272
-
273
- def _lockpath(self, key: str) -> pathlib.Path:
274
- return pathlib.Path(self._lockdir, f"{key}.lock")
275
-
276
- def _acquire(self, key: str) -> bool:
277
- try:
278
- os.symlink(f"{key}", self._lockpath(key))
279
- return True
280
- except FileExistsError:
281
- return False
282
-
283
- def _release(self, key: str):
284
- try:
285
- if os.path.islink(lock_path := self._lockpath(key)):
286
- os.unlink(lock_path)
287
- except FileNotFoundError:
288
- pass
289
-
290
- def _locked(self, key: str) -> bool:
291
- return os.path.islink(self._lockpath(key))
292
-
293
- @asynccontextmanager
294
- async def _reference_lock(self, ref: str):
295
- try:
296
- while not self._acquire(ref):
297
- await asyncio.sleep(0)
298
- yield
299
- finally:
300
- self._release(ref)
301
-
302
- @asynccontextmanager
303
- async def _delete_lock(self, ref: str):
304
- key = f"delete-{ref}"
305
- if not self._acquire(f"delete-{ref}"):
306
- raise RuntimeError(
307
- f"Reference {ref} is currently locked for deletion"
308
- )
309
- else:
310
- yield
311
- self._release(key)
@@ -1,35 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import asdict
4
- from dataclasses import dataclass
5
-
6
- try:
7
- from wool._protobuf.mempool import metadata_pb2 as pb
8
- except ImportError as e:
9
- from wool._protobuf import ProtobufImportError
10
-
11
- raise ProtobufImportError(e) from e
12
-
13
-
14
- @dataclass
15
- class MetadataMessage:
16
- ref: str
17
- mutable: bool
18
- size: int
19
- md5: bytes
20
-
21
- @classmethod
22
- def loads(cls, data: bytes) -> MetadataMessage:
23
- (metadata := pb.MetadataMessage()).ParseFromString(data)
24
- return cls(
25
- ref=metadata.ref,
26
- mutable=metadata.mutable,
27
- size=metadata.size,
28
- md5=metadata.md5,
29
- )
30
-
31
- def dumps(self) -> bytes:
32
- return pb.MetadataMessage(**asdict(self)).SerializeToString()
33
-
34
-
35
- __all__ = ["MetadataMessage"]