Python-3xui 0.0.4__tar.gz → 0.0.5__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.5}/PKG-INFO +2 -1
- {python_3xui-0.0.4 → python_3xui-0.0.5}/pyproject.toml +3 -2
- {python_3xui-0.0.4 → python_3xui-0.0.5}/python_3xui/api.py +21 -18
- {python_3xui-0.0.4 → python_3xui-0.0.5}/python_3xui/base_model.py +1 -1
- {python_3xui-0.0.4 → python_3xui-0.0.5}/python_3xui/endpoints.py +7 -7
- {python_3xui-0.0.4 → python_3xui-0.0.5}/python_3xui/util.py +1 -1
- {python_3xui-0.0.4 → python_3xui-0.0.5}/.gitignore +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/LICENSE +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/README.md +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/python_3xui/__init__.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/python_3xui/models.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/tests/conftest.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/tests/gather_response_stubs.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/tests/pytest.ini +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/tests/test_endpoints_clients.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/tests/test_endpoints_inbounds.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/tests/test_non_idempotent_endpoints_clients.py +0 -0
- {python_3xui-0.0.4 → python_3xui-0.0.5}/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.5
|
|
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'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "Python-3xui"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.5"
|
|
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
|
|
|
@@ -62,7 +62,8 @@ class XUIClient:
|
|
|
62
62
|
|
|
63
63
|
def __init__(self, base_website: str, base_port: int, base_path: str,
|
|
64
64
|
*, username: str | None = None, password: str | None = None,
|
|
65
|
-
two_fac_code: str | None = None, session_duration: int = 3600
|
|
65
|
+
two_fac_code: str | None = None, session_duration: int = 3600,
|
|
66
|
+
custom_prod_string: str = "testing") -> None:
|
|
66
67
|
"""Initialize the XUIClient.
|
|
67
68
|
|
|
68
69
|
Args:
|
|
@@ -76,7 +77,7 @@ class XUIClient:
|
|
|
76
77
|
"""
|
|
77
78
|
from . import endpoints # look, I know it's bad, but we need to evade cyclical imports
|
|
78
79
|
self.connected: bool = False
|
|
79
|
-
self.PROD_STRING =
|
|
80
|
+
self.PROD_STRING = custom_prod_string
|
|
80
81
|
self.session: AsyncClient | None = None
|
|
81
82
|
self.base_host: str = base_website
|
|
82
83
|
self.base_port: int = base_port
|
|
@@ -97,9 +98,9 @@ class XUIClient:
|
|
|
97
98
|
#init self.totp
|
|
98
99
|
if self.two_fac_secret:
|
|
99
100
|
if self.two_fac_secret.isdigit() and len(self.two_fac_secret) <= 8:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
print("WARNING: You seem to have entered a 2FA **code**, not a 2FA secret."
|
|
102
|
+
"Although entering the secret is dangerous, there is no other way to provide a consistent way"
|
|
103
|
+
"for continuous login. This code will only work for this specific login.")
|
|
103
104
|
self.totp = None
|
|
104
105
|
else:
|
|
105
106
|
self.totp = pyotp.TOTP(self.two_fac_secret)
|
|
@@ -115,9 +116,9 @@ class XUIClient:
|
|
|
115
116
|
Returns:
|
|
116
117
|
The singleton XUIClient instance.
|
|
117
118
|
"""
|
|
118
|
-
|
|
119
|
+
print("initializing client")
|
|
119
120
|
if cls._instance is None:
|
|
120
|
-
|
|
121
|
+
print("nu instance")
|
|
121
122
|
cls._instance = super(XUIClient, cls).__new__(cls)
|
|
122
123
|
return cls._instance
|
|
123
124
|
|
|
@@ -140,15 +141,15 @@ class XUIClient:
|
|
|
140
141
|
Raises:
|
|
141
142
|
RuntimeError: If max retries exceeded or session is invalid.
|
|
142
143
|
"""
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
print(f"SAFE REQUEST, {method}, is running to a URL of {kwargs["url"]}")
|
|
145
|
+
print(str(self.session.base_url) + str(kwargs["url"]))
|
|
145
146
|
async for attempt in async_range(self.max_retries):
|
|
146
147
|
resp = await self.session.request(method=method, **kwargs)
|
|
147
148
|
if resp.status_code // 100 != 2: #because it can return either 201 or 202
|
|
148
149
|
if resp.status_code == 404:
|
|
149
150
|
now: float = datetime.now(UTC).timestamp()
|
|
150
151
|
if self.session_start is None or now - self.session_start > self.session_duration:
|
|
151
|
-
|
|
152
|
+
print("Guys, we're not logged in, fixing that rn")
|
|
152
153
|
await self.login()
|
|
153
154
|
continue
|
|
154
155
|
else:
|
|
@@ -270,8 +271,8 @@ class XUIClient:
|
|
|
270
271
|
if self.two_fac_secret:
|
|
271
272
|
payload["twoFactorCode"] = self.two_fac_secret
|
|
272
273
|
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
print(self.session.base_url)
|
|
275
|
+
print("WE'RE LOGGING IN")
|
|
275
276
|
resp = await self.session.post("/login", data=payload)
|
|
276
277
|
if resp.status_code == 200:
|
|
277
278
|
resp_json = resp.json()
|
|
@@ -329,7 +330,7 @@ class XUIClient:
|
|
|
329
330
|
exc_val: The exception value, if an exception occurred.
|
|
330
331
|
exc_tb: The exception traceback, if an exception occurred.
|
|
331
332
|
"""
|
|
332
|
-
|
|
333
|
+
print("disconnectin'")
|
|
333
334
|
await self.disconnect()
|
|
334
335
|
return
|
|
335
336
|
|
|
@@ -350,6 +351,7 @@ class XUIClient:
|
|
|
350
351
|
inbounds = await self.inbounds_end.get_all()
|
|
351
352
|
usable_inbounds: list[Inbound] = []
|
|
352
353
|
for inb in inbounds:
|
|
354
|
+
#TODO: make prod_strings regex instead of STR
|
|
353
355
|
if self.PROD_STRING.lower() in inb.remark.lower():
|
|
354
356
|
usable_inbounds.append(inb)
|
|
355
357
|
if len(usable_inbounds) == 0:
|
|
@@ -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
|
|
|
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
|
|
File without changes
|