Python-3xui 0.0.6__py3-none-any.whl → 0.0.8__py3-none-any.whl
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/api.py +62 -48
- python_3xui/base_model.py +5 -5
- python_3xui/endpoints.py +1 -8
- python_3xui/models.py +3 -4
- python_3xui/util.py +10 -11
- {python_3xui-0.0.6.dist-info → python_3xui-0.0.8.dist-info}/METADATA +5 -6
- python_3xui-0.0.8.dist-info/RECORD +10 -0
- python_3xui-0.0.6.dist-info/RECORD +0 -10
- {python_3xui-0.0.6.dist-info → python_3xui-0.0.8.dist-info}/WHEEL +0 -0
- {python_3xui-0.0.6.dist-info → python_3xui-0.0.8.dist-info}/licenses/LICENSE +0 -0
python_3xui/api.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
1
3
|
import re
|
|
2
4
|
import time
|
|
3
5
|
from collections.abc import Sequence, Mapping
|
|
6
|
+
from logging import DEBUG
|
|
4
7
|
from typing import Self, Optional, Dict, Iterable, AsyncIterable, Type, Union, Any, List, Tuple, Literal
|
|
5
8
|
from datetime import datetime, UTC
|
|
6
9
|
|
|
@@ -12,7 +15,7 @@ import httpx
|
|
|
12
15
|
|
|
13
16
|
from . import util
|
|
14
17
|
from .models import Inbound, SingleInboundClient, ClientStats
|
|
15
|
-
from .util import JsonType, async_range
|
|
18
|
+
from .util import JsonType, async_range, check_xui_response
|
|
16
19
|
|
|
17
20
|
DataType: Type[str | bytes | Iterable[bytes] | AsyncIterable[bytes]] = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
|
|
18
21
|
PrimitiveData = Optional[Union[str, int, float, bool]]
|
|
@@ -76,7 +79,7 @@ class XUIClient:
|
|
|
76
79
|
two_fac_code: Two-factor authentication code (if enabled).
|
|
77
80
|
session_duration: Maximum session duration in seconds. Defaults to 3600.
|
|
78
81
|
"""
|
|
79
|
-
from . import endpoints
|
|
82
|
+
from . import endpoints # look, I know it's bad, but we need to evade cyclical imports
|
|
80
83
|
self.connected: bool = False
|
|
81
84
|
self.PROD_STRING = re.compile(custom_prod_string)
|
|
82
85
|
self.session: AsyncClient | None = None
|
|
@@ -106,23 +109,6 @@ class XUIClient:
|
|
|
106
109
|
else:
|
|
107
110
|
self.totp = pyotp.TOTP(self.two_fac_secret)
|
|
108
111
|
|
|
109
|
-
#========================singleton pattern========================
|
|
110
|
-
def __new__(cls, *args, **kwargs):
|
|
111
|
-
"""Create or return the singleton instance.
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
*args: Positional arguments passed to __init__.
|
|
115
|
-
**kwargs: Keyword arguments passed to __init__.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
The singleton XUIClient instance.
|
|
119
|
-
"""
|
|
120
|
-
print("initializing client")
|
|
121
|
-
if cls._instance is None:
|
|
122
|
-
print("nu instance")
|
|
123
|
-
cls._instance = super(XUIClient, cls).__new__(cls)
|
|
124
|
-
return cls._instance
|
|
125
|
-
|
|
126
112
|
#========================request stuffs========================
|
|
127
113
|
async def _safe_request(self,
|
|
128
114
|
method: Literal["get", "post", "patch", "delete", "put"],
|
|
@@ -142,33 +128,34 @@ class XUIClient:
|
|
|
142
128
|
Raises:
|
|
143
129
|
RuntimeError: If max retries exceeded or session is invalid.
|
|
144
130
|
"""
|
|
145
|
-
|
|
146
|
-
print(str(self.session.base_url) + str(kwargs["url"]))
|
|
131
|
+
logging.debug("Safe request is running to %s%s", str(self.session.base_url), str(kwargs["url"]))
|
|
147
132
|
async for attempt in async_range(self.max_retries):
|
|
148
133
|
resp = await self.session.request(method=method, **kwargs)
|
|
149
134
|
if resp.status_code // 100 != 2: #because it can return either 201 or 202
|
|
150
135
|
if resp.status_code == 404:
|
|
151
136
|
now: float = datetime.now(UTC).timestamp()
|
|
152
137
|
if self.session_start is None or now - self.session_start > self.session_duration:
|
|
153
|
-
|
|
138
|
+
logging.info("Client with IP/Domain %s is not logged in, logging in...", self.base_host)
|
|
154
139
|
await self.login()
|
|
155
140
|
continue
|
|
156
141
|
else:
|
|
142
|
+
logging.error("Server returned a status code of %s with a valid session", resp.status_code)
|
|
157
143
|
raise RuntimeError("""Server returned a 404, and the session should still be valid, likely it's a REAL 404""")
|
|
158
144
|
else:
|
|
159
|
-
|
|
145
|
+
logging.error("Server returned a status code of %s", resp.status_code)
|
|
146
|
+
resp.raise_for_status()
|
|
160
147
|
|
|
161
|
-
status = await util.
|
|
148
|
+
status = await util.check_xui_response(resp)
|
|
162
149
|
if status == "OK":
|
|
163
150
|
return resp
|
|
164
151
|
elif status == "DB_LOCKED":
|
|
165
152
|
if attempt + 1 >= self.max_retries:
|
|
166
|
-
# resp.status_code = 518 # so the error can simply be handled as a "bad request"
|
|
167
|
-
# return resp
|
|
168
153
|
raise RuntimeError("Too many retries")
|
|
169
154
|
await asyncio.sleep(self.retry_delay)
|
|
170
155
|
continue
|
|
171
156
|
else:
|
|
157
|
+
logging.error("A %s request was unsuccessful (code 200, but success=false).\nPayload: %s",
|
|
158
|
+
method, json.dumps(resp.json()))
|
|
172
159
|
return resp
|
|
173
160
|
raise RuntimeError(f"For some reason safe_request didn't exit, dump:\nmethod:\n{method}\n{kwargs}")
|
|
174
161
|
|
|
@@ -265,15 +252,14 @@ class XUIClient:
|
|
|
265
252
|
"password": self.xui_password,
|
|
266
253
|
}
|
|
267
254
|
if self.totp:
|
|
268
|
-
if self.totp.interval - datetime.now().timestamp() % self.totp.interval <
|
|
269
|
-
await asyncio.sleep(
|
|
255
|
+
if self.totp.interval - datetime.now().timestamp() % self.totp.interval < 3:
|
|
256
|
+
await asyncio.sleep(3.1) # just to not submit an invalid code
|
|
270
257
|
payload["twoFactorCode"] = self.totp.now()
|
|
271
258
|
else:
|
|
272
259
|
if self.two_fac_secret:
|
|
273
260
|
payload["twoFactorCode"] = self.two_fac_secret
|
|
274
261
|
|
|
275
|
-
|
|
276
|
-
print("WE'RE LOGGING IN")
|
|
262
|
+
logging.info("Client is logging in with IP/Domain: %s", self.base_host)
|
|
277
263
|
resp = await self.session.post("/login", data=payload)
|
|
278
264
|
if resp.status_code == 200:
|
|
279
265
|
resp_json = resp.json()
|
|
@@ -281,7 +267,7 @@ class XUIClient:
|
|
|
281
267
|
self.session_start: float = (datetime.now(UTC).timestamp())
|
|
282
268
|
return
|
|
283
269
|
else:
|
|
284
|
-
raise ValueError("Error: wrong credentials or failed login")
|
|
270
|
+
raise ValueError("Error: wrong credentials (including status code) or failed login.")
|
|
285
271
|
else:
|
|
286
272
|
raise RuntimeError(f"Error: server returned a status code of {resp.status_code}")
|
|
287
273
|
|
|
@@ -293,6 +279,7 @@ class XUIClient:
|
|
|
293
279
|
Returns:
|
|
294
280
|
Self: The XUIClient instance.
|
|
295
281
|
"""
|
|
282
|
+
logging.log(DEBUG, "Client connected with IP/domain %s", self.base_url)
|
|
296
283
|
self.session = AsyncClient(base_url=self.base_url)
|
|
297
284
|
self.connected = True
|
|
298
285
|
return self
|
|
@@ -331,12 +318,18 @@ class XUIClient:
|
|
|
331
318
|
exc_val: The exception value, if an exception occurred.
|
|
332
319
|
exc_tb: The exception traceback, if an exception occurred.
|
|
333
320
|
"""
|
|
334
|
-
|
|
321
|
+
if exc_type is None or exc_type == asyncio.exceptions.CancelledError:
|
|
322
|
+
logging.info("Client is disconnecting at time with IP/Domain %s", self.base_host)
|
|
323
|
+
else:
|
|
324
|
+
logging.warning("Client is disconnecting due to an error (may be unrelated):"
|
|
325
|
+
"\n%s, with value %s\nStacktrace:%s",
|
|
326
|
+
exc_type, exc_val, exc_tb)
|
|
327
|
+
print(f"Client is disconnecting: {self.base_host}")
|
|
335
328
|
await self.disconnect()
|
|
336
329
|
return
|
|
337
330
|
|
|
338
331
|
#========================inbound management========================
|
|
339
|
-
@alru_cache
|
|
332
|
+
@alru_cache()
|
|
340
333
|
async def get_production_inbounds(self) -> Tuple[Inbound, ...]:
|
|
341
334
|
"""Retrieve production inbounds.
|
|
342
335
|
|
|
@@ -371,12 +364,9 @@ class XUIClient:
|
|
|
371
364
|
timer from 5 to 60*60*24 in the code.
|
|
372
365
|
"""
|
|
373
366
|
while self.connected:
|
|
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!")
|
|
376
367
|
self.get_production_inbounds.cache_clear()
|
|
377
|
-
await self.get_production_inbounds()
|
|
378
|
-
await asyncio.sleep(
|
|
379
|
-
#print(stat)
|
|
368
|
+
await self.get_production_inbounds() #fill the cache
|
|
369
|
+
await asyncio.sleep(3600) #update every 1h
|
|
380
370
|
|
|
381
371
|
#========================clients management========================
|
|
382
372
|
async def get_client_with_tgid(self, tgid: int, inbound_id: int | None = None) -> List[ClientStats]:
|
|
@@ -405,7 +395,12 @@ class XUIClient:
|
|
|
405
395
|
resp = await self.clients_end.get_client_with_uuid(uuid)
|
|
406
396
|
return resp
|
|
407
397
|
|
|
408
|
-
async def create_and_add_prod_client(self, telegram_id: int,
|
|
398
|
+
async def create_and_add_prod_client(self, telegram_id: int, *,
|
|
399
|
+
additional_remark: str | None = None,
|
|
400
|
+
expiry_time: int=0,
|
|
401
|
+
exist_ok: bool = False
|
|
402
|
+
) -> list[Response]:
|
|
403
|
+
#TODO: add exist_ok flag
|
|
409
404
|
"""Create and add a production client.
|
|
410
405
|
|
|
411
406
|
This method creates a new client with the given Telegram ID and
|
|
@@ -416,6 +411,8 @@ class XUIClient:
|
|
|
416
411
|
Args:
|
|
417
412
|
telegram_id: The Telegram ID of the client.
|
|
418
413
|
additional_remark: An optional additional remark for the client.
|
|
414
|
+
expiry_time: Expiry time in SECONDS as a UNIX timestamp.
|
|
415
|
+
exist_ok: Don't raise any errors if the client is already there (good if you need a refresh job)
|
|
419
416
|
|
|
420
417
|
Returns:
|
|
421
418
|
List[Response]: A list of responses from the server for each
|
|
@@ -423,20 +420,30 @@ class XUIClient:
|
|
|
423
420
|
"""
|
|
424
421
|
production_inbounds: List[Inbound] = await self.get_production_inbounds()
|
|
425
422
|
|
|
426
|
-
|
|
423
|
+
tasks = []
|
|
427
424
|
for inb in production_inbounds:
|
|
425
|
+
tmp_email = util.generate_email_from_tgid_inbid(telegram_id, inb.id)
|
|
428
426
|
client = SingleInboundClient.model_construct(
|
|
429
427
|
uuid=util.get_uuid_from_tgid(telegram_id),
|
|
430
428
|
flow="",
|
|
431
|
-
email=
|
|
429
|
+
email=tmp_email,
|
|
432
430
|
limit_gb=0,
|
|
433
431
|
enable=True,
|
|
434
432
|
subscription_id=util.sub_from_tgid(telegram_id),
|
|
435
|
-
comment=f"{additional_remark}, created at {datetime.now(UTC)}"
|
|
436
|
-
|
|
433
|
+
comment=f"{additional_remark}, created at {datetime.now(UTC)}",
|
|
434
|
+
expiry_time=expiry_time * 1000
|
|
435
|
+
)
|
|
436
|
+
tasks.append(asyncio.create_task(self.clients_end.add_client(client, inb.id)))
|
|
437
|
+
responses: list[Response] = await asyncio.gather(*tasks)
|
|
438
|
+
if exist_ok:
|
|
439
|
+
return responses
|
|
440
|
+
for resp in responses:
|
|
441
|
+
json_resp = resp.json()
|
|
442
|
+
if "duplicate email" in json_resp["msg"].lower():
|
|
443
|
+
logging.error("ERROR: Client already exists and exist_ok not set: %s", json_resp["msg"])
|
|
437
444
|
return responses
|
|
438
445
|
|
|
439
|
-
async def update_client_by_tgid(self, telegram_id: int, inbound_id: int, /,
|
|
446
|
+
async def update_client_by_tgid(self, telegram_id: int, inbound_id: int, /, *,
|
|
440
447
|
security: str | None = None,
|
|
441
448
|
password: str | None = None,
|
|
442
449
|
flow: Literal["", "xtls-rprx-vision", "xtls-rprx-vision-udp443"] | None = None,
|
|
@@ -445,7 +452,8 @@ class XUIClient:
|
|
|
445
452
|
expiry_time: int | None = None,
|
|
446
453
|
enable: bool | None = None,
|
|
447
454
|
sub_id: str | None = None,
|
|
448
|
-
comment: str | None = None
|
|
455
|
+
comment: str | None = None,
|
|
456
|
+
verbose: bool=True) -> Response:
|
|
449
457
|
"""
|
|
450
458
|
Update a client in a specific inbound by Telegram ID.
|
|
451
459
|
|
|
@@ -467,6 +475,13 @@ class XUIClient:
|
|
|
467
475
|
"""
|
|
468
476
|
email = util.generate_email_from_tgid_inbid(telegram_id, inbound_id)
|
|
469
477
|
existing_client = await self.clients_end.get_client_with_email(email)
|
|
478
|
+
if verbose:
|
|
479
|
+
if expiry_time < 1e9:
|
|
480
|
+
logging.warning("Warning: You're trying to update a client with expiry time %s. "
|
|
481
|
+
"You set it to expire before 2001, likely because you provided the DURATION. "
|
|
482
|
+
"You need to provide a TIMESTAMP. "
|
|
483
|
+
"If you want to disable this message, set verbose=false.",
|
|
484
|
+
expiry_time)
|
|
470
485
|
|
|
471
486
|
resp = await self.clients_end.update_single_client(
|
|
472
487
|
SingleInboundClient.model_validate(existing_client.model_dump()),
|
|
@@ -497,7 +512,7 @@ class XUIClient:
|
|
|
497
512
|
resp = await self.clients_end.delete_client_by_email(email, inbound_id)
|
|
498
513
|
return resp
|
|
499
514
|
|
|
500
|
-
async def
|
|
515
|
+
async def revoke_client_by_tgid_all_inbounds(self, telegram_id: int) -> List[Response]:
|
|
501
516
|
"""Delete a client from all production inbounds by Telegram ID.
|
|
502
517
|
|
|
503
518
|
Args:
|
|
@@ -513,7 +528,6 @@ class XUIClient:
|
|
|
513
528
|
email = util.generate_email_from_tgid_inbid(telegram_id, inbound.id)
|
|
514
529
|
resp = await self.clients_end.delete_client_by_email(email, inbound.id)
|
|
515
530
|
responses.append(resp)
|
|
516
|
-
|
|
531
|
+
logging.info("Clients of of tgid %s deleted", telegram_id)
|
|
517
532
|
|
|
518
533
|
return responses
|
|
519
|
-
|
python_3xui/base_model.py
CHANGED
|
@@ -27,9 +27,9 @@ class BaseModel(pydantic.BaseModel):
|
|
|
27
27
|
|
|
28
28
|
model_config = pydantic.ConfigDict(ignored_types=(cached_property, ))
|
|
29
29
|
|
|
30
|
-
def model_post_init(self, context: Any, /) -> None:
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# def model_post_init(self, context: Any, /) -> None:
|
|
31
|
+
# #print(f"Model {self.__class__}, {self} initialized")
|
|
32
|
+
# ...
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
@classmethod
|
|
@@ -83,7 +83,7 @@ class BaseModel(pydantic.BaseModel):
|
|
|
83
83
|
inbounds = await Inbound.from_response(response, client, list)
|
|
84
84
|
"""
|
|
85
85
|
json_resp: util.JsonType = response.json()
|
|
86
|
-
valid = util.
|
|
86
|
+
valid = util.check_xui_response(json_resp)
|
|
87
87
|
if valid == "OK":
|
|
88
88
|
obj = json_resp["obj"]
|
|
89
89
|
if expect is list:
|
|
@@ -91,4 +91,4 @@ class BaseModel(pydantic.BaseModel):
|
|
|
91
91
|
if expect is dict:
|
|
92
92
|
return cls(**obj, client=client)
|
|
93
93
|
else:
|
|
94
|
-
raise ValueError(f"Invalid 3X-UI response, code {valid}")
|
|
94
|
+
raise ValueError(f"Invalid 3X-UI response, code {valid}. Don't use from_response on failed requests.")
|
python_3xui/endpoints.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import logging
|
|
2
3
|
from datetime import datetime, UTC
|
|
3
4
|
from typing import Generic, Literal, List, Dict
|
|
4
5
|
|
|
@@ -232,17 +233,9 @@ class Clients(BaseEndpoint):
|
|
|
232
233
|
else:
|
|
233
234
|
raise TypeError
|
|
234
235
|
# send request
|
|
235
|
-
print(type(final))
|
|
236
|
-
print(final)
|
|
237
236
|
data = final.model_dump(by_alias=True)
|
|
238
|
-
print(type(data))
|
|
239
|
-
print(json.dumps(data))
|
|
240
|
-
print(f"{self._url}{endpoint}")
|
|
241
237
|
resp = await self.client.safe_post(f"{self._url}{endpoint}", data=data)
|
|
242
|
-
|
|
243
238
|
#YOU NEED TO PASS SETTINGS AS A STRING, NOT AS A DICT, YOU FUCKING DUMBASS!
|
|
244
|
-
print(resp)
|
|
245
|
-
print(resp.json())
|
|
246
239
|
return resp
|
|
247
240
|
|
|
248
241
|
async def _request_update_client(self, client: models.InboundClients | models.SingleInboundClient,
|
python_3xui/models.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from types import NoneType
|
|
3
2
|
from datetime import datetime, UTC
|
|
4
|
-
from typing import Union,
|
|
3
|
+
from typing import Union, TypeAlias, Any, Annotated, Literal, List, Dict, ClassVar
|
|
5
4
|
|
|
6
|
-
from pydantic import field_validator, Field, field_serializer, AfterValidator
|
|
7
5
|
import pydantic
|
|
6
|
+
from pydantic import field_validator, Field, field_serializer
|
|
8
7
|
|
|
9
8
|
from . import base_model
|
|
10
|
-
from .util import JsonType, auto_s_to_ms_timestamp,
|
|
9
|
+
from .util import JsonType, auto_s_to_ms_timestamp, auto_ms_to_s_timestamp
|
|
11
10
|
|
|
12
11
|
timestamp_seconds: TypeAlias = int
|
|
13
12
|
ip_address: TypeAlias = str
|
python_3xui/util.py
CHANGED
|
@@ -14,7 +14,7 @@ import logging
|
|
|
14
14
|
import random
|
|
15
15
|
import re
|
|
16
16
|
from datetime import UTC, datetime, tzinfo
|
|
17
|
-
from typing import TypeAlias, Union, Dict, Any, List
|
|
17
|
+
from typing import TypeAlias, Union, Dict, Any, List, dataclass_transform
|
|
18
18
|
|
|
19
19
|
import httpx
|
|
20
20
|
|
|
@@ -180,7 +180,7 @@ def generate_new_subscription(length: int = 16):
|
|
|
180
180
|
return s
|
|
181
181
|
|
|
182
182
|
|
|
183
|
-
async def
|
|
183
|
+
async def check_xui_response(response: JsonType | httpx.Response) -> str:
|
|
184
184
|
"""Validate a 3X-UI API response.
|
|
185
185
|
|
|
186
186
|
Checks if the response follows the expected 3X-UI API format with
|
|
@@ -193,17 +193,18 @@ async def check_xui_response_validity(response: JsonType | httpx.Response) -> st
|
|
|
193
193
|
str: One of three status strings:
|
|
194
194
|
- "OK": Response is valid and successful.
|
|
195
195
|
- "DB_LOCKED": Database is locked, operation should be retried.
|
|
196
|
-
- "
|
|
196
|
+
- "FAIL": Operation was unsuccessful.
|
|
197
197
|
|
|
198
198
|
Raises:
|
|
199
199
|
RuntimeError: If the response doesn't match the expected 3X-UI format.
|
|
200
200
|
|
|
201
201
|
Examples:
|
|
202
|
-
>>>
|
|
202
|
+
>>> check_xui_response({"success": True, "msg": "", "obj": {}})
|
|
203
203
|
'OK'
|
|
204
|
-
>>>
|
|
204
|
+
>>> check_xui_response({"success": False, "msg": "database is locked", "obj": None})
|
|
205
205
|
'DB_LOCKED'
|
|
206
206
|
"""
|
|
207
|
+
#TODO: this is trying to do too much. We'll just check if DB is locked, and then use case-to-case basis to see how they are.
|
|
207
208
|
if isinstance(response, httpx.Response):
|
|
208
209
|
json_resp = response.json()
|
|
209
210
|
else:
|
|
@@ -218,11 +219,9 @@ async def check_xui_response_validity(response: JsonType | httpx.Response) -> st
|
|
|
218
219
|
if "database" in msg.lower() and "locked" in msg.lower() and not success:
|
|
219
220
|
logging.log(logging.WARNING, "Database is locked, retrying...")
|
|
220
221
|
return "DB_LOCKED"
|
|
221
|
-
|
|
222
|
-
return "ERROR"
|
|
222
|
+
return "FAIL"
|
|
223
223
|
raise RuntimeError("Validator got something very unexpected (Please don't shove responses with non-20X status codes in here...)")
|
|
224
224
|
|
|
225
|
-
|
|
226
225
|
def get_days_until_expiry(expiry_time: int) -> float:
|
|
227
226
|
"""Calculate the number of days until a client expires.
|
|
228
227
|
|
|
@@ -231,16 +230,16 @@ def get_days_until_expiry(expiry_time: int) -> float:
|
|
|
231
230
|
|
|
232
231
|
Returns:
|
|
233
232
|
Number of days until expiry. Returns negative value if already expired.
|
|
234
|
-
Returns a very
|
|
233
|
+
Returns a very 0 if expiry_time is 0 (no expiry).
|
|
235
234
|
|
|
236
235
|
Examples:
|
|
237
|
-
>>> get_days_until_expiry(int(datetime.now(UTC).
|
|
236
|
+
>>> get_days_until_expiry(int(datetime.now(UTC).timestamp()) + 86400) # 1 day from now
|
|
238
237
|
1.0
|
|
239
238
|
>>> get_days_until_expiry(0) # No expiry
|
|
240
239
|
inf
|
|
241
240
|
"""
|
|
242
241
|
if expiry_time == 0:
|
|
243
|
-
return
|
|
242
|
+
return 0
|
|
244
243
|
|
|
245
244
|
current_timestamp = datetime.now(UTC).timestamp()
|
|
246
245
|
seconds_remaining = expiry_time - current_timestamp
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: Python-3xui
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.8
|
|
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,10 +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.8 Release Notes</h2>
|
|
32
32
|
<ul>
|
|
33
|
-
<li>
|
|
34
|
-
<li>
|
|
35
|
-
<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
|
|
33
|
+
<li>Improve create_and_add_prod_client to have an expiry_time</li>
|
|
34
|
+
<li>delete_client_by_tgid_all_inbounds -> revoke_client_by_tgid_all_inbounds</li>
|
|
35
|
+
<li>Change vulnerable requirements</li>
|
|
37
36
|
</ul>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
python_3xui/__init__.py,sha256=Zb_-gNgvpe1LS95EKksaLFVM2wgWuXzoFb4pb3DZB_o,94
|
|
2
|
+
python_3xui/api.py,sha256=w7vPgQrvxnymzZtkLOqZb8nYjwdCuMrhklVVxyEC8ts,22347
|
|
3
|
+
python_3xui/base_model.py,sha256=qNQKS_oq5um5dAmpHSB5XhKy_06VJLXYu4eO9Jd50ac,3361
|
|
4
|
+
python_3xui/endpoints.py,sha256=6F7bfMV8i-DnRJx186YLWTjSdTqocdfGHSpZ7NxAt7E,13352
|
|
5
|
+
python_3xui/models.py,sha256=LjbzA9H6Zmrv3qRDQBIaxBiq7NACINiDpl0Uhvh_hK4,11369
|
|
6
|
+
python_3xui/util.py,sha256=PgnlUPQrysThhB5_PpwVdsQb94BF0LTEGC-XuIEgNU8,9277
|
|
7
|
+
python_3xui-0.0.8.dist-info/METADATA,sha256=nGsuWThDwbm4whPk-lBtZQeiJyFaD1MVjHfhPuVI4AY,1520
|
|
8
|
+
python_3xui-0.0.8.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
python_3xui-0.0.8.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
10
|
+
python_3xui-0.0.8.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
python_3xui/__init__.py,sha256=Zb_-gNgvpe1LS95EKksaLFVM2wgWuXzoFb4pb3DZB_o,94
|
|
2
|
-
python_3xui/api.py,sha256=8gZmUscIM8Fd2HEOyuSTWYxwpjlVIBxtct7nlYIPw1I,20821
|
|
3
|
-
python_3xui/base_model.py,sha256=gz0IRuxuNfSFJdm6-5g_ImJ76NqNaNbI1vmllQQuw4I,3319
|
|
4
|
-
python_3xui/endpoints.py,sha256=BKbTN9Sfwlur_79vBPHCGU3djMKYHP5zLn_pExSlEqA,13531
|
|
5
|
-
python_3xui/models.py,sha256=WvyZtoiVXCCY3Mm_UiJi8wDcB2hmzENt4vKKNWrC_yM,11460
|
|
6
|
-
python_3xui/util.py,sha256=6tzlL9zv18BYl40eyI0-SxtsdhRHQmv1cvQpx9sW-9A,9272
|
|
7
|
-
python_3xui-0.0.6.dist-info/METADATA,sha256=fIEouOlKj5vkD5AR-MIRDHK1MQwnxbn92E5fHXk7d6U,1589
|
|
8
|
-
python_3xui-0.0.6.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
-
python_3xui-0.0.6.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
10
|
-
python_3xui-0.0.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|