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 CHANGED
@@ -63,4 +63,4 @@ __all__ = (
63
63
  "SystemStats"
64
64
  )
65
65
 
66
- __version__ = "0.2.9"
66
+ __version__ = "0.3.1"
marzban/api.py CHANGED
@@ -1,17 +1,120 @@
1
1
  import httpx
2
- from typing import Any, Dict, Optional, List
3
- from pydantic import BaseModel
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, base_url: str, *, timeout: float = 10.0, verify: bool = False):
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.client = httpx.AsyncClient(base_url=base_url, verify=verify, timeout=timeout)
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, params: Optional[Dict[str, Any]] = None) -> httpx.Response:
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, username: Optional[str] = None) -> List[Admin]:
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, username: Optional[List[str]] = None, status: Optional[str] = None, sort: Optional[str] = None) -> UsersResponse:
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, end: Optional[str] = None) -> UserUsagesResponse:
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, expired_after: Optional[str] = None) -> List[str]:
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, expired_after: Optional[str] = None) -> List[str]:
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, token: str, offset: Optional[int] = None, limit: Optional[int] = None) -> List[UserTemplateResponse]:
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, token: str) -> UserTemplateResponse:
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, end: Optional[str] = None) -> Any:
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
- await self.client.aclose()
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.2.9
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
+ [![Stars](https://img.shields.io/github/stars/sm1ky/marzban_api.svg?style=social)](https://github.com/sm1ky/marzban_api/stargazers)
28
+ [![Forks](https://img.shields.io/github/forks/sm1ky/marzban_api.svg?style=social)](https://github.com/sm1ky/marzban_api/network/members)
29
+ [![Issues](https://img.shields.io/github/issues/sm1ky/marzban_api.svg)](https://github.com/sm1ky/marzban_api/issues)
30
+ [![Supported python versions](https://img.shields.io/pypi/pyversions/marzban.svg)](https://pypi.python.org/pypi/marzban)
31
+ [![Downloads](https://img.shields.io/pypi/dm/marzban.svg)](https://pypi.python.org/pypi/marzban)
32
+ [![PyPi Package Version](https://img.shields.io/pypi/v/marzban)](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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,