Python-3xui 0.0.5__tar.gz → 0.0.7__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.5
3
+ Version: 0.0.7
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
@@ -24,16 +24,13 @@ Requires-Dist: pytest-dependency; extra == 'testing'
24
24
  Requires-Dist: requests; extra == 'testing'
25
25
  Description-Content-Type: text/markdown
26
26
 
27
- <head>
28
- <title>Readme</title>
29
- <style>
30
- .welcome {
31
- color: rgb(1, 170, 170)
32
- }
33
- </style>
34
- </head>
35
-
36
-
37
- <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>
38
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>
39
- <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.7 Release Notes</h2>
32
+ <ul>
33
+ <li>Purge prints and replace them with logging (except when absolutely needed)</li>
34
+ <li>Make prod_string regEx</li>
35
+ <li>Change the test suite</li>
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.7 Release Notes</h2>
6
+ <ul>
7
+ <li>Purge prints and replace them with logging (except when absolutely needed)</li>
8
+ <li>Make prod_string regEx</li>
9
+ <li>Change the test suite</li>
10
+ </ul>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Python-3xui"
3
- version = "0.0.5"
3
+ version = "0.0.7"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -1,5 +1,8 @@
1
+ import logging
2
+ import re
1
3
  import time
2
4
  from collections.abc import Sequence, Mapping
5
+ from logging import DEBUG
3
6
  from typing import Self, Optional, Dict, Iterable, AsyncIterable, Type, Union, Any, List, Tuple, Literal
4
7
  from datetime import datetime, UTC
5
8
 
@@ -77,7 +80,7 @@ class XUIClient:
77
80
  """
78
81
  from . import endpoints # look, I know it's bad, but we need to evade cyclical imports
79
82
  self.connected: bool = False
80
- self.PROD_STRING = custom_prod_string
83
+ self.PROD_STRING = re.compile(custom_prod_string)
81
84
  self.session: AsyncClient | None = None
82
85
  self.base_host: str = base_website
83
86
  self.base_port: int = base_port
@@ -105,23 +108,6 @@ class XUIClient:
105
108
  else:
106
109
  self.totp = pyotp.TOTP(self.two_fac_secret)
107
110
 
108
- #========================singleton pattern========================
109
- def __new__(cls, *args, **kwargs):
110
- """Create or return the singleton instance.
111
-
112
- Args:
113
- *args: Positional arguments passed to __init__.
114
- **kwargs: Keyword arguments passed to __init__.
115
-
116
- Returns:
117
- The singleton XUIClient instance.
118
- """
119
- print("initializing client")
120
- if cls._instance is None:
121
- print("nu instance")
122
- cls._instance = super(XUIClient, cls).__new__(cls)
123
- return cls._instance
124
-
125
111
  #========================request stuffs========================
126
112
  async def _safe_request(self,
127
113
  method: Literal["get", "post", "patch", "delete", "put"],
@@ -141,20 +127,21 @@ class XUIClient:
141
127
  Raises:
142
128
  RuntimeError: If max retries exceeded or session is invalid.
143
129
  """
144
- print(f"SAFE REQUEST, {method}, is running to a URL of {kwargs["url"]}")
145
- print(str(self.session.base_url) + str(kwargs["url"]))
130
+ logging.debug("Safe request is running to %s%s", str(self.session.base_url), str(kwargs["url"]))
146
131
  async for attempt in async_range(self.max_retries):
147
132
  resp = await self.session.request(method=method, **kwargs)
148
133
  if resp.status_code // 100 != 2: #because it can return either 201 or 202
149
134
  if resp.status_code == 404:
150
135
  now: float = datetime.now(UTC).timestamp()
151
136
  if self.session_start is None or now - self.session_start > self.session_duration:
152
- print("Guys, we're not logged in, fixing that rn")
137
+ logging.info("Client with IP/Domain %s is not logged in, logging in...", self.base_host)
153
138
  await self.login()
154
139
  continue
155
140
  else:
141
+ logging.error("Server returned a status code of %s with a valid session", resp.status_code)
156
142
  raise RuntimeError("""Server returned a 404, and the session should still be valid, likely it's a REAL 404""")
157
143
  else:
144
+ logging.error("Server returned a status code of %s", resp.status_code)
158
145
  raise RuntimeError(f"Wrong status code: {resp.status_code}")
159
146
 
160
147
  status = await util.check_xui_response_validity(resp)
@@ -264,15 +251,14 @@ class XUIClient:
264
251
  "password": self.xui_password,
265
252
  }
266
253
  if self.totp:
267
- if self.totp.interval - datetime.now().timestamp() % self.totp.interval < 1:
268
- await asyncio.sleep(1.1) # just to not submit an invalid code
254
+ if self.totp.interval - datetime.now().timestamp() % self.totp.interval < 3:
255
+ await asyncio.sleep(3.1) # just to not submit an invalid code
269
256
  payload["twoFactorCode"] = self.totp.now()
270
257
  else:
271
258
  if self.two_fac_secret:
272
259
  payload["twoFactorCode"] = self.two_fac_secret
273
260
 
274
- print(self.session.base_url)
275
- print("WE'RE LOGGING IN")
261
+ logging.info("Client is logging in with IP/Domain: %s", self.base_host)
276
262
  resp = await self.session.post("/login", data=payload)
277
263
  if resp.status_code == 200:
278
264
  resp_json = resp.json()
@@ -292,6 +278,7 @@ class XUIClient:
292
278
  Returns:
293
279
  Self: The XUIClient instance.
294
280
  """
281
+ logging.log(DEBUG, "Client connected with IP/domain %s", self.base_url)
295
282
  self.session = AsyncClient(base_url=self.base_url)
296
283
  self.connected = True
297
284
  return self
@@ -330,7 +317,13 @@ class XUIClient:
330
317
  exc_val: The exception value, if an exception occurred.
331
318
  exc_tb: The exception traceback, if an exception occurred.
332
319
  """
333
- print("disconnectin'")
320
+ if exc_type is None:
321
+ logging.info("Client is disconnecting at time with IP/Domain %s", self.base_host)
322
+ else:
323
+ logging.warning("Client is disconnecting due to an error (may be unrelated):"
324
+ "\n%s, with value %s\nStacktrace:%s",
325
+ exc_type, exc_val, exc_tb)
326
+ print(f"Client is disconnecting: {self.base_host}")
334
327
  await self.disconnect()
335
328
  return
336
329
 
@@ -351,8 +344,7 @@ class XUIClient:
351
344
  inbounds = await self.inbounds_end.get_all()
352
345
  usable_inbounds: list[Inbound] = []
353
346
  for inb in inbounds:
354
- #TODO: make prod_strings regex instead of STR
355
- if self.PROD_STRING.lower() in inb.remark.lower():
347
+ if self.PROD_STRING.search(inb.remark):
356
348
  usable_inbounds.append(inb)
357
349
  if len(usable_inbounds) == 0:
358
350
  raise RuntimeError("No production inbounds found! Change prod_string!")
@@ -371,12 +363,9 @@ class XUIClient:
371
363
  timer from 5 to 60*60*24 in the code.
372
364
  """
373
365
  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
366
  self.get_production_inbounds.cache_clear()
377
367
  await self.get_production_inbounds() #fill the cache
378
- await asyncio.sleep(10)
379
- #print(stat)
368
+ await asyncio.sleep(3600) #update every 1h
380
369
 
381
370
  #========================clients management========================
382
371
  async def get_client_with_tgid(self, tgid: int, inbound_id: int | None = None) -> List[ClientStats]:
@@ -513,7 +502,7 @@ class XUIClient:
513
502
  email = util.generate_email_from_tgid_inbid(telegram_id, inbound.id)
514
503
  resp = await self.clients_end.delete_client_by_email(email, inbound.id)
515
504
  responses.append(resp)
516
- print("Inbound deleted")
505
+ logging.info("Clients of of tgid %s deleted", telegram_id)
517
506
 
518
507
  return responses
519
508
 
@@ -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
- #print(f"Model {self.__class__}, {self} initialized")
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
@@ -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,
@@ -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, Optional, TypeAlias, Any, Annotated, Literal, List, Dict, ClassVar
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, s_to_ms_timestamp, ms_to_s_timestamp, auto_ms_to_s_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
@@ -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
+ logging.warning("Unsuccessful operation (status code 200, success = false)! Message: %s", 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
 
@@ -36,8 +36,9 @@ async def xui_client() -> XUIClient:
36
36
  base_path = os.getenv("BASE_PATH")
37
37
  username = os.getenv("XUI_USERNAME")
38
38
  password = os.getenv("XUI_PASSWORD")
39
+ two_fac = os.getenv("XUI_2FA_SECRET")
39
40
 
40
- if not all([base_url, port_str, base_path, username, password]):
41
+ if not all([base_url, port_str, base_path, username, password, two_fac]):
41
42
  pytest.skip("Environment variables for XUIClient not configured (.env file required)")
42
43
 
43
44
  try:
@@ -48,7 +49,7 @@ async def xui_client() -> XUIClient:
48
49
  # Reset singleton for clean test state
49
50
  XUIClient._instance = None
50
51
 
51
- client = XUIClient(base_url, port, base_path, username=username, password=password)
52
+ client = XUIClient(base_url, port, base_path, username=username, password=password, two_fac_code=two_fac, custom_prod_string="test3")
52
53
  client.connect()
53
54
 
54
55
  # Authenticate
@@ -36,7 +36,7 @@ class TestClientsEndpoint:
36
36
  # Try to find a suitable inbound (preferably with PROD_STRING in remark)
37
37
  test_inbound = None
38
38
  for inbound in all_inbounds:
39
- if xui_client.PROD_STRING in inbound.remark.lower():
39
+ if xui_client.PROD_STRING.search(inbound.remark.lower()):
40
40
  test_inbound = inbound
41
41
  break
42
42
 
@@ -61,7 +61,8 @@ class TestInboundsEndpoint:
61
61
  )
62
62
 
63
63
  # Create the inbound
64
- response = await xui_client.inbounds_end.addigga
64
+ pytest.skip("Not implemented yet")
65
+ response = await xui_client.inbounds_end.add_inbound()
65
66
 
66
67
  # Validate response
67
68
  assert response.status_code == 200
@@ -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