xoscar 0.9.0__cp312-cp312-macosx_10_13_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.
Files changed (94) hide show
  1. xoscar/__init__.py +61 -0
  2. xoscar/_utils.cpython-312-darwin.so +0 -0
  3. xoscar/_utils.pxd +36 -0
  4. xoscar/_utils.pyx +246 -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 +527 -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 +253 -0
  19. xoscar/backends/communication/errors.py +20 -0
  20. xoscar/backends/communication/socket.py +444 -0
  21. xoscar/backends/communication/ucx.py +538 -0
  22. xoscar/backends/communication/utils.py +97 -0
  23. xoscar/backends/config.py +157 -0
  24. xoscar/backends/context.py +437 -0
  25. xoscar/backends/core.py +352 -0
  26. xoscar/backends/indigen/__init__.py +16 -0
  27. xoscar/backends/indigen/__main__.py +19 -0
  28. xoscar/backends/indigen/backend.py +51 -0
  29. xoscar/backends/indigen/driver.py +26 -0
  30. xoscar/backends/indigen/fate_sharing.py +221 -0
  31. xoscar/backends/indigen/pool.py +515 -0
  32. xoscar/backends/indigen/shared_memory.py +548 -0
  33. xoscar/backends/message.cpython-312-darwin.so +0 -0
  34. xoscar/backends/message.pyi +255 -0
  35. xoscar/backends/message.pyx +646 -0
  36. xoscar/backends/pool.py +1630 -0
  37. xoscar/backends/router.py +285 -0
  38. xoscar/backends/test/__init__.py +16 -0
  39. xoscar/backends/test/backend.py +38 -0
  40. xoscar/backends/test/pool.py +233 -0
  41. xoscar/batch.py +256 -0
  42. xoscar/collective/__init__.py +27 -0
  43. xoscar/collective/backend/__init__.py +13 -0
  44. xoscar/collective/backend/nccl_backend.py +160 -0
  45. xoscar/collective/common.py +102 -0
  46. xoscar/collective/core.py +737 -0
  47. xoscar/collective/process_group.py +687 -0
  48. xoscar/collective/utils.py +41 -0
  49. xoscar/collective/xoscar_pygloo.cpython-312-darwin.so +0 -0
  50. xoscar/collective/xoscar_pygloo.pyi +239 -0
  51. xoscar/constants.py +23 -0
  52. xoscar/context.cpython-312-darwin.so +0 -0
  53. xoscar/context.pxd +21 -0
  54. xoscar/context.pyx +368 -0
  55. xoscar/core.cpython-312-darwin.so +0 -0
  56. xoscar/core.pxd +51 -0
  57. xoscar/core.pyx +664 -0
  58. xoscar/debug.py +188 -0
  59. xoscar/driver.py +42 -0
  60. xoscar/errors.py +63 -0
  61. xoscar/libcpp.pxd +31 -0
  62. xoscar/metrics/__init__.py +21 -0
  63. xoscar/metrics/api.py +288 -0
  64. xoscar/metrics/backends/__init__.py +13 -0
  65. xoscar/metrics/backends/console/__init__.py +13 -0
  66. xoscar/metrics/backends/console/console_metric.py +82 -0
  67. xoscar/metrics/backends/metric.py +149 -0
  68. xoscar/metrics/backends/prometheus/__init__.py +13 -0
  69. xoscar/metrics/backends/prometheus/prometheus_metric.py +70 -0
  70. xoscar/nvutils.py +717 -0
  71. xoscar/profiling.py +260 -0
  72. xoscar/serialization/__init__.py +20 -0
  73. xoscar/serialization/aio.py +141 -0
  74. xoscar/serialization/core.cpython-312-darwin.so +0 -0
  75. xoscar/serialization/core.pxd +28 -0
  76. xoscar/serialization/core.pyi +57 -0
  77. xoscar/serialization/core.pyx +944 -0
  78. xoscar/serialization/cuda.py +111 -0
  79. xoscar/serialization/exception.py +48 -0
  80. xoscar/serialization/mlx.py +67 -0
  81. xoscar/serialization/numpy.py +82 -0
  82. xoscar/serialization/pyfury.py +37 -0
  83. xoscar/serialization/scipy.py +72 -0
  84. xoscar/serialization/torch.py +180 -0
  85. xoscar/utils.py +522 -0
  86. xoscar/virtualenv/__init__.py +34 -0
  87. xoscar/virtualenv/core.py +268 -0
  88. xoscar/virtualenv/platform.py +56 -0
  89. xoscar/virtualenv/utils.py +100 -0
  90. xoscar/virtualenv/uv.py +321 -0
  91. xoscar-0.9.0.dist-info/METADATA +230 -0
  92. xoscar-0.9.0.dist-info/RECORD +94 -0
  93. xoscar-0.9.0.dist-info/WHEEL +6 -0
  94. xoscar-0.9.0.dist-info/top_level.txt +2 -0
xoscar/utils.py ADDED
@@ -0,0 +1,522 @@
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
+ import dataclasses
20
+ import functools
21
+ import importlib
22
+ import importlib.util as importlib_utils
23
+ import inspect
24
+ import io
25
+ import logging
26
+ import os
27
+ import random
28
+ import socket
29
+ import sys
30
+ import time
31
+ import uuid
32
+ from abc import ABC
33
+ from functools import lru_cache
34
+ from types import TracebackType
35
+ from typing import Callable, Type, Union
36
+
37
+ from ._utils import ( # noqa: F401 # pylint: disable=unused-import
38
+ NamedType,
39
+ Timer,
40
+ TypeDispatcher,
41
+ to_binary,
42
+ to_str,
43
+ )
44
+
45
+ # Please refer to https://bugs.python.org/issue41451
46
+ try:
47
+
48
+ class _Dummy(ABC):
49
+ __slots__ = ("__weakref__",)
50
+
51
+ abc_type_require_weakref_slot = True
52
+ except TypeError:
53
+ abc_type_require_weakref_slot = False
54
+
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ _memory_size_indices = {"": 0, "k": 1, "m": 2, "g": 3, "t": 4}
60
+
61
+
62
+ def parse_readable_size(value: str | int | float) -> tuple[float, bool]:
63
+ if isinstance(value, (int, float)):
64
+ return float(value), False
65
+
66
+ value = value.strip().lower()
67
+ num_pos = 0
68
+ while num_pos < len(value) and value[num_pos] in "0123456789.-":
69
+ num_pos += 1
70
+
71
+ value, suffix = value[:num_pos], value[num_pos:]
72
+ suffix = suffix.strip()
73
+ if suffix.endswith("%"):
74
+ return float(value) / 100, True
75
+
76
+ try:
77
+ return float(value) * (1024 ** _memory_size_indices[suffix[:1]]), False
78
+ except (ValueError, KeyError):
79
+ raise ValueError(f"Unknown limitation value: {value}")
80
+
81
+
82
+ def wrap_exception(
83
+ exc: BaseException,
84
+ bases: tuple[Type] | tuple | None = None,
85
+ wrap_name: str | None = None,
86
+ message: str | None = None,
87
+ traceback: TracebackType | None = None,
88
+ attr_dict: dict | None = None,
89
+ ) -> BaseException:
90
+ """Generate an exception wraps the cause exception."""
91
+
92
+ def __init__(self, *args, **kwargs):
93
+ pass
94
+
95
+ def __getattr__(self, item):
96
+ return getattr(exc, item)
97
+
98
+ def __str__(self):
99
+ return message or super(type(self), self).__str__()
100
+
101
+ traceback = traceback or exc.__traceback__
102
+ bases = bases or ()
103
+ attr_dict = attr_dict or {}
104
+ attr_dict.update(
105
+ {
106
+ "__init__": __init__,
107
+ "__getattr__": __getattr__,
108
+ "__str__": __str__,
109
+ "__wrapname__": wrap_name,
110
+ "__wrapped__": exc,
111
+ "__module__": type(exc).__module__,
112
+ "__cause__": exc.__cause__,
113
+ "__context__": exc.__context__,
114
+ "__suppress_context__": exc.__suppress_context__,
115
+ "args": exc.args,
116
+ }
117
+ )
118
+ new_exc_type = type(type(exc).__name__, bases + (type(exc),), attr_dict)
119
+ return new_exc_type().with_traceback(traceback)
120
+
121
+
122
+ # from https://github.com/ericvsmith/dataclasses/blob/master/dataclass_tools.py
123
+ # released under Apache License 2.0
124
+ def dataslots(cls):
125
+ # Need to create a new class, since we can't set __slots__
126
+ # after a class has been created.
127
+
128
+ # Make sure __slots__ isn't already set.
129
+ if "__slots__" in cls.__dict__: # pragma: no cover
130
+ raise TypeError(f"{cls.__name__} already specifies __slots__")
131
+
132
+ # Create a new dict for our new class.
133
+ cls_dict = dict(cls.__dict__)
134
+ field_names = tuple(f.name for f in dataclasses.fields(cls))
135
+ cls_dict["__slots__"] = field_names
136
+ for field_name in field_names:
137
+ # Remove our attributes, if present. They'll still be
138
+ # available in _MARKER.
139
+ cls_dict.pop(field_name, None)
140
+ # Remove __dict__ itself.
141
+ cls_dict.pop("__dict__", None)
142
+ # And finally create the class.
143
+ qualname = getattr(cls, "__qualname__", None)
144
+ cls = type(cls)(cls.__name__, cls.__bases__, cls_dict)
145
+ if qualname is not None:
146
+ cls.__qualname__ = qualname
147
+ return cls
148
+
149
+
150
+ def implements(f: Callable):
151
+ def decorator(g):
152
+ g.__doc__ = f.__doc__
153
+ return g
154
+
155
+ return decorator
156
+
157
+
158
+ class classproperty:
159
+ def __init__(self, f):
160
+ self.f = f
161
+
162
+ def __get__(self, obj, owner):
163
+ return self.f(owner)
164
+
165
+
166
+ LOW_PORT_BOUND = 10000
167
+ HIGH_PORT_BOUND = 65535
168
+ _local_occupied_ports: set = set()
169
+
170
+
171
+ def _get_ports_from_netstat() -> set[int]:
172
+ import subprocess
173
+
174
+ while True:
175
+ p = subprocess.Popen("netstat -a -n -p tcp".split(), stdout=subprocess.PIPE)
176
+ try:
177
+ outs, _ = p.communicate(timeout=5)
178
+ lines = outs.split(to_binary(os.linesep))
179
+ occupied = set()
180
+ for line in lines:
181
+ if b"." not in line:
182
+ continue
183
+ line_str: str = to_str(line)
184
+ for part in line_str.split():
185
+ # in windows, netstat uses ':' to separate host and port
186
+ part = part.replace(":", ".")
187
+ if "." in part:
188
+ _, port_str = part.rsplit(".", 1)
189
+ if port_str == "*":
190
+ continue
191
+ port = int(port_str)
192
+ if LOW_PORT_BOUND <= port <= HIGH_PORT_BOUND:
193
+ occupied.add(int(port_str))
194
+ break
195
+ return occupied
196
+ except subprocess.TimeoutExpired:
197
+ p.kill()
198
+ continue
199
+
200
+
201
+ def get_next_port(typ: int | None = None, occupy: bool = True) -> int:
202
+ import psutil
203
+
204
+ if sys.platform.lower().startswith("win"):
205
+ occupied = _get_ports_from_netstat()
206
+ else:
207
+ try:
208
+ conns = psutil.net_connections()
209
+ typ = typ or socket.SOCK_STREAM
210
+ occupied = set(
211
+ sc.laddr.port
212
+ for sc in conns
213
+ if sc.type == typ and LOW_PORT_BOUND <= sc.laddr.port <= HIGH_PORT_BOUND
214
+ )
215
+ except psutil.AccessDenied:
216
+ occupied = _get_ports_from_netstat()
217
+
218
+ occupied.update(_local_occupied_ports)
219
+ random.seed(uuid.uuid1().bytes)
220
+ randn = random.randint(0, 100000000)
221
+
222
+ idx = int(randn % (1 + HIGH_PORT_BOUND - LOW_PORT_BOUND - len(occupied)))
223
+ for i in range(LOW_PORT_BOUND, HIGH_PORT_BOUND + 1):
224
+ if i in occupied:
225
+ continue
226
+ if idx == 0:
227
+ if occupy:
228
+ _local_occupied_ports.add(i)
229
+ return i
230
+ idx -= 1
231
+ raise SystemError("No ports available.")
232
+
233
+
234
+ def lazy_import(
235
+ name: str,
236
+ package: str | None = None,
237
+ globals: dict | None = None, # pylint: disable=redefined-builtin
238
+ locals: dict | None = None, # pylint: disable=redefined-builtin
239
+ rename: str | None = None,
240
+ placeholder: bool = False,
241
+ ):
242
+ rename = rename or name
243
+ prefix_name = name.split(".", 1)[0]
244
+ globals = globals or inspect.currentframe().f_back.f_globals # type: ignore
245
+
246
+ class LazyModule:
247
+ def __init__(self):
248
+ self._on_loads = []
249
+
250
+ def __getattr__(self, item):
251
+ if item.startswith("_pytest") or item in ("__bases__", "__test__"):
252
+ raise AttributeError(item)
253
+
254
+ real_mod = importlib.import_module(name, package=package)
255
+ if rename in globals:
256
+ globals[rename] = real_mod
257
+ elif locals is not None:
258
+ locals[rename] = real_mod
259
+ ret = getattr(real_mod, item)
260
+ for on_load_func in self._on_loads:
261
+ on_load_func()
262
+ # make sure on_load hooks only executed once
263
+ self._on_loads = []
264
+ return ret
265
+
266
+ def add_load_handler(self, func: Callable):
267
+ self._on_loads.append(func)
268
+ return func
269
+
270
+ if importlib_utils.find_spec(prefix_name) is not None:
271
+ return LazyModule()
272
+ elif placeholder:
273
+ return ModulePlaceholder(prefix_name)
274
+ else:
275
+ return None
276
+
277
+
278
+ def lazy_import_on_load(lazy_mod):
279
+ def wrapper(fun):
280
+ if lazy_mod is not None and hasattr(lazy_mod, "add_load_handler"):
281
+ lazy_mod.add_load_handler(fun)
282
+ return fun
283
+
284
+ return wrapper
285
+
286
+
287
+ class ModulePlaceholder:
288
+ def __init__(self, mod_name: str):
289
+ self._mod_name = mod_name
290
+
291
+ def _raises(self):
292
+ raise AttributeError(f"{self._mod_name} is required but not installed.")
293
+
294
+ def __getattr__(self, key):
295
+ self._raises()
296
+
297
+ def __call__(self, *_args, **_kwargs):
298
+ self._raises()
299
+
300
+
301
+ def patch_asyncio_task_create_time(): # pragma: no cover
302
+ new_loop = False
303
+ try:
304
+ loop = asyncio.get_running_loop()
305
+ except RuntimeError:
306
+ loop = asyncio.new_event_loop()
307
+ new_loop = True
308
+ loop_class = loop.__class__
309
+ # Save raw loop_class.create_task and make multiple apply idempotent
310
+ loop_create_task = getattr(
311
+ patch_asyncio_task_create_time, "loop_create_task", loop_class.create_task
312
+ )
313
+ patch_asyncio_task_create_time.loop_create_task = loop_create_task
314
+
315
+ def new_loop_create_task(*args, **kwargs):
316
+ task = loop_create_task(*args, **kwargs)
317
+ task.__xoscar_asyncio_task_create_time__ = time.time()
318
+ return task
319
+
320
+ if loop_create_task is not new_loop_create_task:
321
+ loop_class.create_task = new_loop_create_task
322
+ if not new_loop and loop.create_task is not new_loop_create_task:
323
+ loop.create_task = functools.partial(new_loop_create_task, loop)
324
+
325
+
326
+ async def asyncio_task_timeout_detector(
327
+ check_interval: int, task_timeout_seconds: int, task_exclude_filters: list[str]
328
+ ):
329
+ task_exclude_filters.append("asyncio_task_timeout_detector")
330
+ while True: # pragma: no cover
331
+ await asyncio.sleep(check_interval)
332
+ loop = asyncio.get_running_loop()
333
+ current_time = (
334
+ time.time()
335
+ ) # avoid invoke `time.time()` frequently if we have plenty of unfinished tasks.
336
+ for task in asyncio.all_tasks(loop=loop):
337
+ # Some task may be create before `patch_asyncio_task_create_time` applied, take them as never timeout.
338
+ create_time = getattr(
339
+ task, "__xoscar_asyncio_task_create_time__", current_time
340
+ )
341
+ if current_time - create_time >= task_timeout_seconds:
342
+ stack = io.StringIO()
343
+ task.print_stack(file=stack)
344
+ task_str = str(task)
345
+ if any(
346
+ excluded_task in task_str for excluded_task in task_exclude_filters
347
+ ):
348
+ continue
349
+ logger.warning(
350
+ """Task %s in event loop %s doesn't finish in %s seconds. %s""",
351
+ task,
352
+ loop,
353
+ time.time() - create_time,
354
+ stack.getvalue(),
355
+ )
356
+
357
+
358
+ def register_asyncio_task_timeout_detector(
359
+ check_interval: int | None = None,
360
+ task_timeout_seconds: int | None = None,
361
+ task_exclude_filters: list[str] | None = None,
362
+ ) -> asyncio.Task | None: # pragma: no cover
363
+ """Register a asyncio task which print timeout task periodically."""
364
+ check_interval = check_interval or int(
365
+ os.environ.get("XOSCAR_DEBUG_ASYNCIO_TASK_TIMEOUT_CHECK_INTERVAL", -1)
366
+ )
367
+ if check_interval > 0:
368
+ patch_asyncio_task_create_time()
369
+ task_timeout_seconds = task_timeout_seconds or int(
370
+ os.environ.get("XOSCAR_DEBUG_ASYNCIO_TASK_TIMEOUT_SECONDS", check_interval)
371
+ )
372
+ if not task_exclude_filters:
373
+ # Ignore Xoscar by default since it has some long-running coroutines.
374
+ task_exclude_filter = os.environ.get(
375
+ "XOSCAR_DEBUG_ASYNCIO_TASK_EXCLUDE_FILTERS", "xoscar"
376
+ )
377
+ task_exclude_filters = task_exclude_filter.split(";")
378
+ if sys.version_info[:2] < (3, 7):
379
+ logger.warning(
380
+ "asyncio tasks timeout detector is not supported under python %s",
381
+ sys.version,
382
+ )
383
+ else:
384
+ loop = asyncio.get_running_loop()
385
+ logger.info(
386
+ "Create asyncio tasks timeout detector with check_interval %s task_timeout_seconds %s "
387
+ "task_exclude_filters %s",
388
+ check_interval,
389
+ task_timeout_seconds,
390
+ task_exclude_filters,
391
+ )
392
+ return loop.create_task(
393
+ asyncio_task_timeout_detector(
394
+ check_interval, task_timeout_seconds, task_exclude_filters
395
+ )
396
+ )
397
+ else:
398
+ return None
399
+
400
+
401
+ def ensure_coverage():
402
+ # make sure coverage is handled when starting with subprocess.Popen
403
+ if (
404
+ not sys.platform.startswith("win") and "COV_CORE_SOURCE" in os.environ
405
+ ): # pragma: no cover
406
+ try:
407
+ from pytest_cov.embed import cleanup_on_sigterm
408
+ except ImportError:
409
+ pass
410
+ else:
411
+ cleanup_on_sigterm()
412
+
413
+
414
+ def retry_callable(
415
+ callable_,
416
+ ex_type: type = Exception,
417
+ wait_interval=1,
418
+ max_retries=-1,
419
+ sync: bool | None = None,
420
+ ):
421
+ if inspect.iscoroutinefunction(callable_) or sync is False:
422
+
423
+ @functools.wraps(callable)
424
+ async def retry_call(*args, **kwargs):
425
+ num_retried = 0
426
+ while max_retries < 0 or num_retried < max_retries:
427
+ num_retried += 1
428
+ try:
429
+ return await callable_(*args, **kwargs)
430
+ except ex_type:
431
+ await asyncio.sleep(wait_interval)
432
+
433
+ else:
434
+
435
+ @functools.wraps(callable)
436
+ def retry_call(*args, **kwargs):
437
+ num_retried = 0
438
+ ex = None
439
+ while max_retries < 0 or num_retried < max_retries:
440
+ num_retried += 1
441
+ try:
442
+ return callable_(*args, **kwargs)
443
+ except ex_type as e:
444
+ ex = e
445
+ time.sleep(wait_interval)
446
+ assert ex is not None
447
+ raise ex # pylint: disable-msg=E0702
448
+
449
+ return retry_call
450
+
451
+
452
+ _cupy = lazy_import("cupy")
453
+ _rmm = lazy_import("rmm")
454
+
455
+
456
+ def is_cuda_buffer(cuda_buffer: Union["_cupy.ndarray", "_rmm.DeviceBuffer"]) -> bool: # type: ignore
457
+ return hasattr(cuda_buffer, "__cuda_array_interface__")
458
+
459
+
460
+ def is_windows():
461
+ return sys.platform.startswith("win")
462
+
463
+
464
+ def is_linux():
465
+ return sys.platform.startswith("linux")
466
+
467
+
468
+ @lru_cache
469
+ def is_py_312():
470
+ return sys.version_info[:2] == (3, 12)
471
+
472
+
473
+ @lru_cache
474
+ def is_py_312_or_above():
475
+ return sys.version_info[:2] >= (3, 12)
476
+
477
+
478
+ def is_v4_zero_ip(ip_port_addr: str) -> bool:
479
+ return ip_port_addr.split("://")[-1].startswith("0.0.0.0:")
480
+
481
+
482
+ def is_v6_zero_ip(ip_port_addr: str) -> bool:
483
+ # tcp6 addr ":::123", ":: means all zero"
484
+ arr = ip_port_addr.split("://")[-1].split(":")
485
+ if len(arr) <= 2: # Not tcp6 or udp6
486
+ return False
487
+ for part in arr[0:-1]:
488
+ if part != "":
489
+ if int(part, 16) != 0:
490
+ return False
491
+ return True
492
+
493
+
494
+ def is_zero_ip(ip_port_addr: str) -> bool:
495
+ return is_v4_zero_ip(ip_port_addr) or is_v6_zero_ip(ip_port_addr)
496
+
497
+
498
+ def is_v6_ip(ip_port_addr: str) -> bool:
499
+ arr = ip_port_addr.split("://", 1)[-1].split(":")
500
+ return len(arr) > 1
501
+
502
+
503
+ def fix_all_zero_ip(remote_addr: str, connect_addr: str) -> str:
504
+ """
505
+ Use connect_addr to fix ActorRef.address return by remote server.
506
+ When remote server listen on "0.0.0.0:port" or ":::port", it will return ActorRef.address set to listening addr,
507
+ it cannot be use by client for the following interaction unless we fix it.
508
+ (client will treat 0.0.0.0 as 127.0.0.1)
509
+
510
+ NOTE: Server might return a different addr from a pool for load-balance purpose.
511
+ """
512
+ if remote_addr == connect_addr:
513
+ return remote_addr
514
+ if not is_v4_zero_ip(remote_addr) and not is_v6_zero_ip(remote_addr):
515
+ # Remote server returns on non-zero ip
516
+ return remote_addr
517
+ if is_v4_zero_ip(connect_addr) or is_v6_zero_ip(connect_addr):
518
+ # Client connect to local server
519
+ return remote_addr
520
+ remote_port = remote_addr.split(":")[-1]
521
+ connect_ip = ":".join(connect_addr.split(":")[0:-1]) # Remote the port
522
+ return f"{connect_ip}:{remote_port}"
@@ -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)