Python-3xui 0.0.9.post3__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.post3 → python_3xui-0.0.10}/PKG-INFO +5 -7
  2. python_3xui-0.0.10/README.md +10 -0
  3. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/pyproject.toml +1 -1
  4. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/__init__.py +1 -1
  5. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/api.py +34 -10
  6. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/models.py +3 -2
  7. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_clients.py +6 -5
  8. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_xuiclient_helpers.py +2 -2
  9. python_3xui-0.0.9.post3/README.md +0 -12
  10. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/.gitignore +0 -0
  11. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/LICENSE +0 -0
  12. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/base_model.py +0 -0
  13. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/custom_exceptions.py +0 -0
  14. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/endpoints.py +0 -0
  15. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/util.py +0 -0
  16. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/conftest.py +0 -0
  17. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/gather_response_stubs.py +0 -0
  18. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/pytest.ini +0 -0
  19. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_endpoints_clients.py +0 -0
  20. {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_endpoints_inbounds.py +0 -0
  21. {python_3xui-0.0.9.post3 → 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.post3
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,11 +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-r3 Release Notes</h2>
31
+ <h2>0.0.10 Release Notes</h2>
32
32
  <ul>
33
- <li>HOTFIX: the importing of util.py fixed with from __future__ import annotations</li>
34
- <li>Make panel_id for better accounting & logging clarity</li>
35
- <li>Fix __aenter__ in XUIClient to not log a warning</li>
36
- <li>Fix total_gb to be int and not float, since that would need refactoring which I don't have time for yet.</li>
37
- <li>ClientsSettings now has extra=ignore instead of extra=forbid.</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>
38
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.9r3"
3
+ version = "0.0.10"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -4,7 +4,7 @@ from .api import XUIClient
4
4
  import python_3xui.custom_exceptions as exceptions
5
5
 
6
6
  __author__ = "JustMe_001"
7
- __version__ = "0.0.9r3"
7
+ __version__ = "0.0.10"
8
8
  __email__ = ""
9
9
 
10
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,7 @@ 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,
79
81
  panel_id: Any = None
80
82
  ) -> None:
81
83
  """Initialize the XUIClient.
@@ -94,6 +96,8 @@ class XUIClient:
94
96
  retry_delay: Seconds to wait between database-lock retries.
95
97
  custom_sub_generator: Sync or async callable that receives a
96
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.
97
101
  panel_id: this is solely for user's purposes to increase logging and accounting clarity. Default is None.
98
102
  """
99
103
  self.connected: bool = False
@@ -112,6 +116,7 @@ class XUIClient:
112
116
  self.max_retries: int = max_retries
113
117
  self.retry_delay: int = retry_delay
114
118
  self.sub_gen = custom_sub_generator
119
+ self.uuid_gen = custom_uuid_generator
115
120
  self.panel_id: int | str | Any = panel_id
116
121
  # endpoints
117
122
  self.server_end = endpoints.Server(self)
@@ -403,6 +408,25 @@ class XUIClient:
403
408
  await self.disconnect()
404
409
  return
405
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
+
406
430
  #========================inbound management========================
407
431
  async def _get_production_inbounds_impl(self) -> tuple[Inbound, ...]:
408
432
  """Retrieve production inbounds.
@@ -470,7 +494,7 @@ class XUIClient:
470
494
  email = util.generate_email_from_tgid_inbid(tgid, inbound_id)
471
495
  resp = [await self.clients_end.get_client_with_email(email)]
472
496
  return resp
473
- uuid = util.get_uuid_from_tgid(tgid)
497
+ uuid = await self._resolve_uuid(tgid)
474
498
  resp = await self.clients_end.get_client_with_uuid(uuid)
475
499
  return resp
476
500
 
@@ -506,14 +530,12 @@ class XUIClient:
506
530
 
507
531
  tasks = []
508
532
  custom_sub: str
509
- if iscoroutinefunction(self.sub_gen):
510
- custom_sub = await self.sub_gen(telegram_id)
511
- else:
512
- custom_sub = self.sub_gen(telegram_id)
533
+ custom_sub = await self._resolve_sub(telegram_id)
534
+ uuid = await self._resolve_uuid(telegram_id)
513
535
  for inb in production_inbounds:
514
536
  tmp_email = util.generate_email_from_tgid_inbid(telegram_id, inb.id)
515
537
  client = SingleInboundClient(
516
- uuid=util.get_uuid_from_tgid(telegram_id),
538
+ uuid=uuid,
517
539
  flow="",
518
540
  email=tmp_email,
519
541
  limit_gb=0,
@@ -624,18 +646,19 @@ class XUIClient:
624
646
  expiry_time)
625
647
 
626
648
  _to_exec: list[Task] = []
649
+ client_uuid = await self._resolve_uuid(telegram_id)
627
650
  if prod_only:
628
651
  self.get_production_inbounds.cache_clear()
629
652
  inbounds = await self.get_production_inbounds()
630
653
  else:
631
654
  inbounds = await self.inbounds_end.get_all()
632
655
  for inbound in inbounds:
633
- 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)
634
657
  if found_client:
635
658
  new_client = found_client.model_copy(update=updates, deep=True)
636
659
  _to_exec.append(
637
660
  asyncio.create_task(self.clients_end.request_update_client(
638
- new_client, inbound.id, original_uuid=util.get_uuid_from_tgid(telegram_id)
661
+ new_client, inbound.id, original_uuid=client_uuid
639
662
  ))
640
663
  )
641
664
  responses = await asyncio.gather(*_to_exec)
@@ -681,8 +704,9 @@ class XUIClient:
681
704
  "If you want to disable this message, set verbose=false.",
682
705
  expiry_time)
683
706
 
707
+ client_uuid = await self._resolve_uuid(telegram_id)
684
708
  resp = await self.clients_end.update_single_client(
685
- inbound_id=inbound_id, client_uuid=util.get_uuid_from_tgid(telegram_id),
709
+ inbound_id=inbound_id, client_uuid=client_uuid,
686
710
  security=security,
687
711
  password=password,
688
712
  email=email,
@@ -57,11 +57,12 @@ 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
67
  limit_gb: Annotated[int, Field(alias="totalGB")] = 0 # total flow
67
68
  expiry_time: Annotated[timestamp_seconds, Field(alias="expiryTime")] = 0
@@ -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,12 +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-r3 Release Notes</h2>
6
- <ul>
7
- <li>HOTFIX: the importing of util.py fixed with from __future__ import annotations</li>
8
- <li>Make panel_id for better accounting & logging clarity</li>
9
- <li>Fix __aenter__ in XUIClient to not log a warning</li>
10
- <li>Fix total_gb to be int and not float, since that would need refactoring which I don't have time for yet.</li>
11
- <li>ClientsSettings now has extra=ignore instead of extra=forbid.</li>
12
- </ul>
File without changes