rebecca-api 0.1.3__py3-none-any.whl

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.
rebecca/__init__.py ADDED
@@ -0,0 +1,98 @@
1
+ from .api import RebeccaAPI
2
+ from .models import (
3
+ Admin,
4
+ AdminCreate,
5
+ AdminModify,
6
+ BulkGroup,
7
+ BulkUser,
8
+ BulkUsersProxy,
9
+ CoreCreate,
10
+ CoreResponse,
11
+ CoreResponseList,
12
+ CoreStats,
13
+ CreateUserFromTemplate,
14
+ GroupCreate,
15
+ GroupModify,
16
+ GroupResponse,
17
+ GroupsResponse,
18
+ HostBase,
19
+ HostResponse,
20
+ HTTPValidationError,
21
+ ModifyUserByTemplate,
22
+ NextPlanModel,
23
+ NodeCreate,
24
+ NodeModify,
25
+ NodeResponse,
26
+ NodesUsageResponse,
27
+ NodeUsageResponse,
28
+ ProxyHost,
29
+ ProxyInbound,
30
+ ProxySettings,
31
+ SubscriptionUserResponse,
32
+ SystemStats,
33
+ Token,
34
+ UserCreate,
35
+ UserModify,
36
+ UserResponse,
37
+ UsersResponse,
38
+ UserStatus,
39
+ UserTemplateCreate,
40
+ UserTemplateModify,
41
+ UserTemplateResponse,
42
+ UserUsageResponse,
43
+ UserUsagesResponse,
44
+ ValidationError,
45
+ )
46
+ from .utils import RebeccaTokenCache
47
+ from .enums import RoleEnum
48
+ __all__ = (
49
+ "__version__",
50
+ "RebeccaAPI",
51
+ "RebeccaTokenCache",
52
+ "Admin",
53
+ "AdminCreate",
54
+ "AdminModify",
55
+ "UserCreate",
56
+ "UserModify",
57
+ "UserResponse",
58
+ "UsersResponse",
59
+ "UserStatus",
60
+ "UserTemplateCreate",
61
+ "UserTemplateModify",
62
+ "UserTemplateResponse",
63
+ "UserUsageResponse",
64
+ "UserUsagesResponse",
65
+ "NodeUsageResponse",
66
+ "NodesUsageResponse",
67
+ "NodeCreate",
68
+ "NodeModify",
69
+ "NodeResponse",
70
+ "GroupCreate",
71
+ "GroupModify",
72
+ "GroupResponse",
73
+ "GroupsResponse",
74
+ "BulkGroup",
75
+ "HostBase",
76
+ "HostResponse",
77
+ "CoreCreate",
78
+ "CoreResponse",
79
+ "CoreResponseList",
80
+ "ModifyUserByTemplate",
81
+ "CreateUserFromTemplate",
82
+ "BulkUser",
83
+ "BulkUsersProxy",
84
+ "SystemStats",
85
+ "CoreStats",
86
+ "ProxySettings",
87
+ "ProxyHost",
88
+ "ProxyInbound",
89
+ "Token",
90
+ "HTTPValidationError",
91
+ "ValidationError",
92
+ "SubscriptionUserResponse",
93
+ "NextPlanModel",
94
+ # Enum
95
+ "RoleEnum",
96
+ )
97
+
98
+ __version__ = "0.1.3"
rebecca/api.py ADDED
@@ -0,0 +1,606 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ import httpx
5
+ import paramiko
6
+ from paramiko.ssh_exception import SSHException
7
+ from sshtunnel import SSHTunnelForwarder
8
+
9
+ from .models import * # noqa: F403
10
+
11
+
12
+ class RebeccaAPI:
13
+ def __init__(
14
+ self,
15
+ base_url: str,
16
+ *,
17
+ timeout: float = 10.0,
18
+ verify: bool = False,
19
+ ssh_username: Optional[str] = None,
20
+ ssh_host: Optional[str] = None,
21
+ ssh_port: Optional[int] = 22,
22
+ ssh_private_key_path: Optional[str] = None,
23
+ ssh_key_passphrase: Optional[str] = None,
24
+ ssh_password: Optional[str] = None,
25
+ local_bind_host: str = "127.0.0.1",
26
+ local_bind_port: int = 8000,
27
+ remote_bind_host: str = "127.0.0.1",
28
+ remote_bind_port: int = 8000,
29
+ ):
30
+ """
31
+ Initializes the RebeccaAPI client with optional SSH tunneling for secure remote access.
32
+
33
+ :param base_url: The base URL of the Rebecca API.
34
+ :param timeout: The request timeout in seconds (default: 10.0).
35
+ :param verify: SSL verification flag; set to False to ignore SSL verification (default: False).
36
+ :param ssh_username: SSH username for tunnel authentication.
37
+ :param ssh_host: SSH server address for setting up the tunnel. If None, no SSH tunnel is used.
38
+ :param ssh_port: SSH port for connecting to the server (default: 22).
39
+ :param ssh_private_key_path: Path to the SSH private key file for authentication.
40
+ :param ssh_key_passphrase: Passphrase for the SSH private key, if applicable.
41
+ :param ssh_password: Password for SSH authentication. Use if no private key is provided.
42
+ :param local_bind_host: Local IP address for binding the SSH tunnel (default: '127.0.0.1').
43
+ :param local_bind_port: Local port for SSH tunnel binding (default: 8000).
44
+ :param remote_bind_host: Remote IP address for binding on the SSH server side (default: '127.0.0.1').
45
+ :param remote_bind_port: Remote port for the SSH server binding (default: 8000).
46
+
47
+ :raises ValueError: If SSH tunneling is requested but neither a private key nor password is provided.
48
+ """
49
+
50
+ self.base_url = base_url
51
+ self.timeout = timeout
52
+ self.verify = verify
53
+ self.ssh_username = ssh_username
54
+ self.ssh_host = ssh_host
55
+ self.ssh_port = ssh_port
56
+ self.ssh_private_key_path = ssh_private_key_path
57
+ self.ssh_key_passphrase = ssh_key_passphrase
58
+ self.ssh_password = ssh_password
59
+ self.local_bind_host = local_bind_host
60
+ self.local_bind_port = local_bind_port
61
+ self.remote_bind_host = remote_bind_host
62
+ self.remote_bind_port = remote_bind_port
63
+ self.client = None
64
+ self._tunnel = None
65
+ self._forwarder = None
66
+ if ssh_host and not ssh_private_key_path and not ssh_password:
67
+ raise ValueError("For an SSH tunnel, you must specify either ssh_private_key_path or ssh_password")
68
+ if not ssh_host:
69
+ self.client = httpx.AsyncClient(base_url=self.base_url, verify=self.verify, timeout=self.timeout)
70
+
71
+ def _load_private_key(self, key_path, passphrase):
72
+ key_classes = [paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey, paramiko.Ed25519Key]
73
+ for key_class in key_classes:
74
+ try:
75
+ if passphrase:
76
+ pkey = key_class.from_private_key_file(key_path, password=passphrase)
77
+ else:
78
+ pkey = key_class.from_private_key_file(key_path)
79
+ return pkey
80
+ except paramiko.ssh_exception.PasswordRequiredException:
81
+ print("Ошибка: Приватный ключ защищен паролем. Пожалуйста, укажите пароль.")
82
+ except SSHException:
83
+ continue
84
+ raise ValueError("Unsupported key format or incorrect passphrase.")
85
+
86
+ def _initialize(self):
87
+ """Initialization of the SSH tunnel and the HTTP client."""
88
+ if self.ssh_host:
89
+ if self._tunnel and self._tunnel.is_active:
90
+ return
91
+ # Uploading the key using paramiko
92
+ private_key = None
93
+ if self.ssh_private_key_path:
94
+ private_key = self._load_private_key(self.ssh_private_key_path, self.ssh_key_passphrase)
95
+ # Installing an SSH tunnel using sshtunnel
96
+ self._tunnel = SSHTunnelForwarder(
97
+ (self.ssh_host, self.ssh_port),
98
+ ssh_username=self.ssh_username,
99
+ ssh_password=self.ssh_password,
100
+ ssh_pkey=private_key,
101
+ remote_bind_address=(self.remote_bind_host, self.remote_bind_port),
102
+ local_bind_address=(self.local_bind_host, self.local_bind_port),
103
+ )
104
+ self._tunnel.start()
105
+ self.client = httpx.AsyncClient(
106
+ base_url=f"http://{self.local_bind_host}:{self.local_bind_port}",
107
+ timeout=self.timeout,
108
+ verify=self.verify,
109
+ )
110
+
111
+ # HTTP-client with local URL
112
+
113
+ def _get_headers(self, token: str) -> Dict[str, str]:
114
+ return {"Authorization": f"Bearer {token}"}
115
+
116
+ async def _request(
117
+ self,
118
+ method: str,
119
+ url: str,
120
+ token: Optional[str] = None,
121
+ data: Optional[BaseModel] = None,
122
+ params: Optional[Dict[str, Any]] = None,
123
+ ) -> httpx.Response:
124
+ if self.ssh_host and (not self.client or not self._tunnel.is_active):
125
+ # Initialize the HTTP client and SSH tunnel if they are closed
126
+ self._initialize()
127
+ return await self._request(method, url, token, data, params)
128
+ headers = self._get_headers(token) if token else {}
129
+ if data is None:
130
+ json_data = None
131
+ elif hasattr(data, "model_dump"):
132
+ json_data = data.model_dump(exclude_none=True)
133
+ else:
134
+ json_data = data
135
+ params = {k: v for k, v in (params or {}).items() if v is not None}
136
+ response = await self.client.request(method, url, headers=headers, json=json_data, params=params)
137
+ response.raise_for_status()
138
+ return response
139
+
140
+ async def get_token(self, username: str, password: str) -> Token:
141
+ url = "/api/admin/token"
142
+ payload = {
143
+ "grant_type": "password",
144
+ "username": username,
145
+ "password": password,
146
+ "scope": "",
147
+ "client_id": "",
148
+ "client_secret": "",
149
+ }
150
+ if self.ssh_host and (not self.client or not self._tunnel.is_active):
151
+ # Initialize the HTTP client and SSH tunnel if they are closed
152
+ self._initialize()
153
+ return await self.get_token(username, password)
154
+ response = await self.client.post(url, data=payload)
155
+ response.raise_for_status()
156
+ return Token(**response.json())
157
+
158
+ async def get_current_admin(self, token: str) -> Admin:
159
+ url = "/api/admin"
160
+ response = await self._request("GET", url, token)
161
+ return Admin(**response.json())
162
+
163
+ async def create_admin(self, admin: AdminCreate, token: str) -> Admin:
164
+ url = "/api/admin"
165
+ response = await self._request("POST", url, token, data=admin)
166
+ return Admin(**response.json())
167
+
168
+ async def modify_admin(self, username: str, admin: AdminModify, token: str) -> Admin:
169
+ url = f"/api/admin/{username}"
170
+ response = await self._request("PUT", url, token, data=admin)
171
+ return Admin(**response.json())
172
+
173
+ async def remove_admin(self, username: str, token: str) -> None:
174
+ url = f"/api/admin/{username}"
175
+ await self._request("DELETE", url, token)
176
+
177
+ async def get_admins(
178
+ self, token: str, offset: Optional[int] = None, limit: Optional[int] = None, username: Optional[str] = None
179
+ ) -> List[Admin]:
180
+ url = "/api/admins"
181
+ params = {"offset": offset, "limit": limit, "username": username}
182
+ response = await self._request("GET", url, token, params=params)
183
+ return [Admin(**admin) for admin in response.json()]
184
+
185
+ async def disable_all_users_admin(self, username: str, token: str) -> None:
186
+ url = f"/api/admin/{username}/users/disable"
187
+ await self._request("POST", url, token)
188
+
189
+ async def activate_all_users_admin(self, username: str, token: str) -> None:
190
+ url = f"/api/admin/{username}/users/activate"
191
+ await self._request("POST", url, token)
192
+
193
+ async def reset_admin_usage(self, username: str, token: str) -> Admin:
194
+ url = f"/api/admin/usage/reset/{username}"
195
+ response = await self._request("POST", url, token)
196
+ return Admin(**response.json())
197
+
198
+ async def get_admin_usage(self, username: str, token: str) -> Admin:
199
+ url = f"/api/admin/usage/{username}"
200
+ response = await self._request("GET", url, token)
201
+ return response.json()
202
+
203
+ async def get_system_stats(self, token: str) -> SystemStats:
204
+ url = "/api/system"
205
+ response = await self._request("GET", url, token)
206
+ return SystemStats(**response.json())
207
+
208
+ async def get_inbounds(self, token: str) -> Dict[str, List[ProxyInbound]]:
209
+ url = "/api/inbounds"
210
+ response = await self._request("GET", url, token)
211
+ return response.json()
212
+
213
+ async def get_hosts(self, token: str) -> Dict[str, List[ProxyHost]]:
214
+ url = "/api/hosts"
215
+ response = await self._request("GET", url, token)
216
+ return response.json()
217
+
218
+ async def modify_hosts(self, hosts: Dict[str, List[ProxyHost]], token: str) -> Dict[str, List[ProxyHost]]:
219
+ url = "/api/hosts"
220
+
221
+ hosts_model = HostsModel(root=hosts)
222
+ response = await self._request("PUT", url, token, data=hosts_model)
223
+ return response.json()
224
+
225
+ async def create_host(self, host: HostBase, token: str) -> HostResponse:
226
+ url = "/api/host"
227
+ response = await self._request("POST", url, token, data=host)
228
+ return HostResponse(**response.json())
229
+
230
+ async def get_host(self, host_id: int, token: str) -> HostResponse:
231
+ url = f"/api/host/{host_id}"
232
+ response = await self._request("GET", url, token)
233
+ return HostResponse(**response.json())
234
+
235
+ async def modify_host(self, host_id: int, host: HostBase, token: str) -> HostResponse:
236
+ url = f"/api/host/{host_id}"
237
+ response = await self._request("PUT", url, token, data=host)
238
+ return HostResponse(**response.json())
239
+
240
+ async def remove_host(self, host_id: int, token: str) -> None:
241
+ url = f"/api/host/{host_id}"
242
+ await self._request("DELETE", url, token)
243
+
244
+ async def get_core_stats(self, token: str) -> CoreStats:
245
+ url = "/api/core"
246
+ response = await self._request("GET", url, token)
247
+ return CoreStats(**response.json())
248
+
249
+ async def create_core_config(self, core: CoreCreate, token: str) -> CoreResponse:
250
+ url = "/api/core"
251
+ response = await self._request("POST", url, token, data=core)
252
+ return CoreResponse(**response.json())
253
+
254
+ async def get_core_config(self, core_id: int, token: str) -> CoreResponse:
255
+ url = f"/api/core/{core_id}"
256
+ response = await self._request("GET", url, token)
257
+ return CoreResponse(**response.json())
258
+
259
+ async def modify_core_config(
260
+ self, core_id: int, config: CoreCreate, token: str, restart_nodes: bool = False
261
+ ) -> CoreResponse:
262
+ url = f"/api/core/{core_id}"
263
+ params = {"restart_nodes": restart_nodes}
264
+ response = await self._request("PUT", url, token, data=config, params=params)
265
+ return CoreResponse(**response.json())
266
+
267
+ async def delete_core_config(self, core_id: int, token: str, restart_nodes: bool = False) -> None:
268
+ url = f"/api/core/{core_id}"
269
+ params = {"restart_nodes": restart_nodes}
270
+ await self._request("DELETE", url, token, params=params)
271
+
272
+ async def get_all_cores(
273
+ self, token: str, offset: Optional[int] = None, limit: Optional[int] = None
274
+ ) -> CoreResponseList:
275
+ url = "/api/cores"
276
+ params = {"offset": offset, "limit": limit}
277
+ response = await self._request("GET", url, token, params=params)
278
+ return CoreResponseList(**response.json())
279
+
280
+ async def restart_core(self, core_id: int, token: str) -> None:
281
+ url = f"/api/core/{core_id}/restart"
282
+ await self._request("POST", url, token)
283
+
284
+ async def add_user(self, user: UserCreate, token: str) -> UserResponse:
285
+ url = "/api/user"
286
+ response = await self._request("POST", url, token, data=user)
287
+ return UserResponse(**response.json())
288
+
289
+ async def create_user_from_template(self, template_user: CreateUserFromTemplate, token: str) -> UserResponse:
290
+ url = "/api/user/from_template"
291
+ response = await self._request("POST", url, token, data=template_user)
292
+ return UserResponse(**response.json())
293
+
294
+ async def get_user(self, username: str, token: str) -> UserResponse:
295
+ url = f"/api/user/{username}"
296
+ response = await self._request("GET", url, token)
297
+ return UserResponse(**response.json())
298
+
299
+ async def modify_user(self, username: str, user: UserModify, token: str) -> UserResponse:
300
+ url = f"/api/user/{username}"
301
+ response = await self._request("PUT", url, token, data=user)
302
+ return UserResponse(**response.json())
303
+
304
+ async def modify_user_with_template(
305
+ self, username: str, template_user: ModifyUserByTemplate, token: str
306
+ ) -> UserResponse:
307
+ url = f"/api/user/from_template/{username}"
308
+ response = await self._request("PUT", url, token, data=template_user)
309
+ return UserResponse(**response.json())
310
+
311
+ async def activate_next_plan(self, username: str, token: str) -> UserResponse:
312
+ url = f"/api/user/{username}/active-next"
313
+ response = await self._request("POST", url, token)
314
+ return UserResponse(**response.json())
315
+
316
+ async def remove_user(self, username: str, token: str) -> None:
317
+ url = f"/api/user/{username}"
318
+ await self._request("DELETE", url, token)
319
+
320
+ async def bulk_modify_users_expire(self, bulk: BulkUser, token: str) -> Dict[str, Any]:
321
+ url = "/api/users/bulk/expire"
322
+ response = await self._request("POST", url, token, data=bulk)
323
+ return response.json()
324
+
325
+ async def bulk_modify_users_datalimit(self, bulk: BulkUser, token: str) -> Dict[str, Any]:
326
+ url = "/api/users/bulk/data_limit"
327
+ response = await self._request("POST", url, token, data=bulk)
328
+ return response.json()
329
+
330
+ async def bulk_modify_users_proxy_settings(self, bulk: BulkUsersProxy, token: str) -> Dict[str, Any]:
331
+ url = "/api/users/bulk/proxy_settings"
332
+ response = await self._request("POST", url, token, data=bulk)
333
+ return response.json()
334
+
335
+ async def reset_user_data_usage(self, username: str, token: str) -> UserResponse:
336
+ url = f"/api/user/{username}/reset"
337
+ response = await self._request("POST", url, token)
338
+ return UserResponse(**response.json())
339
+
340
+ async def revoke_user_subscription(self, username: str, token: str) -> UserResponse:
341
+ url = f"/api/user/{username}/revoke_sub"
342
+ response = await self._request("POST", url, token)
343
+ return UserResponse(**response.json())
344
+
345
+ async def get_users(
346
+ self,
347
+ token: str,
348
+ offset: Optional[int] = None,
349
+ limit: Optional[int] = None,
350
+ username: Optional[List[str]] = None,
351
+ search: Optional[str] = None,
352
+ status: Optional[str] = None,
353
+ sort: Optional[str] = None,
354
+ ) -> UsersResponse:
355
+ url = "/api/users"
356
+ params = {
357
+ "offset": offset,
358
+ "limit": limit,
359
+ "username": username,
360
+ "search": search,
361
+ "status": status,
362
+ "sort": sort,
363
+ }
364
+ response = await self._request("GET", url, token, params=params)
365
+ return UsersResponse(**response.json())
366
+
367
+ async def reset_users_data_usage(self, token: str) -> None:
368
+ url = "/api/users/reset"
369
+ await self._request("POST", url, token)
370
+
371
+ async def get_user_data_usage(
372
+ self, username: str, token: str, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None
373
+ ) -> UserUsagesResponse:
374
+ if isinstance(start_date, str):
375
+ start_date = datetime.fromisoformat(start_date)
376
+ if isinstance(end_date, str):
377
+ end_date = datetime.fromisoformat(end_date)
378
+
379
+ params = {
380
+ "start": start_date.isoformat(timespec="seconds") if start_date else None,
381
+ "end": end_date.isoformat(timespec="seconds") if end_date else None,
382
+ }
383
+ params = {k: v for k, v in params.items() if v is not None}
384
+ url = f"/api/user/{username}/usage"
385
+ response = await self._request("GET", url, token, params=params)
386
+ return UserUsagesResponse(**response.json())
387
+
388
+ async def set_owner(self, username: str, admin_username: str, token: str) -> UserResponse:
389
+ url = f"/api/user/{username}/set-owner?admin_username={admin_username}"
390
+ response = await self._request("PUT", url, token)
391
+ return UserResponse(**response.json())
392
+
393
+ async def get_expired_users(
394
+ self, token: str, expired_before: Optional[str] = None, expired_after: Optional[str] = None
395
+ ) -> List[str]:
396
+ url = "/api/users/expired"
397
+ params = {"expired_before": expired_before, "expired_after": expired_after}
398
+ response = await self._request("GET", url, token, params=params)
399
+ return response.json()
400
+
401
+ async def delete_expired_users(
402
+ self, token: str, expired_before: Optional[str] = None, expired_after: Optional[str] = None
403
+ ) -> List[str]:
404
+ url = "/api/users/expired"
405
+ params = {"expired_before": expired_before, "expired_after": expired_after}
406
+ response = await self._request("DELETE", url, token, params=params)
407
+ return response.json()
408
+
409
+ async def get_user_templates(
410
+ self, token: str, offset: Optional[int] = None, limit: Optional[int] = None
411
+ ) -> List[UserTemplateResponse]:
412
+ url = "/api/user_template"
413
+ params = {"offset": offset, "limit": limit}
414
+ response = await self._request("GET", url, token, params=params)
415
+ return [UserTemplateResponse(**template) for template in response.json()]
416
+
417
+ async def add_user_template(self, template: UserTemplateCreate, token: str) -> UserTemplateResponse:
418
+ url = "/api/user_template"
419
+ response = await self._request("POST", url, token, data=template)
420
+ return UserTemplateResponse(**response.json())
421
+
422
+ async def get_user_template(self, template_id: int, token: str) -> UserTemplateResponse:
423
+ url = f"/api/user_template/{template_id}"
424
+ response = await self._request("GET", url, token)
425
+ return UserTemplateResponse(**response.json())
426
+
427
+ async def modify_user_template(
428
+ self, template_id: int, template: UserTemplateModify, token: str
429
+ ) -> UserTemplateResponse:
430
+ url = f"/api/user_template/{template_id}"
431
+ response = await self._request("PUT", url, token, data=template)
432
+ return UserTemplateResponse(**response.json())
433
+
434
+ async def remove_user_template(self, template_id: int, token: str) -> None:
435
+ url = f"/api/user_template/{template_id}"
436
+ await self._request("DELETE", url, token)
437
+
438
+ # Group endpoints
439
+ async def create_group(self, group: GroupCreate, token: str) -> GroupResponse:
440
+ url = "/api/group"
441
+ response = await self._request("POST", url, token, data=group)
442
+ return GroupResponse(**response.json())
443
+
444
+ async def get_group(self, group_id: int, token: str) -> GroupResponse:
445
+ url = f"/api/group/{group_id}"
446
+ response = await self._request("GET", url, token)
447
+ return GroupResponse(**response.json())
448
+
449
+ async def modify_group(self, group_id: int, group: GroupModify, token: str) -> GroupResponse:
450
+ url = f"/api/group/{group_id}"
451
+ response = await self._request("PUT", url, token, data=group)
452
+ return GroupResponse(**response.json())
453
+
454
+ async def remove_group(self, group_id: int, token: str) -> None:
455
+ url = f"/api/group/{group_id}"
456
+ await self._request("DELETE", url, token)
457
+
458
+ async def get_groups(self, token: str, offset: Optional[int] = None, limit: Optional[int] = None) -> GroupsResponse:
459
+ url = "/api/groups"
460
+ params = {"offset": offset, "limit": limit}
461
+ response = await self._request("GET", url, token, params=params)
462
+ return GroupsResponse(**response.json())
463
+
464
+ async def bulk_add_groups_to_users(self, bulk_group: BulkGroup, token: str) -> Dict[str, Any]:
465
+ url = "/api/groups/bulk/add"
466
+ response = await self._request("POST", url, token, data=bulk_group)
467
+ return response.json()
468
+
469
+ async def bulk_remove_users_from_groups(self, bulk_group: BulkGroup, token: str) -> Dict[str, Any]:
470
+ url = "/api/groups/bulk/remove"
471
+ response = await self._request("POST", url, token, data=bulk_group)
472
+ return response.json()
473
+
474
+ async def get_node_settings(self, token: str) -> Dict[str, Any]:
475
+ url = "/api/node/settings"
476
+ response = await self._request("GET", url, token)
477
+ return response.json()
478
+
479
+ async def add_node(self, node: NodeCreate, token: str) -> NodeResponse:
480
+ url = "/api/node"
481
+ response = await self._request("POST", url, token, data=node)
482
+ return NodeResponse(**response.json())
483
+
484
+ async def get_node(self, node_id: int, token: str) -> NodeResponse:
485
+ url = f"/api/node/{node_id}"
486
+ response = await self._request("GET", url, token)
487
+ return NodeResponse(**response.json())
488
+
489
+ async def modify_node(self, node_id: int, node: NodeModify, token: str) -> NodeResponse:
490
+ url = f"/api/node/{node_id}"
491
+ response = await self._request("PUT", url, token, data=node)
492
+ return NodeResponse(**response.json())
493
+
494
+ async def remove_node(self, node_id: int, token: str) -> None:
495
+ url = f"/api/node/{node_id}"
496
+ await self._request("DELETE", url, token)
497
+
498
+ async def reconnect_node(self, node_id: int, token: str) -> None:
499
+ url = f"/api/node/{node_id}/reconnect"
500
+ await self._request("POST", url, token)
501
+
502
+ async def sync_node(self, node_id: int, token: str, flush_users: bool = False) -> Any:
503
+ url = f"/api/node/{node_id}/sync"
504
+ params = {"flush_users": flush_users}
505
+ response = await self._request("PUT", url, token, params=params)
506
+ return response.json()
507
+
508
+ async def node_logs(self, node_id: int, token: str) -> str:
509
+ url = f"/api/node/{node_id}/logs"
510
+ response = await self._request("GET", url, token)
511
+ return response.text
512
+
513
+ async def get_node_stats_periodic(
514
+ self, node_id: int, token: str, start: Optional[str] = None, end: Optional[str] = None, period: str = "hour"
515
+ ) -> Any:
516
+ url = f"/api/node/{node_id}/stats"
517
+ params = {"start": start, "end": end, "period": period}
518
+ response = await self._request("GET", url, token, params=params)
519
+ return response.json()
520
+
521
+ async def realtime_node_stats(self, node_id: int, token: str) -> Any:
522
+ url = f"/api/node/{node_id}/realtime_stats"
523
+ response = await self._request("GET", url, token)
524
+ return response.json()
525
+
526
+ async def realtime_nodes_stats(self, token: str) -> Any:
527
+ url = "/api/nodes/realtime_stats"
528
+ response = await self._request("GET", url, token)
529
+ return response.json()
530
+
531
+ async def user_online_stats(self, node_id: int, username: str, token: str) -> Any:
532
+ url = f"/api/node/{node_id}/online_stats/{username}"
533
+ response = await self._request("GET", url, token)
534
+ return response.json()
535
+
536
+ async def user_online_ip_list(self, node_id: int, username: str, token: str) -> Any:
537
+ url = f"/api/node/{node_id}/online_stats/{username}/ip"
538
+ response = await self._request("GET", url, token)
539
+ return response.json()
540
+
541
+ async def clear_usage_data(
542
+ self, table: str, token: str, start: Optional[str] = None, end: Optional[str] = None
543
+ ) -> Any:
544
+ url = f"/api/nodes/clear_usage_data/{table}"
545
+ params = {"start": start, "end": end}
546
+ response = await self._request("DELETE", url, token, params=params)
547
+ return response.json()
548
+
549
+ async def get_nodes(self, token: str) -> List[NodeResponse]:
550
+ url = "/api/nodes"
551
+ response = await self._request("GET", url, token)
552
+ return [NodeResponse(**node) for node in response.json()]
553
+
554
+ async def get_usage(self, token: str, start: Optional[str] = None, end: Optional[str] = None) -> NodesUsageResponse:
555
+ url = "/api/nodes/usage"
556
+ params = {"start": start, "end": end}
557
+ response = await self._request("GET", url, token, params=params)
558
+ return NodesUsageResponse(**response.json())
559
+
560
+ async def get_user_subscription_info(self, url: str = None, token: str = None) -> SubscriptionUserResponse:
561
+ if url:
562
+ # Use the provided URL if it is given
563
+ final_url = url + "/info"
564
+ elif token:
565
+ # Form the URL using the token if it is provided
566
+ final_url = f"/sub/{token}/info"
567
+ else:
568
+ raise ValueError("Either url or token must be provided")
569
+
570
+ response = await self._request("GET", final_url)
571
+ return SubscriptionUserResponse(**response.json())
572
+
573
+ async def get_user_usage(
574
+ self, url: str = None, token: str = None, start: Optional[str] = None, end: Optional[str] = None
575
+ ) -> Any:
576
+ if url:
577
+ # Use the provided URL if it is given
578
+ final_url = url + "/usage"
579
+ elif token:
580
+ # Form the URL using the token if it is provided
581
+ final_url = f"/sub/{token}/usage"
582
+ else:
583
+ raise ValueError("Either url or token must be provided")
584
+ params = {"start": start, "end": end}
585
+ response = await self._request("GET", final_url, params=params)
586
+ return response.json()
587
+
588
+ async def get_user_subscription_with_client_type(self, client_type: str, url: str = None, token: str = None) -> Any:
589
+ if url:
590
+ # Use the provided URL if it is given
591
+ final_url = url + f"/{client_type}"
592
+ elif token:
593
+ # Form the URL using the token if it is provided
594
+ final_url = f"/sub/{token}/{client_type}"
595
+ else:
596
+ raise ValueError("Either url or token must be provided")
597
+
598
+ response = await self._request("GET", final_url)
599
+ return response.json()
600
+
601
+ async def close(self):
602
+ """Closing the HTTP client and SSH tunnel."""
603
+ if self.client:
604
+ await self.client.aclose()
605
+ if self._tunnel:
606
+ self._tunnel.stop()
rebecca/enums.py ADDED
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class RoleEnum(str, Enum):
5
+ full_access = "full_access"
6
+ standard = "standard"
7
+ sudo = "sudo"
rebecca/models.py ADDED
@@ -0,0 +1,492 @@
1
+ from typing import Any, ClassVar, Dict, List, Literal, Optional
2
+ from pydantic import BaseModel, RootModel, field_validator, Field
3
+ from .enums import RoleEnum
4
+
5
+
6
+ class Token(BaseModel):
7
+ access_token: str
8
+ token_type: str = "bearer"
9
+
10
+
11
+ class Admin(BaseModel):
12
+ id: Optional[int] = None
13
+ username: Optional[str] = None
14
+ role: Optional[RoleEnum] = RoleEnum.standard
15
+ status: Optional[str] = None
16
+ telegram_id: Optional[int] = None
17
+ users_usage: Optional[int] = None
18
+ data_limit: Optional[int] = None
19
+ users_limit: Optional[int] = None
20
+ active_users: Optional[int] = None
21
+ online_users: Optional[int] = None
22
+ limited_users: Optional[int] = None
23
+ expired_users: Optional[int] = None
24
+
25
+ class Config:
26
+ extra = "ignore"
27
+
28
+
29
+ class AdminListResponse(BaseModel):
30
+ admins: List[Admin]
31
+ total: int
32
+
33
+ class UsersPermissions(BaseModel):
34
+ create: bool = False
35
+ delete: bool = False
36
+ reset_usage: bool = False
37
+ revoke: bool = False
38
+ create_on_hold: bool = False
39
+ allow_unlimited_data: bool = False
40
+ allow_unlimited_expire: bool = False
41
+ allow_next_plan: bool = False
42
+ max_data_limit_per_user: int = 0
43
+
44
+ class AdminManagementPermissions(BaseModel):
45
+ can_view: bool = False
46
+ can_edit: bool = False
47
+ can_manage_sudo: bool = False
48
+
49
+ class SectionsPermissions(BaseModel):
50
+ usage: bool = False
51
+ admins: bool = False
52
+ services: bool = False
53
+ hosts: bool = False
54
+ nodes: bool = False
55
+ integrations: bool = False
56
+ xray: bool = False
57
+
58
+ class Permissions(BaseModel):
59
+ users: Optional[UsersPermissions] = Field(default_factory=UsersPermissions)
60
+ admin_management: Optional[AdminManagementPermissions] = Field(default_factory=AdminManagementPermissions)
61
+ sections: Optional[SectionsPermissions] = Field(default_factory=SectionsPermissions)
62
+
63
+ class AdminCreate(BaseModel):
64
+ username: str
65
+ password: str
66
+ role: Optional[RoleEnum] = RoleEnum.standard
67
+ permissions: Optional[Permissions] = Field(default_factory=Permissions)
68
+ telegram_id: Optional[int] = None
69
+ status: Optional[str] = "active"
70
+ disabled_reason: Optional[str] = ""
71
+ users_usage: Optional[int] = 0
72
+ data_limit: Optional[int] = 0
73
+ users_limit: Optional[int] = 0
74
+ active_users: Optional[int] = 0
75
+ online_users: Optional[int] = 0
76
+ limited_users: Optional[int] = 0
77
+ expired_users: Optional[int] = 0
78
+
79
+ class Config:
80
+ extra = "ignore"
81
+
82
+
83
+ class AdminModify(BaseModel):
84
+ role: Optional[RoleEnum] = RoleEnum.standard
85
+ password: Optional[str] = None
86
+
87
+
88
+ class HTTPValidationError(BaseModel):
89
+ detail: Optional[List[Dict[str, Any]]] = None
90
+
91
+
92
+ class ProxySettings(BaseModel):
93
+ id: Optional[str] = None
94
+ flow: Optional[str] = None
95
+ method: Optional[str] = None
96
+
97
+
98
+ class NextPlanModel(BaseModel):
99
+ add_remaining_traffic: bool = False
100
+ data_limit: Optional[int] = 0
101
+ expire: Optional[int] = 0
102
+ fire_on_either: bool = True
103
+
104
+ @field_validator("data_limit", mode="before")
105
+ def validate_data_limit(cls, value):
106
+ if value is not None and value < 0:
107
+ raise ValueError("Data limit in the next plan must be 0 or greater")
108
+ return value
109
+
110
+
111
+ class UserCreate(BaseModel):
112
+ username: str
113
+ proxy_settings: Optional[Dict[str, ProxySettings]] = None
114
+ group_ids: Optional[List[int]] = None
115
+ expire: Optional[int] = None
116
+ data_limit: Optional[int] = 0
117
+ data_limit_reset_strategy: Optional[str] = "no_reset"
118
+ note: Optional[str] = None
119
+ sub_updated_at: Optional[str] = None
120
+ sub_last_user_agent: Optional[str] = None
121
+ online_at: Optional[str] = None
122
+ on_hold_expire_duration: Optional[int] = 0
123
+ on_hold_timeout: Optional[str] = None
124
+ status: Literal["active", "on_hold"] = "active"
125
+ next_plan: Optional[NextPlanModel] = None
126
+
127
+
128
+ class UserResponse(BaseModel):
129
+ username: Optional[str] = None
130
+ proxy_settings: Optional[Dict[str, ProxySettings]] = {}
131
+ group_ids: Optional[List[int]] = None
132
+ expire: Optional[int] = None
133
+ data_limit: Optional[int] = None
134
+ data_limit_reset_strategy: Optional[str] = None
135
+ note: Optional[str] = None
136
+ sub_updated_at: Optional[str] = None
137
+ sub_last_user_agent: Optional[str] = None
138
+ online_at: Optional[str] = None
139
+ on_hold_expire_duration: Optional[int] = None
140
+ on_hold_timeout: Optional[str] = None
141
+ status: Literal["active", "disabled", "limited", "expired", "on_hold"] = "active"
142
+ used_traffic: Optional[int] = None
143
+ lifetime_used_traffic: Optional[int] = None
144
+ links: Optional[List[str]] = []
145
+ subscription_url: Optional[str] = None
146
+ subscription_token: Optional[str] = None
147
+ next_plan: Optional[NextPlanModel] = None
148
+ admin: Optional[Admin] = None
149
+ created_at: Optional[str] = None
150
+
151
+ def __init__(self, **data):
152
+ super().__init__(**data)
153
+ if not self.subscription_token and self.subscription_url:
154
+ self.subscription_token = self.subscription_url.split("/")[-1]
155
+
156
+
157
+ class NodeCreate(BaseModel):
158
+ name: str
159
+ address: str
160
+ port: int = 62050
161
+ usage_coefficient: float = 1.0
162
+ connection_type: Optional[str] = None
163
+ server_ca: Optional[str] = None
164
+ keep_alive: Optional[int] = None
165
+ max_logs: Optional[int] = 1000
166
+ core_config_id: Optional[int] = None
167
+ api_key: Optional[str] = None
168
+ gather_logs: Optional[bool] = True
169
+ api_port: Optional[int] = None
170
+
171
+
172
+ class NodeModify(BaseModel):
173
+ name: Optional[str] = None
174
+ address: Optional[str] = None
175
+ port: Optional[int] = None
176
+ api_port: Optional[int] = None
177
+ usage_coefficient: Optional[float] = None
178
+ status: Optional[str] = None
179
+ connection_type: Optional[str] = None
180
+ server_ca: Optional[str] = None
181
+ keep_alive: Optional[int] = None
182
+ max_logs: Optional[int] = None
183
+ core_config_id: Optional[int] = None
184
+ api_key: Optional[str] = None
185
+ gather_logs: Optional[bool] = None
186
+
187
+
188
+ class NodeResponse(BaseModel):
189
+ name: str
190
+ address: str
191
+ port: int
192
+ usage_coefficient: float
193
+ id: int
194
+ api_key: Optional[str] = None
195
+ core_config_id: Optional[int] = None
196
+ xray_version: Optional[str] = None
197
+ node_version: Optional[str] = None
198
+ status: str
199
+ message: Optional[str] = None
200
+
201
+
202
+ class NodeUsageResponse(BaseModel):
203
+ node_id: Optional[int] = None
204
+ node_name: Optional[str] = None
205
+ uplink: Optional[int] = None
206
+ downlink: Optional[int] = None
207
+
208
+
209
+ class NodesUsageResponse(BaseModel):
210
+ usages: List[NodeUsageResponse]
211
+
212
+
213
+ class ProxyHost(BaseModel):
214
+ remark: str
215
+ address: str
216
+ port: Optional[int] = None
217
+ sni: Optional[str] = None
218
+ host: Optional[str] = None
219
+ path: Optional[str] = None
220
+ security: str = "inbound_default"
221
+ alpn: str = ""
222
+ fingerprint: str = ""
223
+ allowinsecure: bool
224
+ is_disabled: bool
225
+
226
+
227
+ class HostsModel(RootModel):
228
+ root: Dict[str, List[ProxyHost]]
229
+
230
+
231
+ class ProxyInbound(BaseModel):
232
+ tag: str
233
+ protocol: str
234
+ network: str
235
+ tls: str
236
+ port: Any
237
+
238
+
239
+ class CoreStats(BaseModel):
240
+ version: str
241
+ started: bool
242
+ logs_websocket: str
243
+
244
+
245
+ class UserModify(BaseModel):
246
+ proxy_settings: Optional[Dict[str, ProxySettings]] = {}
247
+ group_ids: Optional[List[int]] = None
248
+ expire: Optional[int] = None
249
+ data_limit: Optional[int] = None
250
+ data_limit_reset_strategy: Optional[Literal["no_reset", "day", "week", "month", "year"]] = None
251
+ note: Optional[str] = None
252
+ sub_updated_at: Optional[str] = None
253
+ sub_last_user_agent: Optional[str] = None
254
+ online_at: Optional[str] = None
255
+ on_hold_expire_duration: Optional[int] = None
256
+ on_hold_timeout: Optional[str] = None
257
+ status: Optional[Literal["active", "disabled", "limited", "expired", "on_hold"]] = None
258
+ next_plan: Optional[NextPlanModel] = None
259
+
260
+
261
+ class UserTemplateCreate(BaseModel):
262
+ name: Optional[str] = None
263
+ group_ids: Optional[List[int]] = []
264
+ data_limit: int = 0
265
+ expire_duration: int = 0
266
+ extra_settings: Optional[ProxySettings] = None
267
+ status: Literal["active", "on_hold"] = "active"
268
+ reset_usages: Optional[bool] = None
269
+
270
+
271
+ class UserTemplateResponse(BaseModel):
272
+ id: int
273
+ name: Optional[str] = None
274
+ group_ids: Optional[List[int]] = None
275
+ data_limit: int
276
+ expire_duration: int
277
+ extra_settings: Optional[ProxySettings] = None
278
+ status: Literal["active", "on_hold"]
279
+ reset_usages: Optional[bool] = None
280
+
281
+
282
+ class UserTemplateModify(BaseModel):
283
+ name: Optional[str] = None
284
+ group_ids: Optional[List[int]] = None
285
+ data_limit: Optional[int] = None
286
+ expire_duration: Optional[int] = None
287
+ extra_settings: Optional[ProxySettings] = None
288
+ status: Optional[Literal["active", "on_hold"]] = None
289
+ reset_usages: Optional[bool] = None
290
+
291
+
292
+ class UserUsageResponse(BaseModel):
293
+ node_id: Optional[int]
294
+ node_name: Optional[str]
295
+ used_traffic: Optional[int]
296
+
297
+
298
+ class UserUsagesResponse(BaseModel):
299
+ username: str
300
+ usages: List[UserUsageResponse]
301
+
302
+
303
+ class UsersResponse(BaseModel):
304
+ users: List[UserResponse]
305
+ total: int
306
+
307
+
308
+ class UserStatus(BaseModel):
309
+ enum: ClassVar[List[str]] = ["active", "disabled", "limited", "expired", "on_hold"]
310
+
311
+
312
+ class ValidationError(BaseModel):
313
+ loc: List[Any]
314
+ msg: str
315
+ type: str
316
+
317
+
318
+ class SubscriptionUserResponse(BaseModel):
319
+ proxies: Dict[str, Any]
320
+ expire: Optional[int] = None
321
+ data_limit: Optional[int] = None
322
+ data_limit_reset_strategy: str = "no_reset"
323
+ inbounds: Dict[str, List[str]] = {}
324
+ note: Optional[str] = None
325
+ sub_updated_at: Optional[str] = None
326
+ sub_last_user_agent: Optional[str] = None
327
+ online_at: Optional[str] = None
328
+ on_hold_expire_duration: Optional[int] = None
329
+ on_hold_timeout: Optional[str] = None
330
+ auto_delete_in_days: Optional[int] = None
331
+ username: str
332
+ status: str
333
+ used_traffic: int
334
+ lifetime_used_traffic: int = 0
335
+ created_at: str
336
+ links: List[str] = []
337
+ subscription_url: str = ""
338
+ excluded_inbounds: Dict[str, List[str]] = {}
339
+ admin: Optional[Admin] = None
340
+
341
+
342
+ class SystemStats(BaseModel):
343
+ version: Optional[str] = None
344
+ cpu_cores: Optional[int] = None
345
+ cpu_usage: Optional[float] = None
346
+ total_user: Optional[int] = None
347
+ users_active: Optional[int] = None
348
+ incoming_bandwidth: Optional[int] = None
349
+ outgoing_bandwidth: Optional[int] = None
350
+ incoming_bandwidth_speed: Optional[int] = None
351
+ outgoing_bandwidth_speed: Optional[int] = None
352
+ online_users: Optional[int] = None
353
+ users_on_hold: Optional[int] = None
354
+ users_disabled: Optional[int] = None
355
+ users_expired: Optional[int] = None
356
+ users_limited: Optional[int] = None
357
+
358
+
359
+ class Settings(BaseModel):
360
+ clients: Optional[List[Dict[str, Any]]] = []
361
+ decryption: Optional[str] = None
362
+ network: Optional[str] = None
363
+
364
+
365
+ class StreamSettings(BaseModel):
366
+ network: Optional[str] = None
367
+ security: Optional[str] = None
368
+ tcpSettings: Optional[Dict[str, Any]] = {}
369
+ wsSettings: Optional[Dict[str, Any]] = {}
370
+ grpcSettings: Optional[Dict[str, Any]] = {}
371
+ tlsSettings: Optional[Dict[str, Any]] = {}
372
+ realitySettings: Optional[Dict[str, Any]] = {}
373
+
374
+
375
+ class Inbound(BaseModel):
376
+ port: Optional[int] = None
377
+ protocol: Optional[str] = None
378
+ settings: Optional[Settings] = Settings()
379
+ streamSettings: Optional[StreamSettings] = StreamSettings()
380
+ sniffing: Optional[Dict[str, Any]] = {}
381
+ tag: Optional[str] = None
382
+
383
+
384
+ class Outbound(BaseModel):
385
+ protocol: Optional[str] = None
386
+ settings: Optional[Dict[str, Any]] = {}
387
+ tag: Optional[str] = None
388
+
389
+
390
+ class RoutingRule(BaseModel):
391
+ type: Optional[str] = None
392
+ ip: Optional[List[str]] = []
393
+ domain: Optional[List[str]] = []
394
+ protocol: Optional[List[str]] = []
395
+ outboundTag: Optional[str] = None
396
+
397
+
398
+ class Routing(BaseModel):
399
+ domainStrategy: Optional[str] = None
400
+ rules: Optional[List[RoutingRule]] = []
401
+
402
+
403
+ class CoreConfig(BaseModel):
404
+ log: Optional[Dict[str, Any]] = {}
405
+ inbounds: Optional[List[Inbound]] = []
406
+ outbounds: Optional[List[Outbound]] = []
407
+ routing: Optional[Routing] = Routing()
408
+
409
+
410
+ class GroupBase(BaseModel):
411
+ name: str
412
+ inbound_tags: Optional[List[str]] = []
413
+ is_disabled: Optional[bool] = False
414
+
415
+
416
+ class GroupCreate(GroupBase):
417
+ pass
418
+
419
+
420
+ class GroupModify(GroupBase):
421
+ pass
422
+
423
+
424
+ class GroupResponse(GroupBase):
425
+ id: int
426
+ total_users: Optional[int] = 0
427
+
428
+
429
+ class GroupsResponse(BaseModel):
430
+ groups: List[GroupResponse]
431
+ total: int
432
+
433
+
434
+ class BulkGroup(BaseModel):
435
+ group_ids: List[int]
436
+ admins: Optional[List[int]] = None
437
+ users: Optional[List[int]] = None
438
+
439
+
440
+ class HostBase(BaseModel):
441
+ remark: str
442
+ address: str
443
+ port: Optional[int] = None
444
+ sni: Optional[str] = None
445
+ inbound_tag: Optional[str] = None
446
+ priority: Optional[int] = None
447
+
448
+
449
+ class HostResponse(HostBase):
450
+ id: Optional[int] = None
451
+
452
+
453
+ class CoreCreate(BaseModel):
454
+ config: Dict[str, Any]
455
+ name: Optional[str] = None
456
+ exclude_inbound_tags: Optional[str] = None
457
+ fallbacks_inbound_tags: Optional[str] = None
458
+
459
+
460
+ class CoreResponse(CoreCreate):
461
+ id: int
462
+ created_at: Optional[str] = None
463
+
464
+
465
+ class CoreResponseList(BaseModel):
466
+ count: int
467
+ cores: List[CoreResponse]
468
+
469
+
470
+ class ModifyUserByTemplate(BaseModel):
471
+ user_template_id: int
472
+ note: Optional[str] = None
473
+
474
+
475
+ class CreateUserFromTemplate(ModifyUserByTemplate):
476
+ username: str
477
+
478
+
479
+ class BulkUser(BaseModel):
480
+ amount: int
481
+ group_ids: Optional[List[int]] = None
482
+ admins: Optional[List[int]] = None
483
+ users: Optional[List[int]] = None
484
+ status: Optional[List[str]] = None
485
+
486
+
487
+ class BulkUsersProxy(BaseModel):
488
+ flow: Optional[str] = None
489
+ method: Optional[str] = None
490
+ group_ids: Optional[List[int]] = None
491
+ admins: Optional[List[int]] = None
492
+ users: Optional[List[int]] = None
rebecca/utils.py ADDED
@@ -0,0 +1,30 @@
1
+ import logging
2
+ from datetime import datetime, timedelta
3
+ from typing import Optional
4
+
5
+ from rebecca import RebeccaAPI
6
+
7
+
8
+ class RebeccaTokenCache:
9
+ def __init__(self, client: RebeccaAPI, username: str, password: str, token_expire_minutes: int = 1440):
10
+ self._client = client
11
+ self._username = username
12
+ self._password = password
13
+ self._token_expire_minutes = token_expire_minutes
14
+ self._token: Optional[str] = None
15
+ self._exp_at: Optional[datetime] = None
16
+
17
+ async def get_token(self):
18
+ if not self._exp_at or self._exp_at < datetime.now():
19
+ logging.info("Get new token")
20
+ self._token = await self.get_new_token()
21
+ self._exp_at = datetime.now() + timedelta(minutes=self._token_expire_minutes - 1)
22
+ return self._token
23
+
24
+ async def get_new_token(self):
25
+ try:
26
+ token = await self._client.get_token(username=self._username, password=self._password)
27
+ return token.access_token
28
+ except Exception as e:
29
+ logging.error(f"{e}", exc_info=True)
30
+ raise e
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: rebecca-api
3
+ Version: 0.1.3
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: datetime>=5.5
7
+ Requires-Dist: httpx>=0.28.1
8
+ Requires-Dist: paramiko>=4.0.0
9
+ Requires-Dist: pydantic>=2.12.0
10
+ Requires-Dist: sshtunnel>=0.4.0
@@ -0,0 +1,8 @@
1
+ rebecca/__init__.py,sha256=av5dwod0p6uiRpdZNu_UGkowAKDv6q4w9lhRf42w03E,1930
2
+ rebecca/api.py,sha256=fUMa9EdBlMArkJ6V_G_pIzHrNsyPXSa4Kg5pZRIKnC0,26824
3
+ rebecca/enums.py,sha256=h2d1Hq-X4GkKEVjxokCY-a5nVJim2T2RSVn8Jv5uTNI,126
4
+ rebecca/models.py,sha256=D5zUNC0hoOUa_j9JSt1rnGTVbypcdCAWP2sDPCQ6xv0,13672
5
+ rebecca/utils.py,sha256=5JQBa8oLVhBQxHDaccwikRymkprmAsf4rAJ1Uwi_xe0,1092
6
+ rebecca_api-0.1.3.dist-info/METADATA,sha256=WphiAB9IUFj4X7AJjJ98p9kHkZd2dD1SJHDK5ul7fSo,267
7
+ rebecca_api-0.1.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ rebecca_api-0.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any