wool 0.1rc7__tar.gz → 0.1rc9__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.

Potentially problematic release.


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

Files changed (32) hide show
  1. {wool-0.1rc7 → wool-0.1rc9}/PKG-INFO +8 -2
  2. {wool-0.1rc7 → wool-0.1rc9}/pyproject.toml +25 -5
  3. wool-0.1rc9/wool/_mempool/__init__.py +4 -0
  4. wool-0.1rc9/wool/_mempool/_client.py +167 -0
  5. wool-0.1rc9/wool/_mempool/_mempool.py +311 -0
  6. wool-0.1rc7/wool/_mempool/_metadata/__init__.py → wool-0.1rc9/wool/_mempool/_metadata.py +8 -14
  7. wool-0.1rc9/wool/_mempool/_service.py +227 -0
  8. wool-0.1rc9/wool/_protobuf/__init__.py +11 -0
  9. wool-0.1rc7/wool/_protobuf/_mempool/_metadata/_metadata_pb2.py → wool-0.1rc9/wool/_protobuf/mempool/metadata_pb2.py +8 -8
  10. wool-0.1rc7/wool/_protobuf/_mempool/_metadata/_metadata_pb2.pyi → wool-0.1rc9/wool/_protobuf/mempool/metadata_pb2.pyi +1 -1
  11. wool-0.1rc9/wool/_protobuf/mempool/metadata_pb2_grpc.py +24 -0
  12. wool-0.1rc9/wool/_protobuf/mempool/service_pb2.py +66 -0
  13. wool-0.1rc9/wool/_protobuf/mempool/service_pb2.pyi +108 -0
  14. wool-0.1rc9/wool/_protobuf/mempool/service_pb2_grpc.py +355 -0
  15. wool-0.1rc7/wool/_mempool/__init__.py +0 -3
  16. wool-0.1rc7/wool/_mempool/_mempool.py +0 -204
  17. wool-0.1rc7/wool/_protobuf/.gitkeep +0 -0
  18. {wool-0.1rc7 → wool-0.1rc9}/.gitignore +0 -0
  19. {wool-0.1rc7 → wool-0.1rc9}/README.md +0 -0
  20. {wool-0.1rc7 → wool-0.1rc9}/wool/__init__.py +0 -0
  21. {wool-0.1rc7 → wool-0.1rc9}/wool/_cli.py +0 -0
  22. {wool-0.1rc7 → wool-0.1rc9}/wool/_event.py +0 -0
  23. {wool-0.1rc7 → wool-0.1rc9}/wool/_future.py +0 -0
  24. {wool-0.1rc7 → wool-0.1rc9}/wool/_logging.py +0 -0
  25. {wool-0.1rc7 → wool-0.1rc9}/wool/_manager.py +0 -0
  26. {wool-0.1rc7 → wool-0.1rc9}/wool/_pool.py +0 -0
  27. {wool-0.1rc7 → wool-0.1rc9}/wool/_queue.py +0 -0
  28. {wool-0.1rc7 → wool-0.1rc9}/wool/_session.py +0 -0
  29. {wool-0.1rc7 → wool-0.1rc9}/wool/_task.py +0 -0
  30. {wool-0.1rc7 → wool-0.1rc9}/wool/_typing.py +0 -0
  31. {wool-0.1rc7 → wool-0.1rc9}/wool/_utils.py +0 -0
  32. {wool-0.1rc7 → wool-0.1rc9}/wool/_worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wool
3
- Version: 0.1rc7
3
+ Version: 0.1rc9
4
4
  Summary: A Python framework for distributed multiprocessing.
5
5
  Author-email: Conrad Bzura <conrad@wool.io>
6
6
  Maintainer-email: maintainers@wool.io
@@ -206,19 +206,25 @@ License: Apache License
206
206
  See the License for the specific language governing permissions and
207
207
  limitations under the License.
208
208
  Classifier: Intended Audience :: Developers
209
+ Classifier: Operating System :: MacOS :: MacOS X
210
+ Classifier: Operating System :: POSIX :: Linux
209
211
  Requires-Python: >=3.10
210
212
  Requires-Dist: annotated-types
211
213
  Requires-Dist: click
212
214
  Requires-Dist: debugpy
215
+ Requires-Dist: grpcio
213
216
  Requires-Dist: protobuf
214
217
  Requires-Dist: shortuuid
215
218
  Requires-Dist: tblib
219
+ Requires-Dist: typing-extensions
216
220
  Provides-Extra: dev
217
221
  Requires-Dist: pytest; extra == 'dev'
218
222
  Requires-Dist: pytest-asyncio; extra == 'dev'
223
+ Requires-Dist: pytest-grpc-aio~=0.2.0; extra == 'dev'
224
+ Requires-Dist: pytest-mock; extra == 'dev'
219
225
  Requires-Dist: ruff; extra == 'dev'
220
226
  Provides-Extra: locking
221
- Requires-Dist: wool-locking==0.1rc7; extra == 'locking'
227
+ Requires-Dist: wool-locking==0.1rc9; extra == 'locking'
222
228
  Description-Content-Type: text/markdown
223
229
 
224
230
  # Wool
@@ -1,11 +1,25 @@
1
1
  [build-system]
2
+
2
3
  build-backend = "hatchling.build"
3
4
  requires = ["debugpy", "hatchling", "packaging", "GitPython", "toml"]
4
5
 
5
6
  [project]
6
7
  authors = [{ name = "Conrad Bzura", email = "conrad@wool.io" }]
7
- classifiers = ["Intended Audience :: Developers"]
8
- dependencies = ["annotated-types", "click", "debugpy", "protobuf", "shortuuid", "tblib"]
8
+ classifiers = [
9
+ "Intended Audience :: Developers",
10
+ "Operating System :: MacOS :: MacOS X",
11
+ "Operating System :: POSIX :: Linux",
12
+ ]
13
+ dependencies = [
14
+ "annotated-types",
15
+ "click",
16
+ "debugpy",
17
+ "grpcio",
18
+ "protobuf",
19
+ "shortuuid",
20
+ "tblib",
21
+ "typing-extensions",
22
+ ]
9
23
  description = "A Python framework for distributed multiprocessing."
10
24
  dynamic = ["version"]
11
25
  license = { file = "../LICENSE" }
@@ -15,7 +29,13 @@ readme = "README.md"
15
29
  requires-python = ">=3.10"
16
30
 
17
31
  [project.optional-dependencies]
18
- dev = ["pytest", "pytest-asyncio", "ruff"]
32
+ dev = [
33
+ "pytest",
34
+ "pytest-asyncio",
35
+ "pytest-grpc-aio~=0.2.0",
36
+ "pytest-mock",
37
+ "ruff",
38
+ ]
19
39
  locking = ["wool-locking"]
20
40
 
21
41
  [project.scripts]
@@ -23,7 +43,7 @@ wool = "wool._cli:cli"
23
43
 
24
44
  [tool.hatch.build.hooks.protobuf]
25
45
  dependencies = ["hatch-protobuf"]
26
- generate_grpc = false
46
+ generate_grpc = true
27
47
  generate_pyi = true
28
48
  output_path = "src/wool/_protobuf"
29
49
  proto_paths = ["protobuf"]
@@ -55,7 +75,7 @@ select = ["E", "F", "I"]
55
75
  quote-style = "double"
56
76
  docstring-code-format = true
57
77
 
58
- [tool.ruff.lint.isort]
78
+ [tool.ruff.lint.isort]
59
79
  combine-as-imports = false
60
80
  force-single-line = true
61
81
  known-first-party = ["wool"]
@@ -0,0 +1,4 @@
1
+ from wool._mempool._mempool import MemoryPool
2
+ from wool._mempool._service import MemoryPoolService
3
+
4
+ __all__ = ["MemoryPool", "MemoryPoolService"]
@@ -0,0 +1,167 @@
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()
@@ -0,0 +1,311 @@
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,18 +1,14 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
3
+ from dataclasses import asdict
4
4
  from dataclasses import dataclass
5
5
 
6
6
  try:
7
- from wool._protobuf._mempool._metadata._metadata_pb2 import (
8
- _MetadataMessage,
9
- )
10
- except ImportError:
11
- logging.error(
12
- "Failed to import _MetadataMessage. "
13
- "Ensure protocol buffers are compiled."
14
- )
15
- raise
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
16
12
 
17
13
 
18
14
  @dataclass
@@ -24,7 +20,7 @@ class MetadataMessage:
24
20
 
25
21
  @classmethod
26
22
  def loads(cls, data: bytes) -> MetadataMessage:
27
- (metadata := _MetadataMessage()).ParseFromString(data)
23
+ (metadata := pb.MetadataMessage()).ParseFromString(data)
28
24
  return cls(
29
25
  ref=metadata.ref,
30
26
  mutable=metadata.mutable,
@@ -33,9 +29,7 @@ class MetadataMessage:
33
29
  )
34
30
 
35
31
  def dumps(self) -> bytes:
36
- return _MetadataMessage(
37
- ref=self.ref, mutable=self.mutable, size=self.size, md5=self.md5
38
- ).SerializeToString()
32
+ return pb.MetadataMessage(**asdict(self)).SerializeToString()
39
33
 
40
34
 
41
35
  __all__ = ["MetadataMessage"]