Python-3xui 0.0.9.post2__tar.gz → 0.0.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/PKG-INFO +5 -11
  2. python_3xui-0.0.10/README.md +10 -0
  3. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/pyproject.toml +1 -1
  4. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/__init__.py +2 -1
  5. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/api.py +57 -21
  6. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/base_model.py +3 -3
  7. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/models.py +8 -7
  8. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/util.py +1 -0
  9. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_clients.py +6 -5
  10. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_xuiclient_helpers.py +2 -2
  11. python_3xui-0.0.9.post2/README.md +0 -16
  12. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/.gitignore +0 -0
  13. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/LICENSE +0 -0
  14. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/custom_exceptions.py +0 -0
  15. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/endpoints.py +0 -0
  16. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/conftest.py +0 -0
  17. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/gather_response_stubs.py +0 -0
  18. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/pytest.ini +0 -0
  19. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_endpoints_clients.py +0 -0
  20. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_endpoints_inbounds.py +0 -0
  21. {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_inbounds.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Python-3xui
3
- Version: 0.0.9.post2
3
+ Version: 0.0.10
4
4
  Summary: 3x-ui wrapper for python
5
5
  Project-URL: Homepage, https://github.com/Artem-Potapov/3x-py
6
6
  Project-URL: Issues, https://github.com/Artem-Potapov/3x-py/issues
@@ -28,15 +28,9 @@ Description-Content-Type: text/markdown
28
28
  <p>I'm not expecting much to be honest, so please feel free to fork it if I abandon the project and you need it!</p>
29
29
  <p>Also, if you REALLY want it I can give you the ownership if I step down, you can find my email in the pyproject.toml (I don't check it that much but trust me I do)</p>
30
30
 
31
- <h2>0.0.9 Release Notes</h2>
31
+ <h2>0.0.10 Release Notes</h2>
32
32
  <ul>
33
- <li>Fix _request_update_client for it to actually work and NOT create "zombies"</li>
34
- <li>DTO un-split because fields reset when not provided, so full inbounds must be fetched</li>
35
- <li>New method: update_client_by_tgid</li>
36
- <li>Fixed test suite</li>
37
- <li>Fix from_response and from_list</li>
38
- <li>Remove obsolete and useless client fields from models</li>
39
- <li>Inbound settings actually get parsed properly into ClientsSettings</li>
40
- <li>New asyncio task management so they won't get destroyed when GCed</li>
41
- <li>XUIClient async_lru cache now binds to event loop at runtime, not in initialization</li>
33
+ <li>HOTFIX: make models.SingleInboundClient default flow "", because turns out panel can not return it because of zombification...</li>
34
+ <li>Add a custom uuid generator for XUIClient that <i>defaults</i> to method in util but you can make your own!</li>
35
+ <li>Uncomplicate self.sub_gen into self._resolve_sub</li>
42
36
  </ul>
@@ -0,0 +1,10 @@
1
+ <h1>Hi! This is my example python 3x-ui wrapper!</h1>
2
+ <p>I'm not expecting much to be honest, so please feel free to fork it if I abandon the project and you need it!</p>
3
+ <p>Also, if you REALLY want it I can give you the ownership if I step down, you can find my email in the pyproject.toml (I don't check it that much but trust me I do)</p>
4
+
5
+ <h2>0.0.10 Release Notes</h2>
6
+ <ul>
7
+ <li>HOTFIX: make models.SingleInboundClient default flow "", because turns out panel can not return it because of zombification...</li>
8
+ <li>Add a custom uuid generator for XUIClient that <i>defaults</i> to method in util but you can make your own!</li>
9
+ <li>Uncomplicate self.sub_gen into self._resolve_sub</li>
10
+ </ul>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Python-3xui"
3
- version = "0.0.9r2"
3
+ version = "0.0.10"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -1,9 +1,10 @@
1
1
  """Public package interface for the python_3xui API wrapper."""
2
2
 
3
3
  from .api import XUIClient
4
+ import python_3xui.custom_exceptions as exceptions
4
5
 
5
6
  __author__ = "JustMe_001"
6
- __version__ = "0.0.9r2"
7
+ __version__ = "0.0.10"
7
8
  __email__ = ""
8
9
 
9
10
 
@@ -64,7 +64,8 @@ class XUIClient:
64
64
  totp: TOTP generator used for repeated logins when a secret is provided.
65
65
  max_retries: Maximum number of retry attempts for failed requests.
66
66
  retry_delay: Delay in seconds between retries.
67
- sub_gen: Callable used to derive subscription IDs from Telegram IDs.
67
+ sub_gen: Callable/Awaitable used to derive subscription IDs from Telegram IDs.
68
+ uuid_gen: Callable/Awaitable used to derive UUIDs from Telegram IDs.
68
69
  server_end: Server endpoint handler.
69
70
  clients_end: Clients endpoint handler.
70
71
  inbounds_end: Inbounds endpoint handler.
@@ -76,6 +77,8 @@ class XUIClient:
76
77
  custom_prod_string: str = "testing",
77
78
  max_retries: int = 5, retry_delay=1,
78
79
  custom_sub_generator: Callable[[int], str] | Callable[[int], Awaitable[str]] = util.default_sub_from_tgid,
80
+ custom_uuid_generator: Callable[[int], str] | Callable[[int], Awaitable[str]] = util.get_uuid_from_tgid,
81
+ panel_id: Any = None
79
82
  ) -> None:
80
83
  """Initialize the XUIClient.
81
84
 
@@ -93,6 +96,9 @@ class XUIClient:
93
96
  retry_delay: Seconds to wait between database-lock retries.
94
97
  custom_sub_generator: Sync or async callable that receives a
95
98
  Telegram ID and returns the subscription ID for new clients.
99
+ custom_uuid_generator: Sync or async callable that receives a
100
+ Telegram ID and returns the UUID for new clients.
101
+ panel_id: this is solely for user's purposes to increase logging and accounting clarity. Default is None.
96
102
  """
97
103
  self.connected: bool = False
98
104
  self.PROD_STRING = re.compile(custom_prod_string)
@@ -110,6 +116,8 @@ class XUIClient:
110
116
  self.max_retries: int = max_retries
111
117
  self.retry_delay: int = retry_delay
112
118
  self.sub_gen = custom_sub_generator
119
+ self.uuid_gen = custom_uuid_generator
120
+ self.panel_id: int | str | Any = panel_id
113
121
  # endpoints
114
122
  self.server_end = endpoints.Server(self)
115
123
  self.clients_end = endpoints.Clients(self)
@@ -198,7 +206,7 @@ class XUIClient:
198
206
  if resp.status_code == 404:
199
207
  now: float = datetime.now(UTC).timestamp()
200
208
  if self.session_start is None or now - self.session_start > self.session_duration:
201
- logging.info("Client with IP/Domain %s is not logged in, logging in...", self.base_host)
209
+ logging.info("Client (panel: %s) is not logged in, logging in...", self.panel_id or self.base_host)
202
210
  await self.login()
203
211
  continue
204
212
  else:
@@ -322,7 +330,7 @@ class XUIClient:
322
330
  if self.two_fac_secret:
323
331
  payload["twoFactorCode"] = self.two_fac_secret.get_secret_value()
324
332
 
325
- logging.info("Client is logging in with IP/Domain: %s", self.base_host)
333
+ logging.info("Client is logging in (panel: %s)", self.panel_id or self.base_host)
326
334
  resp = await self.session.post("/login", data=payload)
327
335
  if resp.status_code == 200:
328
336
  resp_json = resp.json()
@@ -343,7 +351,7 @@ class XUIClient:
343
351
  Returns:
344
352
  Self: The XUIClient instance.
345
353
  """
346
- logging.log(DEBUG, "Client connected with IP/domain %s", self.base_url)
354
+ logging.log(DEBUG, "Client connected (panel: %s)", self.panel_id or self.base_url)
347
355
  self.session = AsyncClient(base_url=self.base_url)
348
356
  self.connected = True
349
357
  return self
@@ -373,9 +381,10 @@ class XUIClient:
373
381
  """
374
382
  self.connect()
375
383
  await self.login()
376
- self._cache_cleaner_task = asyncio.create_task(
377
- self._clear_prod_inbound_cache_task(), name=f"inb_cache_clearer_for_{self.base_url}"
378
- )
384
+ if not self._cache_cleaner_task:
385
+ self._cache_cleaner_task = asyncio.create_task(
386
+ self._clear_prod_inbound_cache_task(create_new=True), name=f"inb_cache_clearer_for_{self.base_url}"
387
+ )
379
388
  return self
380
389
 
381
390
  async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
@@ -390,15 +399,34 @@ class XUIClient:
390
399
  exc_tb: The exception traceback, if an exception occurred.
391
400
  """
392
401
  if exc_type is None or exc_type is asyncio.exceptions.CancelledError:
393
- logging.info("Client is disconnecting at time with IP/Domain %s", self.base_host)
402
+ logging.info("Client is disconnecting (panel: %s)", self.panel_id or self.base_host)
394
403
  else:
395
404
  logging.warning("Client is disconnecting due to an error (may be unrelated):"
396
405
  "\n%s, with value %s\nStacktrace:%s",
397
406
  exc_type, exc_val, exc_tb, exc_info=exc_tb)
398
- print(f"Client is disconnecting: {self.base_host}")
407
+ print(f"Client is disconnecting: {self.panel_id or self.base_host}")
399
408
  await self.disconnect()
400
409
  return
401
410
 
411
+ #=========================="meta" methods==========================
412
+ async def _resolve_uuid(self, telegram_id: int) -> str:
413
+ """Resolve a Telegram ID to a UUID via ``self.uuid_gen``.
414
+
415
+ Handles both sync and async callables.
416
+ """
417
+ if iscoroutinefunction(self.uuid_gen):
418
+ return await self.uuid_gen(telegram_id)
419
+ return self.uuid_gen(telegram_id)
420
+
421
+ async def _resolve_sub(self, telegram_id: int) -> str:
422
+ """Resolve the subscription ID from a telegram id via ``self.sub_gen``
423
+
424
+ Handles both sync and async callables.
425
+ """
426
+ if iscoroutinefunction(self.sub_gen):
427
+ return await self.sub_gen(telegram_id)
428
+ return self.sub_gen(telegram_id)
429
+
402
430
  #========================inbound management========================
403
431
  async def _get_production_inbounds_impl(self) -> tuple[Inbound, ...]:
404
432
  """Retrieve production inbounds.
@@ -424,16 +452,20 @@ class XUIClient:
424
452
 
425
453
  return tuple(usable_inbounds)
426
454
 
427
- async def _clear_prod_inbound_cache_task(self):
455
+ async def _clear_prod_inbound_cache_task(self, *, create_new: bool = False):
428
456
  """Refresh the production inbound cache in the background.
429
457
 
430
458
  The async context manager starts this loop after login. Each cycle
431
459
  clears the cached production inbound list, repopulates it from the
432
460
  panel, and then waits before refreshing again.
461
+
462
+ create_new param is kw-only and for people who know what they're doing, so they won't get the warning.
433
463
  """
434
- if self._cache_cleaner_task is not None:
464
+ if (self._cache_cleaner_task is not None) and (not create_new):
435
465
  logging.warning("You're trying to create another cache cleaner task, which is a FaF (Fire-And-Forget)."
436
466
  "Please destroy the previous task and set _cache_cleaner_task to None, if you know what you're doing.")
467
+ return
468
+ logging.info("Initializing cache cleaner task for %s", self.panel_id)
437
469
  while self.connected:
438
470
  self.get_production_inbounds.cache_clear()
439
471
  await self.get_production_inbounds() # fill the cache
@@ -462,7 +494,7 @@ class XUIClient:
462
494
  email = util.generate_email_from_tgid_inbid(tgid, inbound_id)
463
495
  resp = [await self.clients_end.get_client_with_email(email)]
464
496
  return resp
465
- uuid = util.get_uuid_from_tgid(tgid)
497
+ uuid = await self._resolve_uuid(tgid)
466
498
  resp = await self.clients_end.get_client_with_uuid(uuid)
467
499
  return resp
468
500
 
@@ -498,14 +530,12 @@ class XUIClient:
498
530
 
499
531
  tasks = []
500
532
  custom_sub: str
501
- if iscoroutinefunction(self.sub_gen):
502
- custom_sub = await self.sub_gen(telegram_id)
503
- else:
504
- custom_sub = self.sub_gen(telegram_id)
533
+ custom_sub = await self._resolve_sub(telegram_id)
534
+ uuid = await self._resolve_uuid(telegram_id)
505
535
  for inb in production_inbounds:
506
536
  tmp_email = util.generate_email_from_tgid_inbid(telegram_id, inb.id)
507
537
  client = SingleInboundClient(
508
- uuid=util.get_uuid_from_tgid(telegram_id),
538
+ uuid=uuid,
509
539
  flow="",
510
540
  email=tmp_email,
511
541
  limit_gb=0,
@@ -525,7 +555,8 @@ class XUIClient:
525
555
  raise custom_exceptions.ClientEmailAlreadyExistsError(json_resp["msg"])
526
556
  return responses
527
557
 
528
- async def _find_client_in_inbound(self, client_uuid: str, inbound_id: int, use_cache=False) -> SingleInboundClient | None:
558
+ async def _find_client_in_inbound(self, client_uuid: str, inbound_id: int,
559
+ use_cache=False) -> SingleInboundClient | None:
529
560
  """Note:
530
561
  Cached production inbounds can be stale because the panel may be
531
562
  changed by another actor. If a cached production inbound misses the
@@ -615,18 +646,19 @@ class XUIClient:
615
646
  expiry_time)
616
647
 
617
648
  _to_exec: list[Task] = []
649
+ client_uuid = await self._resolve_uuid(telegram_id)
618
650
  if prod_only:
619
651
  self.get_production_inbounds.cache_clear()
620
652
  inbounds = await self.get_production_inbounds()
621
653
  else:
622
654
  inbounds = await self.inbounds_end.get_all()
623
655
  for inbound in inbounds:
624
- found_client = util.get_inbound_in_client(util.get_uuid_from_tgid(telegram_id), inbound)
656
+ found_client = util.get_inbound_in_client(client_uuid, inbound)
625
657
  if found_client:
626
658
  new_client = found_client.model_copy(update=updates, deep=True)
627
659
  _to_exec.append(
628
660
  asyncio.create_task(self.clients_end.request_update_client(
629
- new_client, inbound.id, original_uuid=util.get_uuid_from_tgid(telegram_id)
661
+ new_client, inbound.id, original_uuid=client_uuid
630
662
  ))
631
663
  )
632
664
  responses = await asyncio.gather(*_to_exec)
@@ -642,6 +674,7 @@ class XUIClient:
642
674
  enable: bool | None = None,
643
675
  sub_id: str | None = None,
644
676
  comment: str | None = None,
677
+ email: str | None = None,
645
678
  verbose: bool = True) -> Response:
646
679
  """
647
680
  Update a client in a specific inbound by Telegram ID. NOT optimized for multiple inbounds.
@@ -658,6 +691,7 @@ class XUIClient:
658
691
  enable: Whether the client is enabled (optional)
659
692
  sub_id: Subscription ID (optional)
660
693
  comment: Client comment/note (optional)
694
+ email: New client email (optional). USE WITH CAUTION BECAUSE THE PANEL WILL NOT TRACK THE NEW EMAIL.
661
695
 
662
696
  Returns:
663
697
  Response from the API
@@ -670,10 +704,12 @@ class XUIClient:
670
704
  "If you want to disable this message, set verbose=false.",
671
705
  expiry_time)
672
706
 
707
+ client_uuid = await self._resolve_uuid(telegram_id)
673
708
  resp = await self.clients_end.update_single_client(
674
- inbound_id=inbound_id, client_uuid=util.get_uuid_from_tgid(telegram_id),
709
+ inbound_id=inbound_id, client_uuid=client_uuid,
675
710
  security=security,
676
711
  password=password,
712
+ email=email,
677
713
  flow=flow,
678
714
  limit_ip=limit_ip,
679
715
  limit_gb=limit_gb,
@@ -9,6 +9,7 @@ from . import util
9
9
  if TYPE_CHECKING:
10
10
  from .api import XUIClient
11
11
 
12
+
12
13
  class BaseModel(pydantic.BaseModel):
13
14
  """Base model for all 3X-UI API data models.
14
15
 
@@ -22,13 +23,12 @@ class BaseModel(pydantic.BaseModel):
22
23
  ERROR_RETRIES: ClassVar[int] = 5
23
24
  ERROR_RETRY_COOLDOWN: ClassVar[int] = 1
24
25
 
25
- model_config = pydantic.ConfigDict(ignored_types=(cached_property, ), validate_by_name=True, validate_by_alias=True)
26
+ model_config = pydantic.ConfigDict(ignored_types=(cached_property,), validate_by_name=True, validate_by_alias=True)
26
27
 
27
28
  # def model_post_init(self, context: Any, /) -> None:
28
29
  # #print(f"Model {self.__class__}, {self} initialized")
29
30
  # ...
30
31
 
31
-
32
32
  @classmethod
33
33
  def from_list(cls, args: List[Dict[str, Any]],
34
34
  ) -> List[Self]:
@@ -50,7 +50,7 @@ class BaseModel(pydantic.BaseModel):
50
50
  cls,
51
51
  response: httpx.Response,
52
52
  client: "XUIClient",
53
- expect: list|dict,
53
+ expect: list | dict,
54
54
  auto_retry: bool = True
55
55
  ) -> Union[Self, List[Self]]:
56
56
  """Create model instance(s) from an HTTP response.
@@ -57,13 +57,14 @@ class SingleInboundClient(base_model.BaseModel):
57
57
  uuid: Annotated[str, Field(alias="id")] #yes they really did that...
58
58
  security: str = ""
59
59
  password: str = ""
60
- flow: Literal["", "xtls-rprx-vision", "xtls-rprx-vision-udp443"]
60
+ # turns out panel can (not return it)
61
+ flow: Literal["", "xtls-rprx-vision", "xtls-rprx-vision-udp443"] = ""
61
62
  email: str
62
63
  limit_ip: Annotated[int, Field(alias="limitIp")] = 20
63
64
  reset: int = 0
64
- #Interestingly, the API expects this value to be called GB but it's actually bytes.
65
+ # Interestingly, the API expects this value to be called GB but it's actually bytes.
65
66
  # I want the pythonic side to be in GB (hence why floats, i.e. 2.5GB), but the API expects bytes.
66
- limit_gb: Annotated[int | float, Field(alias="totalGB")] = 0 # total flow
67
+ limit_gb: Annotated[int, Field(alias="totalGB")] = 0 # total flow
67
68
  expiry_time: Annotated[timestamp_seconds, Field(alias="expiryTime")] = 0
68
69
  enable: bool = True
69
70
  tg_id: Annotated[Union[int, str], Field(alias="tgId")] = ""
@@ -88,15 +89,15 @@ class SingleInboundClient(base_model.BaseModel):
88
89
 
89
90
  @field_serializer("limit_gb")
90
91
  @classmethod
91
- def serialize_total_gb(cls, value: int | float) -> int:
92
+ def serialize_total_gb(cls, value: int) -> int:
92
93
  #API expects an integer of bytes.
93
94
  return value * (1024 ** 3)
94
95
 
95
96
  @field_validator("limit_gb", mode="after")
96
97
  @classmethod
97
- def parse_total_gb(cls, value: int) -> int | float:
98
+ def parse_total_gb(cls, value: int) -> int:
98
99
  #Python wants an int/float of GB.
99
- return value / (1024 ** 3)
100
+ return value // (1024 ** 3)
100
101
 
101
102
 
102
103
  class ClientsSettings(base_model.BaseModel):
@@ -295,7 +296,7 @@ class Inbound(base_model.BaseModel):
295
296
  def parse_settings(cls, value: str) -> ClientsSettings:
296
297
  if value == "":
297
298
  return ClientsSettings(clients=[])
298
- return ClientsSettings.model_validate_json(value, by_alias=True, extra="forbid")
299
+ return ClientsSettings.model_validate_json(value, by_alias=True, extra="ignore")
299
300
 
300
301
  @field_serializer("settings")
301
302
  @classmethod
@@ -8,6 +8,7 @@ including:
8
8
  - Telegram ID-based UUID/email generation
9
9
  - Response validation
10
10
  """
11
+ from __future__ import annotations
11
12
 
12
13
  import asyncio
13
14
  import base64
@@ -6,7 +6,7 @@ from pydantic import ValidationError
6
6
 
7
7
  from python_3xui.api import XUIClient
8
8
  from python_3xui.models import SingleInboundClient, ClientStats
9
- from python_3xui.util import get_uuid_from_tgid, datetime_now_ms, generate_email_from_tgid_inbid
9
+ from python_3xui.util import datetime_now_ms, generate_email_from_tgid_inbid
10
10
 
11
11
 
12
12
  class TestClientsEndpoint:
@@ -54,10 +54,11 @@ class TestClientsEndpoint:
54
54
 
55
55
  # Generate unique test data
56
56
  timestamp = datetime_now_ms(UTC)
57
- test_uuid = get_uuid_from_tgid(TestClientsEndpoint.test_telegram_id)
57
+ test_uuid = await xui_client._resolve_uuid(TestClientsEndpoint.test_telegram_id)
58
58
  test_email = f"testclient_{timestamp}@example.com"
59
59
 
60
60
  # Create a test client
61
+ custom_sub = await xui_client._resolve_sub(TestClientsEndpoint.test_telegram_id)
61
62
  test_client = SingleInboundClient.model_construct(
62
63
  id=test_uuid, # Using alias 'id' for 'uuid'
63
64
  security="",
@@ -69,7 +70,7 @@ class TestClientsEndpoint:
69
70
  expiryTime=timestamp + 86400*1000, # Using alias 'expiryTime' for 'expiry_time'
70
71
  enable=True,
71
72
  tgId="", # Using alias 'tgId' for 'tg_id'
72
- subId=xui_client.sub_gen(TestClientsEndpoint.test_telegram_id), # Using alias 'subId' for 'subscription_id'
73
+ subId=custom_sub, # Using alias 'subId' for 'subscription_id'
73
74
  comment=f"Test client created at {timestamp}, TEST SUITE",
74
75
  created_at=timestamp,
75
76
  updated_at=timestamp
@@ -150,7 +151,7 @@ class TestClientsEndpoint:
150
151
 
151
152
  # Generate new test data
152
153
  timestamp = datetime_now_ms(UTC)
153
- test_uuid = get_uuid_from_tgid(TestClientsEndpoint.test_telegram_id + 1) # Different UUID
154
+ test_uuid = await xui_client._resolve_uuid(TestClientsEndpoint.test_telegram_id + 1) # Different UUID
154
155
  test_email = f"testclient_uuid_{timestamp}@example.com"
155
156
 
156
157
  # Create a new test client
@@ -202,7 +203,7 @@ class TestClientsEndpoint:
202
203
  TEST_TELEGRAM_ID = 420
203
204
 
204
205
  timestamp = datetime_now_ms(UTC)
205
- test_uuid = get_uuid_from_tgid(TEST_TELEGRAM_ID)
206
+ test_uuid = await xui_client._resolve_uuid(TEST_TELEGRAM_ID)
206
207
 
207
208
  template_client = SingleInboundClient.model_construct(
208
209
  id=test_uuid, # Using alias 'id' for 'uuid'
@@ -19,7 +19,7 @@ from python_3xui.custom_exceptions import ClientEmailAlreadyExistsError
19
19
  from python_3xui.models import ClientStats
20
20
  from python_3xui.util import (
21
21
  generate_email_from_tgid_inbid,
22
- get_uuid_from_tgid,
22
+
23
23
  )
24
24
 
25
25
 
@@ -101,7 +101,7 @@ class TestXUIClientHelpers:
101
101
  # inbound_id=None -> uses UUID, returns one entry per production inbound.
102
102
  by_uuid = await xui_client.get_client_with_tgid(_TGID_GET)
103
103
  assert isinstance(by_uuid, list)
104
- expected_uuid = get_uuid_from_tgid(_TGID_GET)
104
+ expected_uuid = await xui_client._resolve_uuid(_TGID_GET)
105
105
  assert all(isinstance(c, ClientStats) for c in by_uuid)
106
106
  assert all(c.uuid == expected_uuid for c in by_uuid)
107
107
  prod_ids = {inb.id for inb in production_inbounds}
@@ -1,16 +0,0 @@
1
- <h1>Hi! This is my example python 3x-ui wrapper!</h1>
2
- <p>I'm not expecting much to be honest, so please feel free to fork it if I abandon the project and you need it!</p>
3
- <p>Also, if you REALLY want it I can give you the ownership if I step down, you can find my email in the pyproject.toml (I don't check it that much but trust me I do)</p>
4
-
5
- <h2>0.0.9 Release Notes</h2>
6
- <ul>
7
- <li>Fix _request_update_client for it to actually work and NOT create "zombies"</li>
8
- <li>DTO un-split because fields reset when not provided, so full inbounds must be fetched</li>
9
- <li>New method: update_client_by_tgid</li>
10
- <li>Fixed test suite</li>
11
- <li>Fix from_response and from_list</li>
12
- <li>Remove obsolete and useless client fields from models</li>
13
- <li>Inbound settings actually get parsed properly into ClientsSettings</li>
14
- <li>New asyncio task management so they won't get destroyed when GCed</li>
15
- <li>XUIClient async_lru cache now binds to event loop at runtime, not in initialization</li>
16
- </ul>
File without changes