maxbot-api-client-python 1.1.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.
@@ -0,0 +1,7 @@
1
+ from maxbot_api_client_python.api import API
2
+ from maxbot_api_client_python.client import Config, Client
3
+ from maxbot_api_client_python.types import models
4
+ from maxbot_api_client_python.types import constants
5
+ from maxbot_api_client_python import utils
6
+
7
+ __all__ = ["API", "Config", "Client", "models", "constants", "utils"]
@@ -0,0 +1,35 @@
1
+ from typing import Any, Self
2
+
3
+ from maxbot_api_client_python.client import Client, Config
4
+ from maxbot_api_client_python import tools
5
+
6
+ class API:
7
+ def __init__(self, cfg: Config) -> None:
8
+ self.client = Client(cfg)
9
+
10
+ self.bots = tools.Bots(self.client)
11
+ self.chats = tools.Chats(self.client)
12
+ self.helpers = tools.Helpers(self.client)
13
+ self.messages = tools.Messages(self.client)
14
+ self.subscriptions = tools.Subscriptions(self.client)
15
+ self.uploads = tools.Uploads(self.client)
16
+
17
+ def __enter__(self) -> Self:
18
+ self.client.__enter__()
19
+ return self
20
+
21
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
22
+ self.client.__exit__(exc_type, exc_val, exc_tb)
23
+
24
+ async def __aenter__(self) -> Self:
25
+ await self.client.__aenter__()
26
+ return self
27
+
28
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
29
+ await self.client.__aexit__(exc_type, exc_val, exc_tb)
30
+
31
+ def close(self) -> None:
32
+ self.client.close()
33
+
34
+ async def aclose(self) -> None:
35
+ await self.client.aclose()
@@ -0,0 +1,195 @@
1
+ import asyncio, httpx, json, logging, time, threading
2
+ from dataclasses import dataclass
3
+ from functools import wraps
4
+ from typing import Any
5
+ from urllib.parse import urljoin
6
+
7
+ from maxbot_api_client_python.exceptions import build_api_error
8
+ logger = logging.getLogger(__name__)
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)
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
55
+
56
+ class Client:
57
+ def __init__(self, cfg: Config):
58
+ if not cfg.base_url or not cfg.token:
59
+ raise ValueError("base_url and token must be set")
60
+
61
+ self.base_url = cfg.base_url.rstrip("/") + "/"
62
+ self.token = cfg.token
63
+
64
+ self.timeout = cfg.timeout if cfg.timeout > 0 else 35
65
+ self.global_limiter = RateLimiter(cfg.ratelimiter if cfg.ratelimiter > 0 else 25)
66
+ self.max_retries = cfg.max_retries
67
+ self.retry_delay_sec = cfg.retry_delay_sec
68
+
69
+ self.headers = {"Authorization": self.token}
70
+
71
+ self._sync_client = httpx.Client(timeout=self.timeout)
72
+ self._async_client: httpx.AsyncClient | None = None
73
+
74
+ def __enter__(self) -> "Client":
75
+ return self
76
+
77
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
78
+ self.close()
79
+
80
+ async def __aenter__(self) -> "Client":
81
+ return self
82
+
83
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
84
+ await self.aclose()
85
+
86
+ async def get_async_client(self) -> httpx.AsyncClient:
87
+ if self._async_client is None:
88
+ self._async_client = httpx.AsyncClient(timeout=self.timeout)
89
+ return self._async_client
90
+
91
+ def close(self) -> None:
92
+ self._sync_client.close()
93
+
94
+ async def aclose(self) -> None:
95
+ if self._async_client:
96
+ await self._async_client.aclose()
97
+
98
+ def _build_url(self, path: str) -> str:
99
+ if path.startswith(("http://", "https://")):
100
+ return path
101
+ return urljoin(self.base_url, path.lstrip("/"))
102
+
103
+ def _prepare_data(self, payload: Any) -> Any:
104
+ if hasattr(payload, "model_dump"):
105
+ return payload.model_dump(exclude_none=True)
106
+ return payload
107
+
108
+ def request(
109
+ self,
110
+ method: str,
111
+ path: str,
112
+ query: dict[str, Any] | None = None,
113
+ payload: Any = None,
114
+ files: dict[str, Any] | None = None
115
+ ) -> bytes:
116
+ self.global_limiter.wait()
117
+
118
+ headers = self.headers.copy()
119
+ if not files:
120
+ headers["Content-Type"] = "application/json"
121
+
122
+ response = self._sync_client.request(
123
+ method=method,
124
+ url=self._build_url(path),
125
+ params=query,
126
+ json=self._prepare_data(payload) if not files else None,
127
+ data=None if not files else payload,
128
+ files=files,
129
+ headers=headers
130
+ )
131
+ logger.debug(f"Response body: {response.text}")
132
+ return self._handle_response(response)
133
+
134
+ async def arequest(
135
+ self,
136
+ method: str,
137
+ path: str,
138
+ query: dict[str, Any] | None = None,
139
+ payload: Any = None,
140
+ files: dict[str, Any] | None = None
141
+ ) -> bytes:
142
+ await self.global_limiter.async_wait()
143
+
144
+ api = await self.get_async_client()
145
+ headers = self.headers.copy()
146
+ if not files:
147
+ headers["Content-Type"] = "application/json"
148
+
149
+ response = await api.request(
150
+ method=method,
151
+ url=self._build_url(path),
152
+ params=query,
153
+ json=self._prepare_data(payload) if not files else None,
154
+ data=None if not files else payload,
155
+ files=files,
156
+ headers=headers
157
+ )
158
+ logger.debug(f"Response body: {response.text}")
159
+ return self._handle_response(response)
160
+
161
+ def _handle_response(self, response: httpx.Response) -> bytes:
162
+ if not (200 <= response.status_code < 300):
163
+ raise build_api_error(response)
164
+ return response.content
165
+
166
+ def _parse_response[T](self, data: bytes, model_class: type[T]) -> T:
167
+ if not data or data.strip() == b"":
168
+ return model_class()
169
+ if data.strip() == b'<retval>1</retval>':
170
+ return model_class()
171
+
172
+ try:
173
+ parsed_json = json.loads(data)
174
+ except json.JSONDecodeError:
175
+ decoded_snippet = data[:100].decode('utf-8', errors='replace')
176
+ logger.warning(f"Server returned non-JSON response: {decoded_snippet}")
177
+ return model_class()
178
+
179
+ if hasattr(model_class, "model_validate"):
180
+ return model_class.model_validate(parsed_json)
181
+ return parsed_json
182
+
183
+ def decode[T](client: Client, method: str, path: str, model_class: type[T], **kwargs: Any) -> T:
184
+ response = client.request(method, path, **kwargs)
185
+ return client._parse_response(response, model_class)
186
+
187
+ async def adecode[T](client: Client, method: str, path: str, model_class: type[T], **kwargs: Any) -> T:
188
+ response = await client.arequest(method, path, **kwargs)
189
+ return client._parse_response(response, model_class)
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
@@ -0,0 +1,36 @@
1
+ import httpx
2
+
3
+ class MaxBotError(Exception):
4
+ default_message = "unexpected API error"
5
+
6
+ def __init__(self, message: str | None = None, status_code: int | None = None, response: str | None = None):
7
+ msg = message or self.default_message
8
+ full_message = f"{msg}: status code {status_code}, response: {response}"
9
+ super().__init__(full_message)
10
+ self.status_code = status_code
11
+ self.response = response
12
+
13
+ class BadRequestError(MaxBotError): default_message = "bad request"
14
+ class UnauthorizedError(MaxBotError): default_message = "unauthorized"
15
+ class NotFoundError(MaxBotError): default_message = "not found"
16
+ class MethodNotAllowedError(MaxBotError): default_message = "method not allowed"
17
+ class TooManyRequestsError(MaxBotError): default_message = "too many requests"
18
+ class ServiceUnavailableError(MaxBotError): default_message = "service unavailable"
19
+
20
+ _ERROR_MAP = {
21
+ 400: BadRequestError,
22
+ 401: UnauthorizedError,
23
+ 404: NotFoundError,
24
+ 405: MethodNotAllowedError,
25
+ 429: TooManyRequestsError,
26
+ 503: ServiceUnavailableError,
27
+ }
28
+
29
+ def get_exception_for_status(status_code: int) -> type[MaxBotError]:
30
+ return _ERROR_MAP.get(status_code, MaxBotError)
31
+
32
+ def build_api_error(response: httpx.Response) -> MaxBotError:
33
+ body_str = response.text
34
+ ExceptionClass = get_exception_for_status(response.status_code)
35
+
36
+ return ExceptionClass(status_code=response.status_code, response=body_str)
@@ -0,0 +1,8 @@
1
+ from maxbot_api_client_python.tools.bots import Bots
2
+ from maxbot_api_client_python.tools.chats import Chats
3
+ from maxbot_api_client_python.tools.messages import Messages
4
+ from maxbot_api_client_python.tools.subscriptions import Subscriptions
5
+ from maxbot_api_client_python.tools.uploads import Uploads
6
+ from maxbot_api_client_python.tools.helpers import Helpers
7
+
8
+ __all__ = ["Bots", "Chats", "Messages", "Subscriptions", "Uploads", "Helpers"]
@@ -0,0 +1,52 @@
1
+ from maxbot_api_client_python.client import Client, decode, adecode
2
+ from maxbot_api_client_python.types.constants import Paths
3
+ from maxbot_api_client_python.types.models import BotInfo, BotPatch
4
+
5
+ class Bots:
6
+ def __init__(self, client: Client):
7
+ self.client = client
8
+
9
+ def GetBot(self) -> BotInfo:
10
+ """
11
+ Retrieves information about the current bot, such as its user ID, name, and description.
12
+
13
+ Example:
14
+ response = api.bots.GetBot()
15
+ """
16
+ return decode(self.client, "GET", Paths.ME, BotInfo)
17
+
18
+ def PatchBot(self, **kwargs) -> BotInfo:
19
+ """
20
+ Edits current bot info.
21
+ Fill only the fields you want to update - all remaining fields will stay untouched.
22
+
23
+ Example:
24
+ response = api.bots.PatchBot(
25
+ name="New Name",
26
+ description="New description"
27
+ )
28
+ """
29
+ req = BotPatch(**kwargs)
30
+ return decode(self.client, "PATCH", Paths.ME, BotInfo, payload=req)
31
+
32
+ async def GetBotAsync(self) -> BotInfo:
33
+ """
34
+ Async version of GetBot.
35
+
36
+ Example:
37
+ response = await api.bots.GetBotAsync()
38
+ """
39
+ return await adecode(self.client, "GET", Paths.ME, BotInfo)
40
+
41
+ async def PatchBotAsync(self, **kwargs) -> BotInfo:
42
+ """
43
+ Async version of PatchBot.
44
+
45
+ Example:
46
+ response = await api.bots.PatchBotAsync(
47
+ name="New Name",
48
+ description="New description"
49
+ )
50
+ """
51
+ req = BotPatch(**kwargs)
52
+ return await adecode(self.client, "PATCH", Paths.ME, BotInfo, payload=req)