Python-3xui 0.0.1__tar.gz → 0.0.2__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,7 +1,8 @@
1
- ../.idea/
2
- __pycache__/
3
- .env
4
- ./tests/response_stubs/
5
- response*
6
- !response*.py
1
+ ../.idea/
2
+ __pycache__/
3
+ .env
4
+ .editorconfig
5
+ ./tests/response_stubs/
6
+ response*
7
+ !response*.py
7
8
  dist/
@@ -1,201 +1,201 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS
177
-
178
- APPENDIX: How to apply the Apache License to your work.
179
-
180
- To apply the Apache License to your work, attach the following
181
- boilerplate notice, with the fields enclosed by brackets "[]"
182
- replaced with your own identifying information. (Don't include
183
- the brackets!) The text should be enclosed in the appropriate
184
- comment syntax for the file format. We also recommend that a
185
- file or class name and description of purpose be included on the
186
- same "printed page" as the copyright notice for easier
187
- identification within third-party archives.
188
-
189
- Copyright [yyyy] [name of copyright owner]
190
-
191
- Licensed under the Apache License, Version 2.0 (the "License");
192
- you may not use this file except in compliance with the License.
193
- You may obtain a copy of the License at
194
-
195
- http://www.apache.org/licenses/LICENSE-2.0
196
-
197
- Unless required by applicable law or agreed to in writing, software
198
- distributed under the License is distributed on an "AS IS" BASIS,
199
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
- See the License for the specific language governing permissions and
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
201
  limitations under the License.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Python-3xui
3
- Version: 0.0.1
3
+ Version: 0.0.2
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Python-3xui"
3
- version = "0.0.1"
3
+ version = "0.0.2"
4
4
  authors = [
5
5
  { name="JustMe_001", email="justme001.causation755@passinbox.com" },
6
6
  ]
@@ -47,8 +47,8 @@ class XUIClient:
47
47
  base_url: The full base URL for API requests.
48
48
  session_start: Timestamp of when the session was created.
49
49
  session_duration: Maximum session duration in seconds.
50
- xui_username: Username for authentication.
51
- xui_password: Password for authentication.
50
+ username: Username for authentication.
51
+ password: Password for authentication.
52
52
  two_fac_code: Two-factor authentication code (if enabled).
53
53
  max_retries: Maximum number of retry attempts for failed requests.
54
54
  retry_delay: Delay in seconds between retries.
@@ -59,7 +59,7 @@ class XUIClient:
59
59
  _instance = None
60
60
 
61
61
  def __init__(self, base_website: str, base_port: int, base_path: str,
62
- *, xui_username: str | None = None, xui_password: str | None = None,
62
+ *, username: str | None = None, password: str | None = None,
63
63
  two_fac_code: str | None = None, session_duration: int = 3600) -> None:
64
64
  """Initialize the XUIClient.
65
65
 
@@ -67,8 +67,8 @@ class XUIClient:
67
67
  base_website: The server hostname (e.g., "example.com").
68
68
  base_port: The server port (e.g., 443).
69
69
  base_path: The base path for the API (e.g., "/panel").
70
- xui_username: Username for authentication.
71
- xui_password: Password for authentication.
70
+ username: Username for authentication.
71
+ password: Password for authentication.
72
72
  two_fac_code: Two-factor authentication code (if enabled).
73
73
  session_duration: Maximum session duration in seconds. Defaults to 3600.
74
74
  """
@@ -81,8 +81,8 @@ class XUIClient:
81
81
  self.base_url: str = f"https://{self.base_host}:{self.base_port}{self.base_path}"
82
82
  self.session_start: float | None = None
83
83
  self.session_duration: int = session_duration
84
- self.xui_username: str | None = xui_username
85
- self.xui_password: str | None = xui_password
84
+ self.xui_username: str | None = username
85
+ self.xui_password: str | None = password
86
86
  self.two_fac_code: str | None = two_fac_code
87
87
  self.max_retries: int = 5
88
88
  self.retry_delay: int = 1
@@ -102,9 +102,9 @@ class XUIClient:
102
102
  Returns:
103
103
  The singleton XUIClient instance.
104
104
  """
105
-
105
+ #print("initializing client")
106
106
  if cls._instance is None:
107
-
107
+ #print("nu instance")
108
108
  cls._instance = super(XUIClient, cls).__new__(cls)
109
109
  return cls._instance
110
110
 
@@ -127,15 +127,15 @@ class XUIClient:
127
127
  Raises:
128
128
  RuntimeError: If max retries exceeded or session is invalid.
129
129
  """
130
-
131
-
130
+ #print(f"SAFE REQUEST, {method}, is running to a URL of {kwargs["url"]}")
131
+ #print(str(self.session.base_url) + str(kwargs["url"]))
132
132
  async for attempt in async_range(self.max_retries):
133
133
  resp = await self.session.request(method=method, **kwargs)
134
134
  if resp.status_code // 100 != 2: #because it can return either 201 or 202
135
135
  if resp.status_code == 404:
136
136
  now: float = datetime.now(UTC).timestamp()
137
137
  if self.session_start is None or now - self.session_start > self.session_duration:
138
-
138
+ #print("Guys, we're not logged in, fixing that rn")
139
139
  await self.login()
140
140
  continue
141
141
  else:
@@ -250,8 +250,8 @@ class XUIClient:
250
250
  "password": self.xui_password,
251
251
  }
252
252
 
253
-
254
-
253
+ #print(self.session.base_url)
254
+ #print("WE'RE LOGGING IN")
255
255
  resp = await self.session.post("/login", data=payload)
256
256
  if resp.status_code == 200:
257
257
  resp_json = resp.json()
@@ -307,7 +307,7 @@ class XUIClient:
307
307
  exc_val: The exception value, if an exception occurred.
308
308
  exc_tb: The exception traceback, if an exception occurred.
309
309
  """
310
-
310
+ #print("disconnectin'")
311
311
  await self.disconnect()
312
312
  return
313
313
 
@@ -346,12 +346,16 @@ class XUIClient:
346
346
  This method currently runs every 10 seconds. Please change the
347
347
  timer from 5 to 60*60*24 in the code.
348
348
  """
349
+ #This is useless for now, but later get_production_inbounds() will be cached,
350
+ # and this will clear the cache every 24 hours or so, to make sure the client
351
+ # always has the most up-to-date inbounds list
349
352
  while True:
350
-
351
-
353
+ #print("You're seeing this message because I forgot to remove it in api.update_inbounds() !")
354
+ #print("Please change the timer from 5 to 60*60*24!")
352
355
  self.get_production_inbounds.cache_clear()
353
356
  await self.get_production_inbounds() #fill the cache
354
- await asyncio.sleep(3600) #every hour
357
+ await asyncio.sleep(10)
358
+ ##print(stat)
355
359
 
356
360
  #========================clients management========================
357
361
  async def get_client_with_tgid(self, tgid: int, inbound_id: int | None = None) -> List[ClientStats]:
@@ -376,7 +380,7 @@ class XUIClient:
376
380
  email = util.generate_email_from_tgid_inbid(tgid, inbound_id)
377
381
  resp = [await self.clients_end.get_client_with_email(email)]
378
382
  return resp
379
- uuid = util.get_telegram_uuid(tgid)
383
+ uuid = util.get_uuid_from_tgid(tgid)
380
384
  resp = await self.clients_end.get_client_with_uuid(uuid)
381
385
  return resp
382
386
 
@@ -400,7 +404,7 @@ class XUIClient:
400
404
  responses = []
401
405
  for inb in production_inbounds:
402
406
  client = SingleInboundClient.model_construct(
403
- uuid=util.get_telegram_uuid(telegram_id),
407
+ uuid=util.get_uuid_from_tgid(telegram_id),
404
408
  flow="",
405
409
  email=util.generate_email_from_tgid_inbid(telegram_id, inb.id),
406
410
  limit_gb=0,
@@ -28,6 +28,7 @@ class BaseModel(pydantic.BaseModel):
28
28
  model_config = pydantic.ConfigDict(ignored_types=(cached_property, ))
29
29
 
30
30
  def model_post_init(self, context: Any, /) -> None:
31
+ ##print(f"Model {self.__class__}, {self} initialized")
31
32
  ...
32
33
 
33
34
 
@@ -232,17 +232,17 @@ class Clients(BaseEndpoint):
232
232
  else:
233
233
  raise TypeError
234
234
  # send request
235
-
236
-
235
+ #print(type(final))
236
+ #print(final)
237
237
  data = final.model_dump(by_alias=True)
238
-
239
-
240
-
238
+ #print(type(data))
239
+ #print(json.dumps(data))
240
+ #print(f"{self._url}{endpoint}")
241
241
  resp = await self.client.safe_post(f"{self._url}{endpoint}", data=data)
242
242
 
243
243
  #YOU NEED TO PASS SETTINGS AS A STRING, NOT AS A DICT, YOU FUCKING DUMBASS!
244
-
245
-
244
+ #print(resp)
245
+ #print(resp.json())
246
246
  return resp
247
247
 
248
248
  async def _request_update_client(self, client: models.InboundClients | models.SingleInboundClient,
@@ -283,7 +283,7 @@ class Clients(BaseEndpoint):
283
283
  email: str | None = None,
284
284
  limit_ip: int | None = None,
285
285
  limit_gb: int | None = None,
286
- expiry_time: models.timestamp | None = None,
286
+ expiry_time: models.timestamp_seconds | None = None,
287
287
  enable: bool | None = None,
288
288
  sub_id: str | None = None,
289
289
  comment: str | None = None,
@@ -342,7 +342,7 @@ class Clients(BaseEndpoint):
342
342
  Returns:
343
343
  The HTTP response from the API.
344
344
  """
345
- _endpoint = f"{inbound_id}/delClient/{email}"
345
+ _endpoint = f"{inbound_id}/delClientByEmail/{email}"
346
346
  resp = await self.client.safe_post(f"{self._url}{_endpoint}")
347
347
  return resp
348
348
 
@@ -1,15 +1,15 @@
1
1
  import json
2
2
  from types import NoneType
3
3
  from datetime import datetime, UTC
4
- from typing import Union, Optional, TypeAlias, Any, Annotated, Literal, List, Dict
4
+ from typing import Union, Optional, TypeAlias, Any, Annotated, Literal, List, Dict, ClassVar
5
5
 
6
- from pydantic import field_validator, Field, field_serializer
6
+ from pydantic import field_validator, Field, field_serializer, AfterValidator
7
7
  import pydantic
8
8
 
9
9
  from . import base_model
10
- from .util import JsonType
10
+ from .util import JsonType, auto_s_to_ms_timestamp, s_to_ms_timestamp, ms_to_s_timestamp, auto_ms_to_s_timestamp
11
11
 
12
- timestamp: TypeAlias = int
12
+ timestamp_seconds: TypeAlias = int
13
13
  ip_address: TypeAlias = str
14
14
  json_string: TypeAlias = str
15
15
 
@@ -48,20 +48,41 @@ class SingleInboundClient(pydantic.BaseModel):
48
48
  created_at: Timestamp of client creation.
49
49
  updated_at: Timestamp of last client update.
50
50
  """
51
+ TIME_FIELDS: ClassVar[List[str]] = ["expiry_time", "created_at", "updated_at"]
51
52
  uuid: Annotated[str, Field(alias="id")] #yes they really did that...
52
53
  security: str = ""
53
54
  password: str = ""
54
55
  flow: Literal["", "xtls-rprx-vision", "xtls-rprx-vision-udp443"]
55
56
  email: Annotated[str, Field(alias="email")]
56
57
  limit_ip: Annotated[int, Field(alias="limitIp")] = 20
58
+ #Interestingly, the API expects this value to be called GB but it's actually bytes.
57
59
  limit_gb: Annotated[int, Field(alias="totalGB")] # total flow
58
- expiry_time: Annotated[timestamp, Field(alias="expiryTime")] = 0
60
+ expiry_time: Annotated[timestamp_seconds, Field(alias="expiryTime")] = 0
59
61
  enable: bool = True
60
62
  tg_id: Annotated[Union[int, str], Field(alias="tgId")] = ""
61
63
  subscription_id: Annotated[str, Field(alias="subId")]
62
64
  comment: str = ""
63
- created_at: Annotated[timestamp, Field(default_factory=(lambda: int(datetime.now(UTC).timestamp())))]
64
- updated_at: Annotated[timestamp, Field(default_factory=(lambda: int(datetime.now(UTC).timestamp())))]
65
+ created_at: Annotated[timestamp_seconds, Field(
66
+ default_factory=(lambda: int(datetime.now(UTC).timestamp())))
67
+ ]
68
+ updated_at: Annotated[timestamp_seconds, Field(
69
+ default_factory=(lambda: int(datetime.now(UTC).timestamp())))
70
+ ]
71
+
72
+ @classmethod
73
+ @field_validator(TIME_FIELDS[0], *TIME_FIELDS[1:], mode="after")
74
+ def ensure_s_timestamp(cls, value: int) -> int:
75
+ return auto_ms_to_s_timestamp(value)
76
+
77
+ @classmethod
78
+ @field_serializer(TIME_FIELDS[0], *TIME_FIELDS[1:])
79
+ def serialize_ms_timestamp(cls, value: int) -> int:
80
+ return auto_s_to_ms_timestamp(value)
81
+
82
+ @field_serializer("limit_gb")
83
+ def serialize_total_gb(self, value: int) -> int:
84
+ return value * (1024 ** 2) # Convert GB to bytes for API
85
+
65
86
 
66
87
  class InboundClients(pydantic.BaseModel):
67
88
  """Represents a collection of clients for an inbound connection.
@@ -90,12 +111,6 @@ class InboundClients(pydantic.BaseModel):
90
111
  """Serialize the settings object to a JSON string.
91
112
 
92
113
  The 3X-UI API expects settings as a JSON string, not an object.
93
-
94
- Args:
95
- value: The Settings object to serialize.
96
-
97
- Returns:
98
- A JSON string representation of the settings.
99
114
  """
100
115
  return json.dumps(value.model_dump(by_alias=True), ensure_ascii=False)
101
116
 
@@ -174,11 +189,12 @@ class ClientStats(base_model.BaseModel):
174
189
  up: Total uploaded bytes.
175
190
  down: Total downloaded bytes.
176
191
  allTime: Total bytes transferred (up + down).
177
- expiryTime: Client expiry time as UNIX timestamp.
192
+ expiryTime: Client expiry time as UNIX timestamp in MILLISECONDS.
178
193
  total: Total data limit in bytes.
179
194
  reset: Counter for traffic resets.
180
195
  lastOnline: UNIX timestamp of last connection.
181
196
  """
197
+ TIME_FIELDS: ClassVar[List[str]] = ["expiryTime", "lastOnline"]
182
198
  id: int
183
199
  inboundId: int
184
200
  enable: bool
@@ -188,10 +204,20 @@ class ClientStats(base_model.BaseModel):
188
204
  up: int # bytes
189
205
  down: int # bytes
190
206
  allTime: int # bytes
191
- expiryTime: timestamp # UNIX timestamp
207
+ expiryTime: timestamp_seconds # UNIX timestamp
192
208
  total: int
193
209
  reset: int
194
- lastOnline: timestamp
210
+ lastOnline: timestamp_seconds
211
+
212
+ @classmethod
213
+ @field_validator(TIME_FIELDS[0], *TIME_FIELDS[1:], mode="after")
214
+ def ensure_s_timestamp(cls, value: int) -> int:
215
+ return auto_ms_to_s_timestamp(value)
216
+
217
+ @classmethod
218
+ @field_serializer(TIME_FIELDS[0], *TIME_FIELDS[1:])
219
+ def serialize_ms_timestamp(cls, value: int) -> int:
220
+ return auto_s_to_ms_timestamp(value)
195
221
 
196
222
 
197
223
  class Inbound(base_model.BaseModel):
@@ -220,6 +246,7 @@ class Inbound(base_model.BaseModel):
220
246
  tag: Internal tag identifier for routing.
221
247
  sniffing: JSON sniffing configuration (auto-parsed from string).
222
248
  """
249
+ TIME_FIELDS: ClassVar[List[str]] = ["expiryTime", "lastTrafficResetTime"]
223
250
  id: int
224
251
  up: int # bytes
225
252
  down: int # bytes
@@ -227,9 +254,9 @@ class Inbound(base_model.BaseModel):
227
254
  allTime: int # bytes
228
255
  remark: str
229
256
  enable: bool
230
- expiryTime: timestamp # UNIX timestamp
257
+ expiryTime: timestamp_seconds # UNIX timestamp
231
258
  trafficReset: str # "Never", "Weekly", "Monthly", "Daily"
232
- lastTrafficResetTime: timestamp # UNIX timestamp
259
+ lastTrafficResetTime: timestamp_seconds # UNIX timestamp
233
260
  clientStats: Union[list[ClientStats], None]
234
261
  listen: str
235
262
  port: int
@@ -247,12 +274,6 @@ class Inbound(base_model.BaseModel):
247
274
 
248
275
  The 3X-UI API returns settings, streamSettings, and sniffing as
249
276
  JSON strings. This validator automatically parses them into dicts.
250
-
251
- Args:
252
- value: The JSON string to parse, or empty string.
253
-
254
- Returns:
255
- Parsed dictionary, or empty string if input was empty.
256
277
  """
257
278
  if value == "":
258
279
  return ""
@@ -265,13 +286,17 @@ class Inbound(base_model.BaseModel):
265
286
  """Serialize dictionary fields back to JSON strings.
266
287
 
267
288
  When sending data back to the API, these fields must be JSON strings.
268
-
269
- Args:
270
- value: The dictionary to serialize, or empty string.
271
-
272
- Returns:
273
- JSON string representation, or empty string if input was empty.
274
289
  """
275
290
  if value == "":
276
291
  return ""
277
292
  return json.dumps(value, ensure_ascii=False)
293
+
294
+ @classmethod
295
+ @field_validator(TIME_FIELDS[0], *TIME_FIELDS[1:], mode="after")
296
+ def ensure_s_timestamp(cls, value: int) -> int:
297
+ return auto_ms_to_s_timestamp(value)
298
+
299
+ @classmethod
300
+ @field_serializer(TIME_FIELDS[0], *TIME_FIELDS[1:])
301
+ def serialize_ms_timestamp(cls, value: int) -> int:
302
+ return auto_s_to_ms_timestamp(value)
@@ -13,7 +13,7 @@ import base64
13
13
  import logging
14
14
  import random
15
15
  import re
16
- from datetime import UTC, datetime
16
+ from datetime import UTC, datetime, tzinfo
17
17
  from typing import TypeAlias, Union, Dict, Any, List
18
18
 
19
19
  import httpx
@@ -94,7 +94,7 @@ def sub_from_tgid(telegram_id: int) -> str:
94
94
  ensure_2_digits = lambda x: str(x) if x >= 10 else f"0{x}"
95
95
 
96
96
 
97
- def get_telegram_uuid(telegram_id: int, fixed: bool = True) -> str:
97
+ def get_uuid_from_tgid(telegram_id: int, fixed: bool = True) -> str:
98
98
  """Generate a UUID v4 format string from a Telegram ID.
99
99
 
100
100
  Creates a deterministic UUID based on the Telegram ID, useful for
@@ -109,9 +109,9 @@ def get_telegram_uuid(telegram_id: int, fixed: bool = True) -> str:
109
109
  A UUID-formatted string with the Telegram ID embedded in the last segment.
110
110
 
111
111
  Examples:
112
- >>> get_telegram_uuid(12345)
112
+ >>> get_uuid_from_tgid(12345)
113
113
  '11111111-1111-1111-1111-0000000012345'
114
- >>> get_telegram_uuid(12345, fixed=False) # Uses current timestamp
114
+ >>> get_uuid_from_tgid(12345, fixed=False) # Uses current timestamp
115
115
  '20260222-1230-1111-1111-0000000012345'
116
116
  """
117
117
  zeros = 12 - len(str(telegram_id))
@@ -142,7 +142,7 @@ def generate_random_email(length: int = 8) -> str:
142
142
  return s
143
143
 
144
144
 
145
- def generate_email_from_tgid_inbid(telegram_id: int, inbound_id: int) -> str:
145
+ def generate_email_from_tgid_inbid(telegram_id: int, /, inbound_id: int) -> str:
146
146
  """Generate a deterministic email from Telegram ID and inbound ID.
147
147
 
148
148
  Creates a unique email identifier that combines the Telegram ID and
@@ -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
-
221
+ #print(f"Unsuccessful operation! Message: {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
 
@@ -234,7 +234,7 @@ def get_days_until_expiry(expiry_time: int) -> float:
234
234
  Returns a very large number (infinity) if expiry_time is 0 (no expiry).
235
235
 
236
236
  Examples:
237
- >>> get_days_until_expiry(int(datetime.now(UTC).timestamp()) + 86400) # 1 day from now
237
+ >>> get_days_until_expiry(int(datetime.now(UTC).timestamp_seconds()) + 86400) # 1 day from now
238
238
  1.0
239
239
  >>> get_days_until_expiry(0) # No expiry
240
240
  inf
@@ -263,3 +263,27 @@ class DBLockedError(Exception):
263
263
  message: Explanation of the error.
264
264
  """
265
265
  super().__init__(message)
266
+
267
+ def s_to_ms_timestamp(s: int|float) -> int:
268
+ """Convert a UNIX timestamp in seconds to milliseconds."""
269
+ return int(s * 1000)
270
+
271
+ def ms_to_s_timestamp(ms: int|float) -> int:
272
+ """Convert a UNIX timestamp in milliseconds to seconds."""
273
+ return ms // 1000
274
+
275
+ def auto_s_to_ms_timestamp(s_or_ms: int) -> int:
276
+ """Automatically convert a UNIX timestamp to milliseconds if it's in seconds."""
277
+ if s_or_ms < 1e12: # If the timestamp is less than 1 trillion, it's likely in seconds
278
+ return s_to_ms_timestamp(s_or_ms)
279
+ return s_or_ms
280
+
281
+ def auto_ms_to_s_timestamp(ms_or_s: int) -> int:
282
+ """Automatically convert a UNIX timestamp to seconds if it's in milliseconds."""
283
+ if ms_or_s > 1e12: # If the timestamp is greater than 1 trillion, it's likely in milliseconds
284
+ return ms_to_s_timestamp(ms_or_s)
285
+ return ms_or_s
286
+
287
+ def datetime_now_ms(tzinfo: tzinfo|None) -> int:
288
+ """Get the current time as a UNIX timestamp in milliseconds."""
289
+ return int(datetime.now(tzinfo).timestamp()) * 1000
@@ -48,7 +48,7 @@ async def xui_client() -> XUIClient:
48
48
  # Reset singleton for clean test state
49
49
  XUIClient._instance = None
50
50
 
51
- client = XUIClient(base_url, port, base_path, xui_username=username, xui_password=password)
51
+ client = XUIClient(base_url, port, base_path, username=username, password=password)
52
52
  client.connect()
53
53
 
54
54
  # Authenticate
@@ -1,8 +1,15 @@
1
+ import asyncio
2
+ import time
3
+
1
4
  import pytest
2
5
  from datetime import datetime, UTC
6
+
7
+ from pydantic import ValidationError
8
+
3
9
  from python_3xui.api import XUIClient
4
10
  from python_3xui.models import SingleInboundClient, ClientStats
5
- from python_3xui.util import get_telegram_uuid, sub_from_tgid
11
+ from python_3xui.util import get_uuid_from_tgid, sub_from_tgid, s_to_ms_timestamp, datetime_now_ms, generate_email_from_tgid_inbid, \
12
+ generate_random_email
6
13
 
7
14
 
8
15
  class TestClientsEndpoint:
@@ -49,8 +56,8 @@ class TestClientsEndpoint:
49
56
  assert inbound_id is not None, "Test inbound should be available"
50
57
 
51
58
  # Generate unique test data
52
- timestamp = int(datetime.now(UTC).timestamp())
53
- test_uuid = get_telegram_uuid(TestClientsEndpoint.test_telegram_id)
59
+ timestamp = datetime_now_ms(UTC)
60
+ test_uuid = get_uuid_from_tgid(TestClientsEndpoint.test_telegram_id)
54
61
  test_email = f"testclient_{timestamp}@example.com"
55
62
 
56
63
  # Create a test client
@@ -61,12 +68,12 @@ class TestClientsEndpoint:
61
68
  flow="",
62
69
  email=test_email,
63
70
  limitIp=20, # Using alias 'limitIp' for 'limit_ip'
64
- totalGB=10, # Using alias 'totalGB' for 'limit_gb'
65
- expiryTime=timestamp + 86400, # Using alias 'expiryTime' for 'expiry_time'
71
+ totalGB=10000, # Using alias 'totalGB' for 'limit_gb'
72
+ expiryTime=timestamp + 86400*1000, # Using alias 'expiryTime' for 'expiry_time'
66
73
  enable=True,
67
74
  tgId="", # Using alias 'tgId' for 'tg_id'
68
75
  subId=sub_from_tgid(TestClientsEndpoint.test_telegram_id), # Using alias 'subId' for 'subscription_id'
69
- comment=f"Test client created at {timestamp}",
76
+ comment=f"Test client created at {timestamp}, TEST SUITE",
70
77
  created_at=timestamp,
71
78
  updated_at=timestamp
72
79
  )
@@ -111,30 +118,27 @@ class TestClientsEndpoint:
111
118
  try:
112
119
  client_stats = await xui_client.clients_end.get_client_with_email(email)
113
120
  assert client_stats.email == email
114
- except Exception as e:
121
+ except ValidationError as e:
115
122
  pytest.skip(f"Test client with email {email} no longer exists: {e}")
116
123
 
117
124
  # Delete the client by email
125
+ print(f"Attempting to delete client with email: {email} from inbound: {inbound_id}")
126
+
118
127
  response = await xui_client.clients_end.delete_client_by_email(email, inbound_id)
119
128
 
120
129
  # Validate response
121
130
  assert response.status_code == 200
122
131
  response_json = response.json()
123
132
  assert response_json["success"] == True
124
- assert "Inbound client has been deleted." in response_json["msg"]
133
+ assert "Client deleted successfully" in response_json["msg"]
125
134
 
126
- # Verify deletion by trying to get the deleted client
127
- # Note: 3x-ui might return an error or null response for deleted client
128
135
  try:
129
136
  await xui_client.clients_end.get_client_with_email(email)
130
- # If we get here, the client still exists
131
- print(f"Warning: Client with email {email} might still exist after deletion")
132
- # For test purposes, we'll consider this acceptable if it's a timing issue
133
- except Exception:
134
- # Expected - client should be deleted
135
- pass
136
-
137
- print(f"Successfully deleted test client by email: {email}")
137
+ #if there's no error meaning the client still exists, fail the test
138
+ await asyncio.sleep(1) # Wait a moment
139
+ pytest.fail("The client still exists after deletion attempt")
140
+ except ValidationError:
141
+ print(f"Successfully deleted test client by email: {email}")
138
142
 
139
143
  # Only clear email, keep UUID for next test
140
144
  TestClientsEndpoint.created_client_email = None
@@ -148,8 +152,8 @@ class TestClientsEndpoint:
148
152
  assert inbound_id is not None, "Test inbound should be available"
149
153
 
150
154
  # Generate new test data
151
- timestamp = int(datetime.now(UTC).timestamp())
152
- test_uuid = get_telegram_uuid(TestClientsEndpoint.test_telegram_id + 1) # Different UUID
155
+ timestamp = datetime_now_ms(UTC)
156
+ test_uuid = get_uuid_from_tgid(TestClientsEndpoint.test_telegram_id + 1) # Different UUID
153
157
  test_email = f"testclient_uuid_{timestamp}@example.com"
154
158
 
155
159
  # Create a new test client
@@ -160,8 +164,8 @@ class TestClientsEndpoint:
160
164
  flow="",
161
165
  email=test_email,
162
166
  limitIp=20, # Using alias 'limitIp' for 'limit_ip'
163
- totalGB=10, # Using alias 'totalGB' for 'limit_gb'
164
- expiryTime=timestamp + 86400, # Using alias 'expiryTime' for 'expiry_time'
167
+ totalGB=0, # Using alias 'totalGB' for 'limit_gb'
168
+ expiryTime=timestamp + 86400*1000, # Using alias 'expiryTime' for 'expiry_time'
165
169
  enable=True,
166
170
  tgId="", # Using alias 'tgId' for 'tg_id'
167
171
  subId=f"test_sub_{timestamp}", # Using alias 'subId' for 'subscription_id'
@@ -175,6 +179,7 @@ class TestClientsEndpoint:
175
179
  assert response.status_code == 200
176
180
 
177
181
  # Delete the client by UUID
182
+ print(f"Attempting to delete client with UUID: {test_uuid} from inbound: {inbound_id}")
178
183
  response = await xui_client.clients_end.delete_client_by_uuid(test_uuid, inbound_id)
179
184
 
180
185
  # Validate response
@@ -183,7 +188,12 @@ class TestClientsEndpoint:
183
188
  assert response_json["success"] == True
184
189
  assert "Inbound client has been deleted." in response_json["msg"]
185
190
 
186
- print(f"Successfully deleted test client by UUID: {test_uuid}")
191
+ print(f"The API said it deleted the client: {test_uuid}")
192
+ check_clients = await xui_client.clients_end.get_client_with_uuid(test_uuid)
193
+ for client in check_clients:
194
+ if client.inboundId == inbound_id and client.uuid == test_uuid:
195
+ pytest.fail("The client still exists after deletion attempt")
196
+ print("Check complete, client not found as expected")
187
197
 
188
198
  @pytest.mark.asyncio
189
199
  @pytest.mark.dependency(depends=["test_add_client", "test_delete_client_email"])
@@ -193,11 +203,12 @@ class TestClientsEndpoint:
193
203
  production_inbounds = await xui_client.get_production_inbounds()
194
204
  if not production_inbounds:
195
205
  pytest.skip("No production inbounds found for testing")
206
+ TEST_TELEGRAM_ID = 420
196
207
 
197
208
  # Generate unique test data
198
- timestamp = int(datetime.now(UTC).timestamp())
199
- test_uuid = get_telegram_uuid(TestClientsEndpoint.test_telegram_id + 2) # Different UUID
200
- test_email = f"testclient_tgid_{timestamp}@example.com"
209
+ timestamp = datetime_now_ms(UTC)
210
+ test_uuid = get_uuid_from_tgid(TEST_TELEGRAM_ID) # Different UUID
211
+ test_email = "IF_YOU_SEE_THIS_SOMETHING_IS_WRONG"
201
212
 
202
213
  # Create a test client
203
214
  test_client = SingleInboundClient.model_construct(
@@ -207,8 +218,8 @@ class TestClientsEndpoint:
207
218
  flow="",
208
219
  email=test_email,
209
220
  limitIp=20, # Using alias 'limitIp' for 'limit_ip'
210
- totalGB=10, # Using alias 'totalGB' for 'limit_gb'
211
- expiryTime=timestamp + 86400, # Using alias 'expiryTime' for 'expiry_time'
221
+ totalGB=0, # Using alias 'totalGB' for 'limit_gb'
222
+ expiryTime=timestamp + 86400 * 1000, # Using alias 'expiryTime' for 'expiry_time'
212
223
  enable=True,
213
224
  tgId="", # Using alias 'tgId' for 'tg_id'
214
225
  subId=f"test_tgid_{timestamp}", # Using alias 'subId' for 'subscription_id'
@@ -220,14 +231,18 @@ class TestClientsEndpoint:
220
231
  # Add client to all production inbounds
221
232
  added_responses = []
222
233
  for inbound in production_inbounds:
223
- response = await xui_client.clients_end.add_client(test_client, inbound.id)
234
+ #same email = exception, so we need to generate a new email for each inbound
235
+ send_client = test_client.model_copy(
236
+ update={"email": generate_email_from_tgid_inbid(TEST_TELEGRAM_ID, inbound.id)}
237
+ )
238
+ response = await xui_client.clients_end.add_client(send_client, inbound.id)
224
239
  assert response.status_code == 200
225
240
  added_responses.append(response)
226
241
 
227
242
  print(f"Added test client with email: {test_email}, UUID: {test_uuid} to {len(production_inbounds)} production inbounds")
228
243
 
229
244
  # Now delete the client from all production inbounds by Telegram ID
230
- responses = await xui_client.delete_client_by_tgid_all_inbounds(TestClientsEndpoint.test_telegram_id + 2)
245
+ responses = await xui_client.delete_client_by_tgid_all_inbounds(TEST_TELEGRAM_ID)
231
246
 
232
247
  # Validate responses
233
248
  assert len(responses) == len(production_inbounds)
@@ -235,16 +250,17 @@ class TestClientsEndpoint:
235
250
  assert response.status_code == 200
236
251
  response_json = response.json()
237
252
  assert response_json["success"] == True
238
- assert "Inbound client has been deleted." in response_json["msg"]
253
+ assert "Client deleted successfully" in response_json["msg"]
239
254
 
240
255
  print(f"Successfully deleted test client by Telegram ID from {len(responses)} production inbounds")
241
256
 
242
257
  # Verify deletion by trying to get the deleted client from each inbound
243
- for inbound in production_inbounds:
258
+ for _ in production_inbounds:
244
259
  try:
245
260
  await xui_client.clients_end.get_client_with_email(test_email)
246
261
  # If we get here, the client still exists in at least one inbound
247
- print(f"Warning: Client with email {test_email} might still exist in inbound {inbound.id} after deletion")
262
+ await asyncio.sleep(1) # Wait a moment in case of timing issues
263
+ pytest.fail("The client still exists after deletion attempt in at least one inbound")
248
264
  except Exception:
249
265
  # Expected - client should be deleted
250
266
  pass
File without changes