marzban 0.2.9__py3-none-any.whl → 0.3.1__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.
- marzban/__init__.py +1 -1
- marzban/api.py +140 -19
- marzban/models.py +31 -4
- {marzban-0.2.9.dist-info → marzban-0.3.1.dist-info}/METADATA +16 -2
- marzban-0.3.1.dist-info/RECORD +8 -0
- {marzban-0.2.9.dist-info → marzban-0.3.1.dist-info}/WHEEL +1 -1
- marzban-0.2.9.dist-info/RECORD +0 -8
- {marzban-0.2.9.dist-info → marzban-0.3.1.dist-info}/LICENSE +0 -0
- {marzban-0.2.9.dist-info → marzban-0.3.1.dist-info}/top_level.txt +0 -0
marzban/__init__.py
CHANGED
marzban/api.py
CHANGED
@@ -1,17 +1,120 @@
|
|
1
1
|
import httpx
|
2
|
-
|
3
|
-
from
|
2
|
+
import paramiko
|
3
|
+
from paramiko.ssh_exception import SSHException
|
4
|
+
from sshtunnel import SSHTunnelForwarder
|
5
|
+
|
4
6
|
from .models import *
|
5
7
|
|
8
|
+
|
6
9
|
class MarzbanAPI:
|
7
|
-
def __init__(self,
|
10
|
+
def __init__(self,
|
11
|
+
base_url: str, *,
|
12
|
+
timeout: float = 10.0, verify: bool = False,
|
13
|
+
ssh_username: Optional[str] = None,
|
14
|
+
ssh_host: Optional[str] = None,
|
15
|
+
ssh_port: Optional[int] = 22,
|
16
|
+
ssh_private_key_path: Optional[str] = None,
|
17
|
+
ssh_key_passphrase: Optional[str] = None,
|
18
|
+
ssh_password: Optional[str] = None,
|
19
|
+
local_bind_host: str = '127.0.0.1',
|
20
|
+
local_bind_port: int = 8000,
|
21
|
+
remote_bind_host: str = '127.0.0.1',
|
22
|
+
remote_bind_port: int = 8000
|
23
|
+
):
|
24
|
+
"""
|
25
|
+
Initializes the MarzbanAPI client with optional SSH tunneling for secure remote access.
|
26
|
+
|
27
|
+
:param base_url: The base URL of the Marzban API.
|
28
|
+
:param timeout: The request timeout in seconds (default: 10.0).
|
29
|
+
:param verify: SSL verification flag; set to False to ignore SSL verification (default: False).
|
30
|
+
:param ssh_username: SSH username for tunnel authentication.
|
31
|
+
:param ssh_host: SSH server address for setting up the tunnel. If None, no SSH tunnel is used.
|
32
|
+
:param ssh_port: SSH port for connecting to the server (default: 22).
|
33
|
+
:param ssh_private_key_path: Path to the SSH private key file for authentication.
|
34
|
+
:param ssh_key_passphrase: Passphrase for the SSH private key, if applicable.
|
35
|
+
:param ssh_password: Password for SSH authentication. Use if no private key is provided.
|
36
|
+
:param local_bind_host: Local IP address for binding the SSH tunnel (default: '127.0.0.1').
|
37
|
+
:param local_bind_port: Local port for SSH tunnel binding (default: 8000).
|
38
|
+
:param remote_bind_host: Remote IP address for binding on the SSH server side (default: '127.0.0.1').
|
39
|
+
:param remote_bind_port: Remote port for the SSH server binding (default: 8000).
|
40
|
+
|
41
|
+
:raises ValueError: If SSH tunneling is requested but neither a private key nor password is provided.
|
42
|
+
"""
|
43
|
+
|
8
44
|
self.base_url = base_url
|
9
|
-
self.
|
45
|
+
self.timeout = timeout
|
46
|
+
self.verify = verify
|
47
|
+
self.ssh_username = ssh_username
|
48
|
+
self.ssh_host = ssh_host
|
49
|
+
self.ssh_port = ssh_port
|
50
|
+
self.ssh_private_key_path = ssh_private_key_path
|
51
|
+
self.ssh_key_passphrase = ssh_key_passphrase
|
52
|
+
self.ssh_password = ssh_password
|
53
|
+
self.local_bind_host = local_bind_host
|
54
|
+
self.local_bind_port = local_bind_port
|
55
|
+
self.remote_bind_host = remote_bind_host
|
56
|
+
self.remote_bind_port = remote_bind_port
|
57
|
+
self.client = None
|
58
|
+
self._tunnel = None
|
59
|
+
self._forwarder = None
|
60
|
+
if ssh_host and not ssh_private_key_path and not ssh_password:
|
61
|
+
raise ValueError('For an SSH tunnel, you must specify either ssh_private_key_path or ssh_password')
|
62
|
+
if not ssh_host:
|
63
|
+
self.client = httpx.AsyncClient(base_url=self.base_url, verify=self.verify, timeout=self.timeout)
|
64
|
+
|
65
|
+
def _load_private_key(self, key_path, passphrase):
|
66
|
+
key_classes = [paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey, paramiko.Ed25519Key]
|
67
|
+
for key_class in key_classes:
|
68
|
+
try:
|
69
|
+
if passphrase:
|
70
|
+
pkey = key_class.from_private_key_file(
|
71
|
+
key_path,
|
72
|
+
password=passphrase
|
73
|
+
)
|
74
|
+
else:
|
75
|
+
pkey = key_class.from_private_key_file(key_path)
|
76
|
+
return pkey
|
77
|
+
except paramiko.ssh_exception.PasswordRequiredException:
|
78
|
+
print("Ошибка: Приватный ключ защищен паролем. Пожалуйста, укажите пароль.")
|
79
|
+
except SSHException:
|
80
|
+
continue
|
81
|
+
raise ValueError("Unsupported key format or incorrect passphrase.")
|
82
|
+
|
83
|
+
def _initialize(self):
|
84
|
+
"""Initialization of the SSH tunnel and the HTTP client."""
|
85
|
+
if self.ssh_host:
|
86
|
+
if self._tunnel and self._tunnel.is_active:
|
87
|
+
return
|
88
|
+
# Uploading the key using paramiko
|
89
|
+
private_key = None
|
90
|
+
if self.ssh_private_key_path:
|
91
|
+
private_key = self._load_private_key(self.ssh_private_key_path, self.ssh_key_passphrase)
|
92
|
+
# Installing an SSH tunnel using sshtunnel
|
93
|
+
self._tunnel = SSHTunnelForwarder(
|
94
|
+
(self.ssh_host, self.ssh_port),
|
95
|
+
ssh_username=self.ssh_username,
|
96
|
+
ssh_password=self.ssh_password,
|
97
|
+
ssh_pkey=private_key,
|
98
|
+
remote_bind_address=(self.remote_bind_host, self.remote_bind_port),
|
99
|
+
local_bind_address=(self.local_bind_host, self.local_bind_port)
|
100
|
+
)
|
101
|
+
self._tunnel.start()
|
102
|
+
self.client = httpx.AsyncClient(
|
103
|
+
base_url=f"http://{self.local_bind_host}:{self.local_bind_port}",
|
104
|
+
timeout=self.timeout, verify=self.verify
|
105
|
+
)
|
106
|
+
|
107
|
+
# HTTP-client with local URL
|
10
108
|
|
11
109
|
def _get_headers(self, token: str) -> Dict[str, str]:
|
12
110
|
return {"Authorization": f"Bearer {token}"}
|
13
111
|
|
14
|
-
async def _request(self, method: str, url: str, token: Optional[str] = None, data: Optional[BaseModel] = None,
|
112
|
+
async def _request(self, method: str, url: str, token: Optional[str] = None, data: Optional[BaseModel] = None,
|
113
|
+
params: Optional[Dict[str, Any]] = None) -> httpx.Response:
|
114
|
+
if self.ssh_host and (not self.client or not self._tunnel.is_active):
|
115
|
+
# Initialize the HTTP client and SSH tunnel if they are closed
|
116
|
+
self._initialize()
|
117
|
+
return await self._request(method, url, token, data, params)
|
15
118
|
headers = self._get_headers(token) if token else {}
|
16
119
|
json_data = data.model_dump(exclude_none=True) if data else None
|
17
120
|
params = {k: v for k, v in (params or {}).items() if v is not None}
|
@@ -29,6 +132,10 @@ class MarzbanAPI:
|
|
29
132
|
"client_id": "",
|
30
133
|
"client_secret": ""
|
31
134
|
}
|
135
|
+
if self.ssh_host and (not self.client or not self._tunnel.is_active):
|
136
|
+
# Initialize the HTTP client and SSH tunnel if they are closed
|
137
|
+
self._initialize()
|
138
|
+
return await self.get_token(username, password)
|
32
139
|
response = await self.client.post(url, data=payload)
|
33
140
|
response.raise_for_status()
|
34
141
|
return Token(**response.json())
|
@@ -52,7 +159,8 @@ class MarzbanAPI:
|
|
52
159
|
url = f"/api/admin/{username}"
|
53
160
|
await self._request("DELETE", url, token)
|
54
161
|
|
55
|
-
async def get_admins(self, token: str, offset: Optional[int] = None, limit: Optional[int] = None,
|
162
|
+
async def get_admins(self, token: str, offset: Optional[int] = None, limit: Optional[int] = None,
|
163
|
+
username: Optional[str] = None) -> List[Admin]:
|
56
164
|
url = "/api/admins"
|
57
165
|
params = {"offset": offset, "limit": limit, "username": username}
|
58
166
|
response = await self._request("GET", url, token, params=params)
|
@@ -72,7 +180,7 @@ class MarzbanAPI:
|
|
72
180
|
url = "/api/hosts"
|
73
181
|
response = await self._request("GET", url, token)
|
74
182
|
return response.json()
|
75
|
-
|
183
|
+
|
76
184
|
async def modify_hosts(self, hosts: Dict[str, List[ProxyHost]], token: str) -> Dict[str, List[ProxyHost]]:
|
77
185
|
url = "/api/hosts"
|
78
186
|
response = await self._request("PUT", url, token, data=hosts)
|
@@ -126,7 +234,9 @@ class MarzbanAPI:
|
|
126
234
|
response = await self._request("POST", url, token)
|
127
235
|
return UserResponse(**response.json())
|
128
236
|
|
129
|
-
async def get_users(self, token: str, offset: Optional[int] = None, limit: Optional[int] = None,
|
237
|
+
async def get_users(self, token: str, offset: Optional[int] = None, limit: Optional[int] = None,
|
238
|
+
username: Optional[List[str]] = None, status: Optional[str] = None,
|
239
|
+
sort: Optional[str] = None) -> UsersResponse:
|
130
240
|
url = "/api/users"
|
131
241
|
params = {"offset": offset, "limit": limit, "username": username, "status": status, "sort": sort}
|
132
242
|
response = await self._request("GET", url, token, params=params)
|
@@ -136,7 +246,8 @@ class MarzbanAPI:
|
|
136
246
|
url = "/api/users/reset"
|
137
247
|
await self._request("POST", url, token)
|
138
248
|
|
139
|
-
async def get_user_usage(self, username: str, token: str, start: Optional[str] = None,
|
249
|
+
async def get_user_usage(self, username: str, token: str, start: Optional[str] = None,
|
250
|
+
end: Optional[str] = None) -> UserUsagesResponse:
|
140
251
|
url = f"/api/user/{username}/usage"
|
141
252
|
params = {"start": start, "end": end}
|
142
253
|
response: httpx.Response = await self._request("GET", url, token, params=params)
|
@@ -147,19 +258,24 @@ class MarzbanAPI:
|
|
147
258
|
response = await self._request("PUT", url, token)
|
148
259
|
return UserResponse(**response.json())
|
149
260
|
|
150
|
-
async def get_expired_users(self, token: str, expired_before: Optional[str] = None,
|
261
|
+
async def get_expired_users(self, token: str, expired_before: Optional[str] = None,
|
262
|
+
expired_after: Optional[str] = None) -> List[str]:
|
151
263
|
url = "/api/users/expired"
|
152
264
|
params = {"expired_before": expired_before, "expired_after": expired_after}
|
153
265
|
response = await self._request("GET", url, token, params=params)
|
154
266
|
return response.json()
|
155
267
|
|
156
|
-
async def delete_expired_users(self, token: str, expired_before: Optional[str] = None,
|
268
|
+
async def delete_expired_users(self, token: str, expired_before: Optional[str] = None,
|
269
|
+
expired_after: Optional[str] = None) -> List[str]:
|
157
270
|
url = "/api/users/expired"
|
158
271
|
params = {"expired_before": expired_before, "expired_after": expired_after}
|
159
272
|
response = await self._request("DELETE", url, token, params=params)
|
160
273
|
return response.json()
|
161
274
|
|
162
|
-
async def get_user_templates(self,
|
275
|
+
async def get_user_templates(self,
|
276
|
+
token: str,
|
277
|
+
offset: Optional[int] = None,
|
278
|
+
limit: Optional[int] = None) -> List[UserTemplateResponse]:
|
163
279
|
url = "/api/user_template"
|
164
280
|
params = {"offset": offset, "limit": limit}
|
165
281
|
response = await self._request("GET", url, token, params=params)
|
@@ -175,11 +291,12 @@ class MarzbanAPI:
|
|
175
291
|
response = await self._request("GET", url, token)
|
176
292
|
return UserTemplateResponse(**response.json())
|
177
293
|
|
178
|
-
async def modify_user_template(self, template_id: int, template: UserTemplateModify,
|
294
|
+
async def modify_user_template(self, template_id: int, template: UserTemplateModify,
|
295
|
+
token: str) -> UserTemplateResponse:
|
179
296
|
url = f"/api/user_template/{template_id}"
|
180
297
|
response = await self._request("PUT", url, token, data=template)
|
181
298
|
return UserTemplateResponse(**response.json())
|
182
|
-
|
299
|
+
|
183
300
|
async def remove_user_template(self, template_id: int, token: str) -> None:
|
184
301
|
url = f"/api/user_template/{template_id}"
|
185
302
|
await self._request("DELETE", url, token)
|
@@ -232,11 +349,12 @@ class MarzbanAPI:
|
|
232
349
|
final_url = f"/sub/{token}/info"
|
233
350
|
else:
|
234
351
|
raise ValueError("Either url or token must be provided")
|
235
|
-
|
352
|
+
|
236
353
|
response = await self._request("GET", final_url)
|
237
354
|
return SubscriptionUserResponse(**response.json())
|
238
355
|
|
239
|
-
async def get_user_usage(self, url: str = None, token: str = None, start: Optional[str] = None,
|
356
|
+
async def get_user_usage(self, url: str = None, token: str = None, start: Optional[str] = None,
|
357
|
+
end: Optional[str] = None) -> Any:
|
240
358
|
if url:
|
241
359
|
# Use the provided URL if it is given
|
242
360
|
final_url = url + "/usage"
|
@@ -245,7 +363,6 @@ class MarzbanAPI:
|
|
245
363
|
final_url = f"/sub/{token}/usage"
|
246
364
|
else:
|
247
365
|
raise ValueError("Either url or token must be provided")
|
248
|
-
|
249
366
|
params = {"start": start, "end": end}
|
250
367
|
response = await self._request("GET", final_url, params=params)
|
251
368
|
return response.json()
|
@@ -259,9 +376,13 @@ class MarzbanAPI:
|
|
259
376
|
final_url = f"/sub/{token}/{client_type}"
|
260
377
|
else:
|
261
378
|
raise ValueError("Either url or token must be provided")
|
262
|
-
|
379
|
+
|
263
380
|
response = await self._request("GET", final_url)
|
264
381
|
return response.json()
|
265
382
|
|
266
383
|
async def close(self):
|
267
|
-
|
384
|
+
"""Closing the HTTP client and SSH tunnel."""
|
385
|
+
if self.client:
|
386
|
+
await self.client.aclose()
|
387
|
+
if self._tunnel:
|
388
|
+
self._tunnel.stop()
|
marzban/models.py
CHANGED
@@ -1,32 +1,39 @@
|
|
1
1
|
from pydantic import BaseModel, field_validator, ValidationInfo, AfterValidator, ValidationError
|
2
2
|
from typing import Optional, List, Dict, Any, ClassVar, Annotated
|
3
3
|
|
4
|
+
|
4
5
|
class Token(BaseModel):
|
5
6
|
access_token: str
|
6
7
|
token_type: str = "bearer"
|
7
8
|
|
9
|
+
|
8
10
|
class Admin(BaseModel):
|
9
11
|
username: str
|
10
12
|
is_sudo: bool
|
11
13
|
telegram_id: Optional[int] = None
|
12
14
|
discord_webhook: Optional[str] = None
|
13
15
|
|
16
|
+
|
14
17
|
class AdminCreate(Admin):
|
15
18
|
password: str
|
16
19
|
|
20
|
+
|
17
21
|
class AdminModify(BaseModel):
|
18
22
|
is_sudo: bool
|
19
23
|
password: Optional[str] = None
|
20
24
|
telegram_id: Optional[int] = None
|
21
25
|
discord_webhook: Optional[str] = None
|
22
26
|
|
27
|
+
|
23
28
|
class HTTPValidationError(BaseModel):
|
24
29
|
detail: Optional[List[Dict[str, Any]]] = None
|
25
30
|
|
31
|
+
|
26
32
|
class ProxySettings(BaseModel):
|
27
33
|
id: Optional[str] = None
|
28
34
|
flow: Optional[str] = None
|
29
|
-
|
35
|
+
|
36
|
+
|
30
37
|
class UserCreate(BaseModel):
|
31
38
|
username: str
|
32
39
|
proxies: Optional[Dict[str, ProxySettings]] = {}
|
@@ -42,6 +49,7 @@ class UserCreate(BaseModel):
|
|
42
49
|
on_hold_timeout: Optional[str] = None
|
43
50
|
status: Optional[str] = "active"
|
44
51
|
|
52
|
+
|
45
53
|
class UserResponse(BaseModel):
|
46
54
|
username: Optional[str] = None
|
47
55
|
proxies: Optional[Dict[str, ProxySettings]] = {}
|
@@ -64,12 +72,13 @@ class UserResponse(BaseModel):
|
|
64
72
|
subscription_url: Optional[str] = None
|
65
73
|
subscription_token: Optional[str] = None
|
66
74
|
excluded_inbounds: Optional[Dict[str, List[str]]] = None
|
67
|
-
|
75
|
+
|
68
76
|
def __init__(self, **data):
|
69
77
|
super().__init__(**data)
|
70
78
|
if not self.subscription_token and self.subscription_url:
|
71
79
|
self.subscription_token = self.subscription_url.split('/')[-1]
|
72
80
|
|
81
|
+
|
73
82
|
class NodeCreate(BaseModel):
|
74
83
|
name: str
|
75
84
|
address: str
|
@@ -78,6 +87,7 @@ class NodeCreate(BaseModel):
|
|
78
87
|
usage_coefficient: float = 1.0
|
79
88
|
add_as_new_host: bool = True
|
80
89
|
|
90
|
+
|
81
91
|
class NodeModify(BaseModel):
|
82
92
|
name: Optional[str] = None
|
83
93
|
address: Optional[str] = None
|
@@ -86,6 +96,7 @@ class NodeModify(BaseModel):
|
|
86
96
|
usage_coefficient: Optional[float] = None
|
87
97
|
status: Optional[str] = None
|
88
98
|
|
99
|
+
|
89
100
|
class NodeResponse(BaseModel):
|
90
101
|
name: str
|
91
102
|
address: str
|
@@ -97,15 +108,18 @@ class NodeResponse(BaseModel):
|
|
97
108
|
status: str
|
98
109
|
message: Optional[str] = None
|
99
110
|
|
111
|
+
|
100
112
|
class NodeUsageResponse(BaseModel):
|
101
113
|
node_id: Optional[int] = None
|
102
114
|
node_name: Optional[str] = None
|
103
115
|
uplink: Optional[int] = None
|
104
116
|
downlink: Optional[int] = None
|
105
117
|
|
118
|
+
|
106
119
|
class NodesUsageResponse(BaseModel):
|
107
120
|
usages: List[NodeUsageResponse]
|
108
121
|
|
122
|
+
|
109
123
|
class ProxyHost(BaseModel):
|
110
124
|
remark: str
|
111
125
|
address: str
|
@@ -119,6 +133,7 @@ class ProxyHost(BaseModel):
|
|
119
133
|
allowinsecure: bool
|
120
134
|
is_disabled: bool
|
121
135
|
|
136
|
+
|
122
137
|
class ProxyInbound(BaseModel):
|
123
138
|
tag: str
|
124
139
|
protocol: str
|
@@ -126,11 +141,13 @@ class ProxyInbound(BaseModel):
|
|
126
141
|
tls: str
|
127
142
|
port: Any
|
128
143
|
|
144
|
+
|
129
145
|
class CoreStats(BaseModel):
|
130
146
|
version: str
|
131
147
|
started: bool
|
132
148
|
logs_websocket: str
|
133
149
|
|
150
|
+
|
134
151
|
class UserModify(BaseModel):
|
135
152
|
proxies: Optional[Dict[str, ProxySettings]] = {}
|
136
153
|
expire: Optional[int] = None
|
@@ -145,6 +162,7 @@ class UserModify(BaseModel):
|
|
145
162
|
on_hold_timeout: Optional[str] = None
|
146
163
|
status: Optional[str] = None
|
147
164
|
|
165
|
+
|
148
166
|
class UserTemplateCreate(BaseModel):
|
149
167
|
name: Optional[str] = None
|
150
168
|
data_limit: int = 0
|
@@ -153,6 +171,7 @@ class UserTemplateCreate(BaseModel):
|
|
153
171
|
username_suffix: Optional[str] = None
|
154
172
|
inbounds: Optional[Dict[str, List[str]]] = {}
|
155
173
|
|
174
|
+
|
156
175
|
class UserTemplateResponse(BaseModel):
|
157
176
|
id: int
|
158
177
|
name: Optional[str] = None
|
@@ -162,6 +181,7 @@ class UserTemplateResponse(BaseModel):
|
|
162
181
|
username_suffix: Optional[str] = None
|
163
182
|
inbounds: Dict[str, List[str]]
|
164
183
|
|
184
|
+
|
165
185
|
class UserTemplateModify(BaseModel):
|
166
186
|
name: Optional[str] = None
|
167
187
|
data_limit: Optional[int] = None
|
@@ -170,27 +190,33 @@ class UserTemplateModify(BaseModel):
|
|
170
190
|
username_suffix: Optional[str] = None
|
171
191
|
inbounds: Optional[Dict[str, List[str]]] = None
|
172
192
|
|
193
|
+
|
173
194
|
class UserUsageResponse(BaseModel):
|
174
195
|
node_id: Optional[int]
|
175
196
|
node_name: Optional[str]
|
176
197
|
used_traffic: Optional[int]
|
177
198
|
|
199
|
+
|
178
200
|
class UserUsagesResponse(BaseModel):
|
179
201
|
username: str
|
180
202
|
usages: List[UserUsageResponse]
|
181
203
|
|
204
|
+
|
182
205
|
class UsersResponse(BaseModel):
|
183
206
|
users: List[UserResponse]
|
184
207
|
total: int
|
185
208
|
|
209
|
+
|
186
210
|
class UserStatus(BaseModel):
|
187
211
|
enum: ClassVar[List[str]] = ["active", "disabled", "limited", "expired", "on_hold"]
|
188
212
|
|
213
|
+
|
189
214
|
class ValidationError(BaseModel):
|
190
215
|
loc: List[Any]
|
191
216
|
msg: str
|
192
217
|
type: str
|
193
218
|
|
219
|
+
|
194
220
|
class SubscriptionUserResponse(BaseModel):
|
195
221
|
proxies: Dict[str, Any]
|
196
222
|
expire: Optional[int] = None
|
@@ -213,7 +239,8 @@ class SubscriptionUserResponse(BaseModel):
|
|
213
239
|
subscription_url: str = ""
|
214
240
|
excluded_inbounds: Dict[str, List[str]] = {}
|
215
241
|
admin: Optional[Admin] = None
|
216
|
-
|
242
|
+
|
243
|
+
|
217
244
|
class SystemStats(BaseModel):
|
218
245
|
version: Optional[str] = None
|
219
246
|
mem_total: Optional[int] = None
|
@@ -225,4 +252,4 @@ class SystemStats(BaseModel):
|
|
225
252
|
incoming_bandwidth: Optional[int] = None
|
226
253
|
outgoing_bandwidth: Optional[int] = None
|
227
254
|
incoming_bandwidth_speed: Optional[int] = None
|
228
|
-
outgoing_bandwidth_speed: Optional[int] = None
|
255
|
+
outgoing_bandwidth_speed: Optional[int] = None
|
@@ -1,10 +1,15 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: marzban
|
3
|
-
Version: 0.
|
4
|
-
Summary: Асинхронная библиотека Python для взаимодействия с MarzbanAPI
|
3
|
+
Version: 0.3.1
|
4
|
+
Summary: Асинхронная библиотека Python для взаимодействия с MarzbanAPI | Поддерживает работу через HTTPS/SSH
|
5
5
|
Home-page: https://github.com/sm1ky/marzban_api
|
6
6
|
Author: Artem
|
7
7
|
Author-email: contant@sm1ky.com
|
8
|
+
Project-URL: Homepage, https://github.com/sm1ky/marzban_api
|
9
|
+
Project-URL: Documentation [EN], https://github.com/sm1ky/marzban_api/blob/development/.readme/README_en.md
|
10
|
+
Project-URL: Documentation [RU], https://github.com/sm1ky/marzban_api/blob/development/.readme/README_ru.md
|
11
|
+
Project-URL: Source, https://github.com/sm1ky/marzban_api
|
12
|
+
Project-URL: Developer, https://t.me/sm1ky
|
8
13
|
Classifier: Programming Language :: Python :: 3
|
9
14
|
Classifier: License :: OSI Approved :: MIT License
|
10
15
|
Classifier: Operating System :: OS Independent
|
@@ -13,10 +18,19 @@ Description-Content-Type: text/markdown
|
|
13
18
|
License-File: LICENSE
|
14
19
|
Requires-Dist: httpx >=0.23.0
|
15
20
|
Requires-Dist: pydantic >=1.10.0
|
21
|
+
Requires-Dist: paramiko >=3.5.0
|
22
|
+
Requires-Dist: sshtunnel >=0.4.0
|
16
23
|
|
17
24
|
|
18
25
|
# MarzbanAPI Client
|
19
26
|
|
27
|
+
[](https://github.com/sm1ky/marzban_api/stargazers)
|
28
|
+
[](https://github.com/sm1ky/marzban_api/network/members)
|
29
|
+
[](https://github.com/sm1ky/marzban_api/issues)
|
30
|
+
[](https://pypi.python.org/pypi/marzban)
|
31
|
+
[](https://pypi.python.org/pypi/marzban)
|
32
|
+
[](https://pypi.python.org/pypi/marzban)
|
33
|
+
|
20
34
|
**MarzbanAPI Client** is an asynchronous Python library designed for interacting with [Marzban](https://github.com/Gozargah/Marzban). It provides comprehensive methods for managing administrators, users, nodes, and system statistics.
|
21
35
|
|
22
36
|
## Installation
|
@@ -0,0 +1,8 @@
|
|
1
|
+
marzban/__init__.py,sha256=zItcSPeCEd4rUrwdPSMAgsuyQAuUFekCf-IF3g7SMqI,1302
|
2
|
+
marzban/api.py,sha256=nRVRyvWbFlKGh9w5HvfPvRqrTSKfiaEAHvC7qskZPTs,18267
|
3
|
+
marzban/models.py,sha256=p9QWCmNcDJQZO1EZm4ihgwS7ZJpayaMjo4fFGOpW3cE,6947
|
4
|
+
marzban-0.3.1.dist-info/LICENSE,sha256=e7OchdHfXoz2OZRHj8iltLIKYwdri9J4_9PMEnov418,1061
|
5
|
+
marzban-0.3.1.dist-info/METADATA,sha256=C6qjMy5G6-LvjkjfuCrjIm_6d2A0obk6cq2Y-oPHcUs,3163
|
6
|
+
marzban-0.3.1.dist-info/WHEEL,sha256=OVMc5UfuAQiSplgO0_WdW7vXVGAt9Hdd6qtN4HotdyA,91
|
7
|
+
marzban-0.3.1.dist-info/top_level.txt,sha256=KUmBWzTarBlzw2GZOuk-d-jM2GU4zPWo1iwvW_mXS-c,8
|
8
|
+
marzban-0.3.1.dist-info/RECORD,,
|
marzban-0.2.9.dist-info/RECORD
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
marzban/__init__.py,sha256=x797eiZAH7Sr-2_dKKMOqSHbYF42FO410LKliHzTpPs,1302
|
2
|
-
marzban/api.py,sha256=Do5W8v8yxMxb5-h6XHKSNM9B2zrCLDcfJIXi4fGmUYs,12474
|
3
|
-
marzban/models.py,sha256=cNL3c_bfZKNp0EKuBDfPpO7vAFjQwK2e0VWqHjnJ64c,6931
|
4
|
-
marzban-0.2.9.dist-info/LICENSE,sha256=e7OchdHfXoz2OZRHj8iltLIKYwdri9J4_9PMEnov418,1061
|
5
|
-
marzban-0.2.9.dist-info/METADATA,sha256=OInLS2XnkrAJVuiLGc5HaT1IVL-GzYRwDot017IgI8s,1952
|
6
|
-
marzban-0.2.9.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
7
|
-
marzban-0.2.9.dist-info/top_level.txt,sha256=KUmBWzTarBlzw2GZOuk-d-jM2GU4zPWo1iwvW_mXS-c,8
|
8
|
-
marzban-0.2.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|