maxbot-api-client-python 1.1.2__py3-none-any.whl → 1.2.0__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.
- maxbot_api_client_python/__init__.py +2 -1
- maxbot_api_client_python/api.py +2 -1
- maxbot_api_client_python/client.py +98 -82
- maxbot_api_client_python/exceptions.py +1 -1
- maxbot_api_client_python/tools/bots.py +21 -19
- maxbot_api_client_python/tools/chats.py +175 -167
- maxbot_api_client_python/tools/helpers.py +92 -70
- maxbot_api_client_python/tools/messages.py +85 -79
- maxbot_api_client_python/tools/subscriptions.py +44 -42
- maxbot_api_client_python/tools/uploads.py +51 -20
- maxbot_api_client_python/types/constants.py +1 -1
- maxbot_api_client_python/types/models.py +82 -65
- maxbot_api_client_python/utils.py +3 -1
- maxbot_api_client_python-1.2.0.dist-info/METADATA +251 -0
- maxbot_api_client_python-1.2.0.dist-info/RECORD +20 -0
- maxbot_api_client_python-1.1.2.dist-info/METADATA +0 -266
- maxbot_api_client_python-1.1.2.dist-info/RECORD +0 -20
- {maxbot_api_client_python-1.1.2.dist-info → maxbot_api_client_python-1.2.0.dist-info}/WHEEL +0 -0
- {maxbot_api_client_python-1.1.2.dist-info → maxbot_api_client_python-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {maxbot_api_client_python-1.1.2.dist-info → maxbot_api_client_python-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from maxbot_api_client_python.api import API
|
|
2
|
-
from maxbot_api_client_python.client import
|
|
2
|
+
from maxbot_api_client_python.client import Client
|
|
3
|
+
from maxbot_api_client_python.types.models import Config
|
|
3
4
|
from maxbot_api_client_python.types import models
|
|
4
5
|
from maxbot_api_client_python.types import constants
|
|
5
6
|
from maxbot_api_client_python import utils
|
maxbot_api_client_python/api.py
CHANGED
|
@@ -1,58 +1,14 @@
|
|
|
1
|
-
import asyncio, httpx,
|
|
2
|
-
from dataclasses import dataclass
|
|
1
|
+
import asyncio, httpx, logging, time, threading
|
|
3
2
|
from functools import wraps
|
|
4
|
-
from typing import Any
|
|
3
|
+
from typing import Any, TypeVar, Type
|
|
5
4
|
from urllib.parse import urljoin
|
|
6
5
|
|
|
7
6
|
from maxbot_api_client_python.exceptions import build_api_error
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class RateLimiter:
|
|
11
|
-
def __init__(self, rps: int):
|
|
12
|
-
self._lock = threading.Lock()
|
|
13
|
-
self._alock = asyncio.Lock()
|
|
14
|
-
self.last_request_time = 0.0
|
|
15
|
-
self.set_limit(rps)
|
|
16
|
-
|
|
17
|
-
def set_limit(self, rps: int) -> None:
|
|
18
|
-
self.limit = rps
|
|
19
|
-
self.interval = 1.0 / rps if rps > 0 else 0.0
|
|
20
|
-
|
|
21
|
-
def wait(self) -> None:
|
|
22
|
-
if self.interval <= 0:
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
with self._lock:
|
|
26
|
-
now = time.monotonic()
|
|
27
|
-
elapsed = now - self.last_request_time
|
|
28
|
-
delay = max(0.0, self.interval - elapsed)
|
|
29
|
-
self.last_request_time = now + delay
|
|
30
|
-
|
|
31
|
-
if delay > 0:
|
|
32
|
-
time.sleep(delay)
|
|
7
|
+
from maxbot_api_client_python.types.models import Config
|
|
33
8
|
|
|
34
|
-
|
|
35
|
-
if self.interval <= 0:
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
async with self._alock:
|
|
39
|
-
now = time.monotonic()
|
|
40
|
-
elapsed = now - self.last_request_time
|
|
41
|
-
delay = max(0.0, self.interval - elapsed)
|
|
42
|
-
self.last_request_time = now + delay
|
|
43
|
-
|
|
44
|
-
if delay > 0:
|
|
45
|
-
await asyncio.sleep(delay)
|
|
46
|
-
|
|
47
|
-
@dataclass
|
|
48
|
-
class Config:
|
|
49
|
-
base_url: str
|
|
50
|
-
token: str
|
|
51
|
-
timeout: int = 35
|
|
52
|
-
ratelimiter: int = 25
|
|
53
|
-
max_retries: int = 3
|
|
54
|
-
retry_delay_sec: int = 3
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
55
10
|
|
|
11
|
+
T = TypeVar('T')
|
|
56
12
|
class Client:
|
|
57
13
|
def __init__(self, cfg: Config):
|
|
58
14
|
if not cfg.base_url or not cfg.token:
|
|
@@ -62,15 +18,47 @@ class Client:
|
|
|
62
18
|
self.token = cfg.token
|
|
63
19
|
|
|
64
20
|
self.timeout = cfg.timeout if cfg.timeout > 0 else 35
|
|
65
|
-
self.global_limiter = RateLimiter(cfg.ratelimiter if cfg.ratelimiter > 0 else 25)
|
|
66
21
|
self.max_retries = cfg.max_retries
|
|
67
22
|
self.retry_delay_sec = cfg.retry_delay_sec
|
|
68
23
|
|
|
24
|
+
rps = cfg.ratelimiter if cfg.ratelimiter > 0 else 25
|
|
25
|
+
self._limiter_interval = 1.0 / rps if rps > 0 else 0.0
|
|
26
|
+
self._sync_lock = threading.Lock()
|
|
27
|
+
self._async_lock: asyncio.Lock | None = None
|
|
28
|
+
self._last_request_time = 0.0
|
|
29
|
+
|
|
69
30
|
self.headers = {"Authorization": self.token}
|
|
70
31
|
|
|
71
32
|
self._sync_client = httpx.Client(timeout=self.timeout)
|
|
72
33
|
self._async_client: httpx.AsyncClient | None = None
|
|
73
34
|
|
|
35
|
+
def _get_delay(self) -> float:
|
|
36
|
+
if self._limiter_interval <= 0:
|
|
37
|
+
return 0.0
|
|
38
|
+
now = time.monotonic()
|
|
39
|
+
elapsed = now - self._last_request_time
|
|
40
|
+
delay = max(0.0, self._limiter_interval - elapsed)
|
|
41
|
+
self._last_request_time = now + delay
|
|
42
|
+
return delay
|
|
43
|
+
|
|
44
|
+
def _wait_limit(self) -> None:
|
|
45
|
+
if self._limiter_interval <= 0:
|
|
46
|
+
return
|
|
47
|
+
with self._sync_lock:
|
|
48
|
+
delay = self._get_delay()
|
|
49
|
+
if delay > 0:
|
|
50
|
+
time.sleep(delay)
|
|
51
|
+
|
|
52
|
+
async def _await_limit(self) -> None:
|
|
53
|
+
if self._limiter_interval <= 0:
|
|
54
|
+
return
|
|
55
|
+
if self._async_lock is None:
|
|
56
|
+
self._async_lock = asyncio.Lock()
|
|
57
|
+
async with self._async_lock:
|
|
58
|
+
delay = self._get_delay()
|
|
59
|
+
if delay > 0:
|
|
60
|
+
await asyncio.sleep(delay)
|
|
61
|
+
|
|
74
62
|
def __enter__(self) -> "Client":
|
|
75
63
|
return self
|
|
76
64
|
|
|
@@ -86,14 +74,27 @@ class Client:
|
|
|
86
74
|
async def get_async_client(self) -> httpx.AsyncClient:
|
|
87
75
|
if self._async_client is None:
|
|
88
76
|
self._async_client = httpx.AsyncClient(timeout=self.timeout)
|
|
77
|
+
if self._async_lock is None:
|
|
78
|
+
self._async_lock = asyncio.Lock()
|
|
89
79
|
return self._async_client
|
|
90
80
|
|
|
81
|
+
async def _await_limit(self) -> None:
|
|
82
|
+
if self._limiter_interval <= 0:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
await self.get_async_client()
|
|
86
|
+
async with self._async_lock:
|
|
87
|
+
delay = self._get_delay()
|
|
88
|
+
if delay > 0:
|
|
89
|
+
await asyncio.sleep(delay)
|
|
90
|
+
|
|
91
91
|
def close(self) -> None:
|
|
92
92
|
self._sync_client.close()
|
|
93
93
|
|
|
94
94
|
async def aclose(self) -> None:
|
|
95
95
|
if self._async_client:
|
|
96
96
|
await self._async_client.aclose()
|
|
97
|
+
self._async_client = None
|
|
97
98
|
|
|
98
99
|
def _build_url(self, path: str) -> str:
|
|
99
100
|
if path.startswith(("http://", "https://")):
|
|
@@ -105,6 +106,30 @@ class Client:
|
|
|
105
106
|
return payload.model_dump(exclude_none=True)
|
|
106
107
|
return payload
|
|
107
108
|
|
|
109
|
+
def split_request(self, req: Any) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
110
|
+
if not hasattr(req, "model_fields"):
|
|
111
|
+
return None, self._prepare_data(req)
|
|
112
|
+
|
|
113
|
+
data = req.model_dump(exclude_unset=True, by_alias=True)
|
|
114
|
+
query, payload = {}, {}
|
|
115
|
+
|
|
116
|
+
for field_name, field in req.model_fields.items():
|
|
117
|
+
key = field.alias or field_name
|
|
118
|
+
if key not in data:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
extra = field.json_schema_extra
|
|
122
|
+
extra_dict = extra if isinstance(extra, dict) else {}
|
|
123
|
+
|
|
124
|
+
if extra_dict.get("in_path"):
|
|
125
|
+
continue
|
|
126
|
+
elif extra_dict.get("in_query"):
|
|
127
|
+
query[key] = data[key]
|
|
128
|
+
else:
|
|
129
|
+
payload[key] = data[key]
|
|
130
|
+
|
|
131
|
+
return (query if query else None, payload if payload else None)
|
|
132
|
+
|
|
108
133
|
def request(
|
|
109
134
|
self,
|
|
110
135
|
method: str,
|
|
@@ -113,7 +138,7 @@ class Client:
|
|
|
113
138
|
payload: Any = None,
|
|
114
139
|
files: dict[str, Any] | None = None
|
|
115
140
|
) -> bytes:
|
|
116
|
-
self.
|
|
141
|
+
self._wait_limit()
|
|
117
142
|
|
|
118
143
|
headers = self.headers.copy()
|
|
119
144
|
if not files:
|
|
@@ -128,7 +153,6 @@ class Client:
|
|
|
128
153
|
files=files,
|
|
129
154
|
headers=headers
|
|
130
155
|
)
|
|
131
|
-
logger.debug(f"Response body: {response.text}")
|
|
132
156
|
return self._handle_response(response)
|
|
133
157
|
|
|
134
158
|
async def arequest(
|
|
@@ -139,7 +163,7 @@ class Client:
|
|
|
139
163
|
payload: Any = None,
|
|
140
164
|
files: dict[str, Any] | None = None
|
|
141
165
|
) -> bytes:
|
|
142
|
-
await self.
|
|
166
|
+
await self._await_limit()
|
|
143
167
|
|
|
144
168
|
api = await self.get_async_client()
|
|
145
169
|
headers = self.headers.copy()
|
|
@@ -155,7 +179,6 @@ class Client:
|
|
|
155
179
|
files=files,
|
|
156
180
|
headers=headers
|
|
157
181
|
)
|
|
158
|
-
logger.debug(f"Response body: {response.text}")
|
|
159
182
|
return self._handle_response(response)
|
|
160
183
|
|
|
161
184
|
def _handle_response(self, response: httpx.Response) -> bytes:
|
|
@@ -163,33 +186,26 @@ class Client:
|
|
|
163
186
|
raise build_api_error(response)
|
|
164
187
|
return response.content
|
|
165
188
|
|
|
166
|
-
def _parse_response
|
|
167
|
-
if not data or data.strip()
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
189
|
+
def _parse_response(self, data: bytes, model_class: Type[T]) -> T:
|
|
190
|
+
if not data or data.strip() in (b"", b"<retval>1</retval>"):
|
|
191
|
+
try:
|
|
192
|
+
return model_class.model_validate({})
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error(f"Cannot construct empty model {model_class.__name__} for empty response.")
|
|
195
|
+
failed_response = httpx.Response(status_code=500, content=data, request=httpx.Request("GET", self.base_url))
|
|
196
|
+
raise build_api_error(failed_response) from e
|
|
197
|
+
|
|
172
198
|
try:
|
|
173
|
-
|
|
174
|
-
except
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if hasattr(model_class, "model_validate"):
|
|
180
|
-
return model_class.model_validate(parsed_json)
|
|
181
|
-
return parsed_json
|
|
199
|
+
return model_class.model_validate_json(data)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f"Server returned invalid response. Size: {len(data)} bytes.")
|
|
202
|
+
failed_response = httpx.Response(status_code=500, content=data, request=httpx.Request("GET", self.base_url))
|
|
203
|
+
raise build_api_error(failed_response) from e
|
|
182
204
|
|
|
183
|
-
def decode
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
async def adecode
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def as_async(func: Any) -> Any:
|
|
192
|
-
@wraps(func)
|
|
193
|
-
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
194
|
-
return await asyncio.to_thread(func, *args, **kwargs)
|
|
195
|
-
return wrapper
|
|
205
|
+
def decode(self, method: str, path: str, model_class: type[T], **kwargs: Any) -> T:
|
|
206
|
+
response = self.request(method, path, **kwargs)
|
|
207
|
+
return self._parse_response(response, model_class)
|
|
208
|
+
|
|
209
|
+
async def adecode(self, method: str, path: str, model_class: type[T], **kwargs: Any) -> T:
|
|
210
|
+
response = await self.arequest(method, path, **kwargs)
|
|
211
|
+
return self._parse_response(response, model_class)
|
|
@@ -30,7 +30,7 @@ def get_exception_for_status(status_code: int) -> type[MaxBotError]:
|
|
|
30
30
|
return _ERROR_MAP.get(status_code, MaxBotError)
|
|
31
31
|
|
|
32
32
|
def build_api_error(response: httpx.Response) -> MaxBotError:
|
|
33
|
-
body_str = response.
|
|
33
|
+
body_str = response.content[:1000].decode('utf-8', errors='replace')
|
|
34
34
|
ExceptionClass = get_exception_for_status(response.status_code)
|
|
35
35
|
|
|
36
36
|
return ExceptionClass(status_code=response.status_code, response=body_str)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from maxbot_api_client_python.client import Client
|
|
2
4
|
from maxbot_api_client_python.types.constants import Paths
|
|
3
5
|
from maxbot_api_client_python.types.models import BotInfo, BotPatch
|
|
4
6
|
|
|
@@ -6,47 +8,47 @@ class Bots:
|
|
|
6
8
|
def __init__(self, client: Client):
|
|
7
9
|
self.client = client
|
|
8
10
|
|
|
9
|
-
def
|
|
11
|
+
def get_bot(self) -> BotInfo:
|
|
10
12
|
"""
|
|
11
13
|
Retrieves information about the current bot, such as its user ID, name, and description.
|
|
12
14
|
|
|
13
15
|
Example:
|
|
14
|
-
response =
|
|
16
|
+
response = bot.bots.get_bot()
|
|
15
17
|
"""
|
|
16
|
-
return
|
|
18
|
+
return self.client.decode("GET", Paths.ME, BotInfo)
|
|
17
19
|
|
|
18
|
-
def
|
|
20
|
+
def patch_bot(self, req: BotPatch) -> BotInfo:
|
|
19
21
|
"""
|
|
20
22
|
Edits current bot info.
|
|
21
23
|
Fill only the fields you want to update - all remaining fields will stay untouched.
|
|
22
24
|
|
|
23
25
|
Example:
|
|
24
|
-
response =
|
|
26
|
+
response = bot.bots.patch_bot(BotPatch(
|
|
25
27
|
name="New Name",
|
|
26
28
|
description="New description"
|
|
27
|
-
)
|
|
29
|
+
))
|
|
28
30
|
"""
|
|
29
|
-
|
|
30
|
-
return
|
|
31
|
+
query, payload = self.client.split_request(req)
|
|
32
|
+
return self.client.decode("PATCH", Paths.ME, BotInfo, query=query, payload=payload)
|
|
31
33
|
|
|
32
|
-
async def
|
|
34
|
+
async def get_bot_async(self) -> BotInfo:
|
|
33
35
|
"""
|
|
34
|
-
Async version of
|
|
36
|
+
Async version of get_bot.
|
|
35
37
|
|
|
36
38
|
Example:
|
|
37
|
-
response = await
|
|
39
|
+
response = await bot.bots.get_bot_async()
|
|
38
40
|
"""
|
|
39
|
-
return await
|
|
41
|
+
return await self.client.adecode("GET", Paths.ME, BotInfo)
|
|
40
42
|
|
|
41
|
-
async def
|
|
43
|
+
async def patch_bot_async(self, req: BotPatch) -> BotInfo:
|
|
42
44
|
"""
|
|
43
|
-
Async version of
|
|
45
|
+
Async version of patch_bot.
|
|
44
46
|
|
|
45
47
|
Example:
|
|
46
|
-
response = await
|
|
48
|
+
response = await bot.bots.patch_bot_async(BotPatch(
|
|
47
49
|
name="New Name",
|
|
48
50
|
description="New description"
|
|
49
|
-
)
|
|
51
|
+
))
|
|
50
52
|
"""
|
|
51
|
-
|
|
52
|
-
return await
|
|
53
|
+
query, payload = self.client.split_request(req)
|
|
54
|
+
return await self.client.adecode("PATCH", Paths.ME, BotInfo, query=query, payload=payload)
|