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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Python-3xui
3
- Version: 0.0.4
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
- <head>
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.4"
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) -> None:
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 = "tester-777"
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
- #print("WARNING: You seem to have entered a 2FA **code**, not a 2FA secret."
101
- #"Although entering the secret is dangerous, there is no other way to provide a consistent way"
102
- #"for continuous login. This code will only work for this specific login.")
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
- #print("initializing client")
120
+ print("initializing client")
119
121
  if cls._instance is None:
120
- #print("nu instance")
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
- #print(f"SAFE REQUEST, {method}, is running to a URL of {kwargs["url"]}")
144
- #print(str(self.session.base_url) + str(kwargs["url"]))
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
- #print("Guys, we're not logged in, fixing that rn")
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
- #print(self.session.base_url)
274
- #print("WE'RE LOGGING IN")
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
- #print("disconnectin'")
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.lower() in inb.remark.lower():
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
- #print("You're seeing this message because I forgot to remove it in api.update_inbounds() !")
373
- #print("Please change the timer from 5 to 60*60*24!")
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(60*10)
377
- ##print(stat)
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
- #print("Inbound deleted")
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
- ##print(f"Model {self.__class__}, {self} initialized")
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
- #print(type(final))
236
- #print(final)
235
+ print(type(final))
236
+ print(final)
237
237
  data = final.model_dump(by_alias=True)
238
- #print(type(data))
239
- #print(json.dumps(data))
240
- #print(f"{self._url}{endpoint}")
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
- #print(resp)
245
- #print(resp.json())
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
- #print(f"Unsuccessful operation! Message: {json_resp["msg"]}")
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
 
@@ -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