Python-3xui 0.0.10__tar.gz → 0.0.11__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 (27) hide show
  1. {python_3xui-0.0.10 → python_3xui-0.0.11}/PKG-INFO +2 -2
  2. {python_3xui-0.0.10 → python_3xui-0.0.11}/pyproject.toml +4 -3
  3. {python_3xui-0.0.10 → python_3xui-0.0.11}/python_3xui/__init__.py +1 -1
  4. python_3xui-0.0.11/python_3xui/api.py +531 -0
  5. python_3xui-0.0.11/python_3xui/api_core/__init__.py +4 -0
  6. python_3xui-0.0.11/python_3xui/api_core/client_service.py +301 -0
  7. python_3xui-0.0.11/python_3xui/api_core/identity.py +28 -0
  8. python_3xui-0.0.11/python_3xui/api_core/prod_cache.py +75 -0
  9. python_3xui-0.0.11/python_3xui/api_core/session_core.py +333 -0
  10. {python_3xui-0.0.10 → python_3xui-0.0.11}/python_3xui/custom_exceptions.py +2 -1
  11. {python_3xui-0.0.10 → python_3xui-0.0.11}/python_3xui/endpoints.py +69 -31
  12. {python_3xui-0.0.10 → python_3xui-0.0.11}/python_3xui/util.py +16 -9
  13. {python_3xui-0.0.10 → python_3xui-0.0.11}/tests/conftest.py +0 -1
  14. {python_3xui-0.0.10 → python_3xui-0.0.11}/tests/gather_response_stubs.py +2 -2
  15. {python_3xui-0.0.10 → python_3xui-0.0.11}/tests/test_non_idempotent_endpoints_clients.py +10 -11
  16. python_3xui-0.0.11/tests/test_non_idempotent_endpoints_inbounds.py +118 -0
  17. {python_3xui-0.0.10 → python_3xui-0.0.11}/tests/test_xuiclient_helpers.py +6 -6
  18. python_3xui-0.0.10/python_3xui/api.py +0 -755
  19. python_3xui-0.0.10/tests/test_non_idempotent_endpoints_inbounds.py +0 -125
  20. {python_3xui-0.0.10 → python_3xui-0.0.11}/.gitignore +0 -0
  21. {python_3xui-0.0.10 → python_3xui-0.0.11}/LICENSE +0 -0
  22. {python_3xui-0.0.10 → python_3xui-0.0.11}/README.md +0 -0
  23. {python_3xui-0.0.10 → python_3xui-0.0.11}/python_3xui/base_model.py +0 -0
  24. {python_3xui-0.0.10 → python_3xui-0.0.11}/python_3xui/models.py +0 -0
  25. {python_3xui-0.0.10 → python_3xui-0.0.11}/tests/pytest.ini +0 -0
  26. {python_3xui-0.0.10 → python_3xui-0.0.11}/tests/test_endpoints_clients.py +0 -0
  27. {python_3xui-0.0.10 → python_3xui-0.0.11}/tests/test_endpoints_inbounds.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Python-3xui
3
- Version: 0.0.10
3
+ Version: 0.0.11
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
@@ -13,10 +13,10 @@ Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Requires-Python: >=3.11
15
15
  Requires-Dist: async-lru~=2.3.0
16
- Requires-Dist: dotenv~=0.9.9
17
16
  Requires-Dist: httpx~=0.28.1
18
17
  Requires-Dist: pydantic<3,~=2.12.5
19
18
  Requires-Dist: pyotp~=2.9.0
19
+ Requires-Dist: python-dotenv==1.2.2
20
20
  Provides-Extra: testing
21
21
  Requires-Dist: pytest; extra == 'testing'
22
22
  Requires-Dist: pytest-asyncio; extra == 'testing'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Python-3xui"
3
- version = "0.0.10"
3
+ version = "0.0.11"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -22,7 +22,7 @@ license-files = ["LICEN[CS]E*"]
22
22
  dependencies = [
23
23
  "pydantic ~= 2.12.5, < 3",
24
24
  "httpx ~=0.28.1",
25
- "dotenv ~= 0.9.9",
25
+ "python-dotenv==1.2.2",
26
26
  "async_lru ~= 2.3.0",
27
27
  "pyotp ~= 2.9.0"
28
28
  ]
@@ -46,7 +46,8 @@ build-backend = "hatchling.build"
46
46
  include = [
47
47
  "python_3xui/*.py",
48
48
  "/tests/*.py",
49
- "/tests/pytest.ini"
49
+ "/tests/pytest.ini",
50
+ "python_3xui/api_core/*.py"
50
51
  ]
51
52
  exclude = [
52
53
  "requirements.txt",
@@ -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.10"
7
+ __version__ = "0.0.11"
8
8
  __email__ = ""
9
9
 
10
10
 
@@ -0,0 +1,531 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Any, Awaitable, Callable, List, Literal, Self, overload
4
+
5
+ import httpx
6
+ import pyotp
7
+ from httpx import AsyncClient, Response
8
+ from pydantic import SecretStr
9
+
10
+ from . import endpoints
11
+ from . import util
12
+ from .models import ClientStats, Inbound
13
+ from .api_core import ProductionInboundCache, SessionCore, TgIDClientService, IdentityResolver
14
+ from .api_core.session_core import (
15
+ CookieType,
16
+ DataType,
17
+ HeaderType,
18
+ ParamType,
19
+ )
20
+ from .util import JsonType
21
+
22
+
23
+ class XUIClient:
24
+ """Facade for the 3X-UI panel API.
25
+
26
+ XUIClient owns and wires up the components that do the real work:
27
+ the HTTP/auth core (``SessionCore``), the production inbound cache,
28
+ the identity resolver, the endpoint handlers, and the high-level
29
+ Telegram-ID client service. It also drives the lifecycle
30
+ (``connect`` / ``login`` / ``disconnect`` and the async context
31
+ manager protocol).
32
+
33
+ All the documented attributes below that map to session or auth state
34
+ are read-only passthroughs to the underlying ``SessionCore`` and remain
35
+ in place for backward compatibility.
36
+
37
+ Attributes:
38
+ connected: Whether an HTTP session is currently open.
39
+ PROD_STRING: Compiled regex used to identify production inbounds
40
+ (alias of ``ProductionInboundCache.PROD_STRING`` on ``_prod_cache``).
41
+ session, base_host, base_port, base_path, base_url,
42
+ session_start, session_duration, xui_username, xui_password,
43
+ two_fac_secret, totp, max_retries, retry_delay:
44
+ Pass-through to ``SessionCore``.
45
+ sub_gen, uuid_gen: Pass-through to ``IdentityResolver``.
46
+ server_end, clients_end, inbounds_end: Endpoint handlers.
47
+ """
48
+
49
+ def __init__(self, base_website: str, base_port: int, base_path: str,
50
+ *, username: str | None = None, password: str | None = None,
51
+ two_fac_code: str | None = None, session_duration: int = 3600,
52
+ custom_prod_string: str = "testing",
53
+ max_retries: int = 5, retry_delay: int = 1,
54
+ custom_sub_generator: Callable[[int], str] | Callable[[int], Awaitable[str]] = util.default_sub_from_tgid,
55
+ custom_uuid_generator: Callable[[int], str] | Callable[[int], Awaitable[str]] = util.get_uuid_from_tgid,
56
+ panel_id: Any = None
57
+ ) -> None:
58
+ """Initialize the XUIClient.
59
+
60
+ Args:
61
+ base_website: The server hostname (e.g., "example.com").
62
+ base_port: The server port (e.g., 443).
63
+ base_path: The base path for the API (e.g., "/panel").
64
+ username: Username for authentication.
65
+ password: Password for authentication.
66
+ two_fac_code: TOTP secret for 2FA. Short one-shot codes are
67
+ accepted for the current login only.
68
+ session_duration: Maximum session duration in seconds. Defaults to 3600.
69
+ custom_prod_string: Regex pattern used to select production inbounds.
70
+ max_retries: Maximum retries for database-lock responses.
71
+ retry_delay: Seconds to wait between database-lock retries.
72
+ custom_sub_generator: Sync or async callable that receives a
73
+ Telegram ID and returns the subscription ID for new clients.
74
+ custom_uuid_generator: Sync or async callable that receives a
75
+ Telegram ID and returns the UUID for new clients.
76
+ panel_id: this is solely for user's purposes to increase logging and accounting clarity. Default is None.
77
+ """
78
+ self._core = SessionCore(
79
+ base_website,
80
+ base_port,
81
+ base_path,
82
+ username=username,
83
+ password=password,
84
+ two_fac_code=two_fac_code,
85
+ session_duration=session_duration,
86
+ max_retries=max_retries,
87
+ retry_delay=retry_delay,
88
+ panel_id=panel_id,
89
+ )
90
+ self._identity = IdentityResolver(custom_sub_generator, custom_uuid_generator)
91
+ self.sub_gen = self._identity.sub_gen
92
+ self.uuid_gen = self._identity.uuid_gen
93
+ self.panel_id: int | str | Any = panel_id
94
+ self.server_end = endpoints.Server(self._core)
95
+ self.clients_end = endpoints.Clients(self._core)
96
+ self.inbounds_end = endpoints.Inbounds(self._core)
97
+ self._prod_cache = ProductionInboundCache(
98
+ self.inbounds_end,
99
+ custom_prod_string,
100
+ panel_id=self.panel_id,
101
+ )
102
+ self.PROD_STRING = self._prod_cache.PROD_STRING
103
+ self.get_production_inbounds = self._prod_cache.get
104
+ self._tg_client_service = TgIDClientService(
105
+ self.clients_end,
106
+ self.inbounds_end,
107
+ self._identity,
108
+ self._prod_cache,
109
+ )
110
+
111
+ @property
112
+ def session(self) -> AsyncClient | None:
113
+ return self._core.session
114
+
115
+ @property
116
+ def session_start(self) -> float | None:
117
+ return self._core.session_start
118
+
119
+ @property
120
+ def session_duration(self) -> int:
121
+ return self._core.session_duration
122
+
123
+ @property
124
+ def base_host(self) -> str:
125
+ return self._core.base_host
126
+
127
+ @property
128
+ def base_port(self) -> int:
129
+ return self._core.base_port
130
+
131
+ @property
132
+ def base_path(self) -> str:
133
+ return self._core.base_path
134
+
135
+ @property
136
+ def base_url(self) -> str:
137
+ return self._core.base_url
138
+
139
+ @property
140
+ def xui_username(self) -> str | None:
141
+ return self._core.xui_username
142
+
143
+ @property
144
+ def xui_password(self) -> str | None:
145
+ return self._core.xui_password
146
+
147
+ @property
148
+ def two_fac_secret(self) -> SecretStr | None:
149
+ return self._core.two_fac_secret
150
+
151
+ @property
152
+ def totp(self) -> pyotp.TOTP | None:
153
+ return self._core.totp
154
+
155
+ @property
156
+ def max_retries(self) -> int:
157
+ return self._core.max_retries
158
+
159
+ @property
160
+ def retry_delay(self) -> int:
161
+ return self._core.retry_delay
162
+
163
+ @property
164
+ def connected(self) -> bool:
165
+ return self._core.connected
166
+
167
+ @overload
168
+ async def _safe_request(self, *, request_to_send: httpx.Request) -> Response:
169
+ ...
170
+
171
+ @overload
172
+ async def _safe_request(self,
173
+ method: Literal["get", "post", "patch", "delete", "put"],
174
+ **kwargs: Any,
175
+ ) -> Response:
176
+ ...
177
+
178
+ async def _safe_request(self,
179
+ method: Literal["get", "post", "patch", "delete", "put"] | None = None,
180
+ **kwargs: Any,
181
+ ) -> Response:
182
+ """Delegate for :meth:`BaseModel.from_response` DB-lock retries."""
183
+ return await self._core._safe_request(method=method, **kwargs)
184
+
185
+ async def safe_get(self,
186
+ url: httpx.URL | str,
187
+ *,
188
+ params: ParamType | None = None,
189
+ headers: HeaderType | None = None,
190
+ cookies: CookieType | None = None,
191
+ ) -> Response:
192
+ """Execute a safe GET request with automatic retry on database lock.
193
+
194
+ Note:
195
+ "Safe" only means "with retries if database is locked".
196
+
197
+ Args:
198
+ url: The URL to request.
199
+ params: Query parameters (optional).
200
+ headers: Request headers (optional).
201
+ cookies: Request cookies (optional).
202
+
203
+ Returns:
204
+ The HTTP response.
205
+
206
+ Raises:
207
+ RuntimeError: If the session is not initialized.
208
+ """
209
+ return await self._core.safe_get(
210
+ url, params=params, headers=headers, cookies=cookies
211
+ )
212
+
213
+ async def safe_post(self,
214
+ url: httpx.URL | str,
215
+ *,
216
+ content: DataType | None = None,
217
+ data: JsonType | None = None,
218
+ json: Any | None = None,
219
+ params: ParamType | None = None,
220
+ headers: HeaderType | None = None,
221
+ cookies: CookieType | None = None,
222
+ ) -> Response:
223
+ """Execute a safe POST request with automatic retry on database lock.
224
+
225
+ Note:
226
+ "Safe" only means "with retries if database is locked".
227
+
228
+ Args:
229
+ url: The URL to request.
230
+ content: Request content (optional).
231
+ data: Form data (optional).
232
+ json: JSON body (optional).
233
+ params: Query parameters (optional).
234
+ headers: Request headers (optional).
235
+ cookies: Request cookies (optional).
236
+
237
+ Returns:
238
+ The HTTP response.
239
+
240
+ Raises:
241
+ RuntimeError: If the session is not initialized.
242
+ """
243
+ return await self._core.safe_post(
244
+ url,
245
+ content=content,
246
+ data=data,
247
+ json=json,
248
+ params=params,
249
+ headers=headers,
250
+ cookies=cookies,
251
+ )
252
+
253
+ async def login(self) -> None:
254
+ """Authenticate the client with the 3X-UI panel.
255
+
256
+ This method performs the login action, establishing a session for
257
+ subsequent API requests.
258
+
259
+ Raises:
260
+ ValueError: If the login credentials are incorrect.
261
+ RuntimeError: If the server returns an error status code.
262
+ """
263
+ await self._core.login()
264
+
265
+ def connect(self) -> Self:
266
+ """Establish a connection to the 3X-UI panel.
267
+
268
+ This method creates an async HTTP client session.
269
+
270
+ Returns:
271
+ Self: The XUIClient instance.
272
+ """
273
+ self._core.connect()
274
+ return self
275
+
276
+ async def disconnect(self) -> None:
277
+ """Close the client session.
278
+
279
+ This method closes the async HTTP client session.
280
+ """
281
+ await self._prod_cache.stop()
282
+ await self._core.disconnect()
283
+
284
+ async def __aenter__(self) -> Self:
285
+ """Enter the async context manager.
286
+
287
+ This method is called when the client is used in an `async with`
288
+ statement. It establishes a connection and starts the cache clearing
289
+ task.
290
+
291
+ Returns:
292
+ Self: The XUIClient instance.
293
+ """
294
+ self.connect()
295
+ await self.login()
296
+ self._prod_cache.start()
297
+ return self
298
+
299
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
300
+ """Exit the async context manager.
301
+
302
+ This method is called when the client context is exited. It closes
303
+ the client session.
304
+
305
+ Args:
306
+ exc_type: The exception type, if an exception occurred.
307
+ exc_val: The exception value, if an exception occurred.
308
+ exc_tb: The exception traceback, if an exception occurred.
309
+ """
310
+ if exc_type is None or exc_type is asyncio.exceptions.CancelledError:
311
+ logging.info("Client is disconnecting (panel: %s)", self.panel_id or self.base_host)
312
+ else:
313
+ logging.warning("Client is disconnecting due to an error (may be unrelated):"
314
+ "\n%s, with value %s\nStacktrace:%s",
315
+ exc_type, exc_val, exc_tb, exc_info=exc_tb)
316
+ logging.info("Client is disconnecting: %s", self.panel_id or self.base_host)
317
+ await self.disconnect()
318
+ return
319
+
320
+ #=========================="meta" methods==========================
321
+ async def _resolve_uuid(self, telegram_id: int) -> str:
322
+ """Resolve a Telegram ID to a UUID via ``self.uuid_gen``.
323
+
324
+ Handles both sync and async callables.
325
+ """
326
+ return await self._identity.resolve_uuid(telegram_id)
327
+
328
+ async def _resolve_sub(self, telegram_id: int) -> str:
329
+ """Resolve the subscription ID from a telegram id via ``self.sub_gen``
330
+
331
+ Handles both sync and async callables.
332
+ """
333
+ return await self._identity.resolve_sub(telegram_id)
334
+
335
+ #========================inbound management========================
336
+ async def add_inbound(self, inbound: Inbound) -> Response:
337
+ """Create a new inbound. Returns the panel HTTP response."""
338
+ return await self.inbounds_end.add_inbound(inbound)
339
+
340
+ async def delete_inbound(self, inbound_id: int) -> Response:
341
+ """Delete an inbound by panel ID. Returns the panel HTTP response."""
342
+ return await self.inbounds_end.delete_inbound_by_id(inbound_id)
343
+
344
+ #========================clients management========================
345
+ async def get_client_with_tgid(self, tgid: int, inbound_id: int | None = None) -> list[ClientStats]:
346
+ """Retrieve client information by Telegram ID.
347
+
348
+ This method fetches client information using the Telegram ID. If
349
+ an inbound ID is provided, it fetches the client by email derived
350
+ from the Telegram ID and inbound ID.
351
+
352
+ Args:
353
+ tgid: The Telegram ID of the client.
354
+ inbound_id: The ID of the inbound (optional).
355
+
356
+ Returns:
357
+ List[ClientStats]: A list of client statistics.
358
+
359
+ Note:
360
+ If the client is not found by Telegram ID, the method falls back
361
+ to using the Telegram ID and inbound ID to fetch the client.
362
+ """
363
+ return await self._tg_client_service.get_client_with_tgid(tgid, inbound_id)
364
+
365
+ async def create_and_add_prod_client(self, telegram_id: int, *,
366
+ additional_remark: str | None = None,
367
+ expiry_time: int = 0,
368
+ exist_ok: bool = False,
369
+ replace_if_exist: bool = False,
370
+ ) -> dict[int, Response]:
371
+ """Create and add a production client.
372
+
373
+ This method creates a new client with the given Telegram ID and
374
+ adds it to the production inbounds. The client is configured with
375
+ default settings and the additional remark. The subscription ID is
376
+ created by ``self.sub_gen``; by default this is
377
+ ``util.default_sub_from_tgid``.
378
+
379
+ Args:
380
+ telegram_id: The Telegram ID of the client.
381
+ additional_remark: An optional additional remark for the client.
382
+ expiry_time: Expiry time in SECONDS as a UNIX timestamp.
383
+ exist_ok: If True, return API responses even when the panel reports
384
+ a duplicate email.
385
+ replace_if_exist: If True, inbounds that respond with
386
+ "duplicate email" will have their existing client updated
387
+ (via ``request_update_client``) instead of raising an error.
388
+ Updates are burst-shot in a second ``asyncio.gather`` for
389
+ minimal latency.
390
+
391
+ Returns:
392
+ Dict[int, Response]: A mapping of inbound IDs to API responses.
393
+ For inbounds where the add succeeded, the response is from the
394
+ add call. For inbounds where ``replace_if_exist`` replaced a
395
+ duplicate, the response is from the update call.
396
+
397
+ Raises:
398
+ ClientEmailAlreadyExistsError: If a duplicate client is reported,
399
+ ``replace_if_exist`` is False, and ``exist_ok`` is False.
400
+ """
401
+ return await self._tg_client_service.create_and_add_prod_client(
402
+ telegram_id,
403
+ additional_remark=additional_remark,
404
+ expiry_time=expiry_time,
405
+ exist_ok=exist_ok,
406
+ replace_if_exist=replace_if_exist,
407
+ )
408
+
409
+ async def update_client_by_tgid_only(self, telegram_id: int, prod_only: bool, /, *,
410
+ security: str | None = None,
411
+ password: str | None = None,
412
+ flow: Literal["", "xtls-rprx-vision", "xtls-rprx-vision-udp443"] | None = None,
413
+ limit_ip: int | None = None,
414
+ limit_gb: int | None = None,
415
+ expiry_time: int | None = None,
416
+ enable: bool | None = None,
417
+ sub_id: str | None = None,
418
+ comment: str | None = None,
419
+ verbose: bool = True
420
+ ) -> list[Response]:
421
+ """Update every matching client found by Telegram ID.
422
+
423
+ The client UUID is derived from ``telegram_id`` and searched across
424
+ either production inbounds or all inbounds. Only keyword arguments with
425
+ non-None values are applied to the client model before sending update
426
+ requests.
427
+
428
+ Args:
429
+ telegram_id: Telegram ID used to derive the client UUID.
430
+ prod_only: If True, search only production inbounds. If False,
431
+ search every inbound returned by the panel.
432
+ security: New security setting.
433
+ password: New password.
434
+ flow: New VLESS flow value.
435
+ limit_ip: New simultaneous IP connection limit.
436
+ limit_gb: New traffic limit in gigabytes.
437
+ expiry_time: New expiry timestamp in seconds.
438
+ enable: New enabled state.
439
+ sub_id: New subscription ID.
440
+ comment: New client comment.
441
+ verbose: If True, warn when ``expiry_time`` looks like a duration
442
+ instead of a UNIX timestamp.
443
+
444
+ Returns:
445
+ Responses from each inbound where a matching client was updated.
446
+ """
447
+ return await self._tg_client_service.update_client_by_tgid_only(
448
+ telegram_id,
449
+ prod_only,
450
+ security=security,
451
+ password=password,
452
+ flow=flow,
453
+ limit_ip=limit_ip,
454
+ limit_gb=limit_gb,
455
+ expiry_time=expiry_time,
456
+ enable=enable,
457
+ sub_id=sub_id,
458
+ comment=comment,
459
+ verbose=verbose,
460
+ )
461
+
462
+ async def update_client_by_tgid_inbid(self, telegram_id: int, inbound_id: int, /, *,
463
+ security: str | None = None,
464
+ password: str | None = None,
465
+ flow: Literal["", "xtls-rprx-vision", "xtls-rprx-vision-udp443"] | None = None,
466
+ limit_ip: int | None = None,
467
+ limit_gb: int | None = None,
468
+ expiry_time: int | None = None,
469
+ enable: bool | None = None,
470
+ sub_id: str | None = None,
471
+ comment: str | None = None,
472
+ email: str | None = None,
473
+ verbose: bool = True) -> Response:
474
+ """
475
+ Update a client in a specific inbound by Telegram ID. NOT optimized for multiple inbounds.
476
+
477
+ Args:
478
+ telegram_id: The Telegram ID of the client
479
+ inbound_id: The ID of the inbound where the client exists
480
+ security: Client security setting (optional)
481
+ password: Client password (optional)
482
+ flow: VLESS flow type (optional)
483
+ limit_ip: IP connection limit (optional)
484
+ limit_gb: Data limit in GB (optional)
485
+ expiry_time: Client expiry time (UNIX timestamp) (optional)
486
+ enable: Whether the client is enabled (optional)
487
+ sub_id: Subscription ID (optional)
488
+ comment: Client comment/note (optional)
489
+ email: New client email (optional). USE WITH CAUTION BECAUSE THE PANEL WILL NOT TRACK THE NEW EMAIL.
490
+
491
+ Returns:
492
+ Response from the API
493
+ """
494
+ return await self._tg_client_service.update_client_by_tgid_inbid(
495
+ telegram_id,
496
+ inbound_id,
497
+ security=security,
498
+ password=password,
499
+ flow=flow,
500
+ limit_ip=limit_ip,
501
+ limit_gb=limit_gb,
502
+ expiry_time=expiry_time,
503
+ enable=enable,
504
+ sub_id=sub_id,
505
+ comment=comment,
506
+ email=email,
507
+ verbose=verbose,
508
+ )
509
+
510
+ async def delete_client_by_tgid(self, telegram_id: int, inbound_id: int) -> Response:
511
+ """Delete a client from a specific inbound by Telegram ID.
512
+
513
+ Args:
514
+ telegram_id: The Telegram ID of the client
515
+ inbound_id: The ID of the inbound
516
+
517
+ Returns:
518
+ Response from the API
519
+ """
520
+ return await self._tg_client_service.delete_client_by_tgid(telegram_id, inbound_id)
521
+
522
+ async def revoke_client_by_tgid_all_inbounds(self, telegram_id: int) -> List[Response]:
523
+ """Delete a client from all production inbounds by Telegram ID.
524
+
525
+ Args:
526
+ telegram_id: The Telegram ID of the client
527
+
528
+ Returns:
529
+ List of Response objects from each deletion attempt
530
+ """
531
+ return await self._tg_client_service.revoke_client_by_tgid_all_inbounds(telegram_id)
@@ -0,0 +1,4 @@
1
+ from .identity import IdentityResolver
2
+ from .prod_cache import ProductionInboundCache
3
+ from .session_core import SessionCore
4
+ from .client_service import TgIDClientService