xoscar 0.4.0__cp312-cp312-macosx_11_0_arm64.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 xoscar might be problematic. Click here for more details.

Files changed (82) hide show
  1. xoscar/__init__.py +60 -0
  2. xoscar/_utils.cpython-312-darwin.so +0 -0
  3. xoscar/_utils.pxd +36 -0
  4. xoscar/_utils.pyx +241 -0
  5. xoscar/_version.py +693 -0
  6. xoscar/aio/__init__.py +16 -0
  7. xoscar/aio/base.py +86 -0
  8. xoscar/aio/file.py +59 -0
  9. xoscar/aio/lru.py +228 -0
  10. xoscar/aio/parallelism.py +39 -0
  11. xoscar/api.py +493 -0
  12. xoscar/backend.py +67 -0
  13. xoscar/backends/__init__.py +14 -0
  14. xoscar/backends/allocate_strategy.py +160 -0
  15. xoscar/backends/communication/__init__.py +30 -0
  16. xoscar/backends/communication/base.py +315 -0
  17. xoscar/backends/communication/core.py +69 -0
  18. xoscar/backends/communication/dummy.py +242 -0
  19. xoscar/backends/communication/errors.py +20 -0
  20. xoscar/backends/communication/socket.py +414 -0
  21. xoscar/backends/communication/ucx.py +531 -0
  22. xoscar/backends/communication/utils.py +97 -0
  23. xoscar/backends/config.py +145 -0
  24. xoscar/backends/context.py +404 -0
  25. xoscar/backends/core.py +193 -0
  26. xoscar/backends/indigen/__init__.py +16 -0
  27. xoscar/backends/indigen/backend.py +51 -0
  28. xoscar/backends/indigen/driver.py +26 -0
  29. xoscar/backends/indigen/pool.py +469 -0
  30. xoscar/backends/message.cpython-312-darwin.so +0 -0
  31. xoscar/backends/message.pyi +239 -0
  32. xoscar/backends/message.pyx +599 -0
  33. xoscar/backends/pool.py +1596 -0
  34. xoscar/backends/router.py +207 -0
  35. xoscar/backends/test/__init__.py +16 -0
  36. xoscar/backends/test/backend.py +38 -0
  37. xoscar/backends/test/pool.py +208 -0
  38. xoscar/batch.py +256 -0
  39. xoscar/collective/__init__.py +27 -0
  40. xoscar/collective/common.py +102 -0
  41. xoscar/collective/core.py +737 -0
  42. xoscar/collective/process_group.py +687 -0
  43. xoscar/collective/utils.py +41 -0
  44. xoscar/collective/xoscar_pygloo.cpython-312-darwin.so +0 -0
  45. xoscar/collective/xoscar_pygloo.pyi +239 -0
  46. xoscar/constants.py +21 -0
  47. xoscar/context.cpython-312-darwin.so +0 -0
  48. xoscar/context.pxd +21 -0
  49. xoscar/context.pyx +368 -0
  50. xoscar/core.cpython-312-darwin.so +0 -0
  51. xoscar/core.pxd +50 -0
  52. xoscar/core.pyx +658 -0
  53. xoscar/debug.py +188 -0
  54. xoscar/driver.py +42 -0
  55. xoscar/errors.py +63 -0
  56. xoscar/libcpp.pxd +31 -0
  57. xoscar/metrics/__init__.py +21 -0
  58. xoscar/metrics/api.py +288 -0
  59. xoscar/metrics/backends/__init__.py +13 -0
  60. xoscar/metrics/backends/console/__init__.py +13 -0
  61. xoscar/metrics/backends/console/console_metric.py +82 -0
  62. xoscar/metrics/backends/metric.py +149 -0
  63. xoscar/metrics/backends/prometheus/__init__.py +13 -0
  64. xoscar/metrics/backends/prometheus/prometheus_metric.py +70 -0
  65. xoscar/nvutils.py +717 -0
  66. xoscar/profiling.py +260 -0
  67. xoscar/serialization/__init__.py +20 -0
  68. xoscar/serialization/aio.py +138 -0
  69. xoscar/serialization/core.cpython-312-darwin.so +0 -0
  70. xoscar/serialization/core.pxd +28 -0
  71. xoscar/serialization/core.pyi +57 -0
  72. xoscar/serialization/core.pyx +944 -0
  73. xoscar/serialization/cuda.py +111 -0
  74. xoscar/serialization/exception.py +48 -0
  75. xoscar/serialization/numpy.py +82 -0
  76. xoscar/serialization/pyfury.py +37 -0
  77. xoscar/serialization/scipy.py +72 -0
  78. xoscar/utils.py +517 -0
  79. xoscar-0.4.0.dist-info/METADATA +223 -0
  80. xoscar-0.4.0.dist-info/RECORD +82 -0
  81. xoscar-0.4.0.dist-info/WHEEL +5 -0
  82. xoscar-0.4.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,145 @@
1
+ # Copyright 2022-2023 XProbe Inc.
2
+ # derived from copyright 1999-2021 Alibaba Group Holding Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+
20
+
21
+ class ActorPoolConfig:
22
+ __slots__ = ("_conf",)
23
+
24
+ def __init__(self, conf: dict | None = None):
25
+ if conf is None:
26
+ conf = dict()
27
+ self._conf = conf
28
+ if "pools" not in self._conf:
29
+ self._conf["pools"] = dict()
30
+ if "mapping" not in self._conf:
31
+ self._conf["mapping"] = dict()
32
+ if "metrics" not in self._conf:
33
+ self._conf["metrics"] = dict()
34
+ if "comm" not in self._conf:
35
+ self._conf["comm"] = dict()
36
+
37
+ @property
38
+ def n_pool(self):
39
+ return len(self._conf["pools"])
40
+
41
+ def add_pool_conf(
42
+ self,
43
+ process_index: int,
44
+ label: str | None,
45
+ internal_address: str | None,
46
+ external_address: str | list[str],
47
+ env: dict | None = None,
48
+ modules: list[str] | None = None,
49
+ suspend_sigint: bool | None = False,
50
+ use_uvloop: bool | None = False,
51
+ logging_conf: dict | None = None,
52
+ kwargs: dict | None = None,
53
+ ):
54
+ pools: dict = self._conf["pools"]
55
+ if not isinstance(external_address, list):
56
+ external_address = [external_address]
57
+ pools[process_index] = {
58
+ "label": label,
59
+ "internal_address": internal_address,
60
+ "external_address": external_address,
61
+ "env": env,
62
+ "modules": modules,
63
+ "suspend_sigint": suspend_sigint,
64
+ "use_uvloop": use_uvloop,
65
+ "logging_conf": logging_conf,
66
+ "kwargs": kwargs or {},
67
+ }
68
+
69
+ mapping: dict = self._conf["mapping"]
70
+ for addr in external_address:
71
+ mapping[addr] = internal_address
72
+
73
+ def remove_pool_config(self, process_index: int):
74
+ addr = self.get_external_address(process_index)
75
+ del self._conf["pools"][process_index]
76
+ del self._conf["mapping"][addr]
77
+
78
+ def get_pool_config(self, process_index: int):
79
+ return self._conf["pools"][process_index]
80
+
81
+ def get_external_address(self, process_index: int) -> str:
82
+ return self._conf["pools"][process_index]["external_address"][0]
83
+
84
+ def get_process_indexes(self):
85
+ return list(self._conf["pools"])
86
+
87
+ def get_process_index(self, external_address: str):
88
+ for process_index, conf in self._conf["pools"].items():
89
+ if external_address in conf["external_address"]:
90
+ return process_index
91
+ raise ValueError(
92
+ f"Cannot get process_index for {external_address}"
93
+ ) # pragma: no cover
94
+
95
+ def reset_pool_external_address(
96
+ self,
97
+ process_index: int,
98
+ external_address: str | list[str],
99
+ ):
100
+ if not isinstance(external_address, list):
101
+ external_address = [external_address]
102
+ cur_pool_config = self._conf["pools"][process_index]
103
+ internal_address = cur_pool_config["internal_address"]
104
+
105
+ mapping: dict = self._conf["mapping"]
106
+ for addr in cur_pool_config["external_address"]:
107
+ if internal_address == addr:
108
+ # internal address may be the same as external address in Windows
109
+ internal_address = external_address[0]
110
+ mapping.pop(addr, None)
111
+
112
+ cur_pool_config["external_address"] = external_address
113
+ for addr in external_address:
114
+ mapping[addr] = internal_address
115
+
116
+ def get_external_addresses(self, label=None) -> list[str]:
117
+ result = []
118
+ for c in self._conf["pools"].values():
119
+ if label is not None:
120
+ if label == c["label"]:
121
+ result.append(c["external_address"][0])
122
+ else:
123
+ result.append(c["external_address"][0])
124
+ return result
125
+
126
+ @property
127
+ def external_to_internal_address_map(self) -> dict[str, str]:
128
+ return self._conf["mapping"]
129
+
130
+ def as_dict(self):
131
+ return self._conf
132
+
133
+ def add_metric_configs(self, metrics: dict[str, Any]):
134
+ if metrics:
135
+ self._conf["metrics"].update(metrics)
136
+
137
+ def get_metric_configs(self):
138
+ return self._conf["metrics"]
139
+
140
+ def add_comm_config(self, comm_config: dict[str, Any] | None):
141
+ if comm_config:
142
+ self._conf["comm"].update(comm_config)
143
+
144
+ def get_comm_config(self) -> dict:
145
+ return self._conf["comm"]
@@ -0,0 +1,404 @@
1
+ # Copyright 2022-2023 XProbe Inc.
2
+ # derived from copyright 1999-2021 Alibaba Group Holding Ltd.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ from dataclasses import dataclass
20
+ from typing import Any, List, Optional, Tuple, Type, Union
21
+
22
+ from .._utils import create_actor_ref, to_binary
23
+ from ..aio import AioFileObject
24
+ from ..api import Actor
25
+ from ..context import BaseActorContext
26
+ from ..core import ActorRef, BufferRef, FileObjectRef, create_local_actor_ref
27
+ from ..debug import debug_async_timeout, detect_cycle_send
28
+ from ..errors import CannotCancelTask
29
+ from ..utils import dataslots, fix_all_zero_ip
30
+ from .allocate_strategy import AddressSpecified, AllocateStrategy
31
+ from .communication import Client, DummyClient, UCXClient
32
+ from .core import ActorCaller
33
+ from .message import (
34
+ DEFAULT_PROTOCOL,
35
+ ActorRefMessage,
36
+ CancelMessage,
37
+ ControlMessage,
38
+ ControlMessageType,
39
+ CopyToBuffersMessage,
40
+ CopyToFileObjectsMessage,
41
+ CreateActorMessage,
42
+ DestroyActorMessage,
43
+ ErrorMessage,
44
+ HasActorMessage,
45
+ ResultMessage,
46
+ SendMessage,
47
+ _MessageBase,
48
+ new_message_id,
49
+ )
50
+ from .router import Router
51
+
52
+ DEFAULT_TRANSFER_BLOCK_SIZE = 4 * 1024**2
53
+
54
+
55
+ @dataslots
56
+ @dataclass
57
+ class ProfilingContext:
58
+ task_id: str
59
+
60
+
61
+ class IndigenActorContext(BaseActorContext):
62
+ __slots__ = ("_caller", "_lock")
63
+
64
+ support_allocate_strategy = True
65
+
66
+ def __init__(self, address: str | None = None):
67
+ BaseActorContext.__init__(self, address)
68
+ self._caller = ActorCaller()
69
+ self._lock = asyncio.Lock()
70
+
71
+ def __del__(self):
72
+ self._caller.cancel_tasks()
73
+
74
+ async def _call(
75
+ self, address: str, message: _MessageBase, wait: bool = True
76
+ ) -> Union[ResultMessage, ErrorMessage, asyncio.Future]:
77
+ return await self._caller.call(
78
+ Router.get_instance_or_empty(), address, message, wait=wait
79
+ )
80
+
81
+ async def _call_with_client(
82
+ self, client: Client, message: _MessageBase, wait: bool = True
83
+ ) -> Union[ResultMessage, ErrorMessage, asyncio.Future]:
84
+ return await self._caller.call_with_client(client, message, wait)
85
+
86
+ async def _call_send_buffers(
87
+ self,
88
+ client: UCXClient,
89
+ local_buffers: list,
90
+ meta_message: _MessageBase,
91
+ wait: bool = True,
92
+ ) -> Union[ResultMessage, ErrorMessage, asyncio.Future]:
93
+ return await self._caller.call_send_buffers(
94
+ client, local_buffers, meta_message, wait
95
+ )
96
+
97
+ @staticmethod
98
+ def _process_result_message(message: Union[ResultMessage, ErrorMessage]):
99
+ if isinstance(message, ResultMessage):
100
+ return message.result
101
+ else:
102
+ raise message.as_instanceof_cause()
103
+
104
+ async def _wait(self, future: asyncio.Future, address: str, message: _MessageBase):
105
+ try:
106
+ await asyncio.shield(future)
107
+ except asyncio.CancelledError:
108
+ try:
109
+ await self.cancel(address, message.message_id)
110
+ except CannotCancelTask:
111
+ # cancel failed, already finished
112
+ raise asyncio.CancelledError
113
+ except: # noqa: E722 # nosec # pylint: disable=bare-except
114
+ pass
115
+ return await future
116
+
117
+ async def create_actor(
118
+ self,
119
+ actor_cls: Type[Actor],
120
+ *args,
121
+ uid=None,
122
+ address: str | None = None,
123
+ **kwargs,
124
+ ) -> ActorRef:
125
+ router = Router.get_instance_or_empty()
126
+ address = address or self._address or router.external_address
127
+ allocate_strategy = kwargs.get("allocate_strategy", None)
128
+ if isinstance(allocate_strategy, AllocateStrategy):
129
+ allocate_strategy = kwargs.pop("allocate_strategy")
130
+ else:
131
+ allocate_strategy = AddressSpecified(address)
132
+ create_actor_message = CreateActorMessage(
133
+ new_message_id(),
134
+ actor_cls,
135
+ to_binary(uid),
136
+ args,
137
+ kwargs,
138
+ allocate_strategy,
139
+ protocol=DEFAULT_PROTOCOL,
140
+ )
141
+ future = await self._call(address, create_actor_message, wait=False)
142
+ result = await self._wait(future, address, create_actor_message) # type: ignore
143
+ return self._process_result_message(result)
144
+
145
+ async def has_actor(self, actor_ref: ActorRef) -> bool:
146
+ message = HasActorMessage(
147
+ new_message_id(), actor_ref, protocol=DEFAULT_PROTOCOL
148
+ )
149
+ future = await self._call(actor_ref.address, message, wait=False)
150
+ result = await self._wait(future, actor_ref.address, message) # type: ignore
151
+ return self._process_result_message(result)
152
+
153
+ async def destroy_actor(self, actor_ref: ActorRef):
154
+ message = DestroyActorMessage(
155
+ new_message_id(), actor_ref, protocol=DEFAULT_PROTOCOL
156
+ )
157
+ future = await self._call(actor_ref.address, message, wait=False)
158
+ result = await self._wait(future, actor_ref.address, message) # type: ignore
159
+ return self._process_result_message(result)
160
+
161
+ async def kill_actor(self, actor_ref: ActorRef, force: bool = True):
162
+ # get main_pool_address
163
+ control_message = ControlMessage(
164
+ new_message_id(),
165
+ actor_ref.address,
166
+ ControlMessageType.get_config,
167
+ "main_pool_address",
168
+ protocol=DEFAULT_PROTOCOL,
169
+ )
170
+ main_address = self._process_result_message(
171
+ await self._call(actor_ref.address, control_message) # type: ignore
172
+ )
173
+ real_actor_ref = await self.actor_ref(actor_ref)
174
+ if real_actor_ref.address == main_address:
175
+ raise ValueError("Cannot kill actor on main pool")
176
+ stop_message = ControlMessage(
177
+ new_message_id(),
178
+ real_actor_ref.address,
179
+ ControlMessageType.stop,
180
+ # default timeout (3 secs) and force
181
+ (3.0, force),
182
+ protocol=DEFAULT_PROTOCOL,
183
+ )
184
+ # stop server
185
+ result = await self._call(main_address, stop_message)
186
+ return self._process_result_message(result) # type: ignore
187
+
188
+ async def actor_ref(self, *args, **kwargs):
189
+ actor_ref = create_actor_ref(*args, **kwargs)
190
+ connect_addr = actor_ref.address
191
+ local_actor_ref = create_local_actor_ref(actor_ref.address, actor_ref.uid)
192
+ if local_actor_ref is not None:
193
+ return local_actor_ref
194
+ message = ActorRefMessage(
195
+ new_message_id(), actor_ref, protocol=DEFAULT_PROTOCOL
196
+ )
197
+ future = await self._call(actor_ref.address, message, wait=False)
198
+ result = await self._wait(future, actor_ref.address, message)
199
+ res = self._process_result_message(result)
200
+ if res.address != connect_addr:
201
+ res.address = fix_all_zero_ip(res.address, connect_addr)
202
+ return res
203
+
204
+ async def send(
205
+ self,
206
+ actor_ref: ActorRef,
207
+ message: Tuple,
208
+ wait_response: bool = True,
209
+ profiling_context: ProfilingContext | None = None,
210
+ ):
211
+ send_message = SendMessage(
212
+ new_message_id(),
213
+ actor_ref,
214
+ message,
215
+ protocol=DEFAULT_PROTOCOL,
216
+ profiling_context=profiling_context,
217
+ )
218
+
219
+ # use `%.500` to avoid print too long messages
220
+ with debug_async_timeout(
221
+ "actor_call_timeout",
222
+ "Calling %.500r on %s at %s timed out",
223
+ send_message.content,
224
+ actor_ref.uid,
225
+ actor_ref.address,
226
+ ):
227
+ detect_cycle_send(send_message, wait_response)
228
+ future = await self._call(actor_ref.address, send_message, wait=False)
229
+ if wait_response:
230
+ result = await self._wait(future, actor_ref.address, send_message) # type: ignore
231
+ return self._process_result_message(result)
232
+ else:
233
+ return future
234
+
235
+ async def cancel(self, address: str, cancel_message_id: bytes):
236
+ message = CancelMessage(
237
+ new_message_id(), address, cancel_message_id, protocol=DEFAULT_PROTOCOL
238
+ )
239
+ result = await self._call(address, message)
240
+ return self._process_result_message(result) # type: ignore
241
+
242
+ async def wait_actor_pool_recovered(
243
+ self, address: str, main_address: str | None = None
244
+ ):
245
+ if main_address is None:
246
+ # get main_pool_address
247
+ control_message = ControlMessage(
248
+ new_message_id(),
249
+ address,
250
+ ControlMessageType.get_config,
251
+ "main_pool_address",
252
+ protocol=DEFAULT_PROTOCOL,
253
+ )
254
+ main_address = self._process_result_message(
255
+ await self._call(address, control_message) # type: ignore
256
+ )
257
+
258
+ # if address is main pool, it is never recovered
259
+ if address == main_address:
260
+ return
261
+
262
+ control_message = ControlMessage(
263
+ new_message_id(),
264
+ address,
265
+ ControlMessageType.wait_pool_recovered,
266
+ None,
267
+ protocol=DEFAULT_PROTOCOL,
268
+ )
269
+ self._process_result_message(await self._call(main_address, control_message)) # type: ignore
270
+
271
+ async def get_pool_config(self, address: str):
272
+ control_message = ControlMessage(
273
+ new_message_id(),
274
+ address,
275
+ ControlMessageType.get_config,
276
+ None,
277
+ protocol=DEFAULT_PROTOCOL,
278
+ )
279
+ return self._process_result_message(await self._call(address, control_message)) # type: ignore
280
+
281
+ @staticmethod
282
+ def _gen_switch_to_copy_to_control_message(content: Any):
283
+ return ControlMessage(
284
+ message_id=new_message_id(),
285
+ control_message_type=ControlMessageType.switch_to_copy_to,
286
+ content=content,
287
+ )
288
+
289
+ @staticmethod
290
+ def _gen_copy_to_buffers_message(content: Any):
291
+ return CopyToBuffersMessage(message_id=new_message_id(), content=content) # type: ignore
292
+
293
+ @staticmethod
294
+ def _gen_copy_to_fileobjs_message(content: Any):
295
+ return CopyToFileObjectsMessage(message_id=new_message_id(), content=content) # type: ignore
296
+
297
+ async def _get_copy_to_client(self, router, address) -> Client:
298
+ client = await self._caller.get_client(router, address)
299
+ if isinstance(client, DummyClient) or hasattr(client, "send_buffers"):
300
+ return client
301
+ client_types = router.get_all_client_types(address)
302
+ # For inter-process communication, the ``self._caller.get_client`` interface would not look for UCX Client,
303
+ # we still try to find UCXClient for this case.
304
+ try:
305
+ client_type = next(
306
+ client_type
307
+ for client_type in client_types
308
+ if hasattr(client_type, "send_buffers")
309
+ )
310
+ except StopIteration:
311
+ return client
312
+ else:
313
+ return await self._caller.get_client_via_type(router, address, client_type)
314
+
315
+ async def _get_client(self, address: str) -> Client:
316
+ router = Router.get_instance()
317
+ assert router is not None, "`copy_to` can only be used inside pools"
318
+ return await self._get_copy_to_client(router, address)
319
+
320
+ async def copy_to_buffers(
321
+ self,
322
+ local_buffers: list,
323
+ remote_buffer_refs: List[BufferRef],
324
+ block_size: Optional[int] = None,
325
+ ):
326
+ address = remote_buffer_refs[0].address
327
+ client = await self._get_client(address)
328
+ if isinstance(client, UCXClient):
329
+ message = [(buf.address, buf.uid) for buf in remote_buffer_refs]
330
+ await self._call_send_buffers(
331
+ client,
332
+ local_buffers,
333
+ self._gen_switch_to_copy_to_control_message(message),
334
+ )
335
+ else:
336
+ # ``local_buffers`` will be divided into buffers of the specified block size for transmission.
337
+ # Smaller buffers will be accumulated and sent together,
338
+ # while larger buffers will be divided and sent.
339
+ current_buf_size = 0
340
+ one_block_data = []
341
+ block_size = block_size or DEFAULT_TRANSFER_BLOCK_SIZE
342
+ for i, (l_buf, r_buf) in enumerate(zip(local_buffers, remote_buffer_refs)):
343
+ if current_buf_size + len(l_buf) < block_size:
344
+ one_block_data.append(
345
+ (r_buf.address, r_buf.uid, 0, len(l_buf), l_buf)
346
+ )
347
+ current_buf_size += len(l_buf)
348
+ continue
349
+ last_start = 0
350
+ while current_buf_size + len(l_buf) > block_size:
351
+ remain = block_size - current_buf_size
352
+ one_block_data.append(
353
+ (r_buf.address, r_buf.uid, last_start, remain, l_buf[:remain])
354
+ )
355
+ await self._call_with_client(
356
+ client, self._gen_copy_to_buffers_message(one_block_data)
357
+ )
358
+ one_block_data = []
359
+ current_buf_size = 0
360
+ last_start += remain
361
+ l_buf = l_buf[remain:]
362
+
363
+ if len(l_buf) > 0:
364
+ one_block_data.append(
365
+ (r_buf.address, r_buf.uid, last_start, len(l_buf), l_buf)
366
+ )
367
+ current_buf_size = len(l_buf)
368
+
369
+ if one_block_data:
370
+ await self._call_with_client(
371
+ client, self._gen_copy_to_buffers_message(one_block_data)
372
+ )
373
+
374
+ async def copy_to_fileobjs(
375
+ self,
376
+ local_fileobjs: List[AioFileObject],
377
+ remote_fileobj_refs: List[FileObjectRef],
378
+ block_size: Optional[int] = None,
379
+ ):
380
+ address = remote_fileobj_refs[0].address
381
+ client = await self._get_client(address)
382
+ block_size = block_size or DEFAULT_TRANSFER_BLOCK_SIZE
383
+ one_block_data = []
384
+ current_file_size = 0
385
+ for file_obj, remote_ref in zip(local_fileobjs, remote_fileobj_refs):
386
+ while True:
387
+ file_data = await file_obj.read(block_size) # type: ignore
388
+ if file_data:
389
+ one_block_data.append(
390
+ (remote_ref.address, remote_ref.uid, file_data)
391
+ )
392
+ current_file_size += len(file_data)
393
+ if current_file_size >= block_size:
394
+ message = self._gen_copy_to_fileobjs_message(one_block_data)
395
+ await self._call_with_client(client, message)
396
+ one_block_data.clear()
397
+ current_file_size = 0
398
+ else:
399
+ break
400
+
401
+ if current_file_size > 0:
402
+ message = self._gen_copy_to_fileobjs_message(one_block_data)
403
+ await self._call_with_client(client, message)
404
+ one_block_data.clear()