maxapi-sdk 0.12.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.
Files changed (48) hide show
  1. maxapi/__init__.py +49 -0
  2. maxapi/bot.py +674 -0
  3. maxapi/builders/__init__.py +23 -0
  4. maxapi/builders/keyboards.py +133 -0
  5. maxapi/builders/media.py +81 -0
  6. maxapi/callback_schema.py +140 -0
  7. maxapi/client/__init__.py +3 -0
  8. maxapi/client/default.py +89 -0
  9. maxapi/compat/__init__.py +15 -0
  10. maxapi/connection/__init__.py +3 -0
  11. maxapi/connection/base.py +136 -0
  12. maxapi/dispatcher.py +487 -0
  13. maxapi/exceptions/__init__.py +3 -0
  14. maxapi/exceptions/max.py +45 -0
  15. maxapi/filters/__init__.py +25 -0
  16. maxapi/filters/base.py +73 -0
  17. maxapi/filters/command.py +28 -0
  18. maxapi/filters/common.py +78 -0
  19. maxapi/filters/text.py +71 -0
  20. maxapi/fsm/__init__.py +17 -0
  21. maxapi/fsm/context.py +41 -0
  22. maxapi/fsm/filters.py +30 -0
  23. maxapi/fsm/middleware.py +42 -0
  24. maxapi/fsm/state.py +33 -0
  25. maxapi/fsm/storage/__init__.py +4 -0
  26. maxapi/fsm/storage/base.py +30 -0
  27. maxapi/fsm/storage/memory.py +38 -0
  28. maxapi/middlewares/__init__.py +3 -0
  29. maxapi/middlewares/base.py +34 -0
  30. maxapi/plugins/__init__.py +3 -0
  31. maxapi/plugins/base.py +19 -0
  32. maxapi/py.typed +1 -0
  33. maxapi/runners/__init__.py +4 -0
  34. maxapi/runners/polling.py +55 -0
  35. maxapi/runners/webhook.py +72 -0
  36. maxapi/transport/__init__.py +13 -0
  37. maxapi/transport/client.py +239 -0
  38. maxapi/transport/config.py +81 -0
  39. maxapi/transport/errors.py +45 -0
  40. maxapi/types/__init__.py +73 -0
  41. maxapi/types/base.py +9 -0
  42. maxapi/types/bot_mixin.py +14 -0
  43. maxapi/types/models.py +308 -0
  44. maxapi_sdk-0.12.0.dist-info/METADATA +270 -0
  45. maxapi_sdk-0.12.0.dist-info/RECORD +48 -0
  46. maxapi_sdk-0.12.0.dist-info/WHEEL +5 -0
  47. maxapi_sdk-0.12.0.dist-info/licenses/LICENSE +21 -0
  48. maxapi_sdk-0.12.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable
4
+
5
+ SPECIAL_ROW_LIMIT_TYPES = {
6
+ "link",
7
+ "open_app",
8
+ "request_contact",
9
+ "request_geo_location",
10
+ }
11
+
12
+
13
+ class InlineKeyboardBuilder:
14
+ """Builder для MAX inline keyboard."""
15
+
16
+ def __init__(self) -> None:
17
+ self._rows: list[list[dict[str, Any]]] = []
18
+ self._pending: list[dict[str, Any]] = []
19
+
20
+ def add(self, *buttons: dict[str, Any]) -> "InlineKeyboardBuilder":
21
+ for button in buttons:
22
+ self._pending.append(_normalize_button(button))
23
+ return self
24
+
25
+ def button(self, *, button_type: str, text: str, **extra: Any) -> "InlineKeyboardBuilder":
26
+ button = {"type": button_type, "text": text}
27
+ button.update({key: value for key, value in extra.items() if value is not None})
28
+ return self.add(button)
29
+
30
+ def callback(self, text: str, payload: str | dict[str, Any]) -> "InlineKeyboardBuilder":
31
+ return self.button(button_type="callback", text=text, payload=payload)
32
+
33
+ def link(self, text: str, url: str) -> "InlineKeyboardBuilder":
34
+ return self.button(button_type="link", text=text, url=url)
35
+
36
+ def request_contact(self, text: str) -> "InlineKeyboardBuilder":
37
+ return self.button(button_type="request_contact", text=text)
38
+
39
+ def request_geo_location(self, text: str) -> "InlineKeyboardBuilder":
40
+ return self.button(button_type="request_geo_location", text=text)
41
+
42
+ def open_app(
43
+ self,
44
+ text: str,
45
+ *,
46
+ url: str | None = None,
47
+ app_id: str | None = None,
48
+ payload: str | dict[str, Any] | None = None,
49
+ ) -> "InlineKeyboardBuilder":
50
+ return self.button(
51
+ button_type="open_app",
52
+ text=text,
53
+ url=url,
54
+ app_id=app_id,
55
+ payload=payload,
56
+ )
57
+
58
+ def message(self, text: str, message_text: str) -> "InlineKeyboardBuilder":
59
+ return self.button(button_type="message", text=text, message_text=message_text)
60
+
61
+ def row(self, *buttons: dict[str, Any]) -> "InlineKeyboardBuilder":
62
+ normalized = [_normalize_button(button) for button in buttons]
63
+ if normalized:
64
+ self._append_row(normalized)
65
+ elif self._pending:
66
+ self._append_row(self._consume_pending())
67
+ return self
68
+
69
+ def adjust(self, *sizes: int, repeat: bool = False) -> "InlineKeyboardBuilder":
70
+ if not self._pending:
71
+ return self
72
+ if not sizes:
73
+ sizes = (7,)
74
+ index = 0
75
+ pattern_index = 0
76
+ while index < len(self._pending):
77
+ row_size = sizes[pattern_index]
78
+ row = self._pending[index:index + row_size]
79
+ self._append_row(row)
80
+ index += row_size
81
+ if repeat:
82
+ pattern_index = (pattern_index + 1) % len(sizes)
83
+ else:
84
+ pattern_index = min(pattern_index + 1, len(sizes) - 1)
85
+ self._pending.clear()
86
+ return self
87
+
88
+ def as_markup(self) -> dict[str, Any]:
89
+ self._flush_pending()
90
+ return {"type": "inline_keyboard", "payload": {"buttons": self._rows}}
91
+
92
+ def as_attachment(self) -> dict[str, Any]:
93
+ return self.as_markup()
94
+
95
+ def export(self) -> list[list[dict[str, Any]]]:
96
+ self._flush_pending()
97
+ return [[dict(button) for button in row] for row in self._rows]
98
+
99
+ def _consume_pending(self) -> list[dict[str, Any]]:
100
+ items = list(self._pending)
101
+ self._pending.clear()
102
+ return items
103
+
104
+ def _flush_pending(self) -> None:
105
+ if not self._pending:
106
+ return
107
+ self.adjust(7, repeat=True)
108
+
109
+ def _append_row(self, row: Iterable[dict[str, Any]]) -> None:
110
+ normalized = [dict(item) for item in row]
111
+ if not normalized:
112
+ return
113
+ if len(normalized) > 7:
114
+ raise ValueError("В одном ряду MAX inline keyboard может быть не более 7 кнопок.")
115
+ if any(item.get("type") in SPECIAL_ROW_LIMIT_TYPES for item in normalized) and len(normalized) > 3:
116
+ raise ValueError(
117
+ "Ряд с кнопками link/open_app/request_contact/request_geo_location "
118
+ "может содержать не более 3 кнопок."
119
+ )
120
+ if len(self._rows) >= 30:
121
+ raise ValueError("MAX inline keyboard поддерживает не более 30 рядов.")
122
+ total_buttons = sum(len(existing_row) for existing_row in self._rows) + len(normalized)
123
+ if total_buttons > 210:
124
+ raise ValueError("MAX inline keyboard поддерживает не более 210 кнопок.")
125
+ self._rows.append(normalized)
126
+
127
+
128
+ def _normalize_button(button: dict[str, Any]) -> dict[str, Any]:
129
+ if not isinstance(button, dict):
130
+ raise TypeError(f"Кнопка должна быть dict, получено: {button!r}")
131
+ if "type" not in button or "text" not in button:
132
+ raise ValueError("Кнопка должна содержать поля 'type' и 'text'.")
133
+ return {key: value for key, value in button.items() if value is not None}
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Iterable
4
+
5
+
6
+ def make_attachment(attachment_type: str, payload: dict[str, Any]) -> dict[str, Any]:
7
+ return {"type": attachment_type, "payload": payload}
8
+
9
+
10
+ def image_attachment(*, token: str | None = None, url: str | None = None, **extra: Any) -> dict[str, Any]:
11
+ payload = _build_payload(token=token, url=url, extra=extra)
12
+ return make_attachment("image", payload)
13
+
14
+
15
+ def video_attachment(*, token: str | None = None, url: str | None = None, **extra: Any) -> dict[str, Any]:
16
+ payload = _build_payload(token=token, url=url, extra=extra)
17
+ return make_attachment("video", payload)
18
+
19
+
20
+ def audio_attachment(*, token: str | None = None, url: str | None = None, **extra: Any) -> dict[str, Any]:
21
+ payload = _build_payload(token=token, url=url, extra=extra)
22
+ return make_attachment("audio", payload)
23
+
24
+
25
+ def file_attachment(*, token: str | None = None, url: str | None = None, **extra: Any) -> dict[str, Any]:
26
+ payload = _build_payload(token=token, url=url, extra=extra)
27
+ return make_attachment("file", payload)
28
+
29
+
30
+ def normalize_attachment(item: Any) -> dict[str, Any]:
31
+ if item is None:
32
+ raise TypeError("Attachment не может быть None.")
33
+ if isinstance(item, dict):
34
+ return item
35
+ if hasattr(item, "as_attachment"):
36
+ return item.as_attachment()
37
+ if hasattr(item, "model_dump"):
38
+ return item.model_dump(by_alias=True, exclude_none=True)
39
+ raise TypeError(f"Неподдерживаемый attachment: {item!r}")
40
+
41
+
42
+ def normalize_attachments(
43
+ attachments: Iterable[Any] | None = None,
44
+ *,
45
+ keyboard: Any | None = None,
46
+ ) -> list[dict[str, Any]] | None:
47
+ normalized: list[dict[str, Any]] = []
48
+ if attachments is not None:
49
+ for item in attachments:
50
+ normalized.append(normalize_attachment(item))
51
+ if keyboard is not None:
52
+ normalized.append(normalize_attachment(keyboard))
53
+ if not normalized:
54
+ return None
55
+ return normalized
56
+
57
+
58
+ def build_uploaded_attachment(
59
+ *,
60
+ upload_type: str,
61
+ upload_response_token: str | None,
62
+ uploaded_payload: dict[str, Any],
63
+ ) -> dict[str, Any]:
64
+ payload = dict(uploaded_payload)
65
+ if payload.get("token") is None and upload_response_token is not None:
66
+ payload["token"] = upload_response_token
67
+ return make_attachment(upload_type, payload)
68
+
69
+
70
+ def _build_payload(
71
+ *,
72
+ token: str | None,
73
+ url: str | None,
74
+ extra: dict[str, Any],
75
+ ) -> dict[str, Any]:
76
+ payload = dict(extra)
77
+ if token is not None:
78
+ payload["token"] = token
79
+ if url is not None:
80
+ payload["url"] = url
81
+ return payload
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, ClassVar, TypeVar, get_origin
5
+ from urllib.parse import quote, unquote
6
+
7
+ from pydantic import TypeAdapter
8
+
9
+ from .types import ApiModel
10
+
11
+ PayloadSchemaType = TypeVar("PayloadSchemaType", bound="CallbackPayloadSchema")
12
+
13
+
14
+ class CallbackPayloadError(ValueError):
15
+ """Ошибка сериализации или десериализации callback payload."""
16
+
17
+
18
+ class CallbackPayloadSchema(ApiModel):
19
+ """Структурированная схема callback payload.
20
+
21
+ Пример:
22
+
23
+ class AdminAction(CallbackPayloadSchema):
24
+ prefix = "admin"
25
+ action: str
26
+ user_id: int
27
+
28
+ payload = AdminAction(action="ban", user_id=42).pack()
29
+ parsed = AdminAction.unpack(payload)
30
+ """
31
+
32
+ prefix: ClassVar[str] = ""
33
+ sep: ClassVar[str] = ":"
34
+
35
+ def pack(self) -> str:
36
+ field_names = list(type(self).model_fields)
37
+ values = [self.prefix or type(self).__name__.lower()]
38
+ for field_name in field_names:
39
+ values.append(_encode_component(getattr(self, field_name)))
40
+ return self.sep.join(values)
41
+
42
+ @classmethod
43
+ def unpack(cls, payload: Any) -> PayloadSchemaType:
44
+ payload_text = extract_callback_value(payload)
45
+ if payload_text is None:
46
+ raise CallbackPayloadError("Не удалось извлечь текстовый callback payload.")
47
+ parts = payload_text.split(cls.sep)
48
+ expected_prefix = cls.prefix or cls.__name__.lower()
49
+ if not parts or parts[0] != expected_prefix:
50
+ raise CallbackPayloadError(
51
+ f"Некорректный prefix callback payload: ожидался {expected_prefix!r}."
52
+ )
53
+ field_names = list(cls.model_fields)
54
+ raw_values = parts[1:]
55
+ if len(raw_values) != len(field_names):
56
+ raise CallbackPayloadError(
57
+ "Количество значений callback payload не совпадает с количеством полей схемы."
58
+ )
59
+ converted: dict[str, Any] = {}
60
+ for index, field_name in enumerate(field_names):
61
+ field_info = cls.model_fields[field_name]
62
+ converted[field_name] = _convert_component(
63
+ unquote(raw_values[index]),
64
+ field_info.annotation,
65
+ )
66
+ return cls(**converted)
67
+
68
+ @classmethod
69
+ def filter(cls, **conditions: Any):
70
+ return CallbackSchemaFilter(cls, **conditions)
71
+
72
+
73
+ class CallbackSchemaFilter:
74
+ def __init__(self, schema: type[CallbackPayloadSchema], **conditions: Any) -> None:
75
+ self.schema = schema
76
+ self.conditions = conditions
77
+
78
+ async def __call__(self, event: Any) -> bool:
79
+ update = getattr(event, "update", None)
80
+ callback = getattr(update, "callback", None)
81
+ payload = getattr(callback, "payload", None)
82
+ try:
83
+ parsed = self.schema.unpack(payload)
84
+ except CallbackPayloadError:
85
+ return False
86
+ for key, expected_value in self.conditions.items():
87
+ if getattr(parsed, key, None) != expected_value:
88
+ return False
89
+ return True
90
+
91
+
92
+ def extract_callback_value(payload: Any) -> str | None:
93
+ if payload is None:
94
+ return None
95
+ if isinstance(payload, str):
96
+ return payload
97
+ if isinstance(payload, dict):
98
+ for key in ("data", "payload", "value"):
99
+ item = payload.get(key)
100
+ if isinstance(item, str):
101
+ return item
102
+ return None
103
+ return None
104
+
105
+
106
+ def extract_callback_mapping(payload: Any) -> dict[str, Any] | None:
107
+ if payload is None:
108
+ return None
109
+ if isinstance(payload, dict):
110
+ return dict(payload)
111
+ if isinstance(payload, str):
112
+ try:
113
+ parsed = json.loads(payload)
114
+ except json.JSONDecodeError:
115
+ return None
116
+ if isinstance(parsed, dict):
117
+ return parsed
118
+ return None
119
+
120
+
121
+ def _encode_component(value: Any) -> str:
122
+ if isinstance(value, (dict, list)):
123
+ serialized = json.dumps(value, ensure_ascii=False, separators=(",", ":"))
124
+ else:
125
+ serialized = str(value)
126
+ return quote(serialized, safe="")
127
+
128
+
129
+ def _convert_component(raw_value: str, annotation: Any) -> Any:
130
+ origin = get_origin(annotation)
131
+ candidate: Any = raw_value
132
+ if annotation is Any:
133
+ return raw_value
134
+ if annotation in {dict, list} or origin in {dict, list, tuple, set}:
135
+ try:
136
+ candidate = json.loads(raw_value)
137
+ except json.JSONDecodeError:
138
+ candidate = raw_value
139
+ adapter = TypeAdapter(annotation)
140
+ return adapter.validate_python(candidate)
@@ -0,0 +1,3 @@
1
+ from .default import DefaultConnectionProperties
2
+
3
+ __all__ = ["DefaultConnectionProperties"]
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from aiohttp import ClientTimeout
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class DefaultConnectionProperties:
11
+ """Расширенная конфигурация HTTP-соединения."""
12
+
13
+ timeout_seconds: float = 150.0
14
+ sock_connect: float = 30.0
15
+ sock_read: float = 60.0
16
+ connector_limit: int = 100
17
+ connector_limit_per_host: int = 30
18
+ trust_env: bool = False
19
+ auto_decompress: bool = True
20
+ raise_for_status: bool = False
21
+ request_retries: int = 5
22
+ request_retry_delay: float = 0.5
23
+ request_retry_backoff: float = 2.0
24
+ request_retry_max_delay: float = 8.0
25
+ respect_retry_after: bool = True
26
+ retry_statuses: tuple[int, ...] = (408, 425, 429, 500, 502, 503, 504)
27
+ retry_methods: tuple[str, ...] = (
28
+ "DELETE",
29
+ "GET",
30
+ "HEAD",
31
+ "OPTIONS",
32
+ "PATCH",
33
+ "POST",
34
+ "PUT",
35
+ )
36
+ timeout: ClientTimeout = field(init=False)
37
+ kwargs: dict[str, Any] = field(default_factory=dict)
38
+
39
+ def __init__(
40
+ self,
41
+ timeout: float = 150.0,
42
+ sock_connect: float = 30.0,
43
+ sock_read: float = 60.0,
44
+ connector_limit: int = 100,
45
+ connector_limit_per_host: int = 30,
46
+ trust_env: bool = False,
47
+ auto_decompress: bool = True,
48
+ raise_for_status: bool = False,
49
+ request_retries: int = 5,
50
+ request_retry_delay: float = 0.5,
51
+ request_retry_backoff: float = 2.0,
52
+ request_retry_max_delay: float = 8.0,
53
+ respect_retry_after: bool = True,
54
+ retry_statuses: tuple[int, ...] | None = None,
55
+ retry_methods: tuple[str, ...] | None = None,
56
+ **kwargs: Any,
57
+ ) -> None:
58
+ self.timeout_seconds = timeout
59
+ self.sock_connect = sock_connect
60
+ self.sock_read = sock_read
61
+ self.connector_limit = connector_limit
62
+ self.connector_limit_per_host = connector_limit_per_host
63
+ self.trust_env = trust_env
64
+ self.auto_decompress = auto_decompress
65
+ self.raise_for_status = raise_for_status
66
+ self.request_retries = request_retries
67
+ self.request_retry_delay = request_retry_delay
68
+ self.request_retry_backoff = request_retry_backoff
69
+ self.request_retry_max_delay = request_retry_max_delay
70
+ self.respect_retry_after = respect_retry_after
71
+ self.retry_statuses = retry_statuses or (408, 425, 429, 500, 502, 503, 504)
72
+ self.retry_methods = retry_methods or (
73
+ "DELETE",
74
+ "GET",
75
+ "HEAD",
76
+ "OPTIONS",
77
+ "PATCH",
78
+ "POST",
79
+ "PUT",
80
+ )
81
+ self.timeout = ClientTimeout(
82
+ total=timeout,
83
+ sock_connect=sock_connect,
84
+ sock_read=sock_read,
85
+ )
86
+ self.kwargs = dict(kwargs)
87
+ self.kwargs.setdefault("trust_env", trust_env)
88
+ self.kwargs.setdefault("auto_decompress", auto_decompress)
89
+ self.kwargs.setdefault("raise_for_status", raise_for_status)
@@ -0,0 +1,15 @@
1
+ from __future__ import annotations
2
+
3
+ from ..bot import Bot as LegacyBot
4
+ from ..builders import InlineKeyboardBuilder as Keyboard
5
+ from ..dispatcher import Dispatcher as LegacyDispatcher
6
+ from ..dispatcher import Router as LegacyRouter
7
+ from ..types import MessageBody as NewMessageBody
8
+
9
+ __all__ = [
10
+ "Keyboard",
11
+ "LegacyBot",
12
+ "LegacyDispatcher",
13
+ "LegacyRouter",
14
+ "NewMessageBody",
15
+ ]
@@ -0,0 +1,3 @@
1
+ from .base import BaseConnection
2
+
3
+ __all__ = ["BaseConnection"]
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import mimetypes
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ import aiofiles
9
+ from aiohttp import FormData
10
+
11
+ from ..transport import MaxApiTransport, TransportConfig
12
+ from ..types.bot_mixin import BotMixin
13
+
14
+ if TYPE_CHECKING:
15
+ from aiohttp import ClientSession
16
+
17
+
18
+ class BaseConnection(BotMixin):
19
+ """Базовый класс transport-aware соединения."""
20
+
21
+ API_URL = "https://platform-api.max.ru"
22
+
23
+ def __init__(self) -> None:
24
+ self.bot = None
25
+ self.session: ClientSession | None = None
26
+ self.api_url = self.API_URL
27
+ self._transport: MaxApiTransport | None = None
28
+
29
+ def set_api_url(self, url: str) -> None:
30
+ self.api_url = url.rstrip("/")
31
+ if self.bot is not None:
32
+ self.bot.api_url = self.api_url
33
+ self._transport = None
34
+
35
+ async def request(
36
+ self,
37
+ method: str,
38
+ path: str,
39
+ model: Any = None,
40
+ *,
41
+ is_return_raw: bool = False,
42
+ **kwargs: Any,
43
+ ) -> Any:
44
+ bot = self._ensure_bot()
45
+ transport = self._ensure_transport(bot)
46
+ response = await transport.request(method=method, path=path, model=model, **kwargs)
47
+ if is_return_raw:
48
+ return response.raw
49
+ parsed = response.parsed
50
+ self._bind_bot(bot=bot, payload=parsed)
51
+ return parsed
52
+
53
+ async def upload_file(self, url: str, path: str, upload_type: str) -> Any:
54
+ async with aiofiles.open(path, "rb") as file_object:
55
+ file_data = await file_object.read()
56
+ filename = Path(path).name
57
+ return await self.upload_file_buffer(
58
+ filename=filename,
59
+ url=url,
60
+ buffer=file_data,
61
+ upload_type=upload_type,
62
+ )
63
+
64
+ async def upload_file_buffer(
65
+ self,
66
+ *,
67
+ filename: str,
68
+ url: str,
69
+ buffer: bytes,
70
+ upload_type: str,
71
+ ) -> Any:
72
+ ext = Path(filename).suffix
73
+ if not ext:
74
+ guessed_ext = mimetypes.guess_extension(f"{upload_type}/*") or ""
75
+ ext = guessed_ext
76
+ content_type = mimetypes.guess_type(filename)[0] or f"{upload_type}/*"
77
+ form = FormData(quote_fields=False)
78
+ form.add_field(
79
+ name="data",
80
+ value=buffer,
81
+ filename=f"{Path(filename).stem}{ext}",
82
+ content_type=content_type,
83
+ )
84
+ transport = self._ensure_transport(self._ensure_bot())
85
+ payload_text = await transport.upload(url=url, data=form)
86
+ try:
87
+ return json.loads(payload_text)
88
+ except json.JSONDecodeError:
89
+ return {"text": payload_text}
90
+
91
+ def _ensure_transport(self, bot: Any) -> MaxApiTransport:
92
+ if self._transport is not None:
93
+ return self._transport
94
+ config = TransportConfig.from_default_connection(bot.default_connection)
95
+ config.session_kwargs.setdefault(
96
+ "connector_limit",
97
+ getattr(bot.default_connection, "connector_limit", 100),
98
+ )
99
+ config.session_kwargs.setdefault(
100
+ "connector_limit_per_host",
101
+ getattr(bot.default_connection, "connector_limit_per_host", 30),
102
+ )
103
+ self._transport = MaxApiTransport(
104
+ base_url=bot.api_url,
105
+ headers=dict(getattr(bot, "headers", {})),
106
+ config=config,
107
+ session=getattr(bot, "session", None),
108
+ )
109
+ if self._transport.session is not None:
110
+ bot.session = self._transport.session
111
+ self.session = self._transport.session
112
+ return self._transport
113
+
114
+ def _bind_bot(self, *, bot: Any, payload: Any) -> None:
115
+ if payload is None:
116
+ return
117
+ if hasattr(payload, "bind_bot"):
118
+ payload.bind_bot(bot)
119
+ if hasattr(payload, "message") and getattr(payload, "message") is not None:
120
+ message = getattr(payload, "message")
121
+ if hasattr(message, "bind_bot"):
122
+ message.bind_bot(bot)
123
+ if hasattr(payload, "messages"):
124
+ for message in getattr(payload, "messages") or []:
125
+ if hasattr(message, "bind_bot"):
126
+ message.bind_bot(bot)
127
+ if hasattr(payload, "updates"):
128
+ for update in getattr(payload, "updates") or []:
129
+ message = getattr(update, "message", None)
130
+ if message is not None and hasattr(message, "bind_bot"):
131
+ message.bind_bot(bot)
132
+ callback = getattr(update, "callback", None)
133
+ if callback is not None and getattr(callback, "message", None) is not None:
134
+ callback_message = callback.message
135
+ if hasattr(callback_message, "bind_bot"):
136
+ callback_message.bind_bot(bot)