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.
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/PKG-INFO +5 -7
- python_3xui-0.0.10/README.md +10 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/pyproject.toml +1 -1
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/__init__.py +1 -1
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/api.py +34 -10
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/models.py +3 -2
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_clients.py +6 -5
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_xuiclient_helpers.py +2 -2
- python_3xui-0.0.9.post3/README.md +0 -12
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/.gitignore +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/LICENSE +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/base_model.py +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/custom_exceptions.py +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/endpoints.py +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/python_3xui/util.py +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/conftest.py +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/gather_response_stubs.py +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/pytest.ini +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_endpoints_clients.py +0 -0
- {python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_endpoints_inbounds.py +0 -0
- {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.
|
|
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.
|
|
31
|
+
<h2>0.0.10 Release Notes</h2>
|
|
32
32
|
<ul>
|
|
33
|
-
<li>HOTFIX:
|
|
34
|
-
<li>
|
|
35
|
-
<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>
|
|
@@ -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 =
|
|
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
|
-
|
|
510
|
-
|
|
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=
|
|
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(
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
{python_3xui-0.0.9.post3 → 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,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
|
|
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
|
|
File without changes
|
{python_3xui-0.0.9.post3 → python_3xui-0.0.10}/tests/test_non_idempotent_endpoints_inbounds.py
RENAMED
|
File without changes
|