xoscar 0.4.6__cp39-cp39-macosx_10_9_x86_64.whl → 0.6.0__cp39-cp39-macosx_10_9_x86_64.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.
- xoscar/_utils.cpython-39-darwin.so +0 -0
- xoscar/_utils.pyx +6 -2
- xoscar/backends/communication/__init__.py +1 -1
- xoscar/backends/communication/socket.py +10 -2
- xoscar/backends/config.py +12 -0
- xoscar/backends/context.py +41 -8
- xoscar/backends/core.py +31 -5
- xoscar/backends/message.cpython-39-darwin.so +0 -0
- xoscar/backends/message.pyx +56 -9
- xoscar/backends/pool.py +33 -2
- xoscar/backends/router.py +86 -8
- xoscar/collective/xoscar_pygloo.cpython-39-darwin.so +0 -0
- xoscar/context.cpython-39-darwin.so +0 -0
- xoscar/core.cpython-39-darwin.so +0 -0
- xoscar/core.pxd +1 -0
- xoscar/core.pyx +11 -5
- xoscar/serialization/__init__.py +2 -2
- xoscar/serialization/core.cpython-39-darwin.so +0 -0
- xoscar/serialization/mlx.py +63 -0
- xoscar/virtualenv/__init__.py +34 -0
- xoscar/virtualenv/core.py +48 -0
- xoscar/virtualenv/uv.py +85 -0
- {xoscar-0.4.6.dist-info → xoscar-0.6.0.dist-info}/METADATA +2 -1
- {xoscar-0.4.6.dist-info → xoscar-0.6.0.dist-info}/RECORD +26 -22
- {xoscar-0.4.6.dist-info → xoscar-0.6.0.dist-info}/WHEEL +0 -0
- {xoscar-0.4.6.dist-info → xoscar-0.6.0.dist-info}/top_level.txt +0 -0
|
Binary file
|
xoscar/_utils.pyx
CHANGED
|
@@ -208,28 +208,32 @@ def create_actor_ref(*args, **kwargs):
|
|
|
208
208
|
|
|
209
209
|
address = to_str(kwargs.pop('address', None))
|
|
210
210
|
uid = kwargs.pop('uid', None)
|
|
211
|
+
proxy_addresses = kwargs.pop("proxy_addresses", None)
|
|
211
212
|
|
|
212
213
|
if kwargs:
|
|
213
214
|
raise ValueError('Only `address` or `uid` keywords are supported')
|
|
214
215
|
|
|
215
|
-
if len(args)
|
|
216
|
+
if 2 <= len(args) <= 3:
|
|
216
217
|
if address:
|
|
217
218
|
raise ValueError('address has been specified')
|
|
218
219
|
address = to_str(args[0])
|
|
219
220
|
uid = args[1]
|
|
221
|
+
if len(args) == 3:
|
|
222
|
+
proxy_addresses = args[2]
|
|
220
223
|
elif len(args) == 1:
|
|
221
224
|
tp0 = type(args[0])
|
|
222
225
|
if tp0 is ActorRef or tp0 is LocalActorRef:
|
|
223
226
|
existing_ref = <ActorRef>(args[0])
|
|
224
227
|
uid = existing_ref.uid
|
|
225
228
|
address = to_str(address or existing_ref.address)
|
|
229
|
+
proxy_addresses = existing_ref.proxy_addresses
|
|
226
230
|
else:
|
|
227
231
|
uid = args[0]
|
|
228
232
|
|
|
229
233
|
if uid is None:
|
|
230
234
|
raise ValueError('Actor uid should be provided')
|
|
231
235
|
|
|
232
|
-
return ActorRef(address, uid)
|
|
236
|
+
return ActorRef(address, uid, proxy_addresses=proxy_addresses)
|
|
233
237
|
|
|
234
238
|
|
|
235
239
|
cdef class Timer:
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
from .base import Channel, Client, Server
|
|
16
|
+
from .base import Channel, ChannelType, Client, Server
|
|
17
17
|
from .core import gen_local_address, get_client_type, get_server_type
|
|
18
18
|
from .dummy import DummyChannel, DummyClient, DummyServer
|
|
19
19
|
from .socket import (
|
|
@@ -315,7 +315,11 @@ class SocketClient(Client):
|
|
|
315
315
|
except asyncio.TimeoutError:
|
|
316
316
|
raise ConnectionError("connect timeout")
|
|
317
317
|
channel = SocketChannel(
|
|
318
|
-
reader,
|
|
318
|
+
reader,
|
|
319
|
+
writer,
|
|
320
|
+
local_address=local_address,
|
|
321
|
+
dest_address=dest_address,
|
|
322
|
+
channel_type=ChannelType.remote,
|
|
319
323
|
)
|
|
320
324
|
return SocketClient(local_address, dest_address, channel)
|
|
321
325
|
|
|
@@ -431,6 +435,10 @@ class UnixSocketClient(Client):
|
|
|
431
435
|
"Cannot connect unix socket due to file not exists"
|
|
432
436
|
)
|
|
433
437
|
channel = SocketChannel(
|
|
434
|
-
reader,
|
|
438
|
+
reader,
|
|
439
|
+
writer,
|
|
440
|
+
local_address=local_address,
|
|
441
|
+
dest_address=dest_address,
|
|
442
|
+
channel_type=ChannelType.ipc,
|
|
435
443
|
)
|
|
436
444
|
return UnixSocketClient(local_address, dest_address, channel)
|
xoscar/backends/config.py
CHANGED
|
@@ -33,6 +33,8 @@ class ActorPoolConfig:
|
|
|
33
33
|
self._conf["metrics"] = dict()
|
|
34
34
|
if "comm" not in self._conf:
|
|
35
35
|
self._conf["comm"] = dict()
|
|
36
|
+
if "proxy" not in self._conf:
|
|
37
|
+
self._conf["proxy"] = dict()
|
|
36
38
|
|
|
37
39
|
@property
|
|
38
40
|
def n_pool(self):
|
|
@@ -143,3 +145,13 @@ class ActorPoolConfig:
|
|
|
143
145
|
|
|
144
146
|
def get_comm_config(self) -> dict:
|
|
145
147
|
return self._conf["comm"]
|
|
148
|
+
|
|
149
|
+
def get_proxy_config(self) -> dict:
|
|
150
|
+
return self._conf["proxy"]
|
|
151
|
+
|
|
152
|
+
def add_proxy_config(self, proxy_config: dict[str, str] | None):
|
|
153
|
+
if proxy_config:
|
|
154
|
+
self._conf["proxy"].update(proxy_config)
|
|
155
|
+
|
|
156
|
+
def remove_proxy(self, from_addr: str):
|
|
157
|
+
del self._conf["proxy"][from_addr]
|
xoscar/backends/context.py
CHANGED
|
@@ -72,15 +72,24 @@ class IndigenActorContext(BaseActorContext):
|
|
|
72
72
|
self._caller.cancel_tasks()
|
|
73
73
|
|
|
74
74
|
async def _call(
|
|
75
|
-
self,
|
|
75
|
+
self,
|
|
76
|
+
address: str,
|
|
77
|
+
message: _MessageBase,
|
|
78
|
+
wait: bool = True,
|
|
79
|
+
proxy_addresses: list[str] | None = None,
|
|
76
80
|
) -> Union[ResultMessage, ErrorMessage, asyncio.Future]:
|
|
77
81
|
return await self._caller.call(
|
|
78
|
-
Router.get_instance_or_empty(),
|
|
82
|
+
Router.get_instance_or_empty(),
|
|
83
|
+
address,
|
|
84
|
+
message,
|
|
85
|
+
wait=wait,
|
|
86
|
+
proxy_addresses=proxy_addresses,
|
|
79
87
|
)
|
|
80
88
|
|
|
81
89
|
async def _call_with_client(
|
|
82
90
|
self, client: Client, message: _MessageBase, wait: bool = True
|
|
83
91
|
) -> Union[ResultMessage, ErrorMessage, asyncio.Future]:
|
|
92
|
+
# NOTE: used by copyto, cannot support proxy
|
|
84
93
|
return await self._caller.call_with_client(client, message, wait)
|
|
85
94
|
|
|
86
95
|
async def _call_send_buffers(
|
|
@@ -146,7 +155,12 @@ class IndigenActorContext(BaseActorContext):
|
|
|
146
155
|
message = HasActorMessage(
|
|
147
156
|
new_message_id(), actor_ref, protocol=DEFAULT_PROTOCOL
|
|
148
157
|
)
|
|
149
|
-
future = await self._call(
|
|
158
|
+
future = await self._call(
|
|
159
|
+
actor_ref.address,
|
|
160
|
+
message,
|
|
161
|
+
wait=False,
|
|
162
|
+
proxy_addresses=actor_ref.proxy_addresses,
|
|
163
|
+
)
|
|
150
164
|
result = await self._wait(future, actor_ref.address, message) # type: ignore
|
|
151
165
|
return self._process_result_message(result)
|
|
152
166
|
|
|
@@ -154,7 +168,12 @@ class IndigenActorContext(BaseActorContext):
|
|
|
154
168
|
message = DestroyActorMessage(
|
|
155
169
|
new_message_id(), actor_ref, protocol=DEFAULT_PROTOCOL
|
|
156
170
|
)
|
|
157
|
-
future = await self._call(
|
|
171
|
+
future = await self._call(
|
|
172
|
+
actor_ref.address,
|
|
173
|
+
message,
|
|
174
|
+
wait=False,
|
|
175
|
+
proxy_addresses=actor_ref.proxy_addresses,
|
|
176
|
+
)
|
|
158
177
|
result = await self._wait(future, actor_ref.address, message) # type: ignore
|
|
159
178
|
return self._process_result_message(result)
|
|
160
179
|
|
|
@@ -168,7 +187,7 @@ class IndigenActorContext(BaseActorContext):
|
|
|
168
187
|
protocol=DEFAULT_PROTOCOL,
|
|
169
188
|
)
|
|
170
189
|
main_address = self._process_result_message(
|
|
171
|
-
await self._call(actor_ref.address, control_message) # type: ignore
|
|
190
|
+
await self._call(actor_ref.address, control_message, proxy_addresses=actor_ref.proxy_addresses) # type: ignore
|
|
172
191
|
)
|
|
173
192
|
real_actor_ref = await self.actor_ref(actor_ref)
|
|
174
193
|
if real_actor_ref.address == main_address:
|
|
@@ -182,7 +201,9 @@ class IndigenActorContext(BaseActorContext):
|
|
|
182
201
|
protocol=DEFAULT_PROTOCOL,
|
|
183
202
|
)
|
|
184
203
|
# stop server
|
|
185
|
-
result = await self._call(
|
|
204
|
+
result = await self._call(
|
|
205
|
+
main_address, stop_message, proxy_addresses=actor_ref.proxy_addresses
|
|
206
|
+
)
|
|
186
207
|
return self._process_result_message(result) # type: ignore
|
|
187
208
|
|
|
188
209
|
async def actor_ref(self, *args, **kwargs):
|
|
@@ -194,7 +215,12 @@ class IndigenActorContext(BaseActorContext):
|
|
|
194
215
|
message = ActorRefMessage(
|
|
195
216
|
new_message_id(), actor_ref, protocol=DEFAULT_PROTOCOL
|
|
196
217
|
)
|
|
197
|
-
future = await self._call(
|
|
218
|
+
future = await self._call(
|
|
219
|
+
actor_ref.address,
|
|
220
|
+
message,
|
|
221
|
+
wait=False,
|
|
222
|
+
proxy_addresses=actor_ref.proxy_addresses,
|
|
223
|
+
)
|
|
198
224
|
result = await self._wait(future, actor_ref.address, message)
|
|
199
225
|
res = self._process_result_message(result)
|
|
200
226
|
if res.address != connect_addr:
|
|
@@ -225,7 +251,12 @@ class IndigenActorContext(BaseActorContext):
|
|
|
225
251
|
actor_ref.address,
|
|
226
252
|
):
|
|
227
253
|
detect_cycle_send(send_message, wait_response)
|
|
228
|
-
future = await self._call(
|
|
254
|
+
future = await self._call(
|
|
255
|
+
actor_ref.address,
|
|
256
|
+
send_message,
|
|
257
|
+
wait=False,
|
|
258
|
+
proxy_addresses=actor_ref.proxy_addresses,
|
|
259
|
+
)
|
|
229
260
|
if wait_response:
|
|
230
261
|
result = await self._wait(future, actor_ref.address, send_message) # type: ignore
|
|
231
262
|
return self._process_result_message(result)
|
|
@@ -315,6 +346,8 @@ class IndigenActorContext(BaseActorContext):
|
|
|
315
346
|
async def _get_client(self, address: str) -> Client:
|
|
316
347
|
router = Router.get_instance()
|
|
317
348
|
assert router is not None, "`copy_to` can only be used inside pools"
|
|
349
|
+
if router.get_proxy(address):
|
|
350
|
+
raise RuntimeError("Cannot run `copy_to` when enabling proxy")
|
|
318
351
|
return await self._get_copy_to_client(router, address)
|
|
319
352
|
|
|
320
353
|
async def copy_to_buffers(
|
xoscar/backends/core.py
CHANGED
|
@@ -26,8 +26,15 @@ from typing import Type, Union
|
|
|
26
26
|
from .._utils import Timer
|
|
27
27
|
from ..errors import ServerClosed
|
|
28
28
|
from ..profiling import get_profiling_data
|
|
29
|
-
from .communication import Client, UCXClient
|
|
30
|
-
from .message import
|
|
29
|
+
from .communication import ChannelType, Client, UCXClient
|
|
30
|
+
from .message import (
|
|
31
|
+
DeserializeMessageFailed,
|
|
32
|
+
ErrorMessage,
|
|
33
|
+
ForwardMessage,
|
|
34
|
+
MessageType,
|
|
35
|
+
ResultMessage,
|
|
36
|
+
_MessageBase,
|
|
37
|
+
)
|
|
31
38
|
from .router import Router
|
|
32
39
|
|
|
33
40
|
ResultMessageType = Union[ResultMessage, ErrorMessage]
|
|
@@ -67,8 +74,15 @@ class ActorCallerThreadLocal:
|
|
|
67
74
|
self._listen_client(client)
|
|
68
75
|
return client
|
|
69
76
|
|
|
70
|
-
async def get_client(
|
|
71
|
-
|
|
77
|
+
async def get_client(
|
|
78
|
+
self,
|
|
79
|
+
router: Router,
|
|
80
|
+
dest_address: str,
|
|
81
|
+
proxy_addresses: list[str] | None = None,
|
|
82
|
+
) -> Client:
|
|
83
|
+
client = await router.get_client(
|
|
84
|
+
dest_address, from_who=self, proxy_addresses=proxy_addresses
|
|
85
|
+
)
|
|
72
86
|
self._listen_client(client)
|
|
73
87
|
return client
|
|
74
88
|
|
|
@@ -191,8 +205,20 @@ class ActorCallerThreadLocal:
|
|
|
191
205
|
dest_address: str,
|
|
192
206
|
message: _MessageBase,
|
|
193
207
|
wait: bool = True,
|
|
208
|
+
proxy_addresses: list[str] | None = None,
|
|
194
209
|
) -> ResultMessage | ErrorMessage | asyncio.Future:
|
|
195
|
-
client = await self.get_client(
|
|
210
|
+
client = await self.get_client(
|
|
211
|
+
router, dest_address, proxy_addresses=proxy_addresses
|
|
212
|
+
)
|
|
213
|
+
if (
|
|
214
|
+
client.channel_type == ChannelType.remote
|
|
215
|
+
and client.dest_address != dest_address
|
|
216
|
+
and message.message_type != MessageType.control
|
|
217
|
+
):
|
|
218
|
+
# wrap message with forward message
|
|
219
|
+
message = ForwardMessage(
|
|
220
|
+
message_id=message.message_id, address=dest_address, raw_message=message
|
|
221
|
+
)
|
|
196
222
|
return await self.call_with_client(client, message, wait)
|
|
197
223
|
|
|
198
224
|
async def stop(self):
|
|
Binary file
|
xoscar/backends/message.pyx
CHANGED
|
@@ -47,6 +47,7 @@ class MessageType(Enum):
|
|
|
47
47
|
cancel = 9
|
|
48
48
|
copy_to_buffers = 10
|
|
49
49
|
copy_to_fileobjs = 11
|
|
50
|
+
forward = 12
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
class ControlMessageType(Enum):
|
|
@@ -350,13 +351,14 @@ cdef class DestroyActorMessage(_MessageBase):
|
|
|
350
351
|
cdef _MessageSerialItem serial(self):
|
|
351
352
|
cdef _MessageSerialItem item = _MessageBase.serial(self)
|
|
352
353
|
item.serialized += (
|
|
353
|
-
self.actor_ref.address, self.actor_ref.uid,
|
|
354
|
+
self.actor_ref.address, self.actor_ref.uid,
|
|
355
|
+
self.actor_ref.proxy_addresses, self.from_main
|
|
354
356
|
)
|
|
355
357
|
return item
|
|
356
358
|
|
|
357
359
|
cdef deserial_members(self, tuple serialized, list subs):
|
|
358
360
|
_MessageBase.deserial_members(self, serialized, subs)
|
|
359
|
-
self.actor_ref = ActorRef(serialized[-3], serialized[-2])
|
|
361
|
+
self.actor_ref = ActorRef(serialized[-4], serialized[-3], serialized[-2])
|
|
360
362
|
self.from_main = serialized[-1]
|
|
361
363
|
|
|
362
364
|
|
|
@@ -381,13 +383,13 @@ cdef class HasActorMessage(_MessageBase):
|
|
|
381
383
|
cdef _MessageSerialItem serial(self):
|
|
382
384
|
cdef _MessageSerialItem item = _MessageBase.serial(self)
|
|
383
385
|
item.serialized += (
|
|
384
|
-
self.actor_ref.address, self.actor_ref.uid
|
|
386
|
+
self.actor_ref.address, self.actor_ref.uid, self.actor_ref.proxy_addresses
|
|
385
387
|
)
|
|
386
388
|
return item
|
|
387
389
|
|
|
388
390
|
cdef deserial_members(self, tuple serialized, list subs):
|
|
389
391
|
_MessageBase.deserial_members(self, serialized, subs)
|
|
390
|
-
self.actor_ref = ActorRef(serialized[-2], serialized[-1])
|
|
392
|
+
self.actor_ref = ActorRef(serialized[-3], serialized[-2], serialized[-1])
|
|
391
393
|
|
|
392
394
|
|
|
393
395
|
cdef class ActorRefMessage(_MessageBase):
|
|
@@ -411,13 +413,13 @@ cdef class ActorRefMessage(_MessageBase):
|
|
|
411
413
|
cdef _MessageSerialItem serial(self):
|
|
412
414
|
cdef _MessageSerialItem item = _MessageBase.serial(self)
|
|
413
415
|
item.serialized += (
|
|
414
|
-
self.actor_ref.address, self.actor_ref.uid
|
|
416
|
+
self.actor_ref.address, self.actor_ref.uid, self.actor_ref.proxy_addresses
|
|
415
417
|
)
|
|
416
418
|
return item
|
|
417
419
|
|
|
418
420
|
cdef deserial_members(self, tuple serialized, list subs):
|
|
419
421
|
_MessageBase.deserial_members(self, serialized, subs)
|
|
420
|
-
self.actor_ref = ActorRef(serialized[-2], serialized[-1])
|
|
422
|
+
self.actor_ref = ActorRef(serialized[-3], serialized[-2], serialized[-1])
|
|
421
423
|
|
|
422
424
|
|
|
423
425
|
cdef class SendMessage(_MessageBase):
|
|
@@ -449,14 +451,14 @@ cdef class SendMessage(_MessageBase):
|
|
|
449
451
|
cdef _MessageSerialItem serial(self):
|
|
450
452
|
cdef _MessageSerialItem item = _MessageBase.serial(self)
|
|
451
453
|
item.serialized += (
|
|
452
|
-
self.actor_ref.address, self.actor_ref.uid
|
|
454
|
+
self.actor_ref.address, self.actor_ref.uid, self.actor_ref.proxy_addresses
|
|
453
455
|
)
|
|
454
456
|
item.subs = [self.content]
|
|
455
457
|
return item
|
|
456
458
|
|
|
457
459
|
cdef deserial_members(self, tuple serialized, list subs):
|
|
458
460
|
_MessageBase.deserial_members(self, serialized, subs)
|
|
459
|
-
self.actor_ref = ActorRef(serialized[-2], serialized[-1])
|
|
461
|
+
self.actor_ref = ActorRef(serialized[-3], serialized[-2], serialized[-1])
|
|
460
462
|
self.content = subs[0]
|
|
461
463
|
|
|
462
464
|
|
|
@@ -533,6 +535,50 @@ cdef class CopyToFileObjectsMessage(CopyToBuffersMessage):
|
|
|
533
535
|
message_type = MessageType.copy_to_fileobjs
|
|
534
536
|
|
|
535
537
|
|
|
538
|
+
cdef class ForwardMessage(_MessageBase):
|
|
539
|
+
message_type = MessageType.forward
|
|
540
|
+
|
|
541
|
+
cdef:
|
|
542
|
+
public str address
|
|
543
|
+
public _MessageBase raw_message
|
|
544
|
+
|
|
545
|
+
def __init__(
|
|
546
|
+
self,
|
|
547
|
+
bytes message_id = None,
|
|
548
|
+
str address = None,
|
|
549
|
+
_MessageBase raw_message = None,
|
|
550
|
+
int protocol = DEFAULT_PROTOCOL,
|
|
551
|
+
list message_trace = None,
|
|
552
|
+
):
|
|
553
|
+
_MessageBase.__init__(
|
|
554
|
+
self,
|
|
555
|
+
message_id,
|
|
556
|
+
protocol=protocol,
|
|
557
|
+
message_trace=message_trace
|
|
558
|
+
)
|
|
559
|
+
self.address = address
|
|
560
|
+
self.raw_message = raw_message
|
|
561
|
+
|
|
562
|
+
cdef _MessageSerialItem serial(self):
|
|
563
|
+
cdef _MessageSerialItem item = _MessageBase.serial(self)
|
|
564
|
+
cdef _MessageSerialItem raw_message_serialized = self.raw_message.serial()
|
|
565
|
+
item.serialized += (self.address,)
|
|
566
|
+
item.serialized += raw_message_serialized.serialized
|
|
567
|
+
item.subs += raw_message_serialized.subs
|
|
568
|
+
return item
|
|
569
|
+
|
|
570
|
+
cdef deserial_members(self, tuple serialized, list subs):
|
|
571
|
+
# 5 is magic number that means serialized for _MessageBase
|
|
572
|
+
base_serialized = serialized[:5]
|
|
573
|
+
_MessageBase.deserial_members(self, base_serialized, [])
|
|
574
|
+
self.address = serialized[5]
|
|
575
|
+
# process raw message
|
|
576
|
+
tp = _message_type_to_message_cls[serialized[6]]
|
|
577
|
+
cdef _MessageBase raw_message = <_MessageBase>(tp())
|
|
578
|
+
raw_message.deserial_members(serialized[6:], subs)
|
|
579
|
+
self.raw_message = raw_message
|
|
580
|
+
|
|
581
|
+
|
|
536
582
|
cdef dict _message_type_to_message_cls = {
|
|
537
583
|
MessageType.control.value: ControlMessage,
|
|
538
584
|
MessageType.result.value: ResultMessage,
|
|
@@ -545,7 +591,8 @@ cdef dict _message_type_to_message_cls = {
|
|
|
545
591
|
MessageType.tell.value: TellMessage,
|
|
546
592
|
MessageType.cancel.value: CancelMessage,
|
|
547
593
|
MessageType.copy_to_buffers.value: CopyToBuffersMessage,
|
|
548
|
-
MessageType.copy_to_fileobjs.value: CopyToFileObjectsMessage
|
|
594
|
+
MessageType.copy_to_fileobjs.value: CopyToFileObjectsMessage,
|
|
595
|
+
MessageType.forward.value: ForwardMessage,
|
|
549
596
|
}
|
|
550
597
|
|
|
551
598
|
|
xoscar/backends/pool.py
CHANGED
|
@@ -22,6 +22,7 @@ import itertools
|
|
|
22
22
|
import logging
|
|
23
23
|
import multiprocessing
|
|
24
24
|
import os
|
|
25
|
+
import sys
|
|
25
26
|
import threading
|
|
26
27
|
import traceback
|
|
27
28
|
from abc import ABC, ABCMeta, abstractmethod
|
|
@@ -62,6 +63,7 @@ from .message import (
|
|
|
62
63
|
CreateActorMessage,
|
|
63
64
|
DestroyActorMessage,
|
|
64
65
|
ErrorMessage,
|
|
66
|
+
ForwardMessage,
|
|
65
67
|
HasActorMessage,
|
|
66
68
|
MessageType,
|
|
67
69
|
ResultMessage,
|
|
@@ -123,6 +125,7 @@ def _register_message_handler(pool_type: Type["AbstractActorPool"]):
|
|
|
123
125
|
(MessageType.send, pool_type.send),
|
|
124
126
|
(MessageType.tell, pool_type.tell),
|
|
125
127
|
(MessageType.cancel, pool_type.cancel),
|
|
128
|
+
(MessageType.forward, pool_type.forward),
|
|
126
129
|
(MessageType.control, pool_type.handle_control_command),
|
|
127
130
|
(MessageType.copy_to_buffers, pool_type.handle_copy_to_buffers_message),
|
|
128
131
|
(MessageType.copy_to_fileobjs, pool_type.handle_copy_to_fileobjs_message),
|
|
@@ -311,6 +314,22 @@ class AbstractActorPool(ABC):
|
|
|
311
314
|
result or error message
|
|
312
315
|
"""
|
|
313
316
|
|
|
317
|
+
async def forward(self, message: ForwardMessage) -> ResultMessageType:
|
|
318
|
+
"""
|
|
319
|
+
Forward message
|
|
320
|
+
|
|
321
|
+
Parameters
|
|
322
|
+
----------
|
|
323
|
+
message: ForwardMessage
|
|
324
|
+
Forward message.
|
|
325
|
+
|
|
326
|
+
Returns
|
|
327
|
+
-------
|
|
328
|
+
result_message
|
|
329
|
+
result or error message
|
|
330
|
+
"""
|
|
331
|
+
return await self.call(message.address, message.raw_message)
|
|
332
|
+
|
|
314
333
|
def _sync_pool_config(self, actor_pool_config: ActorPoolConfig):
|
|
315
334
|
self._config = actor_pool_config
|
|
316
335
|
# remove router from global one
|
|
@@ -443,6 +462,7 @@ class AbstractActorPool(ABC):
|
|
|
443
462
|
gen_local_address(process_index),
|
|
444
463
|
actor_pool_config.external_to_internal_address_map,
|
|
445
464
|
comm_config=actor_pool_config.get_comm_config(),
|
|
465
|
+
proxy_config=actor_pool_config.get_proxy_config(),
|
|
446
466
|
)
|
|
447
467
|
kw["env"] = curr_pool_config["env"]
|
|
448
468
|
|
|
@@ -605,7 +625,8 @@ class ActorPoolBase(AbstractActorPool, metaclass=ABCMeta):
|
|
|
605
625
|
self._actors[actor_id] = actor
|
|
606
626
|
await self._run_coro(message.message_id, actor.__post_create__())
|
|
607
627
|
|
|
608
|
-
|
|
628
|
+
proxies = self._router.get_proxies(address)
|
|
629
|
+
result = ActorRef(address, actor_id, proxy_addresses=proxies)
|
|
609
630
|
# ensemble result message
|
|
610
631
|
processor.result = ResultMessage(
|
|
611
632
|
message.message_id, result, protocol=message.protocol
|
|
@@ -647,9 +668,10 @@ class ActorPoolBase(AbstractActorPool, metaclass=ABCMeta):
|
|
|
647
668
|
actor_id = message.actor_ref.uid
|
|
648
669
|
if actor_id not in self._actors:
|
|
649
670
|
raise ActorNotExist(f"Actor {actor_id} does not exist")
|
|
671
|
+
proxies = self._router.get_proxies(self.external_address)
|
|
650
672
|
result = ResultMessage(
|
|
651
673
|
message.message_id,
|
|
652
|
-
ActorRef(self.external_address, actor_id),
|
|
674
|
+
ActorRef(self.external_address, actor_id, proxy_addresses=proxies),
|
|
653
675
|
protocol=message.protocol,
|
|
654
676
|
)
|
|
655
677
|
processor.result = result
|
|
@@ -762,6 +784,7 @@ class ActorPoolBase(AbstractActorPool, metaclass=ABCMeta):
|
|
|
762
784
|
gen_local_address(process_index),
|
|
763
785
|
actor_pool_config.external_to_internal_address_map,
|
|
764
786
|
comm_config=actor_pool_config.get_comm_config(),
|
|
787
|
+
proxy_config=actor_pool_config.get_proxy_config(),
|
|
765
788
|
)
|
|
766
789
|
|
|
767
790
|
@classmethod
|
|
@@ -802,6 +825,9 @@ class ActorPoolBase(AbstractActorPool, metaclass=ABCMeta):
|
|
|
802
825
|
with _disable_log_temporally():
|
|
803
826
|
TypeDispatcher.reload_all_lazy_handlers()
|
|
804
827
|
|
|
828
|
+
if "PYTHONPATH" in os.environ:
|
|
829
|
+
sys.path.insert(0, os.environ["PYTHONPATH"])
|
|
830
|
+
|
|
805
831
|
def handle_channel(channel):
|
|
806
832
|
return pool.on_new_channel(channel)
|
|
807
833
|
|
|
@@ -1155,6 +1181,7 @@ class MainActorPoolBase(ActorPoolBase):
|
|
|
1155
1181
|
actor_ref = message.actor_ref
|
|
1156
1182
|
actor_ref.uid = to_binary(actor_ref.uid)
|
|
1157
1183
|
if actor_ref.address == self.external_address and actor_ref.uid in self._actors:
|
|
1184
|
+
actor_ref.proxy_addresses = self._router.get_proxies(actor_ref.address)
|
|
1158
1185
|
return ResultMessage(
|
|
1159
1186
|
message.message_id, actor_ref, protocol=message.protocol
|
|
1160
1187
|
)
|
|
@@ -1163,6 +1190,7 @@ class MainActorPoolBase(ActorPoolBase):
|
|
|
1163
1190
|
for address, item in self._allocated_actors.items():
|
|
1164
1191
|
ref = create_actor_ref(address, actor_ref.uid)
|
|
1165
1192
|
if ref in item:
|
|
1193
|
+
ref.proxy_addresses = self._router.get_proxies(ref.address)
|
|
1166
1194
|
return ResultMessage(message.message_id, ref, protocol=message.protocol)
|
|
1167
1195
|
|
|
1168
1196
|
with _ErrorProcessor(
|
|
@@ -1503,6 +1531,7 @@ async def create_actor_pool(
|
|
|
1503
1531
|
suspend_sigint: bool | None = None,
|
|
1504
1532
|
use_uvloop: str | bool = "auto",
|
|
1505
1533
|
logging_conf: dict | None = None,
|
|
1534
|
+
proxy_conf: dict | None = None,
|
|
1506
1535
|
on_process_down: Callable[[MainActorPoolType, str], None] | None = None,
|
|
1507
1536
|
on_process_recover: Callable[[MainActorPoolType, str], None] | None = None,
|
|
1508
1537
|
extra_conf: dict | None = None,
|
|
@@ -1549,6 +1578,8 @@ async def create_actor_pool(
|
|
|
1549
1578
|
)
|
|
1550
1579
|
actor_pool_config = ActorPoolConfig()
|
|
1551
1580
|
actor_pool_config.add_metric_configs(kwargs.get("metrics", {}))
|
|
1581
|
+
# add proxy config
|
|
1582
|
+
actor_pool_config.add_proxy_config(proxy_conf)
|
|
1552
1583
|
# add main config
|
|
1553
1584
|
process_index_gen = pool_cls.process_index_gen(address)
|
|
1554
1585
|
main_process_index = next(process_index_gen)
|
xoscar/backends/router.py
CHANGED
|
@@ -17,10 +17,15 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import asyncio
|
|
19
19
|
import threading
|
|
20
|
-
from typing import Any,
|
|
20
|
+
from typing import Any, Optional, Type, Union
|
|
21
21
|
|
|
22
22
|
from .communication import Client, get_client_type
|
|
23
23
|
|
|
24
|
+
_CACHE_KEY_TYPE = Union[
|
|
25
|
+
tuple[str, Any, Optional[Type[Client]]],
|
|
26
|
+
tuple[str, Any, Optional[Type[Client]], Optional[tuple[str, ...]]],
|
|
27
|
+
]
|
|
28
|
+
|
|
24
29
|
|
|
25
30
|
class Router:
|
|
26
31
|
"""
|
|
@@ -32,6 +37,7 @@ class Router:
|
|
|
32
37
|
"_local_mapping",
|
|
33
38
|
"_mapping",
|
|
34
39
|
"_comm_config",
|
|
40
|
+
"_proxy_config",
|
|
35
41
|
"_cache_local",
|
|
36
42
|
)
|
|
37
43
|
|
|
@@ -56,6 +62,7 @@ class Router:
|
|
|
56
62
|
local_address: str | None,
|
|
57
63
|
mapping: dict[str, str] | None = None,
|
|
58
64
|
comm_config: dict | None = None,
|
|
65
|
+
proxy_config: dict | None = None,
|
|
59
66
|
):
|
|
60
67
|
self._curr_external_addresses = external_addresses
|
|
61
68
|
self._local_mapping = dict()
|
|
@@ -65,10 +72,11 @@ class Router:
|
|
|
65
72
|
mapping = dict()
|
|
66
73
|
self._mapping = mapping
|
|
67
74
|
self._comm_config = comm_config or dict()
|
|
75
|
+
self._proxy_config = proxy_config or dict()
|
|
68
76
|
self._cache_local = threading.local()
|
|
69
77
|
|
|
70
78
|
@property
|
|
71
|
-
def _cache(self) -> dict[
|
|
79
|
+
def _cache(self) -> dict[_CACHE_KEY_TYPE, Client]:
|
|
72
80
|
try:
|
|
73
81
|
return self._cache_local.cache
|
|
74
82
|
except AttributeError:
|
|
@@ -92,6 +100,7 @@ class Router:
|
|
|
92
100
|
self._local_mapping.update(router._local_mapping)
|
|
93
101
|
self._mapping.update(router._mapping)
|
|
94
102
|
self._comm_config.update(router._comm_config)
|
|
103
|
+
self._proxy_config.update(router._proxy_config)
|
|
95
104
|
self._cache_local = threading.local()
|
|
96
105
|
|
|
97
106
|
def remove_router(self, router: "Router"):
|
|
@@ -124,14 +133,23 @@ class Router:
|
|
|
124
133
|
external_address: str,
|
|
125
134
|
from_who: Any = None,
|
|
126
135
|
cached: bool = True,
|
|
136
|
+
proxy_addresses: list[str] | None = None,
|
|
127
137
|
**kw,
|
|
128
138
|
) -> Client:
|
|
129
139
|
async with self._lock:
|
|
130
|
-
|
|
131
|
-
|
|
140
|
+
proxy_addrs: tuple[str, ...] | None = (
|
|
141
|
+
tuple(proxy_addresses) if proxy_addresses else None
|
|
142
|
+
)
|
|
143
|
+
if (
|
|
144
|
+
cached
|
|
145
|
+
and (external_address, from_who, None, proxy_addrs) in self._cache
|
|
146
|
+
):
|
|
147
|
+
cached_client = self._cache[
|
|
148
|
+
external_address, from_who, None, proxy_addrs
|
|
149
|
+
]
|
|
132
150
|
if cached_client.closed:
|
|
133
151
|
# closed before, ignore it
|
|
134
|
-
del self._cache[external_address, from_who, None]
|
|
152
|
+
del self._cache[external_address, from_who, None, proxy_addrs]
|
|
135
153
|
else:
|
|
136
154
|
return cached_client
|
|
137
155
|
|
|
@@ -139,10 +157,22 @@ class Router:
|
|
|
139
157
|
if address is None:
|
|
140
158
|
# no inner address, just use external address
|
|
141
159
|
address = external_address
|
|
160
|
+
# check if proxy address exists
|
|
161
|
+
proxy_address = proxy_addresses[-1] if proxy_addresses else None
|
|
162
|
+
if proxy_address is None:
|
|
163
|
+
proxy_address = self.get_proxy(address)
|
|
164
|
+
if proxy_address and proxy_address != self.external_address:
|
|
165
|
+
address = proxy_address
|
|
166
|
+
else:
|
|
167
|
+
if new_proxy_address := self.get_proxy(proxy_address):
|
|
168
|
+
address = new_proxy_address
|
|
169
|
+
else:
|
|
170
|
+
address = proxy_address
|
|
171
|
+
|
|
142
172
|
client_type: Type[Client] = get_client_type(address)
|
|
143
173
|
client = await self._create_client(client_type, address, **kw)
|
|
144
174
|
if cached:
|
|
145
|
-
self._cache[external_address, from_who, None] = client
|
|
175
|
+
self._cache[external_address, from_who, None, proxy_addrs] = client
|
|
146
176
|
return client
|
|
147
177
|
|
|
148
178
|
async def _create_client(
|
|
@@ -158,7 +188,7 @@ class Router:
|
|
|
158
188
|
|
|
159
189
|
def _get_client_type_to_addresses(
|
|
160
190
|
self, external_address: str
|
|
161
|
-
) ->
|
|
191
|
+
) -> dict[Type[Client], str]:
|
|
162
192
|
client_type_to_addresses = dict()
|
|
163
193
|
client_type_to_addresses[get_client_type(external_address)] = external_address
|
|
164
194
|
if external_address in self._curr_external_addresses: # pragma: no cover
|
|
@@ -173,7 +203,7 @@ class Router:
|
|
|
173
203
|
client_type_to_addresses[client_type] = addr # type: ignore
|
|
174
204
|
return client_type_to_addresses
|
|
175
205
|
|
|
176
|
-
def get_all_client_types(self, external_address: str) ->
|
|
206
|
+
def get_all_client_types(self, external_address: str) -> list[Type[Client]]:
|
|
177
207
|
return list(self._get_client_type_to_addresses(external_address))
|
|
178
208
|
|
|
179
209
|
async def get_client_via_type(
|
|
@@ -205,3 +235,51 @@ class Router:
|
|
|
205
235
|
if cached:
|
|
206
236
|
self._cache[external_address, from_who, client_type] = client
|
|
207
237
|
return client
|
|
238
|
+
|
|
239
|
+
def get_proxy(self, from_addr: str) -> str | None:
|
|
240
|
+
"""
|
|
241
|
+
Get proxy address that sent to.
|
|
242
|
+
|
|
243
|
+
Some patterns can be supported:
|
|
244
|
+
|
|
245
|
+
1. Direct address mapping, e.g. mapping 127.0.0.1:12345 -> 127.0.0.1:12346
|
|
246
|
+
The message will be sent to 127.0.0.1:12346 as forward one.
|
|
247
|
+
2. Host match, e.g. mapping 127.0.0.1 -> 127.0.0.1:12346
|
|
248
|
+
All the messages that match the host, e.g. 127.0.0.1:12345 and 127.0.0.1:12347
|
|
249
|
+
will be sent to 127.0.0.1:12346 as forward one.
|
|
250
|
+
3. Wildcard, e.g. mapping * -> 127.0.0.1:12346
|
|
251
|
+
All the messages will be sent to 127.0.0.1:12346 as forward one.
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
host = from_addr.split(":", 1)[0]
|
|
255
|
+
|
|
256
|
+
proxy_map = self._proxy_config
|
|
257
|
+
addr = proxy_map.get(from_addr)
|
|
258
|
+
if addr and addr != from_addr:
|
|
259
|
+
return addr
|
|
260
|
+
addr = proxy_map.get(host)
|
|
261
|
+
if addr and addr != from_addr:
|
|
262
|
+
return addr
|
|
263
|
+
addr = proxy_map.get("*")
|
|
264
|
+
if addr and addr != from_addr:
|
|
265
|
+
return addr
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
def get_proxies(self, from_addr: str) -> list[str] | None:
|
|
269
|
+
"""
|
|
270
|
+
Get all proxies
|
|
271
|
+
|
|
272
|
+
e.g. Proxy mapping {'a': 'b', 'b': 'c'}
|
|
273
|
+
get_proxies('a') will return ['b', 'c']
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
proxies: list[str] = []
|
|
277
|
+
while True:
|
|
278
|
+
proxy = self.get_proxy(from_addr)
|
|
279
|
+
if not proxies and not proxy:
|
|
280
|
+
return None
|
|
281
|
+
elif not proxy:
|
|
282
|
+
return proxies
|
|
283
|
+
else:
|
|
284
|
+
proxies.append(proxy)
|
|
285
|
+
from_addr = proxy
|
|
Binary file
|
|
Binary file
|
xoscar/core.cpython-39-darwin.so
CHANGED
|
Binary file
|
xoscar/core.pxd
CHANGED
xoscar/core.pyx
CHANGED
|
@@ -95,7 +95,7 @@ cpdef create_local_actor_ref(address, uid):
|
|
|
95
95
|
return None
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
cpdef create_actor_ref(address, uid):
|
|
98
|
+
cpdef create_actor_ref(address, uid, list proxy_addresses = None):
|
|
99
99
|
"""
|
|
100
100
|
Create an actor reference.
|
|
101
101
|
TODO(fyrestone): Remove the create_actor_ref in _utils.pyx
|
|
@@ -105,18 +105,20 @@ cpdef create_actor_ref(address, uid):
|
|
|
105
105
|
ActorRef or LocalActorRef
|
|
106
106
|
"""
|
|
107
107
|
actor = _get_local_actor(address, uid)
|
|
108
|
-
return ActorRef(address, uid)
|
|
108
|
+
return ActorRef(address, uid, proxy_addresses=proxy_addresses) \
|
|
109
|
+
if actor is None else LocalActorRef(actor)
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
cdef class ActorRef:
|
|
112
113
|
"""
|
|
113
114
|
Reference of an Actor at user side
|
|
114
115
|
"""
|
|
115
|
-
def __init__(self, str address, object uid):
|
|
116
|
+
def __init__(self, str address, object uid, list proxy_addresses = None):
|
|
116
117
|
if isinstance(uid, str):
|
|
117
118
|
uid = uid.encode()
|
|
118
119
|
self.uid = uid
|
|
119
120
|
self.address = address
|
|
121
|
+
self.proxy_addresses = proxy_addresses
|
|
120
122
|
self._methods = dict()
|
|
121
123
|
|
|
122
124
|
def destroy(self, object callback=None):
|
|
@@ -124,7 +126,7 @@ cdef class ActorRef:
|
|
|
124
126
|
return ctx.destroy_actor(self)
|
|
125
127
|
|
|
126
128
|
def __reduce__(self):
|
|
127
|
-
return create_actor_ref, (self.address, self.uid)
|
|
129
|
+
return create_actor_ref, (self.address, self.uid, self.proxy_addresses)
|
|
128
130
|
|
|
129
131
|
def __getattr__(self, item):
|
|
130
132
|
if item.startswith('_') and item not in ["__xoscar_next__", "__xoscar_destroy_generator__"]:
|
|
@@ -146,7 +148,11 @@ cdef class ActorRef:
|
|
|
146
148
|
return False
|
|
147
149
|
|
|
148
150
|
def __repr__(self):
|
|
149
|
-
|
|
151
|
+
if not self.proxy_addresses:
|
|
152
|
+
return 'ActorRef(uid={!r}, address={!r})'.format(self.uid, self.address)
|
|
153
|
+
else:
|
|
154
|
+
return (f"ActorRef(uid={self.uid}, address={self.address}, "
|
|
155
|
+
f"proxy_addresses={self.proxy_addresses})")
|
|
150
156
|
|
|
151
157
|
|
|
152
158
|
cdef class _DelayedArgument:
|
xoscar/serialization/__init__.py
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
|
|
16
|
-
from . import cuda, exception, numpy, scipy
|
|
16
|
+
from . import cuda, exception, mlx, numpy, scipy
|
|
17
17
|
from .aio import AioDeserializer, AioSerializer
|
|
18
18
|
from .core import Serializer, deserialize, serialize, serialize_with_spawn
|
|
19
19
|
|
|
20
|
-
del cuda, numpy, scipy, exception
|
|
20
|
+
del cuda, numpy, scipy, mlx, exception
|
|
Binary file
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright 2022-2025 XProbe Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from typing import Any, List
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from ..utils import lazy_import
|
|
20
|
+
from .core import Serializer, buffered
|
|
21
|
+
|
|
22
|
+
mx = lazy_import("mlx.core")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
dtype_map = {
|
|
26
|
+
"b": np.int8,
|
|
27
|
+
"B": np.uint8,
|
|
28
|
+
"h": np.int16,
|
|
29
|
+
"H": np.uint16,
|
|
30
|
+
"i": np.int32,
|
|
31
|
+
"I": np.uint32,
|
|
32
|
+
"q": np.int64,
|
|
33
|
+
"Q": np.uint64,
|
|
34
|
+
"e": np.float16,
|
|
35
|
+
"f": np.float32,
|
|
36
|
+
"d": np.float64,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MLXSerislizer(Serializer):
|
|
41
|
+
@buffered
|
|
42
|
+
def serial(self, obj: "mx.array", context: dict): # type: ignore
|
|
43
|
+
mv = memoryview(obj)
|
|
44
|
+
header = dict(shape=mv.shape, format=mv.format)
|
|
45
|
+
if not mv.c_contiguous:
|
|
46
|
+
mv = memoryview(bytes(mv))
|
|
47
|
+
return (header,), [mv], True
|
|
48
|
+
|
|
49
|
+
def deserial(self, serialized: tuple, context: dict, subs: List[Any]):
|
|
50
|
+
header = serialized[0]
|
|
51
|
+
shape, format = header["shape"], header["format"]
|
|
52
|
+
mv = memoryview(subs[0])
|
|
53
|
+
if mv.format != format:
|
|
54
|
+
dtype = dtype_map.get(format, np.uint8)
|
|
55
|
+
np_arr = np.frombuffer(mv, dtype=dtype).reshape(shape) # parse
|
|
56
|
+
mv = memoryview(np_arr) # recreate memoryview
|
|
57
|
+
elif mv.shape != shape:
|
|
58
|
+
mv = mv.cast(format, shape) # cast directly
|
|
59
|
+
return mx.array(mv)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if mx is not None:
|
|
63
|
+
MLXSerislizer.register(mx.array)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Copyright 2022-2025 XProbe Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from .core import VirtualEnvManager
|
|
20
|
+
from .uv import UVVirtualEnvManager
|
|
21
|
+
|
|
22
|
+
_name_to_managers = {"uv": UVVirtualEnvManager}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_virtual_env_manager(env_name: str, env_path: str | Path) -> VirtualEnvManager:
|
|
26
|
+
try:
|
|
27
|
+
manager_cls = _name_to_managers[env_name]
|
|
28
|
+
except KeyError:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Unknown virtualenv manager {env_name}, available: {list(_name_to_managers)}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
path = Path(env_path)
|
|
34
|
+
return manager_cls(path)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2022-2025 XProbe Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VirtualEnvManager(ABC):
|
|
22
|
+
@classmethod
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def is_available(cls):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def __init__(self, env_path: Path):
|
|
28
|
+
self.env_path = env_path.resolve()
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def create_env(self, python_path: Path | None = None) -> None:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def install_packages(self, packages: list[str], **kwargs):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def cancel_install(self):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_lib_path(self) -> str:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def remove_env(self):
|
|
48
|
+
pass
|
xoscar/virtualenv/uv.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Copyright 2022-2025 XProbe Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sysconfig
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from .core import VirtualEnvManager
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class UVVirtualEnvManager(VirtualEnvManager):
|
|
27
|
+
def __init__(self, env_path: Path):
|
|
28
|
+
super().__init__(env_path)
|
|
29
|
+
self._install_process: Optional[subprocess.Popen] = None
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def is_available(cls):
|
|
33
|
+
return shutil.which("uv") is not None
|
|
34
|
+
|
|
35
|
+
def create_env(self, python_path: Path | None = None) -> None:
|
|
36
|
+
cmd = ["uv", "venv", str(self.env_path)]
|
|
37
|
+
if python_path:
|
|
38
|
+
cmd += ["--python", str(python_path)]
|
|
39
|
+
subprocess.run(cmd, check=True)
|
|
40
|
+
|
|
41
|
+
def install_packages(self, packages: list[str], **kwargs):
|
|
42
|
+
"""
|
|
43
|
+
Install packages into the virtual environment using uv.
|
|
44
|
+
Supports pip-compatible kwargs: index_url, extra_index_url, find_links.
|
|
45
|
+
"""
|
|
46
|
+
if not packages:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
cmd = ["uv", "pip", "install", "-p", str(self.env_path)] + packages
|
|
50
|
+
|
|
51
|
+
# Handle known pip-related kwargs
|
|
52
|
+
if "index_url" in kwargs and kwargs["index_url"]:
|
|
53
|
+
cmd += ["-i", kwargs["index_url"]]
|
|
54
|
+
if "extra_index_url" in kwargs and kwargs["extra_index_url"]:
|
|
55
|
+
cmd += ["--extra-index-url", kwargs["extra_index_url"]]
|
|
56
|
+
if "find_links" in kwargs and kwargs["find_links"]:
|
|
57
|
+
cmd += ["-f", kwargs["find_links"]]
|
|
58
|
+
if "trusted_host" in kwargs and kwargs["trusted_host"]:
|
|
59
|
+
cmd += ["--trusted-host", kwargs["trusted_host"]]
|
|
60
|
+
|
|
61
|
+
self._install_process = subprocess.Popen(
|
|
62
|
+
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
stdout, stderr = self._install_process.communicate()
|
|
66
|
+
returncode = self._install_process.returncode
|
|
67
|
+
|
|
68
|
+
self._install_process = None # install finished, clear reference
|
|
69
|
+
|
|
70
|
+
if returncode != 0:
|
|
71
|
+
raise subprocess.CalledProcessError(
|
|
72
|
+
returncode, cmd, output=stdout, stderr=stderr
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def cancel_install(self):
|
|
76
|
+
if self._install_process and self._install_process.poll() is None:
|
|
77
|
+
self._install_process.terminate()
|
|
78
|
+
self._install_process.wait()
|
|
79
|
+
|
|
80
|
+
def get_lib_path(self) -> str:
|
|
81
|
+
return sysconfig.get_path("purelib", vars={"base": str(self.env_path)})
|
|
82
|
+
|
|
83
|
+
def remove_env(self):
|
|
84
|
+
if self.env_path.exists():
|
|
85
|
+
shutil.rmtree(self.env_path, ignore_errors=True)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: xoscar
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Python actor framework for heterogeneous computing.
|
|
5
5
|
Home-page: http://github.com/xorbitsai/xoscar
|
|
6
6
|
Author: Qin Xuye
|
|
@@ -40,6 +40,7 @@ Requires-Dist: pydata-sphinx-theme>=0.3.0; extra == "dev"
|
|
|
40
40
|
Requires-Dist: sphinx-intl>=0.9.9; extra == "dev"
|
|
41
41
|
Requires-Dist: flake8>=3.8.0; extra == "dev"
|
|
42
42
|
Requires-Dist: black; extra == "dev"
|
|
43
|
+
Requires-Dist: uv; extra == "dev"
|
|
43
44
|
Provides-Extra: doc
|
|
44
45
|
Requires-Dist: ipython>=6.5.0; extra == "doc"
|
|
45
46
|
Requires-Dist: sphinx; extra == "doc"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
xoscar-0.
|
|
2
|
-
xoscar-0.
|
|
3
|
-
xoscar-0.
|
|
4
|
-
xoscar-0.
|
|
5
|
-
xoscar/_utils.pyx,sha256=
|
|
1
|
+
xoscar-0.6.0.dist-info/RECORD,,
|
|
2
|
+
xoscar-0.6.0.dist-info/WHEEL,sha256=FoxzhRRMXcm8mSmqIxEPOmfhajsNIlMLdYtHdyz-ZKQ,109
|
|
3
|
+
xoscar-0.6.0.dist-info/top_level.txt,sha256=vYlqqY4Nys8Thm1hePIuUv8eQePdULVWMmt7lXtX_ZA,21
|
|
4
|
+
xoscar-0.6.0.dist-info/METADATA,sha256=PxvcFM64ACvq8BuPaniMOGyosrQHR0u7aBcnnBtW-1w,9076
|
|
5
|
+
xoscar/_utils.pyx,sha256=6iqO4eTwEI-n9i39n_TKz7MWqbytMRnVNubdJ5egL6o,7279
|
|
6
6
|
xoscar/backend.py,sha256=is436OPkZfSpQXaoqTRVta5eoye_pp45RFgCstAk2hU,1850
|
|
7
|
-
xoscar/core.pxd,sha256=
|
|
7
|
+
xoscar/core.pxd,sha256=I_C2ka7XryyGnnAVXUVm8xfS1gtIrCs6X-9rswgOcUU,1317
|
|
8
8
|
xoscar/_version.py,sha256=ClSPrUjgGRGHIkVMQV9XQnkQ-n0akJMnq_rh819nqFE,23719
|
|
9
9
|
xoscar/context.pxd,sha256=qKa0OyDPZtVymftSh447m-RzFZgmz8rGqQBa7qlauvc,725
|
|
10
10
|
xoscar/batch.py,sha256=DpArS0L3WYJ_HVPG-6hSYEwoAFY1mY2-mlC4Jp5M_Dw,7872
|
|
@@ -12,18 +12,18 @@ xoscar/nvutils.py,sha256=qmW4mKLU0WB2yCs198ccQOgLL02zB7Fsa-AotO3NOmg,20412
|
|
|
12
12
|
xoscar/constants.py,sha256=QHHSREw6uWBBjQDCFqlNfTvBZgniJPGy42KSIsR8Fqw,787
|
|
13
13
|
xoscar/__init__.py,sha256=0zX8kKaio3ZIrlzB79WybcravMJw1OxPWjDspTgJFyQ,1608
|
|
14
14
|
xoscar/api.py,sha256=3hztPoOxg8A_mlhWyWgVP7FMXG0PATA1TP4Rbaj7A-g,13327
|
|
15
|
-
xoscar/core.cpython-39-darwin.so,sha256=
|
|
15
|
+
xoscar/core.cpython-39-darwin.so,sha256=7B1z1qOb6-ruBdnK7d1yArz978q5xEBkI9CuoY_g39w,448640
|
|
16
16
|
xoscar/utils.py,sha256=jUw6OICZUPBbmS1b3GE4vLctJf6fCKXrYtLtBuK-Oqc,16483
|
|
17
17
|
xoscar/debug.py,sha256=9Z8SgE2WaKYQcyDo-5-DxEJQ533v7kWjrvCd28pSx3E,5069
|
|
18
18
|
xoscar/libcpp.pxd,sha256=DJqBxLFOKL4iRr9Kale5UH3rbvPRD1x5bTSOPHFpz9I,1147
|
|
19
19
|
xoscar/context.pyx,sha256=8CdgPnWcE9eOp3N600WgDQ03MCi8P73eUOGcfV7Zksg,10942
|
|
20
20
|
xoscar/errors.py,sha256=wBlQOKsXf0Fc4skN39tDie0YZT-VIAuLNRgoDl2pZcA,1241
|
|
21
|
-
xoscar/_utils.cpython-39-darwin.so,sha256=
|
|
22
|
-
xoscar/core.pyx,sha256=
|
|
21
|
+
xoscar/_utils.cpython-39-darwin.so,sha256=kqK1QYSwM4zToUOHpksMz5_8c_Q2ECcqD-RjXRYg6fQ,160888
|
|
22
|
+
xoscar/core.pyx,sha256=phN-yYV0A0QI8WFi2jCu0nc4CnShTepfDi0V7ZrLYPY,22092
|
|
23
23
|
xoscar/driver.py,sha256=498fowtJr6b3FE8FIOA_Tc1Vwx88nfZw7p0FxrML0h4,1372
|
|
24
24
|
xoscar/profiling.py,sha256=BC5OF0HzSaXv8V7w-y-B8r5gV5DgxHFoTEIF6jCMioQ,8015
|
|
25
25
|
xoscar/_utils.pxd,sha256=5KYAL3jfPdejsHnrGGT2s--ZUX5SXznQWpHVSno429k,1157
|
|
26
|
-
xoscar/context.cpython-39-darwin.so,sha256=
|
|
26
|
+
xoscar/context.cpython-39-darwin.so,sha256=1oIkZ4FobP4XFhqAzKO9sYLHmO5gTG9JLg1XKyUnLk8,200448
|
|
27
27
|
xoscar/metrics/__init__.py,sha256=9Badi7rxYikGm2dQiNCrj9GgMRBxwuR3JaEKcFZmfak,705
|
|
28
28
|
xoscar/metrics/api.py,sha256=BBlMIFvVAGVfrtpeJ1YlH9Tqhy9OzGavwvGyeHcQ0Tk,8856
|
|
29
29
|
xoscar/metrics/backends/__init__.py,sha256=h_JgzSqV5lP6vQ6XX_17kE4IY4BRnvKta_7VLQAL1ms,581
|
|
@@ -32,7 +32,7 @@ xoscar/metrics/backends/prometheus/__init__.py,sha256=h_JgzSqV5lP6vQ6XX_17kE4IY4
|
|
|
32
32
|
xoscar/metrics/backends/prometheus/prometheus_metric.py,sha256=MxoMvVrg0pOkKpkjJ0PcAuEaaEJR2FZljmPrLjQ1-oc,2050
|
|
33
33
|
xoscar/metrics/backends/console/console_metric.py,sha256=y5CCtH33j3AqI5_Uhwi4mgOcAhyhb4cWv_YvR6fxcbQ,2082
|
|
34
34
|
xoscar/metrics/backends/console/__init__.py,sha256=h_JgzSqV5lP6vQ6XX_17kE4IY4BRnvKta_7VLQAL1ms,581
|
|
35
|
-
xoscar/collective/xoscar_pygloo.cpython-39-darwin.so,sha256=
|
|
35
|
+
xoscar/collective/xoscar_pygloo.cpython-39-darwin.so,sha256=woE63XGbZAJyAq7gfz53Gd_cFY6rTaGYo5VahtEi8A0,1264200
|
|
36
36
|
xoscar/collective/__init__.py,sha256=XsClIkO_3Jd8GDifYuAbZCmJLAo9ZqGvnjUn9iuogmU,774
|
|
37
37
|
xoscar/collective/core.py,sha256=NVR-7Iaq3aDPCN6fgXcq9Ew6uFEszRwxYqmUG9FLcws,23502
|
|
38
38
|
xoscar/collective/common.py,sha256=INAnISbfnRicbbbDHTqbSr9ITb89ZphH5BUkSpEdXXU,3561
|
|
@@ -41,22 +41,23 @@ xoscar/collective/process_group.py,sha256=zy7LcIFnEcmrcxuECI89v0bQlUbSqQMkVyBw46
|
|
|
41
41
|
xoscar/serialization/exception.py,sha256=Jy8Lsk0z-VJyEUaWeuZIwkmxqaoB-nLKMa1D15Cl4js,1634
|
|
42
42
|
xoscar/serialization/pyfury.py,sha256=sifOnVMYoS82PzZEkzkfxesmMHei23k5UAUUKUyoOYQ,1163
|
|
43
43
|
xoscar/serialization/core.pxd,sha256=k4RoJgX5E5LGs4jdCQ7vvcn26MabXbrWoWhkO49X6YI,985
|
|
44
|
-
xoscar/serialization/__init__.py,sha256=
|
|
44
|
+
xoscar/serialization/__init__.py,sha256=v76XC2OQLp-Yk4_U3_IVguEylMeyRw1UrkU_DPDMh0U,856
|
|
45
45
|
xoscar/serialization/numpy.py,sha256=5Kem87CvpJmzUMp3QHk4WeHU30FoQWTJJP2SwIcaQG0,2919
|
|
46
|
-
xoscar/serialization/core.cpython-39-darwin.so,sha256=
|
|
46
|
+
xoscar/serialization/core.cpython-39-darwin.so,sha256=udMoJ-VLgImjwzGyUm1tjJoNAFcGPOfThuSByEGRNBc,408080
|
|
47
47
|
xoscar/serialization/cuda.py,sha256=iFUEnN4SiquBIhyieyOrfw3TnKnW-tU_vYgqOxO_DrA,3758
|
|
48
48
|
xoscar/serialization/scipy.py,sha256=yOEi0NB8cqQ6e2UnCZ1w006RsB7T725tIL-DM_hNcsU,2482
|
|
49
49
|
xoscar/serialization/aio.py,sha256=5DySPgDxU43ec7_5Ct44-Oqt7YNSJBfuf8VdQgQlChA,4731
|
|
50
50
|
xoscar/serialization/core.pyx,sha256=bjR-zXGm9qersk7kYPzpjpMIxDl_Auur4BCubRfKmfA,29626
|
|
51
|
-
xoscar/
|
|
52
|
-
xoscar/backends/
|
|
51
|
+
xoscar/serialization/mlx.py,sha256=N_cvbTUBKc14XWYsPIMz4kDstyRN1DNhb4BVRgnQm8Y,1872
|
|
52
|
+
xoscar/backends/message.cpython-39-darwin.so,sha256=YZiXRkw7ntjXQ4ejArXZxFKroNpCKbFfXNbwCAw1rKk,387584
|
|
53
|
+
xoscar/backends/config.py,sha256=4tZMiXAMMS8qQ4SX_LjONLtSQVfZTx3m-IK3EqbkYdk,5375
|
|
53
54
|
xoscar/backends/allocate_strategy.py,sha256=tC1Nbq2tJohahUwd-zoRYHEDX65wyuX8tmeY45uWj_w,4845
|
|
54
55
|
xoscar/backends/__init__.py,sha256=VHEBQcUWM5bj027W8EUf9PiJUAP7JoMrRw3Tsvy5ySw,643
|
|
55
|
-
xoscar/backends/core.py,sha256=
|
|
56
|
-
xoscar/backends/context.py,sha256=
|
|
57
|
-
xoscar/backends/router.py,sha256=
|
|
58
|
-
xoscar/backends/message.pyx,sha256=
|
|
59
|
-
xoscar/backends/pool.py,sha256=
|
|
56
|
+
xoscar/backends/core.py,sha256=EH-fHlV9x3bnruEHaUtGYO7osKLfLJ4AQHtuzA_mr2g,10857
|
|
57
|
+
xoscar/backends/context.py,sha256=XfDPG2eDhAhE6hWBEkEsHTnyyOYN9R3houlMjAL7BFw,16329
|
|
58
|
+
xoscar/backends/router.py,sha256=MVl5naz-FYf-Wla7XRn3kRxOpWV0SjKDsKNluifVA8M,10532
|
|
59
|
+
xoscar/backends/message.pyx,sha256=krGVtZ1YDaZX8yWhaNHwZiudQooLvcGlw6x3Sq7jxjE,19685
|
|
60
|
+
xoscar/backends/pool.py,sha256=OezOhvvXAV3TpODhLHmJVgqCfowb3aA_fWZKPodm8bE,61003
|
|
60
61
|
xoscar/backends/indigen/backend.py,sha256=znl_fZzWGEtLH8hZ9j9Kkf0fva25jEem2_KO7I1RVvc,1612
|
|
61
62
|
xoscar/backends/indigen/__init__.py,sha256=tKHP5ClzedBRBpZsLRVErR3EUNbbDm4CY4u0rCFJr44,685
|
|
62
63
|
xoscar/backends/indigen/driver.py,sha256=VGzkacYKykegW5qhCuhx01gdgBZEKJjNIyfNCnA6Nm8,952
|
|
@@ -65,11 +66,11 @@ xoscar/backends/test/backend.py,sha256=nv9WFhH5Bbq4Q1HB9yfpciZBaeHT4IQAtzugBWESr
|
|
|
65
66
|
xoscar/backends/test/__init__.py,sha256=j2ZfD6prD9WjUxRUDC7Eq5Z7N7TkL6fFr59oNyc_vY4,682
|
|
66
67
|
xoscar/backends/test/pool.py,sha256=TW4X6J-92Pti66103poQBNDBznX6CBD3RLOc_zixjTo,7257
|
|
67
68
|
xoscar/backends/communication/ucx.py,sha256=_Dp9Ld2MWIa1txSGMnmfYwJDT0esxS-GOd2FQ4BdHiM,19960
|
|
68
|
-
xoscar/backends/communication/__init__.py,sha256=
|
|
69
|
+
xoscar/backends/communication/__init__.py,sha256=oFIg83Ga93-AhrG52TE85Z2LgpGZu1RCgQu1RWi62zQ,1063
|
|
69
70
|
xoscar/backends/communication/core.py,sha256=sJeE3foRIqVPXldzYpFKHDSsabfAIFBU4JuXY4OyklY,2130
|
|
70
71
|
xoscar/backends/communication/utils.py,sha256=AmovE-hmWLXNCPwHafYuaRjOk8m42BUyT3XBqfXQRVI,3664
|
|
71
72
|
xoscar/backends/communication/errors.py,sha256=V3CdBe2xX9Rwv32f2dH2Msc84yaUhlyerZ42-739o1Q,723
|
|
72
|
-
xoscar/backends/communication/socket.py,sha256=
|
|
73
|
+
xoscar/backends/communication/socket.py,sha256=6Pf9RJWHuvobjZ1eAU6bUuGY7PzYga3Vyc4PAKF-k2M,14428
|
|
73
74
|
xoscar/backends/communication/dummy.py,sha256=6kLkxjNk4xTQ-IlNZD6cftNCx5UsGOur2jk7ikrNUCg,8157
|
|
74
75
|
xoscar/backends/communication/base.py,sha256=0P4Tr35GSWpRp394e9jVWUUoKKa-gIk177eYPw1BnSU,7421
|
|
75
76
|
xoscar/aio/__init__.py,sha256=kViDKR_kJe59VQViHITKEfBcIgN4ZJblUyd8zl0E3ZI,675
|
|
@@ -77,3 +78,6 @@ xoscar/aio/file.py,sha256=PBtkLp-Q7XtYl-zk00s18TtgIrkNr60J3Itf66ctO1o,1486
|
|
|
77
78
|
xoscar/aio/lru.py,sha256=rpXCqSLtPV5xnWtd6uDwQQFGgIPEgvmWEQDkPNUx9cM,6311
|
|
78
79
|
xoscar/aio/parallelism.py,sha256=VSsjk8wP-Bw7tLeUsTyLVNgp91thjxEfE3pCrw_vF5Q,1293
|
|
79
80
|
xoscar/aio/base.py,sha256=9j0f1piwfE5R5GIvV212vSD03ixdaeSzSSsO2kxJZVE,2249
|
|
81
|
+
xoscar/virtualenv/__init__.py,sha256=65t9_X1DvbanNjFy366SiiWZrRTpa9SXWMXPmqayE-4,1117
|
|
82
|
+
xoscar/virtualenv/core.py,sha256=YMN6yHoNeEc8ecbJMbZkKeWKUABK1mUZ_OYOjcbRqWs,1263
|
|
83
|
+
xoscar/virtualenv/uv.py,sha256=wP0wVgWkncABpXnNRVPuOPaeIP7fy3c0LRVrKBFtqU8,3042
|
|
File without changes
|
|
File without changes
|