maxbot-api-client-python 1.1.2__py3-none-any.whl → 2.0.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 +80 -82
- maxbot_api_client_python/exceptions.py +1 -1
- maxbot_api_client_python/tools/bots.py +19 -15
- maxbot_api_client_python/tools/chats.py +168 -134
- maxbot_api_client_python/tools/helpers.py +87 -62
- maxbot_api_client_python/tools/messages.py +83 -67
- maxbot_api_client_python/tools/subscriptions.py +44 -36
- maxbot_api_client_python/tools/uploads.py +41 -15
- 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.1.2.dist-info → maxbot_api_client_python-2.0.0.dist-info}/METADATA +35 -46
- maxbot_api_client_python-2.0.0.dist-info/RECORD +20 -0
- 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-2.0.0.dist-info}/WHEEL +0 -0
- {maxbot_api_client_python-1.1.2.dist-info → maxbot_api_client_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {maxbot_api_client_python-1.1.2.dist-info → maxbot_api_client_python-2.0.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)
|
|
33
|
-
|
|
34
|
-
async def async_wait(self) -> None:
|
|
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)
|
|
7
|
+
from maxbot_api_client_python.types.models import Config
|
|
46
8
|
|
|
47
|
-
|
|
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
|
|
|
@@ -94,6 +82,7 @@ class Client:
|
|
|
94
82
|
async def aclose(self) -> None:
|
|
95
83
|
if self._async_client:
|
|
96
84
|
await self._async_client.aclose()
|
|
85
|
+
self._async_client = None
|
|
97
86
|
|
|
98
87
|
def _build_url(self, path: str) -> str:
|
|
99
88
|
if path.startswith(("http://", "https://")):
|
|
@@ -105,6 +94,30 @@ class Client:
|
|
|
105
94
|
return payload.model_dump(exclude_none=True)
|
|
106
95
|
return payload
|
|
107
96
|
|
|
97
|
+
def split_request(self, req: Any) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
|
|
98
|
+
if not hasattr(req, "model_fields"):
|
|
99
|
+
return None, self._prepare_data(req)
|
|
100
|
+
|
|
101
|
+
data = req.model_dump(exclude_none=True, by_alias=True)
|
|
102
|
+
query, payload = {}, {}
|
|
103
|
+
|
|
104
|
+
for field_name, field in req.model_fields.items():
|
|
105
|
+
key = field.alias or field_name
|
|
106
|
+
if key not in data:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
extra = field.json_schema_extra
|
|
110
|
+
extra_dict = extra if isinstance(extra, dict) else {}
|
|
111
|
+
|
|
112
|
+
if extra_dict.get("in_path"):
|
|
113
|
+
continue
|
|
114
|
+
elif extra_dict.get("in_query"):
|
|
115
|
+
query[key] = data[key]
|
|
116
|
+
else:
|
|
117
|
+
payload[key] = data[key]
|
|
118
|
+
|
|
119
|
+
return (query if query else None, payload if payload else None)
|
|
120
|
+
|
|
108
121
|
def request(
|
|
109
122
|
self,
|
|
110
123
|
method: str,
|
|
@@ -113,7 +126,7 @@ class Client:
|
|
|
113
126
|
payload: Any = None,
|
|
114
127
|
files: dict[str, Any] | None = None
|
|
115
128
|
) -> bytes:
|
|
116
|
-
self.
|
|
129
|
+
self._wait_limit()
|
|
117
130
|
|
|
118
131
|
headers = self.headers.copy()
|
|
119
132
|
if not files:
|
|
@@ -128,7 +141,6 @@ class Client:
|
|
|
128
141
|
files=files,
|
|
129
142
|
headers=headers
|
|
130
143
|
)
|
|
131
|
-
logger.debug(f"Response body: {response.text}")
|
|
132
144
|
return self._handle_response(response)
|
|
133
145
|
|
|
134
146
|
async def arequest(
|
|
@@ -139,7 +151,7 @@ class Client:
|
|
|
139
151
|
payload: Any = None,
|
|
140
152
|
files: dict[str, Any] | None = None
|
|
141
153
|
) -> bytes:
|
|
142
|
-
await self.
|
|
154
|
+
await self._await_limit()
|
|
143
155
|
|
|
144
156
|
api = await self.get_async_client()
|
|
145
157
|
headers = self.headers.copy()
|
|
@@ -155,7 +167,6 @@ class Client:
|
|
|
155
167
|
files=files,
|
|
156
168
|
headers=headers
|
|
157
169
|
)
|
|
158
|
-
logger.debug(f"Response body: {response.text}")
|
|
159
170
|
return self._handle_response(response)
|
|
160
171
|
|
|
161
172
|
def _handle_response(self, response: httpx.Response) -> bytes:
|
|
@@ -163,33 +174,20 @@ class Client:
|
|
|
163
174
|
raise build_api_error(response)
|
|
164
175
|
return response.content
|
|
165
176
|
|
|
166
|
-
def _parse_response
|
|
167
|
-
if not data or data.strip()
|
|
168
|
-
return model_class()
|
|
169
|
-
if data.strip() == b'<retval>1</retval>':
|
|
170
|
-
return model_class()
|
|
171
|
-
|
|
177
|
+
def _parse_response(self, data: bytes, model_class: Type[T]) -> T:
|
|
178
|
+
if not data or data.strip() in (b"", b"<retval>1</retval>"):
|
|
179
|
+
return model_class.model_construct()
|
|
172
180
|
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
|
|
181
|
+
return model_class.model_validate_json(data)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Server returned invalid response. Size: {len(data)} bytes.")
|
|
184
|
+
failed_response = httpx.Response(status_code=500, content=data, request=httpx.Request("GET", self.base_url))
|
|
185
|
+
raise build_api_error(failed_response) from e
|
|
182
186
|
|
|
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
|
|
187
|
+
def decode(self, method: str, path: str, model_class: type[T], **kwargs: Any) -> T:
|
|
188
|
+
response = self.request(method, path, **kwargs)
|
|
189
|
+
return self._parse_response(response, model_class)
|
|
190
|
+
|
|
191
|
+
async def adecode(self, method: str, path: str, model_class: type[T], **kwargs: Any) -> T:
|
|
192
|
+
response = await self.arequest(method, path, **kwargs)
|
|
193
|
+
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,49 @@ 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, **kwargs: Any) -> 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(
|
|
25
27
|
name="New Name",
|
|
26
28
|
description="New description"
|
|
27
29
|
)
|
|
28
30
|
"""
|
|
29
31
|
req = BotPatch(**kwargs)
|
|
30
|
-
|
|
32
|
+
query, payload = self.client.split_request(req)
|
|
33
|
+
return self.client.decode("PATCH", Paths.ME, BotInfo, query=query, payload=payload)
|
|
31
34
|
|
|
32
|
-
async def
|
|
35
|
+
async def get_bot_async(self) -> BotInfo:
|
|
33
36
|
"""
|
|
34
|
-
Async version of
|
|
37
|
+
Async version of get_bot.
|
|
35
38
|
|
|
36
39
|
Example:
|
|
37
|
-
response = await
|
|
40
|
+
response = await bot.bots.get_bot_async()
|
|
38
41
|
"""
|
|
39
|
-
return await
|
|
42
|
+
return await self.client.adecode("GET", Paths.ME, BotInfo)
|
|
40
43
|
|
|
41
|
-
async def
|
|
44
|
+
async def patch_bot_async(self, **kwargs: Any) -> BotInfo:
|
|
42
45
|
"""
|
|
43
|
-
Async version of
|
|
46
|
+
Async version of patch_bot.
|
|
44
47
|
|
|
45
48
|
Example:
|
|
46
|
-
response = await
|
|
49
|
+
response = await bot.bots.patch_bot_async(
|
|
47
50
|
name="New Name",
|
|
48
51
|
description="New description"
|
|
49
52
|
)
|
|
50
53
|
"""
|
|
51
54
|
req = BotPatch(**kwargs)
|
|
52
|
-
|
|
55
|
+
query, payload = self.client.split_request(req)
|
|
56
|
+
return await self.client.adecode("PATCH", Paths.ME, BotInfo, query=query, payload=payload)
|