Python-3xui 0.0.11__tar.gz → 0.0.12__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 (26) hide show
  1. {python_3xui-0.0.11 → python_3xui-0.0.12}/PKG-INFO +4 -8
  2. python_3xui-0.0.12/README.md +6 -0
  3. {python_3xui-0.0.11 → python_3xui-0.0.12}/pyproject.toml +2 -2
  4. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/__init__.py +1 -1
  5. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/api.py +10 -5
  6. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/api_core/client_service.py +34 -7
  7. python_3xui-0.0.11/README.md +0 -10
  8. {python_3xui-0.0.11 → python_3xui-0.0.12}/.gitignore +0 -0
  9. {python_3xui-0.0.11 → python_3xui-0.0.12}/LICENSE +0 -0
  10. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/api_core/__init__.py +0 -0
  11. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/api_core/identity.py +0 -0
  12. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/api_core/prod_cache.py +0 -0
  13. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/api_core/session_core.py +0 -0
  14. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/base_model.py +0 -0
  15. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/custom_exceptions.py +0 -0
  16. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/endpoints.py +0 -0
  17. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/models.py +0 -0
  18. {python_3xui-0.0.11 → python_3xui-0.0.12}/python_3xui/util.py +0 -0
  19. {python_3xui-0.0.11 → python_3xui-0.0.12}/tests/conftest.py +0 -0
  20. {python_3xui-0.0.11 → python_3xui-0.0.12}/tests/gather_response_stubs.py +0 -0
  21. {python_3xui-0.0.11 → python_3xui-0.0.12}/tests/pytest.ini +0 -0
  22. {python_3xui-0.0.11 → python_3xui-0.0.12}/tests/test_endpoints_clients.py +0 -0
  23. {python_3xui-0.0.11 → python_3xui-0.0.12}/tests/test_endpoints_inbounds.py +0 -0
  24. {python_3xui-0.0.11 → python_3xui-0.0.12}/tests/test_non_idempotent_endpoints_clients.py +0 -0
  25. {python_3xui-0.0.11 → python_3xui-0.0.12}/tests/test_non_idempotent_endpoints_inbounds.py +0 -0
  26. {python_3xui-0.0.11 → python_3xui-0.0.12}/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.11
3
+ Version: 0.0.12
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
@@ -11,7 +11,7 @@ Classifier: Development Status :: 3 - Alpha
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
- Requires-Python: >=3.11
14
+ Requires-Python: >=3.12
15
15
  Requires-Dist: async-lru~=2.3.0
16
16
  Requires-Dist: httpx~=0.28.1
17
17
  Requires-Dist: pydantic<3,~=2.12.5
@@ -28,9 +28,5 @@ 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.10 Release Notes</h2>
32
- <ul>
33
- <li>HOTFIX: make models.SingleInboundClient default flow "", because turns out panel can not return it because of zombification...</li>
34
- <li>Add a custom uuid generator for XUIClient that <i>defaults</i> to method in util but you can make your own!</li>
35
- <li>Uncomplicate self.sub_gen into self._resolve_sub</li>
36
- </ul>
31
+ <h2>0.0.12 Release Notes</h2>
32
+ Mainly just bugfixes. Fix email fallback, fix adding clients, add inbound management.
@@ -0,0 +1,6 @@
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.12 Release Notes</h2>
6
+ Mainly just bugfixes. Fix email fallback, fix adding clients, add inbound management.
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "Python-3xui"
3
- version = "0.0.11"
3
+ version = "0.0.12"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
7
7
  description = "3x-ui wrapper for python"
8
8
  readme = "README.md"
9
9
 
10
- requires-python = ">=3.11"
10
+ requires-python = ">=3.12"
11
11
  classifiers = [
12
12
  "Programming Language :: Python :: 3",
13
13
  "Operating System :: OS Independent",
@@ -4,7 +4,7 @@ from .api import XUIClient
4
4
  import python_3xui.custom_exceptions as exceptions
5
5
 
6
6
  __author__ = "JustMe_001"
7
- __version__ = "0.0.11"
7
+ __version__ = "0.0.12"
8
8
  __email__ = ""
9
9
 
10
10
 
@@ -470,7 +470,8 @@ class XUIClient:
470
470
  sub_id: str | None = None,
471
471
  comment: str | None = None,
472
472
  email: str | None = None,
473
- verbose: bool = True) -> Response:
473
+ verbose: bool = True,
474
+ force_resolve_by_email: bool = False) -> Response:
474
475
  """
475
476
  Update a client in a specific inbound by Telegram ID. NOT optimized for multiple inbounds.
476
477
 
@@ -486,8 +487,9 @@ class XUIClient:
486
487
  enable: Whether the client is enabled (optional)
487
488
  sub_id: Subscription ID (optional)
488
489
  comment: Client comment/note (optional)
489
- email: New client email (optional). USE WITH CAUTION BECAUSE THE PANEL WILL NOT TRACK THE NEW EMAIL.
490
-
490
+ email: New client email (optional). USE WITH CAUTION BECAUSE THE XUIClient WILL NOT TRACK THE NEW EMAIL.
491
+ verbose: Enables guardrails.
492
+ force_resolve_by_email: Whether to enable fetch-thru-email fallback when a client is not found, uses ~3 extra fetches but provides an extra layer of protection.
491
493
  Returns:
492
494
  Response from the API
493
495
  """
@@ -505,19 +507,22 @@ class XUIClient:
505
507
  comment=comment,
506
508
  email=email,
507
509
  verbose=verbose,
510
+ force_resolve_by_email=force_resolve_by_email,
508
511
  )
509
512
 
510
- async def delete_client_by_tgid(self, telegram_id: int, inbound_id: int) -> Response:
513
+ async def delete_client_by_tgid(self, telegram_id: int, inbound_id: int, *, suffix: str = "") -> Response:
511
514
  """Delete a client from a specific inbound by Telegram ID.
512
515
 
513
516
  Args:
514
517
  telegram_id: The Telegram ID of the client
515
518
  inbound_id: The ID of the inbound
519
+ suffix: Appended to the generated email before deletion (use when the
520
+ target client was created with a custom email suffix).
516
521
 
517
522
  Returns:
518
523
  Response from the API
519
524
  """
520
- return await self._tg_client_service.delete_client_by_tgid(telegram_id, inbound_id)
525
+ return await self._tg_client_service.delete_client_by_tgid(telegram_id, inbound_id, suffix=suffix)
521
526
 
522
527
  async def revoke_client_by_tgid_all_inbounds(self, telegram_id: int) -> List[Response]:
523
528
  """Delete a client from all production inbounds by Telegram ID.
@@ -107,13 +107,29 @@ class TgIDClientService:
107
107
 
108
108
  if _update_exec:
109
109
  update_results: list[Response] = await asyncio.gather(*_update_exec)
110
+ _search_update_exec: list[Task] = []
111
+ _search_update_resp: dict[int, Task] = {}
110
112
  for i, inb_id in enumerate(update_inbound_ids):
111
- responses[inb_id] = update_results[i]
113
+ _resp = update_results[i]
114
+ _msg: str = _resp.json()["msg"]
115
+ if "empty client id" in _msg.lower():
116
+ t = asyncio.create_task(
117
+ self._resolve_update_client(telegram_id, inb_id, clients_by_inbound[inb_id])
118
+ )
119
+ _search_update_exec.append(t)
120
+ _search_update_resp[inb_id] = t
121
+ else:
122
+ responses[inb_id] = _resp
123
+
124
+ if _search_update_exec:
125
+ await asyncio.gather(*_search_update_exec)
126
+ for inb_id, task in _search_update_resp.items():
127
+ responses[inb_id] = task.result()
112
128
 
113
129
  # --- Phase 3: raise on remaining duplicates if not exist_ok ---
114
130
  if not exist_ok:
115
131
  for inb_id, resp in responses.items():
116
- json_resp = resp.json()
132
+ json_resp: dict = resp.json()
117
133
  msg = json_resp.get("msg", "")
118
134
  if "duplicate email" in msg.lower():
119
135
  logging.error(
@@ -124,13 +140,22 @@ class TgIDClientService:
124
140
 
125
141
  return responses
126
142
 
143
+ async def _resolve_update_client(self, telegram_id: int, inb_id: int,
144
+ inbound_client: SingleInboundClient) -> Response:
145
+ _found = await self._clients.get_client_with_email(
146
+ util.generate_email_from_tgid_inbid(telegram_id, inb_id)
147
+ )
148
+ return await self._clients.request_update_client(
149
+ inbound_client, inb_id, original_uuid=_found.uuid,
150
+ )
151
+
127
152
  async def _find_client_in_inbound(self,
128
153
  client_uuid: str,
129
154
  inbound_id: int,
130
155
  *,
131
- use_cache: bool = False,
156
+ use_prod_cache: bool = False,
132
157
  ) -> SingleInboundClient | None:
133
- if use_cache:
158
+ if use_prod_cache:
134
159
  prod_inbs = await self._prod_cache.get()
135
160
  prod_inb_index = None
136
161
  for i, prod_inb in enumerate(prod_inbs):
@@ -256,7 +281,7 @@ class TgIDClientService:
256
281
  )
257
282
 
258
283
  client_uuid = await self._identity.resolve_uuid(telegram_id)
259
- found = await self._find_client_in_inbound(client_uuid, inbound_id)
284
+ found = await self._find_client_in_inbound(client_uuid, inbound_id, use_prod_cache=True)
260
285
  if not found:
261
286
  if force_resolve_by_email:
262
287
  _email_to_search = util.generate_email_from_tgid_inbid(telegram_id, inbound_id)
@@ -264,6 +289,8 @@ class TgIDClientService:
264
289
  if resp is None:
265
290
  raise ClientDoesNotExistError(f"The target inbound was force-checked by email but client {_email_to_search} was not found.")
266
291
  client_uuid = resp.uuid
292
+ found_inbound_id = resp.inboundId
293
+ found = await self._find_client_in_inbound(client_uuid, found_inbound_id, use_prod_cache=True)
267
294
  else:
268
295
  raise ClientDoesNotExistError(
269
296
  f"The target inbound was checked but client {client_uuid} was not found."
@@ -283,8 +310,8 @@ class TgIDClientService:
283
310
  comment=comment,
284
311
  )
285
312
 
286
- async def delete_client_by_tgid(self, telegram_id: int, inbound_id: int) -> Response:
287
- email = util.generate_email_from_tgid_inbid(telegram_id, inbound_id)
313
+ async def delete_client_by_tgid(self, telegram_id: int, inbound_id: int, *, suffix: str = "") -> Response:
314
+ email = util.generate_email_from_tgid_inbid(telegram_id, inbound_id) + suffix
288
315
  return await self._clients.delete_client_by_email(email, inbound_id)
289
316
 
290
317
  async def revoke_client_by_tgid_all_inbounds(self, telegram_id: int) -> List[Response]:
@@ -1,10 +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.10 Release Notes</h2>
6
- <ul>
7
- <li>HOTFIX: make models.SingleInboundClient default flow "", because turns out panel can not return it because of zombification...</li>
8
- <li>Add a custom uuid generator for XUIClient that <i>defaults</i> to method in util but you can make your own!</li>
9
- <li>Uncomplicate self.sub_gen into self._resolve_sub</li>
10
- </ul>
File without changes
File without changes