Python-3xui 0.0.9.post2__tar.gz → 0.0.9.post3__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.
Files changed (21) hide show
  1. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/PKG-INFO +7 -11
  2. python_3xui-0.0.9.post3/README.md +12 -0
  3. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/pyproject.toml +1 -1
  4. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/python_3xui/__init__.py +2 -1
  5. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/python_3xui/api.py +23 -11
  6. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/python_3xui/base_model.py +3 -3
  7. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/python_3xui/models.py +5 -5
  8. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/python_3xui/util.py +1 -0
  9. python_3xui-0.0.9.post2/README.md +0 -16
  10. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/.gitignore +0 -0
  11. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/LICENSE +0 -0
  12. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/python_3xui/custom_exceptions.py +0 -0
  13. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/python_3xui/endpoints.py +0 -0
  14. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/conftest.py +0 -0
  15. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/gather_response_stubs.py +0 -0
  16. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/pytest.ini +0 -0
  17. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/test_endpoints_clients.py +0 -0
  18. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/test_endpoints_inbounds.py +0 -0
  19. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/test_non_idempotent_endpoints_clients.py +0 -0
  20. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/test_non_idempotent_endpoints_inbounds.py +0 -0
  21. {python_3xui-0.0.9.post2 → python_3xui-0.0.9.post3}/tests/test_xuiclient_helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Python-3xui
3
- Version: 0.0.9.post2
3
+ Version: 0.0.9.post3
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,15 +28,11 @@ 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.9 Release Notes</h2>
31
+ <h2>0.0.9-r3 Release Notes</h2>
32
32
  <ul>
33
- <li>Fix _request_update_client for it to actually work and NOT create "zombies"</li>
34
- <li>DTO un-split because fields reset when not provided, so full inbounds must be fetched</li>
35
- <li>New method: update_client_by_tgid</li>
36
- <li>Fixed test suite</li>
37
- <li>Fix from_response and from_list</li>
38
- <li>Remove obsolete and useless client fields from models</li>
39
- <li>Inbound settings actually get parsed properly into ClientsSettings</li>
40
- <li>New asyncio task management so they won't get destroyed when GCed</li>
41
- <li>XUIClient async_lru cache now binds to event loop at runtime, not in initialization</li>
33
+ <li>HOTFIX: the importing of util.py fixed with from __future__ import annotations</li>
34
+ <li>Make panel_id for better accounting & logging clarity</li>
35
+ <li>Fix __aenter__ in XUIClient to not log a warning</li>
36
+ <li>Fix total_gb to be int and not float, since that would need refactoring which I don't have time for yet.</li>
37
+ <li>ClientsSettings now has extra=ignore instead of extra=forbid.</li>
42
38
  </ul>
@@ -0,0 +1,12 @@
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.9-r3 Release Notes</h2>
6
+ <ul>
7
+ <li>HOTFIX: the importing of util.py fixed with from __future__ import annotations</li>
8
+ <li>Make panel_id for better accounting & logging clarity</li>
9
+ <li>Fix __aenter__ in XUIClient to not log a warning</li>
10
+ <li>Fix total_gb to be int and not float, since that would need refactoring which I don't have time for yet.</li>
11
+ <li>ClientsSettings now has extra=ignore instead of extra=forbid.</li>
12
+ </ul>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Python-3xui"
3
- version = "0.0.9r2"
3
+ version = "0.0.9r3"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -1,9 +1,10 @@
1
1
  """Public package interface for the python_3xui API wrapper."""
2
2
 
3
3
  from .api import XUIClient
4
+ import python_3xui.custom_exceptions as exceptions
4
5
 
5
6
  __author__ = "JustMe_001"
6
- __version__ = "0.0.9r2"
7
+ __version__ = "0.0.9r3"
7
8
  __email__ = ""
8
9
 
9
10
 
@@ -76,6 +76,7 @@ class XUIClient:
76
76
  custom_prod_string: str = "testing",
77
77
  max_retries: int = 5, retry_delay=1,
78
78
  custom_sub_generator: Callable[[int], str] | Callable[[int], Awaitable[str]] = util.default_sub_from_tgid,
79
+ panel_id: Any = None
79
80
  ) -> None:
80
81
  """Initialize the XUIClient.
81
82
 
@@ -93,6 +94,7 @@ class XUIClient:
93
94
  retry_delay: Seconds to wait between database-lock retries.
94
95
  custom_sub_generator: Sync or async callable that receives a
95
96
  Telegram ID and returns the subscription ID for new clients.
97
+ panel_id: this is solely for user's purposes to increase logging and accounting clarity. Default is None.
96
98
  """
97
99
  self.connected: bool = False
98
100
  self.PROD_STRING = re.compile(custom_prod_string)
@@ -110,6 +112,7 @@ class XUIClient:
110
112
  self.max_retries: int = max_retries
111
113
  self.retry_delay: int = retry_delay
112
114
  self.sub_gen = custom_sub_generator
115
+ self.panel_id: int | str | Any = panel_id
113
116
  # endpoints
114
117
  self.server_end = endpoints.Server(self)
115
118
  self.clients_end = endpoints.Clients(self)
@@ -198,7 +201,7 @@ class XUIClient:
198
201
  if resp.status_code == 404:
199
202
  now: float = datetime.now(UTC).timestamp()
200
203
  if self.session_start is None or now - self.session_start > self.session_duration:
201
- logging.info("Client with IP/Domain %s is not logged in, logging in...", self.base_host)
204
+ logging.info("Client (panel: %s) is not logged in, logging in...", self.panel_id or self.base_host)
202
205
  await self.login()
203
206
  continue
204
207
  else:
@@ -322,7 +325,7 @@ class XUIClient:
322
325
  if self.two_fac_secret:
323
326
  payload["twoFactorCode"] = self.two_fac_secret.get_secret_value()
324
327
 
325
- logging.info("Client is logging in with IP/Domain: %s", self.base_host)
328
+ logging.info("Client is logging in (panel: %s)", self.panel_id or self.base_host)
326
329
  resp = await self.session.post("/login", data=payload)
327
330
  if resp.status_code == 200:
328
331
  resp_json = resp.json()
@@ -343,7 +346,7 @@ class XUIClient:
343
346
  Returns:
344
347
  Self: The XUIClient instance.
345
348
  """
346
- logging.log(DEBUG, "Client connected with IP/domain %s", self.base_url)
349
+ logging.log(DEBUG, "Client connected (panel: %s)", self.panel_id or self.base_url)
347
350
  self.session = AsyncClient(base_url=self.base_url)
348
351
  self.connected = True
349
352
  return self
@@ -373,9 +376,10 @@ class XUIClient:
373
376
  """
374
377
  self.connect()
375
378
  await self.login()
376
- self._cache_cleaner_task = asyncio.create_task(
377
- self._clear_prod_inbound_cache_task(), name=f"inb_cache_clearer_for_{self.base_url}"
378
- )
379
+ if not self._cache_cleaner_task:
380
+ self._cache_cleaner_task = asyncio.create_task(
381
+ self._clear_prod_inbound_cache_task(create_new=True), name=f"inb_cache_clearer_for_{self.base_url}"
382
+ )
379
383
  return self
380
384
 
381
385
  async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
@@ -390,12 +394,12 @@ class XUIClient:
390
394
  exc_tb: The exception traceback, if an exception occurred.
391
395
  """
392
396
  if exc_type is None or exc_type is asyncio.exceptions.CancelledError:
393
- logging.info("Client is disconnecting at time with IP/Domain %s", self.base_host)
397
+ logging.info("Client is disconnecting (panel: %s)", self.panel_id or self.base_host)
394
398
  else:
395
399
  logging.warning("Client is disconnecting due to an error (may be unrelated):"
396
400
  "\n%s, with value %s\nStacktrace:%s",
397
401
  exc_type, exc_val, exc_tb, exc_info=exc_tb)
398
- print(f"Client is disconnecting: {self.base_host}")
402
+ print(f"Client is disconnecting: {self.panel_id or self.base_host}")
399
403
  await self.disconnect()
400
404
  return
401
405
 
@@ -424,16 +428,20 @@ class XUIClient:
424
428
 
425
429
  return tuple(usable_inbounds)
426
430
 
427
- async def _clear_prod_inbound_cache_task(self):
431
+ async def _clear_prod_inbound_cache_task(self, *, create_new: bool = False):
428
432
  """Refresh the production inbound cache in the background.
429
433
 
430
434
  The async context manager starts this loop after login. Each cycle
431
435
  clears the cached production inbound list, repopulates it from the
432
436
  panel, and then waits before refreshing again.
437
+
438
+ create_new param is kw-only and for people who know what they're doing, so they won't get the warning.
433
439
  """
434
- if self._cache_cleaner_task is not None:
440
+ if (self._cache_cleaner_task is not None) and (not create_new):
435
441
  logging.warning("You're trying to create another cache cleaner task, which is a FaF (Fire-And-Forget)."
436
442
  "Please destroy the previous task and set _cache_cleaner_task to None, if you know what you're doing.")
443
+ return
444
+ logging.info("Initializing cache cleaner task for %s", self.panel_id)
437
445
  while self.connected:
438
446
  self.get_production_inbounds.cache_clear()
439
447
  await self.get_production_inbounds() # fill the cache
@@ -525,7 +533,8 @@ class XUIClient:
525
533
  raise custom_exceptions.ClientEmailAlreadyExistsError(json_resp["msg"])
526
534
  return responses
527
535
 
528
- async def _find_client_in_inbound(self, client_uuid: str, inbound_id: int, use_cache=False) -> SingleInboundClient | None:
536
+ async def _find_client_in_inbound(self, client_uuid: str, inbound_id: int,
537
+ use_cache=False) -> SingleInboundClient | None:
529
538
  """Note:
530
539
  Cached production inbounds can be stale because the panel may be
531
540
  changed by another actor. If a cached production inbound misses the
@@ -642,6 +651,7 @@ class XUIClient:
642
651
  enable: bool | None = None,
643
652
  sub_id: str | None = None,
644
653
  comment: str | None = None,
654
+ email: str | None = None,
645
655
  verbose: bool = True) -> Response:
646
656
  """
647
657
  Update a client in a specific inbound by Telegram ID. NOT optimized for multiple inbounds.
@@ -658,6 +668,7 @@ class XUIClient:
658
668
  enable: Whether the client is enabled (optional)
659
669
  sub_id: Subscription ID (optional)
660
670
  comment: Client comment/note (optional)
671
+ email: New client email (optional). USE WITH CAUTION BECAUSE THE PANEL WILL NOT TRACK THE NEW EMAIL.
661
672
 
662
673
  Returns:
663
674
  Response from the API
@@ -674,6 +685,7 @@ class XUIClient:
674
685
  inbound_id=inbound_id, client_uuid=util.get_uuid_from_tgid(telegram_id),
675
686
  security=security,
676
687
  password=password,
688
+ email=email,
677
689
  flow=flow,
678
690
  limit_ip=limit_ip,
679
691
  limit_gb=limit_gb,
@@ -9,6 +9,7 @@ from . import util
9
9
  if TYPE_CHECKING:
10
10
  from .api import XUIClient
11
11
 
12
+
12
13
  class BaseModel(pydantic.BaseModel):
13
14
  """Base model for all 3X-UI API data models.
14
15
 
@@ -22,13 +23,12 @@ class BaseModel(pydantic.BaseModel):
22
23
  ERROR_RETRIES: ClassVar[int] = 5
23
24
  ERROR_RETRY_COOLDOWN: ClassVar[int] = 1
24
25
 
25
- model_config = pydantic.ConfigDict(ignored_types=(cached_property, ), validate_by_name=True, validate_by_alias=True)
26
+ model_config = pydantic.ConfigDict(ignored_types=(cached_property,), validate_by_name=True, validate_by_alias=True)
26
27
 
27
28
  # def model_post_init(self, context: Any, /) -> None:
28
29
  # #print(f"Model {self.__class__}, {self} initialized")
29
30
  # ...
30
31
 
31
-
32
32
  @classmethod
33
33
  def from_list(cls, args: List[Dict[str, Any]],
34
34
  ) -> List[Self]:
@@ -50,7 +50,7 @@ class BaseModel(pydantic.BaseModel):
50
50
  cls,
51
51
  response: httpx.Response,
52
52
  client: "XUIClient",
53
- expect: list|dict,
53
+ expect: list | dict,
54
54
  auto_retry: bool = True
55
55
  ) -> Union[Self, List[Self]]:
56
56
  """Create model instance(s) from an HTTP response.
@@ -63,7 +63,7 @@ class SingleInboundClient(base_model.BaseModel):
63
63
  reset: int = 0
64
64
  #Interestingly, the API expects this value to be called GB but it's actually bytes.
65
65
  # I want the pythonic side to be in GB (hence why floats, i.e. 2.5GB), but the API expects bytes.
66
- limit_gb: Annotated[int | float, Field(alias="totalGB")] = 0 # total flow
66
+ limit_gb: Annotated[int, Field(alias="totalGB")] = 0 # total flow
67
67
  expiry_time: Annotated[timestamp_seconds, Field(alias="expiryTime")] = 0
68
68
  enable: bool = True
69
69
  tg_id: Annotated[Union[int, str], Field(alias="tgId")] = ""
@@ -88,15 +88,15 @@ class SingleInboundClient(base_model.BaseModel):
88
88
 
89
89
  @field_serializer("limit_gb")
90
90
  @classmethod
91
- def serialize_total_gb(cls, value: int | float) -> int:
91
+ def serialize_total_gb(cls, value: int) -> int:
92
92
  #API expects an integer of bytes.
93
93
  return value * (1024 ** 3)
94
94
 
95
95
  @field_validator("limit_gb", mode="after")
96
96
  @classmethod
97
- def parse_total_gb(cls, value: int) -> int | float:
97
+ def parse_total_gb(cls, value: int) -> int:
98
98
  #Python wants an int/float of GB.
99
- return value / (1024 ** 3)
99
+ return value // (1024 ** 3)
100
100
 
101
101
 
102
102
  class ClientsSettings(base_model.BaseModel):
@@ -295,7 +295,7 @@ class Inbound(base_model.BaseModel):
295
295
  def parse_settings(cls, value: str) -> ClientsSettings:
296
296
  if value == "":
297
297
  return ClientsSettings(clients=[])
298
- return ClientsSettings.model_validate_json(value, by_alias=True, extra="forbid")
298
+ return ClientsSettings.model_validate_json(value, by_alias=True, extra="ignore")
299
299
 
300
300
  @field_serializer("settings")
301
301
  @classmethod
@@ -8,6 +8,7 @@ including:
8
8
  - Telegram ID-based UUID/email generation
9
9
  - Response validation
10
10
  """
11
+ from __future__ import annotations
11
12
 
12
13
  import asyncio
13
14
  import base64
@@ -1,16 +0,0 @@
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.9 Release Notes</h2>
6
- <ul>
7
- <li>Fix _request_update_client for it to actually work and NOT create "zombies"</li>
8
- <li>DTO un-split because fields reset when not provided, so full inbounds must be fetched</li>
9
- <li>New method: update_client_by_tgid</li>
10
- <li>Fixed test suite</li>
11
- <li>Fix from_response and from_list</li>
12
- <li>Remove obsolete and useless client fields from models</li>
13
- <li>Inbound settings actually get parsed properly into ClientsSettings</li>
14
- <li>New asyncio task management so they won't get destroyed when GCed</li>
15
- <li>XUIClient async_lru cache now binds to event loop at runtime, not in initialization</li>
16
- </ul>