Python-3xui 0.0.4__tar.gz → 0.0.6__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.4 → python_3xui-0.0.6}/PKG-INFO +12 -13
- python_3xui-0.0.6/README.md +11 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/pyproject.toml +3 -2
- {python_3xui-0.0.4 → python_3xui-0.0.6}/python_3xui/api.py +22 -19
- {python_3xui-0.0.4 → python_3xui-0.0.6}/python_3xui/base_model.py +1 -1
- {python_3xui-0.0.4 → python_3xui-0.0.6}/python_3xui/endpoints.py +7 -7
- {python_3xui-0.0.4 → python_3xui-0.0.6}/python_3xui/util.py +1 -1
- python_3xui-0.0.4/README.md +0 -13
- {python_3xui-0.0.4 → python_3xui-0.0.6}/.gitignore +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/LICENSE +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/python_3xui/__init__.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/python_3xui/models.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/tests/conftest.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/tests/gather_response_stubs.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/tests/pytest.ini +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/tests/test_endpoints_clients.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/tests/test_endpoints_inbounds.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/tests/test_non_idempotent_endpoints_clients.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.6}/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.6
|
|
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
|
|
@@ -16,6 +16,7 @@ Requires-Dist: async-lru~=2.2.0
|
|
|
16
16
|
Requires-Dist: dotenv~=0.9.9
|
|
17
17
|
Requires-Dist: httpx~=0.28.1
|
|
18
18
|
Requires-Dist: pydantic<3,~=2.12.5
|
|
19
|
+
Requires-Dist: pyotp~=2.9.0
|
|
19
20
|
Provides-Extra: testing
|
|
20
21
|
Requires-Dist: pytest; extra == 'testing'
|
|
21
22
|
Requires-Dist: pytest-asyncio; extra == 'testing'
|
|
@@ -23,16 +24,14 @@ Requires-Dist: pytest-dependency; extra == 'testing'
|
|
|
23
24
|
Requires-Dist: requests; extra == 'testing'
|
|
24
25
|
Description-Content-Type: text/markdown
|
|
25
26
|
|
|
26
|
-
<
|
|
27
|
-
<title>Readme</title>
|
|
28
|
-
<style>
|
|
29
|
-
.welcome {
|
|
30
|
-
color: rgb(1, 170, 170)
|
|
31
|
-
}
|
|
32
|
-
</style>
|
|
33
|
-
</head>
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<h1 class="welcome">Hi! This is my example python 3x-ui wrapper!</h1>
|
|
27
|
+
<h1>Hi! This is my example python 3x-ui wrapper!</h1>
|
|
37
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>
|
|
38
|
-
<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>
|
|
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
|
+
|
|
31
|
+
<h2>0.0.6 Release Notes</h2>
|
|
32
|
+
<ul>
|
|
33
|
+
<li>Added RegEx support for prod_strings</li>
|
|
34
|
+
<li>Fix dependencies for pyOTP</li>
|
|
35
|
+
<li>Add One-time code support, as well as secrets</li>
|
|
36
|
+
Note that one-time codes only work one time... To ensure consistent logins you need OTP <b>secrets</b> to form new codes
|
|
37
|
+
</ul>
|
|
@@ -0,0 +1,11 @@
|
|
|
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.6 Release Notes</h2>
|
|
6
|
+
<ul>
|
|
7
|
+
<li>Added RegEx support for prod_strings</li>
|
|
8
|
+
<li>Fix dependencies for pyOTP</li>
|
|
9
|
+
<li>Add One-time code support, as well as secrets</li>
|
|
10
|
+
Note that one-time codes only work one time... To ensure consistent logins you need OTP <b>secrets</b> to form new codes
|
|
11
|
+
</ul>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "Python-3xui"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.6"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="JustMe_001", email="justme001.causation755@passinbox.com" },
|
|
6
6
|
]
|
|
@@ -23,7 +23,8 @@ dependencies = [
|
|
|
23
23
|
"pydantic ~= 2.12.5, < 3",
|
|
24
24
|
"httpx ~=0.28.1",
|
|
25
25
|
"dotenv ~= 0.9.9",
|
|
26
|
-
"async_lru ~= 2.2.0"
|
|
26
|
+
"async_lru ~= 2.2.0",
|
|
27
|
+
"pyotp ~= 2.9.0"
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
import time
|
|
2
3
|
from collections.abc import Sequence, Mapping
|
|
3
4
|
from typing import Self, Optional, Dict, Iterable, AsyncIterable, Type, Union, Any, List, Tuple, Literal
|
|
@@ -62,7 +63,8 @@ class XUIClient:
|
|
|
62
63
|
|
|
63
64
|
def __init__(self, base_website: str, base_port: int, base_path: str,
|
|
64
65
|
*, username: str | None = None, password: str | None = None,
|
|
65
|
-
two_fac_code: str | None = None, session_duration: int = 3600
|
|
66
|
+
two_fac_code: str | None = None, session_duration: int = 3600,
|
|
67
|
+
custom_prod_string: str = "testing") -> None:
|
|
66
68
|
"""Initialize the XUIClient.
|
|
67
69
|
|
|
68
70
|
Args:
|
|
@@ -76,7 +78,7 @@ class XUIClient:
|
|
|
76
78
|
"""
|
|
77
79
|
from . import endpoints # look, I know it's bad, but we need to evade cyclical imports
|
|
78
80
|
self.connected: bool = False
|
|
79
|
-
self.PROD_STRING =
|
|
81
|
+
self.PROD_STRING = re.compile(custom_prod_string)
|
|
80
82
|
self.session: AsyncClient | None = None
|
|
81
83
|
self.base_host: str = base_website
|
|
82
84
|
self.base_port: int = base_port
|
|
@@ -97,9 +99,9 @@ class XUIClient:
|
|
|
97
99
|
#init self.totp
|
|
98
100
|
if self.two_fac_secret:
|
|
99
101
|
if self.two_fac_secret.isdigit() and len(self.two_fac_secret) <= 8:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
print("WARNING: You seem to have entered a 2FA **code**, not a 2FA secret."
|
|
103
|
+
"Although entering the secret is dangerous, there is no other way to provide a consistent way"
|
|
104
|
+
"for continuous login. This code will only work for this specific login.")
|
|
103
105
|
self.totp = None
|
|
104
106
|
else:
|
|
105
107
|
self.totp = pyotp.TOTP(self.two_fac_secret)
|
|
@@ -115,9 +117,9 @@ class XUIClient:
|
|
|
115
117
|
Returns:
|
|
116
118
|
The singleton XUIClient instance.
|
|
117
119
|
"""
|
|
118
|
-
|
|
120
|
+
print("initializing client")
|
|
119
121
|
if cls._instance is None:
|
|
120
|
-
|
|
122
|
+
print("nu instance")
|
|
121
123
|
cls._instance = super(XUIClient, cls).__new__(cls)
|
|
122
124
|
return cls._instance
|
|
123
125
|
|
|
@@ -140,15 +142,15 @@ class XUIClient:
|
|
|
140
142
|
Raises:
|
|
141
143
|
RuntimeError: If max retries exceeded or session is invalid.
|
|
142
144
|
"""
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
print(f"SAFE REQUEST, {method}, is running to a URL of {kwargs["url"]}")
|
|
146
|
+
print(str(self.session.base_url) + str(kwargs["url"]))
|
|
145
147
|
async for attempt in async_range(self.max_retries):
|
|
146
148
|
resp = await self.session.request(method=method, **kwargs)
|
|
147
149
|
if resp.status_code // 100 != 2: #because it can return either 201 or 202
|
|
148
150
|
if resp.status_code == 404:
|
|
149
151
|
now: float = datetime.now(UTC).timestamp()
|
|
150
152
|
if self.session_start is None or now - self.session_start > self.session_duration:
|
|
151
|
-
|
|
153
|
+
print("Guys, we're not logged in, fixing that rn")
|
|
152
154
|
await self.login()
|
|
153
155
|
continue
|
|
154
156
|
else:
|
|
@@ -270,8 +272,8 @@ class XUIClient:
|
|
|
270
272
|
if self.two_fac_secret:
|
|
271
273
|
payload["twoFactorCode"] = self.two_fac_secret
|
|
272
274
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
print(self.session.base_url)
|
|
276
|
+
print("WE'RE LOGGING IN")
|
|
275
277
|
resp = await self.session.post("/login", data=payload)
|
|
276
278
|
if resp.status_code == 200:
|
|
277
279
|
resp_json = resp.json()
|
|
@@ -329,7 +331,7 @@ class XUIClient:
|
|
|
329
331
|
exc_val: The exception value, if an exception occurred.
|
|
330
332
|
exc_tb: The exception traceback, if an exception occurred.
|
|
331
333
|
"""
|
|
332
|
-
|
|
334
|
+
print("disconnectin'")
|
|
333
335
|
await self.disconnect()
|
|
334
336
|
return
|
|
335
337
|
|
|
@@ -350,7 +352,7 @@ class XUIClient:
|
|
|
350
352
|
inbounds = await self.inbounds_end.get_all()
|
|
351
353
|
usable_inbounds: list[Inbound] = []
|
|
352
354
|
for inb in inbounds:
|
|
353
|
-
if self.PROD_STRING.
|
|
355
|
+
if self.PROD_STRING.search(inb.remark):
|
|
354
356
|
usable_inbounds.append(inb)
|
|
355
357
|
if len(usable_inbounds) == 0:
|
|
356
358
|
raise RuntimeError("No production inbounds found! Change prod_string!")
|
|
@@ -369,12 +371,12 @@ class XUIClient:
|
|
|
369
371
|
timer from 5 to 60*60*24 in the code.
|
|
370
372
|
"""
|
|
371
373
|
while self.connected:
|
|
372
|
-
|
|
373
|
-
|
|
374
|
+
print("You're seeing this message because I forgot to remove it in api.update_inbounds() !")
|
|
375
|
+
print("Please change the timer from 5 to 60*60*24!")
|
|
374
376
|
self.get_production_inbounds.cache_clear()
|
|
375
377
|
await self.get_production_inbounds() #fill the cache
|
|
376
|
-
await asyncio.sleep(
|
|
377
|
-
|
|
378
|
+
await asyncio.sleep(10)
|
|
379
|
+
#print(stat)
|
|
378
380
|
|
|
379
381
|
#========================clients management========================
|
|
380
382
|
async def get_client_with_tgid(self, tgid: int, inbound_id: int | None = None) -> List[ClientStats]:
|
|
@@ -409,6 +411,7 @@ class XUIClient:
|
|
|
409
411
|
This method creates a new client with the given Telegram ID and
|
|
410
412
|
adds it to the production inbounds. The client is configured with
|
|
411
413
|
default settings and the additional remark.
|
|
414
|
+
Note that the sub id is created by util.generate_email_from_tgid_inbid, so use that to retrieve.
|
|
412
415
|
|
|
413
416
|
Args:
|
|
414
417
|
telegram_id: The Telegram ID of the client.
|
|
@@ -510,7 +513,7 @@ class XUIClient:
|
|
|
510
513
|
email = util.generate_email_from_tgid_inbid(telegram_id, inbound.id)
|
|
511
514
|
resp = await self.clients_end.delete_client_by_email(email, inbound.id)
|
|
512
515
|
responses.append(resp)
|
|
513
|
-
|
|
516
|
+
print("Inbound deleted")
|
|
514
517
|
|
|
515
518
|
return responses
|
|
516
519
|
|
|
@@ -28,7 +28,7 @@ class BaseModel(pydantic.BaseModel):
|
|
|
28
28
|
model_config = pydantic.ConfigDict(ignored_types=(cached_property, ))
|
|
29
29
|
|
|
30
30
|
def model_post_init(self, context: Any, /) -> None:
|
|
31
|
-
|
|
31
|
+
#print(f"Model {self.__class__}, {self} initialized")
|
|
32
32
|
...
|
|
33
33
|
|
|
34
34
|
|
|
@@ -232,17 +232,17 @@ class Clients(BaseEndpoint):
|
|
|
232
232
|
else:
|
|
233
233
|
raise TypeError
|
|
234
234
|
# send request
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
print(type(final))
|
|
236
|
+
print(final)
|
|
237
237
|
data = final.model_dump(by_alias=True)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
238
|
+
print(type(data))
|
|
239
|
+
print(json.dumps(data))
|
|
240
|
+
print(f"{self._url}{endpoint}")
|
|
241
241
|
resp = await self.client.safe_post(f"{self._url}{endpoint}", data=data)
|
|
242
242
|
|
|
243
243
|
#YOU NEED TO PASS SETTINGS AS A STRING, NOT AS A DICT, YOU FUCKING DUMBASS!
|
|
244
|
-
|
|
245
|
-
|
|
244
|
+
print(resp)
|
|
245
|
+
print(resp.json())
|
|
246
246
|
return resp
|
|
247
247
|
|
|
248
248
|
async def _request_update_client(self, client: models.InboundClients | models.SingleInboundClient,
|
|
@@ -218,7 +218,7 @@ async def check_xui_response_validity(response: JsonType | httpx.Response) -> st
|
|
|
218
218
|
if "database" in msg.lower() and "locked" in msg.lower() and not success:
|
|
219
219
|
logging.log(logging.WARNING, "Database is locked, retrying...")
|
|
220
220
|
return "DB_LOCKED"
|
|
221
|
-
|
|
221
|
+
print(f"Unsuccessful operation! Message: {json_resp["msg"]}")
|
|
222
222
|
return "ERROR"
|
|
223
223
|
raise RuntimeError("Validator got something very unexpected (Please don't shove responses with non-20X status codes in here...)")
|
|
224
224
|
|
python_3xui-0.0.4/README.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
<head>
|
|
2
|
-
<title>Readme</title>
|
|
3
|
-
<style>
|
|
4
|
-
.welcome {
|
|
5
|
-
color: rgb(1, 170, 170)
|
|
6
|
-
}
|
|
7
|
-
</style>
|
|
8
|
-
</head>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<h1 class="welcome">Hi! This is my example python 3x-ui wrapper!</h1>
|
|
12
|
-
<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>
|
|
13
|
-
<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>
|
|
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
|