Python-3xui 0.0.6__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.6
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
@@ -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.6 Release Notes</h2>
31
+ <h2>0.0.7 Release Notes</h2>
32
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
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>
37
36
  </ul>
@@ -2,10 +2,9 @@
2
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
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
4
 
5
- <h2>0.0.6 Release Notes</h2>
5
+ <h2>0.0.7 Release Notes</h2>
6
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
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>
11
10
  </ul>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Python-3xui"
3
- version = "0.0.6"
3
+ version = "0.0.7"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -1,6 +1,8 @@
1
+ import logging
1
2
  import re
2
3
  import time
3
4
  from collections.abc import Sequence, Mapping
5
+ from logging import DEBUG
4
6
  from typing import Self, Optional, Dict, Iterable, AsyncIterable, Type, Union, Any, List, Tuple, Literal
5
7
  from datetime import datetime, UTC
6
8
 
@@ -106,23 +108,6 @@ class XUIClient:
106
108
  else:
107
109
  self.totp = pyotp.TOTP(self.two_fac_secret)
108
110
 
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
111
  #========================request stuffs========================
127
112
  async def _safe_request(self,
128
113
  method: Literal["get", "post", "patch", "delete", "put"],
@@ -142,20 +127,21 @@ class XUIClient:
142
127
  Raises:
143
128
  RuntimeError: If max retries exceeded or session is invalid.
144
129
  """
145
- print(f"SAFE REQUEST, {method}, is running to a URL of {kwargs["url"]}")
146
- 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"]))
147
131
  async for attempt in async_range(self.max_retries):
148
132
  resp = await self.session.request(method=method, **kwargs)
149
133
  if resp.status_code // 100 != 2: #because it can return either 201 or 202
150
134
  if resp.status_code == 404:
151
135
  now: float = datetime.now(UTC).timestamp()
152
136
  if self.session_start is None or now - self.session_start > self.session_duration:
153
- 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)
154
138
  await self.login()
155
139
  continue
156
140
  else:
141
+ logging.error("Server returned a status code of %s with a valid session", resp.status_code)
157
142
  raise RuntimeError("""Server returned a 404, and the session should still be valid, likely it's a REAL 404""")
158
143
  else:
144
+ logging.error("Server returned a status code of %s", resp.status_code)
159
145
  raise RuntimeError(f"Wrong status code: {resp.status_code}")
160
146
 
161
147
  status = await util.check_xui_response_validity(resp)
@@ -265,15 +251,14 @@ class XUIClient:
265
251
  "password": self.xui_password,
266
252
  }
267
253
  if self.totp:
268
- if self.totp.interval - datetime.now().timestamp() % self.totp.interval < 1:
269
- 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
270
256
  payload["twoFactorCode"] = self.totp.now()
271
257
  else:
272
258
  if self.two_fac_secret:
273
259
  payload["twoFactorCode"] = self.two_fac_secret
274
260
 
275
- print(self.session.base_url)
276
- print("WE'RE LOGGING IN")
261
+ logging.info("Client is logging in with IP/Domain: %s", self.base_host)
277
262
  resp = await self.session.post("/login", data=payload)
278
263
  if resp.status_code == 200:
279
264
  resp_json = resp.json()
@@ -293,6 +278,7 @@ class XUIClient:
293
278
  Returns:
294
279
  Self: The XUIClient instance.
295
280
  """
281
+ logging.log(DEBUG, "Client connected with IP/domain %s", self.base_url)
296
282
  self.session = AsyncClient(base_url=self.base_url)
297
283
  self.connected = True
298
284
  return self
@@ -331,7 +317,13 @@ class XUIClient:
331
317
  exc_val: The exception value, if an exception occurred.
332
318
  exc_tb: The exception traceback, if an exception occurred.
333
319
  """
334
- 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}")
335
327
  await self.disconnect()
336
328
  return
337
329
 
@@ -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
File without changes
File without changes