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.
@@ -1,5 +1,6 @@
1
1
  from maxbot_api_client_python.api import API
2
- from maxbot_api_client_python.client import Config, Client
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
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Self
2
2
 
3
- from maxbot_api_client_python.client import Client, Config
3
+ from maxbot_api_client_python.client import Client
4
+ from maxbot_api_client_python.types.models import Config
4
5
  from maxbot_api_client_python import tools
5
6
 
6
7
  class API:
@@ -1,58 +1,14 @@
1
- import asyncio, httpx, json, logging, time, threading
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
- 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)
7
+ from maxbot_api_client_python.types.models import Config
46
8
 
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
 
@@ -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.global_limiter.wait()
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.global_limiter.async_wait()
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[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
-
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
- 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
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[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
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.text
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 maxbot_api_client_python.client import Client, decode, adecode
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 GetBot(self) -> BotInfo:
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 = api.bots.GetBot()
16
+ response = bot.bots.get_bot()
15
17
  """
16
- return decode(self.client, "GET", Paths.ME, BotInfo)
18
+ return self.client.decode("GET", Paths.ME, BotInfo)
17
19
 
18
- def PatchBot(self, **kwargs) -> BotInfo:
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 = api.bots.PatchBot(
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
- return decode(self.client, "PATCH", Paths.ME, BotInfo, payload=req)
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 GetBotAsync(self) -> BotInfo:
35
+ async def get_bot_async(self) -> BotInfo:
33
36
  """
34
- Async version of GetBot.
37
+ Async version of get_bot.
35
38
 
36
39
  Example:
37
- response = await api.bots.GetBotAsync()
40
+ response = await bot.bots.get_bot_async()
38
41
  """
39
- return await adecode(self.client, "GET", Paths.ME, BotInfo)
42
+ return await self.client.adecode("GET", Paths.ME, BotInfo)
40
43
 
41
- async def PatchBotAsync(self, **kwargs) -> BotInfo:
44
+ async def patch_bot_async(self, **kwargs: Any) -> BotInfo:
42
45
  """
43
- Async version of PatchBot.
46
+ Async version of patch_bot.
44
47
 
45
48
  Example:
46
- response = await api.bots.PatchBotAsync(
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
- return await adecode(self.client, "PATCH", Paths.ME, BotInfo, payload=req)
55
+ query, payload = self.client.split_request(req)
56
+ return await self.client.adecode("PATCH", Paths.ME, BotInfo, query=query, payload=payload)