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.
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/PKG-INFO +5 -11
- python_3xui-0.0.10/README.md +10 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/pyproject.toml +1 -1
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/__init__.py +2 -1
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/api.py +57 -21
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/base_model.py +3 -3
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/models.py +8 -7
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/util.py +1 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_clients.py +6 -5
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_xuiclient_helpers.py +2 -2
- python_3xui-0.0.9.post2/README.md +0 -16
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/.gitignore +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/LICENSE +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/custom_exceptions.py +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/python_3xui/endpoints.py +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/conftest.py +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/gather_response_stubs.py +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/pytest.ini +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_endpoints_clients.py +0 -0
- {python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_endpoints_inbounds.py +0 -0
- {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.
|
|
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.
|
|
31
|
+
<h2>0.0.10 Release Notes</h2>
|
|
32
32
|
<ul>
|
|
33
|
-
<li>
|
|
34
|
-
<li>
|
|
35
|
-
<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>
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
377
|
-
self.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
502
|
-
|
|
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=
|
|
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,
|
|
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(
|
|
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=
|
|
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=
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
98
|
+
def parse_total_gb(cls, value: int) -> int:
|
|
98
99
|
#Python wants an int/float of GB.
|
|
99
|
-
return value
|
|
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="
|
|
299
|
+
return ClientsSettings.model_validate_json(value, by_alias=True, extra="ignore")
|
|
299
300
|
|
|
300
301
|
@field_serializer("settings")
|
|
301
302
|
@classmethod
|
{python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_clients.py
RENAMED
|
@@ -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
|
|
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 =
|
|
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=
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_3xui-0.0.9.post2 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_inbounds.py
RENAMED
|
File without changes
|