Python-3xui 0.0.6__tar.gz → 0.0.8__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.8
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.8 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>Improve create_and_add_prod_client to have an expiry_time</li>
34
+ <li>delete_client_by_tgid_all_inbounds -> revoke_client_by_tgid_all_inbounds</li>
35
+ <li>Change vulnerable requirements</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.8 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>Improve create_and_add_prod_client to have an expiry_time</li>
8
+ <li>delete_client_by_tgid_all_inbounds -> revoke_client_by_tgid_all_inbounds</li>
9
+ <li>Change vulnerable requirements</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.8"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -1,6 +1,9 @@
1
+ import json
2
+ import logging
1
3
  import re
2
4
  import time
3
5
  from collections.abc import Sequence, Mapping
6
+ from logging import DEBUG
4
7
  from typing import Self, Optional, Dict, Iterable, AsyncIterable, Type, Union, Any, List, Tuple, Literal
5
8
  from datetime import datetime, UTC
6
9
 
@@ -12,7 +15,7 @@ import httpx
12
15
 
13
16
  from . import util
14
17
  from .models import Inbound, SingleInboundClient, ClientStats
15
- from .util import JsonType, async_range
18
+ from .util import JsonType, async_range, check_xui_response
16
19
 
17
20
  DataType: Type[str | bytes | Iterable[bytes] | AsyncIterable[bytes]] = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
18
21
  PrimitiveData = Optional[Union[str, int, float, bool]]
@@ -76,7 +79,7 @@ class XUIClient:
76
79
  two_fac_code: Two-factor authentication code (if enabled).
77
80
  session_duration: Maximum session duration in seconds. Defaults to 3600.
78
81
  """
79
- from . import endpoints # look, I know it's bad, but we need to evade cyclical imports
82
+ from . import endpoints # look, I know it's bad, but we need to evade cyclical imports
80
83
  self.connected: bool = False
81
84
  self.PROD_STRING = re.compile(custom_prod_string)
82
85
  self.session: AsyncClient | None = None
@@ -106,23 +109,6 @@ class XUIClient:
106
109
  else:
107
110
  self.totp = pyotp.TOTP(self.two_fac_secret)
108
111
 
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
112
  #========================request stuffs========================
127
113
  async def _safe_request(self,
128
114
  method: Literal["get", "post", "patch", "delete", "put"],
@@ -142,33 +128,34 @@ class XUIClient:
142
128
  Raises:
143
129
  RuntimeError: If max retries exceeded or session is invalid.
144
130
  """
145
- print(f"SAFE REQUEST, {method}, is running to a URL of {kwargs["url"]}")
146
- print(str(self.session.base_url) + str(kwargs["url"]))
131
+ logging.debug("Safe request is running to %s%s", str(self.session.base_url), str(kwargs["url"]))
147
132
  async for attempt in async_range(self.max_retries):
148
133
  resp = await self.session.request(method=method, **kwargs)
149
134
  if resp.status_code // 100 != 2: #because it can return either 201 or 202
150
135
  if resp.status_code == 404:
151
136
  now: float = datetime.now(UTC).timestamp()
152
137
  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")
138
+ logging.info("Client with IP/Domain %s is not logged in, logging in...", self.base_host)
154
139
  await self.login()
155
140
  continue
156
141
  else:
142
+ logging.error("Server returned a status code of %s with a valid session", resp.status_code)
157
143
  raise RuntimeError("""Server returned a 404, and the session should still be valid, likely it's a REAL 404""")
158
144
  else:
159
- raise RuntimeError(f"Wrong status code: {resp.status_code}")
145
+ logging.error("Server returned a status code of %s", resp.status_code)
146
+ resp.raise_for_status()
160
147
 
161
- status = await util.check_xui_response_validity(resp)
148
+ status = await util.check_xui_response(resp)
162
149
  if status == "OK":
163
150
  return resp
164
151
  elif status == "DB_LOCKED":
165
152
  if attempt + 1 >= self.max_retries:
166
- # resp.status_code = 518 # so the error can simply be handled as a "bad request"
167
- # return resp
168
153
  raise RuntimeError("Too many retries")
169
154
  await asyncio.sleep(self.retry_delay)
170
155
  continue
171
156
  else:
157
+ logging.error("A %s request was unsuccessful (code 200, but success=false).\nPayload: %s",
158
+ method, json.dumps(resp.json()))
172
159
  return resp
173
160
  raise RuntimeError(f"For some reason safe_request didn't exit, dump:\nmethod:\n{method}\n{kwargs}")
174
161
 
@@ -265,15 +252,14 @@ class XUIClient:
265
252
  "password": self.xui_password,
266
253
  }
267
254
  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
255
+ if self.totp.interval - datetime.now().timestamp() % self.totp.interval < 3:
256
+ await asyncio.sleep(3.1) # just to not submit an invalid code
270
257
  payload["twoFactorCode"] = self.totp.now()
271
258
  else:
272
259
  if self.two_fac_secret:
273
260
  payload["twoFactorCode"] = self.two_fac_secret
274
261
 
275
- print(self.session.base_url)
276
- print("WE'RE LOGGING IN")
262
+ logging.info("Client is logging in with IP/Domain: %s", self.base_host)
277
263
  resp = await self.session.post("/login", data=payload)
278
264
  if resp.status_code == 200:
279
265
  resp_json = resp.json()
@@ -281,7 +267,7 @@ class XUIClient:
281
267
  self.session_start: float = (datetime.now(UTC).timestamp())
282
268
  return
283
269
  else:
284
- raise ValueError("Error: wrong credentials or failed login")
270
+ raise ValueError("Error: wrong credentials (including status code) or failed login.")
285
271
  else:
286
272
  raise RuntimeError(f"Error: server returned a status code of {resp.status_code}")
287
273
 
@@ -293,6 +279,7 @@ class XUIClient:
293
279
  Returns:
294
280
  Self: The XUIClient instance.
295
281
  """
282
+ logging.log(DEBUG, "Client connected with IP/domain %s", self.base_url)
296
283
  self.session = AsyncClient(base_url=self.base_url)
297
284
  self.connected = True
298
285
  return self
@@ -331,12 +318,18 @@ class XUIClient:
331
318
  exc_val: The exception value, if an exception occurred.
332
319
  exc_tb: The exception traceback, if an exception occurred.
333
320
  """
334
- print("disconnectin'")
321
+ if exc_type is None or exc_type == asyncio.exceptions.CancelledError:
322
+ logging.info("Client is disconnecting at time with IP/Domain %s", self.base_host)
323
+ else:
324
+ logging.warning("Client is disconnecting due to an error (may be unrelated):"
325
+ "\n%s, with value %s\nStacktrace:%s",
326
+ exc_type, exc_val, exc_tb)
327
+ print(f"Client is disconnecting: {self.base_host}")
335
328
  await self.disconnect()
336
329
  return
337
330
 
338
331
  #========================inbound management========================
339
- @alru_cache
332
+ @alru_cache()
340
333
  async def get_production_inbounds(self) -> Tuple[Inbound, ...]:
341
334
  """Retrieve production inbounds.
342
335
 
@@ -371,12 +364,9 @@ class XUIClient:
371
364
  timer from 5 to 60*60*24 in the code.
372
365
  """
373
366
  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
367
  self.get_production_inbounds.cache_clear()
377
- await self.get_production_inbounds() #fill the cache
378
- await asyncio.sleep(10)
379
- #print(stat)
368
+ await self.get_production_inbounds() #fill the cache
369
+ await asyncio.sleep(3600) #update every 1h
380
370
 
381
371
  #========================clients management========================
382
372
  async def get_client_with_tgid(self, tgid: int, inbound_id: int | None = None) -> List[ClientStats]:
@@ -405,7 +395,12 @@ class XUIClient:
405
395
  resp = await self.clients_end.get_client_with_uuid(uuid)
406
396
  return resp
407
397
 
408
- async def create_and_add_prod_client(self, telegram_id: int, additional_remark: str = None):
398
+ async def create_and_add_prod_client(self, telegram_id: int, *,
399
+ additional_remark: str | None = None,
400
+ expiry_time: int=0,
401
+ exist_ok: bool = False
402
+ ) -> list[Response]:
403
+ #TODO: add exist_ok flag
409
404
  """Create and add a production client.
410
405
 
411
406
  This method creates a new client with the given Telegram ID and
@@ -416,6 +411,8 @@ class XUIClient:
416
411
  Args:
417
412
  telegram_id: The Telegram ID of the client.
418
413
  additional_remark: An optional additional remark for the client.
414
+ expiry_time: Expiry time in SECONDS as a UNIX timestamp.
415
+ exist_ok: Don't raise any errors if the client is already there (good if you need a refresh job)
419
416
 
420
417
  Returns:
421
418
  List[Response]: A list of responses from the server for each
@@ -423,20 +420,30 @@ class XUIClient:
423
420
  """
424
421
  production_inbounds: List[Inbound] = await self.get_production_inbounds()
425
422
 
426
- responses = []
423
+ tasks = []
427
424
  for inb in production_inbounds:
425
+ tmp_email = util.generate_email_from_tgid_inbid(telegram_id, inb.id)
428
426
  client = SingleInboundClient.model_construct(
429
427
  uuid=util.get_uuid_from_tgid(telegram_id),
430
428
  flow="",
431
- email=util.generate_email_from_tgid_inbid(telegram_id, inb.id),
429
+ email=tmp_email,
432
430
  limit_gb=0,
433
431
  enable=True,
434
432
  subscription_id=util.sub_from_tgid(telegram_id),
435
- comment=f"{additional_remark}, created at {datetime.now(UTC)}")
436
- responses.append(await self.clients_end.add_client(client, inb.id))
433
+ comment=f"{additional_remark}, created at {datetime.now(UTC)}",
434
+ expiry_time=expiry_time * 1000
435
+ )
436
+ tasks.append(asyncio.create_task(self.clients_end.add_client(client, inb.id)))
437
+ responses: list[Response] = await asyncio.gather(*tasks)
438
+ if exist_ok:
439
+ return responses
440
+ for resp in responses:
441
+ json_resp = resp.json()
442
+ if "duplicate email" in json_resp["msg"].lower():
443
+ logging.error("ERROR: Client already exists and exist_ok not set: %s", json_resp["msg"])
437
444
  return responses
438
445
 
439
- async def update_client_by_tgid(self, telegram_id: int, inbound_id: int, /,
446
+ async def update_client_by_tgid(self, telegram_id: int, inbound_id: int, /, *,
440
447
  security: str | None = None,
441
448
  password: str | None = None,
442
449
  flow: Literal["", "xtls-rprx-vision", "xtls-rprx-vision-udp443"] | None = None,
@@ -445,7 +452,8 @@ class XUIClient:
445
452
  expiry_time: int | None = None,
446
453
  enable: bool | None = None,
447
454
  sub_id: str | None = None,
448
- comment: str | None = None) -> Response:
455
+ comment: str | None = None,
456
+ verbose: bool=True) -> Response:
449
457
  """
450
458
  Update a client in a specific inbound by Telegram ID.
451
459
 
@@ -467,6 +475,13 @@ class XUIClient:
467
475
  """
468
476
  email = util.generate_email_from_tgid_inbid(telegram_id, inbound_id)
469
477
  existing_client = await self.clients_end.get_client_with_email(email)
478
+ if verbose:
479
+ if expiry_time < 1e9:
480
+ logging.warning("Warning: You're trying to update a client with expiry time %s. "
481
+ "You set it to expire before 2001, likely because you provided the DURATION. "
482
+ "You need to provide a TIMESTAMP. "
483
+ "If you want to disable this message, set verbose=false.",
484
+ expiry_time)
470
485
 
471
486
  resp = await self.clients_end.update_single_client(
472
487
  SingleInboundClient.model_validate(existing_client.model_dump()),
@@ -497,7 +512,7 @@ class XUIClient:
497
512
  resp = await self.clients_end.delete_client_by_email(email, inbound_id)
498
513
  return resp
499
514
 
500
- async def delete_client_by_tgid_all_inbounds(self, telegram_id: int) -> List[Response]:
515
+ async def revoke_client_by_tgid_all_inbounds(self, telegram_id: int) -> List[Response]:
501
516
  """Delete a client from all production inbounds by Telegram ID.
502
517
 
503
518
  Args:
@@ -513,7 +528,6 @@ class XUIClient:
513
528
  email = util.generate_email_from_tgid_inbid(telegram_id, inbound.id)
514
529
  resp = await self.clients_end.delete_client_by_email(email, inbound.id)
515
530
  responses.append(resp)
516
- print("Inbound deleted")
531
+ logging.info("Clients of of tgid %s deleted", telegram_id)
517
532
 
518
533
  return responses
519
-
@@ -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
@@ -83,7 +83,7 @@ class BaseModel(pydantic.BaseModel):
83
83
  inbounds = await Inbound.from_response(response, client, list)
84
84
  """
85
85
  json_resp: util.JsonType = response.json()
86
- valid = util.check_xui_response_validity(json_resp)
86
+ valid = util.check_xui_response(json_resp)
87
87
  if valid == "OK":
88
88
  obj = json_resp["obj"]
89
89
  if expect is list:
@@ -91,4 +91,4 @@ class BaseModel(pydantic.BaseModel):
91
91
  if expect is dict:
92
92
  return cls(**obj, client=client)
93
93
  else:
94
- raise ValueError(f"Invalid 3X-UI response, code {valid}")
94
+ raise ValueError(f"Invalid 3X-UI response, code {valid}. Don't use from_response on failed requests.")
@@ -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
@@ -14,7 +14,7 @@ import logging
14
14
  import random
15
15
  import re
16
16
  from datetime import UTC, datetime, tzinfo
17
- from typing import TypeAlias, Union, Dict, Any, List
17
+ from typing import TypeAlias, Union, Dict, Any, List, dataclass_transform
18
18
 
19
19
  import httpx
20
20
 
@@ -180,7 +180,7 @@ def generate_new_subscription(length: int = 16):
180
180
  return s
181
181
 
182
182
 
183
- async def check_xui_response_validity(response: JsonType | httpx.Response) -> str:
183
+ async def check_xui_response(response: JsonType | httpx.Response) -> str:
184
184
  """Validate a 3X-UI API response.
185
185
 
186
186
  Checks if the response follows the expected 3X-UI API format with
@@ -193,17 +193,18 @@ async def check_xui_response_validity(response: JsonType | httpx.Response) -> st
193
193
  str: One of three status strings:
194
194
  - "OK": Response is valid and successful.
195
195
  - "DB_LOCKED": Database is locked, operation should be retried.
196
- - "ERROR": Operation was unsuccessful.
196
+ - "FAIL": Operation was unsuccessful.
197
197
 
198
198
  Raises:
199
199
  RuntimeError: If the response doesn't match the expected 3X-UI format.
200
200
 
201
201
  Examples:
202
- >>> check_xui_response_validity({"success": True, "msg": "", "obj": {}})
202
+ >>> check_xui_response({"success": True, "msg": "", "obj": {}})
203
203
  'OK'
204
- >>> check_xui_response_validity({"success": False, "msg": "database is locked", "obj": None})
204
+ >>> check_xui_response({"success": False, "msg": "database is locked", "obj": None})
205
205
  'DB_LOCKED'
206
206
  """
207
+ #TODO: this is trying to do too much. We'll just check if DB is locked, and then use case-to-case basis to see how they are.
207
208
  if isinstance(response, httpx.Response):
208
209
  json_resp = response.json()
209
210
  else:
@@ -218,11 +219,9 @@ async def check_xui_response_validity(response: JsonType | httpx.Response) -> st
218
219
  if "database" in msg.lower() and "locked" in msg.lower() and not success:
219
220
  logging.log(logging.WARNING, "Database is locked, retrying...")
220
221
  return "DB_LOCKED"
221
- print(f"Unsuccessful operation! Message: {json_resp["msg"]}")
222
- return "ERROR"
222
+ return "FAIL"
223
223
  raise RuntimeError("Validator got something very unexpected (Please don't shove responses with non-20X status codes in here...)")
224
224
 
225
-
226
225
  def get_days_until_expiry(expiry_time: int) -> float:
227
226
  """Calculate the number of days until a client expires.
228
227
 
@@ -231,16 +230,16 @@ def get_days_until_expiry(expiry_time: int) -> float:
231
230
 
232
231
  Returns:
233
232
  Number of days until expiry. Returns negative value if already expired.
234
- Returns a very large number (infinity) if expiry_time is 0 (no expiry).
233
+ Returns a very 0 if expiry_time is 0 (no expiry).
235
234
 
236
235
  Examples:
237
- >>> get_days_until_expiry(int(datetime.now(UTC).timestamp_seconds()) + 86400) # 1 day from now
236
+ >>> get_days_until_expiry(int(datetime.now(UTC).timestamp()) + 86400) # 1 day from now
238
237
  1.0
239
238
  >>> get_days_until_expiry(0) # No expiry
240
239
  inf
241
240
  """
242
241
  if expiry_time == 0:
243
- return float('inf')
242
+ return 0
244
243
 
245
244
  current_timestamp = datetime.now(UTC).timestamp()
246
245
  seconds_remaining = expiry_time - current_timestamp
@@ -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
 
@@ -242,7 +242,7 @@ class TestClientsEndpoint:
242
242
  print(f"Added test client with email: {test_email}, UUID: {test_uuid} to {len(production_inbounds)} production inbounds")
243
243
 
244
244
  # Now delete the client from all production inbounds by Telegram ID
245
- responses = await xui_client.delete_client_by_tgid_all_inbounds(TEST_TELEGRAM_ID)
245
+ responses = await xui_client.revoke_client_by_tgid_all_inbounds(TEST_TELEGRAM_ID)
246
246
 
247
247
  # Validate responses
248
248
  assert len(responses) == len(production_inbounds)
@@ -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