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.
- maxapi/__init__.py +49 -0
- maxapi/bot.py +674 -0
- maxapi/builders/__init__.py +23 -0
- maxapi/builders/keyboards.py +133 -0
- maxapi/builders/media.py +81 -0
- maxapi/callback_schema.py +140 -0
- maxapi/client/__init__.py +3 -0
- maxapi/client/default.py +89 -0
- maxapi/compat/__init__.py +15 -0
- maxapi/connection/__init__.py +3 -0
- maxapi/connection/base.py +136 -0
- maxapi/dispatcher.py +487 -0
- maxapi/exceptions/__init__.py +3 -0
- maxapi/exceptions/max.py +45 -0
- maxapi/filters/__init__.py +25 -0
- maxapi/filters/base.py +73 -0
- maxapi/filters/command.py +28 -0
- maxapi/filters/common.py +78 -0
- maxapi/filters/text.py +71 -0
- maxapi/fsm/__init__.py +17 -0
- maxapi/fsm/context.py +41 -0
- maxapi/fsm/filters.py +30 -0
- maxapi/fsm/middleware.py +42 -0
- maxapi/fsm/state.py +33 -0
- maxapi/fsm/storage/__init__.py +4 -0
- maxapi/fsm/storage/base.py +30 -0
- maxapi/fsm/storage/memory.py +38 -0
- maxapi/middlewares/__init__.py +3 -0
- maxapi/middlewares/base.py +34 -0
- maxapi/plugins/__init__.py +3 -0
- maxapi/plugins/base.py +19 -0
- maxapi/py.typed +1 -0
- maxapi/runners/__init__.py +4 -0
- maxapi/runners/polling.py +55 -0
- maxapi/runners/webhook.py +72 -0
- maxapi/transport/__init__.py +13 -0
- maxapi/transport/client.py +239 -0
- maxapi/transport/config.py +81 -0
- maxapi/transport/errors.py +45 -0
- maxapi/types/__init__.py +73 -0
- maxapi/types/base.py +9 -0
- maxapi/types/bot_mixin.py +14 -0
- maxapi/types/models.py +308 -0
- maxapi_sdk-0.12.0.dist-info/METADATA +270 -0
- maxapi_sdk-0.12.0.dist-info/RECORD +48 -0
- maxapi_sdk-0.12.0.dist-info/WHEEL +5 -0
- maxapi_sdk-0.12.0.dist-info/licenses/LICENSE +21 -0
- maxapi_sdk-0.12.0.dist-info/top_level.txt +1 -0
maxapi/filters/common.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..callback_schema import extract_callback_value
|
|
6
|
+
from .base import BaseFilter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ChatId(BaseFilter):
|
|
10
|
+
def __init__(self, *chat_ids: int) -> None:
|
|
11
|
+
self.chat_ids = set(chat_ids)
|
|
12
|
+
|
|
13
|
+
async def __call__(self, event: Any) -> bool:
|
|
14
|
+
chat_id = getattr(event, "chat_id", None)
|
|
15
|
+
return chat_id in self.chat_ids
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class UserId(BaseFilter):
|
|
19
|
+
def __init__(self, *user_ids: int) -> None:
|
|
20
|
+
self.user_ids = set(user_ids)
|
|
21
|
+
|
|
22
|
+
async def __call__(self, event: Any) -> bool:
|
|
23
|
+
user_id = getattr(event, "user_id", None)
|
|
24
|
+
return user_id in self.user_ids
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CallbackData(BaseFilter):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
value: str,
|
|
31
|
+
*,
|
|
32
|
+
case_sensitive: bool = True,
|
|
33
|
+
startswith: bool = False,
|
|
34
|
+
contains: bool = False,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.value = value
|
|
37
|
+
self.case_sensitive = case_sensitive
|
|
38
|
+
self.startswith = startswith
|
|
39
|
+
self.contains = contains
|
|
40
|
+
|
|
41
|
+
async def __call__(self, event: Any) -> bool:
|
|
42
|
+
callback = getattr(getattr(event, "update", None), "callback", None)
|
|
43
|
+
payload = getattr(callback, "payload", None)
|
|
44
|
+
payload_value = extract_callback_value(payload)
|
|
45
|
+
if payload_value is None:
|
|
46
|
+
return False
|
|
47
|
+
return self._compare(payload_value)
|
|
48
|
+
|
|
49
|
+
def _compare(self, other: str) -> bool:
|
|
50
|
+
left = other if self.case_sensitive else other.lower()
|
|
51
|
+
right = self.value if self.case_sensitive else self.value.lower()
|
|
52
|
+
if self.startswith:
|
|
53
|
+
return left.startswith(right)
|
|
54
|
+
if self.contains:
|
|
55
|
+
return right in left
|
|
56
|
+
return left == right
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class HasAttachments(BaseFilter):
|
|
60
|
+
async def __call__(self, event: Any) -> bool:
|
|
61
|
+
message = getattr(event, "message", None)
|
|
62
|
+
if message is None or getattr(message, "body", None) is None:
|
|
63
|
+
return False
|
|
64
|
+
attachments = getattr(message.body, "attachments", None)
|
|
65
|
+
return bool(attachments)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ChatType(BaseFilter):
|
|
69
|
+
def __init__(self, *chat_types: str) -> None:
|
|
70
|
+
self.chat_types = {value.lower() for value in chat_types}
|
|
71
|
+
|
|
72
|
+
async def __call__(self, event: Any) -> bool:
|
|
73
|
+
message = getattr(event, "message", None)
|
|
74
|
+
recipient = getattr(message, "recipient", None) if message is not None else None
|
|
75
|
+
recipient_type = getattr(recipient, "type", None)
|
|
76
|
+
if recipient_type is None:
|
|
77
|
+
return False
|
|
78
|
+
return recipient_type.lower() in self.chat_types
|
maxapi/filters/text.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Pattern
|
|
5
|
+
|
|
6
|
+
from .base import BaseFilter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Text(BaseFilter):
|
|
10
|
+
"""Точное совпадение текста сообщения."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, value: str, *, case_sensitive: bool = False, strip: bool = True) -> None:
|
|
13
|
+
self.value = value
|
|
14
|
+
self.case_sensitive = case_sensitive
|
|
15
|
+
self.strip = strip
|
|
16
|
+
|
|
17
|
+
async def __call__(self, event: Any) -> bool:
|
|
18
|
+
text = _get_message_text(event)
|
|
19
|
+
if text is None:
|
|
20
|
+
return False
|
|
21
|
+
if self.strip:
|
|
22
|
+
text = text.strip()
|
|
23
|
+
if self.case_sensitive:
|
|
24
|
+
return text == self.value
|
|
25
|
+
return text.lower() == self.value.lower()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TextContains(BaseFilter):
|
|
29
|
+
def __init__(self, value: str, *, case_sensitive: bool = False) -> None:
|
|
30
|
+
self.value = value
|
|
31
|
+
self.case_sensitive = case_sensitive
|
|
32
|
+
|
|
33
|
+
async def __call__(self, event: Any) -> bool:
|
|
34
|
+
text = _get_message_text(event)
|
|
35
|
+
if text is None:
|
|
36
|
+
return False
|
|
37
|
+
if self.case_sensitive:
|
|
38
|
+
return self.value in text
|
|
39
|
+
return self.value.lower() in text.lower()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TextStartsWith(BaseFilter):
|
|
43
|
+
def __init__(self, value: str, *, case_sensitive: bool = False) -> None:
|
|
44
|
+
self.value = value
|
|
45
|
+
self.case_sensitive = case_sensitive
|
|
46
|
+
|
|
47
|
+
async def __call__(self, event: Any) -> bool:
|
|
48
|
+
text = _get_message_text(event)
|
|
49
|
+
if text is None:
|
|
50
|
+
return False
|
|
51
|
+
if self.case_sensitive:
|
|
52
|
+
return text.startswith(self.value)
|
|
53
|
+
return text.lower().startswith(self.value.lower())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Regex(BaseFilter):
|
|
57
|
+
def __init__(self, pattern: str | Pattern[str], *, flags: int = 0) -> None:
|
|
58
|
+
self.pattern = re.compile(pattern, flags) if isinstance(pattern, str) else pattern
|
|
59
|
+
|
|
60
|
+
async def __call__(self, event: Any) -> bool:
|
|
61
|
+
text = _get_message_text(event)
|
|
62
|
+
if text is None:
|
|
63
|
+
return False
|
|
64
|
+
return self.pattern.search(text) is not None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_message_text(event: Any) -> str | None:
|
|
68
|
+
message = getattr(event, "message", None)
|
|
69
|
+
if message is None or getattr(message, "body", None) is None:
|
|
70
|
+
return None
|
|
71
|
+
return getattr(message.body, "text", None)
|
maxapi/fsm/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .context import FSMContext
|
|
2
|
+
from .filters import StateFilter
|
|
3
|
+
from .middleware import FSMMiddleware
|
|
4
|
+
from .state import State, StatesGroup
|
|
5
|
+
from .storage.base import BaseStorage, StorageKey
|
|
6
|
+
from .storage.memory import MemoryStorage
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BaseStorage",
|
|
10
|
+
"FSMContext",
|
|
11
|
+
"FSMMiddleware",
|
|
12
|
+
"MemoryStorage",
|
|
13
|
+
"State",
|
|
14
|
+
"StateFilter",
|
|
15
|
+
"StatesGroup",
|
|
16
|
+
"StorageKey",
|
|
17
|
+
]
|
maxapi/fsm/context.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .storage.base import BaseStorage, StorageKey
|
|
6
|
+
from .state import State
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FSMContext:
|
|
10
|
+
"""Контекст состояния для конкретного chat_id/user_id."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, *, storage: BaseStorage, key: StorageKey) -> None:
|
|
13
|
+
self.storage = storage
|
|
14
|
+
self.key = key
|
|
15
|
+
|
|
16
|
+
async def get_state(self) -> str | None:
|
|
17
|
+
return await self.storage.get_state(self.key)
|
|
18
|
+
|
|
19
|
+
async def set_state(self, state: State | str | None) -> None:
|
|
20
|
+
normalized = _normalize_state(state)
|
|
21
|
+
await self.storage.set_state(self.key, normalized)
|
|
22
|
+
|
|
23
|
+
async def get_data(self) -> dict[str, Any]:
|
|
24
|
+
return await self.storage.get_data(self.key)
|
|
25
|
+
|
|
26
|
+
async def set_data(self, data: dict[str, Any]) -> None:
|
|
27
|
+
await self.storage.set_data(self.key, data)
|
|
28
|
+
|
|
29
|
+
async def update_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
30
|
+
return await self.storage.update_data(self.key, kwargs)
|
|
31
|
+
|
|
32
|
+
async def clear(self) -> None:
|
|
33
|
+
await self.storage.clear(self.key)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalize_state(state: State | str | None) -> str | None:
|
|
37
|
+
if state is None:
|
|
38
|
+
return None
|
|
39
|
+
if isinstance(state, State):
|
|
40
|
+
return str(state)
|
|
41
|
+
return state
|
maxapi/fsm/filters.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..filters.base import BaseFilter
|
|
6
|
+
from .middleware import build_storage_key
|
|
7
|
+
from .state import State
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StateFilter(BaseFilter):
|
|
11
|
+
def __init__(self, *states: State | str) -> None:
|
|
12
|
+
self.states = {_normalize_state(item) for item in states}
|
|
13
|
+
|
|
14
|
+
async def __call__(self, event: Any) -> bool:
|
|
15
|
+
bot = getattr(event, "bot", None)
|
|
16
|
+
dispatcher = getattr(bot, "dispatcher", None)
|
|
17
|
+
storage = getattr(dispatcher, "fsm_storage", None)
|
|
18
|
+
if storage is None:
|
|
19
|
+
return False
|
|
20
|
+
key = build_storage_key(event)
|
|
21
|
+
if key is None:
|
|
22
|
+
return False
|
|
23
|
+
current_state = await storage.get_state(key)
|
|
24
|
+
return current_state in self.states
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_state(state: State | str) -> str:
|
|
28
|
+
if isinstance(state, State):
|
|
29
|
+
return str(state)
|
|
30
|
+
return state
|
maxapi/fsm/middleware.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..middlewares import BaseMiddleware, MiddlewareHandler
|
|
6
|
+
from .context import FSMContext
|
|
7
|
+
from .storage.base import BaseStorage, StorageKey
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FSMMiddleware(BaseMiddleware):
|
|
11
|
+
"""Middleware, который внедряет FSMContext в handlers."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, storage: BaseStorage) -> None:
|
|
14
|
+
self.storage = storage
|
|
15
|
+
|
|
16
|
+
async def __call__(
|
|
17
|
+
self,
|
|
18
|
+
handler: MiddlewareHandler,
|
|
19
|
+
event: Any,
|
|
20
|
+
data: dict[str, Any],
|
|
21
|
+
) -> Any:
|
|
22
|
+
key = build_storage_key(event)
|
|
23
|
+
if key is None:
|
|
24
|
+
data.setdefault("state", None)
|
|
25
|
+
data.setdefault("fsm_context", None)
|
|
26
|
+
data.setdefault("raw_state", None)
|
|
27
|
+
data.setdefault("state_data", {})
|
|
28
|
+
return await handler(event, data)
|
|
29
|
+
context = FSMContext(storage=self.storage, key=key)
|
|
30
|
+
data["fsm_context"] = context
|
|
31
|
+
data["state"] = context
|
|
32
|
+
data["raw_state"] = await context.get_state()
|
|
33
|
+
data["state_data"] = await context.get_data()
|
|
34
|
+
return await handler(event, data)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_storage_key(event: Any) -> StorageKey | None:
|
|
38
|
+
chat_id = getattr(event, "chat_id", None)
|
|
39
|
+
user_id = getattr(event, "user_id", None)
|
|
40
|
+
if chat_id is None and user_id is None:
|
|
41
|
+
return None
|
|
42
|
+
return StorageKey(chat_id=chat_id, user_id=user_id)
|
maxapi/fsm/state.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class State:
|
|
7
|
+
"""Описание состояния FSM."""
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.state: str | None = None
|
|
11
|
+
|
|
12
|
+
def __set_name__(self, owner: type[StatesGroup], name: str) -> None:
|
|
13
|
+
self.state = f"{owner.__name__}:{name}"
|
|
14
|
+
|
|
15
|
+
def __get__(self, instance: Any, owner: type[StatesGroup]) -> "State":
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
if self.state is None:
|
|
20
|
+
raise RuntimeError("Состояние ещё не связано с классом StatesGroup.")
|
|
21
|
+
return self.state
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class StatesGroup:
|
|
25
|
+
"""Базовый контейнер состояний."""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def states(cls) -> list[State]:
|
|
29
|
+
values: list[State] = []
|
|
30
|
+
for item in cls.__dict__.values():
|
|
31
|
+
if isinstance(item, State):
|
|
32
|
+
values.append(item)
|
|
33
|
+
return values
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True, slots=True)
|
|
8
|
+
class StorageKey:
|
|
9
|
+
chat_id: int | None
|
|
10
|
+
user_id: int | None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseStorage(Protocol):
|
|
14
|
+
async def get_state(self, key: StorageKey) -> str | None:
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
async def set_state(self, key: StorageKey, state: str | None) -> None:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None:
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
async def update_data(self, key: StorageKey, data: dict[str, Any]) -> dict[str, Any]:
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
async def clear(self, key: StorageKey) -> None:
|
|
30
|
+
...
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .base import StorageKey
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MemoryStorage:
|
|
9
|
+
"""In-memory storage для FSM."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._states: dict[StorageKey, str | None] = {}
|
|
13
|
+
self._data: dict[StorageKey, dict[str, Any]] = {}
|
|
14
|
+
|
|
15
|
+
async def get_state(self, key: StorageKey) -> str | None:
|
|
16
|
+
return self._states.get(key)
|
|
17
|
+
|
|
18
|
+
async def set_state(self, key: StorageKey, state: str | None) -> None:
|
|
19
|
+
if state is None:
|
|
20
|
+
self._states.pop(key, None)
|
|
21
|
+
return
|
|
22
|
+
self._states[key] = state
|
|
23
|
+
|
|
24
|
+
async def get_data(self, key: StorageKey) -> dict[str, Any]:
|
|
25
|
+
return dict(self._data.get(key, {}))
|
|
26
|
+
|
|
27
|
+
async def set_data(self, key: StorageKey, data: dict[str, Any]) -> None:
|
|
28
|
+
self._data[key] = dict(data)
|
|
29
|
+
|
|
30
|
+
async def update_data(self, key: StorageKey, data: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
current = await self.get_data(key)
|
|
32
|
+
current.update(data)
|
|
33
|
+
await self.set_data(key, current)
|
|
34
|
+
return current
|
|
35
|
+
|
|
36
|
+
async def clear(self, key: StorageKey) -> None:
|
|
37
|
+
self._states.pop(key, None)
|
|
38
|
+
self._data.pop(key, None)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Any, Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
MiddlewareHandler = Callable[[Any, dict[str, Any]], Awaitable[Any]]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseMiddleware:
|
|
10
|
+
"""Базовый middleware."""
|
|
11
|
+
|
|
12
|
+
async def __call__(
|
|
13
|
+
self,
|
|
14
|
+
handler: MiddlewareHandler,
|
|
15
|
+
event: Any,
|
|
16
|
+
data: dict[str, Any],
|
|
17
|
+
) -> Any:
|
|
18
|
+
return await handler(event, data)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FunctionMiddleware(BaseMiddleware):
|
|
22
|
+
def __init__(self, callback: Callable[[MiddlewareHandler, Any, dict[str, Any]], Any]) -> None:
|
|
23
|
+
self.callback = callback
|
|
24
|
+
|
|
25
|
+
async def __call__(
|
|
26
|
+
self,
|
|
27
|
+
handler: MiddlewareHandler,
|
|
28
|
+
event: Any,
|
|
29
|
+
data: dict[str, Any],
|
|
30
|
+
) -> Any:
|
|
31
|
+
result = self.callback(handler, event, data)
|
|
32
|
+
if inspect.isawaitable(result):
|
|
33
|
+
return await result
|
|
34
|
+
return result
|
maxapi/plugins/base.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PluginProtocol(Protocol):
|
|
7
|
+
name: str
|
|
8
|
+
|
|
9
|
+
def setup(self, router) -> None:
|
|
10
|
+
...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BasePlugin:
|
|
14
|
+
"""Базовый плагин для расширения Router/Dispatcher."""
|
|
15
|
+
|
|
16
|
+
name = "base"
|
|
17
|
+
|
|
18
|
+
def setup(self, router) -> None: # pragma: no cover - interface hook
|
|
19
|
+
raise NotImplementedError
|
maxapi/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from ..types import UpdateType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PollingRunner:
|
|
10
|
+
"""Отдельный runtime-класс для long polling."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
*,
|
|
15
|
+
bot,
|
|
16
|
+
dispatcher,
|
|
17
|
+
limit: int = 100,
|
|
18
|
+
timeout: int = 30,
|
|
19
|
+
allowed_updates: Iterable[UpdateType | str] | None = None,
|
|
20
|
+
retry_delay: float = 2.0,
|
|
21
|
+
raise_exceptions: bool = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.bot = bot
|
|
24
|
+
self.dispatcher = dispatcher
|
|
25
|
+
self.limit = limit
|
|
26
|
+
self.timeout = timeout
|
|
27
|
+
self.allowed_updates = list(allowed_updates or [])
|
|
28
|
+
self.retry_delay = retry_delay
|
|
29
|
+
self.raise_exceptions = raise_exceptions
|
|
30
|
+
self._stop_event = asyncio.Event()
|
|
31
|
+
|
|
32
|
+
async def run_once(self) -> None:
|
|
33
|
+
page = await self.bot.get_updates(
|
|
34
|
+
marker=self.bot.marker_updates,
|
|
35
|
+
limit=self.limit,
|
|
36
|
+
timeout=self.timeout,
|
|
37
|
+
types=self.allowed_updates or None,
|
|
38
|
+
)
|
|
39
|
+
for update in page.updates:
|
|
40
|
+
await self.dispatcher.process_update(update, bot=self.bot)
|
|
41
|
+
self.bot.marker_updates = page.marker
|
|
42
|
+
|
|
43
|
+
async def start(self) -> None:
|
|
44
|
+
while not self._stop_event.is_set():
|
|
45
|
+
try:
|
|
46
|
+
await self.run_once()
|
|
47
|
+
except asyncio.CancelledError:
|
|
48
|
+
raise
|
|
49
|
+
except Exception:
|
|
50
|
+
if self.raise_exceptions:
|
|
51
|
+
raise
|
|
52
|
+
await asyncio.sleep(self.retry_delay)
|
|
53
|
+
|
|
54
|
+
async def stop(self) -> None:
|
|
55
|
+
self._stop_event.set()
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from ..exceptions import MaxConnection
|
|
4
|
+
|
|
5
|
+
try: # pragma: no cover - optional dependency at import time
|
|
6
|
+
from fastapi import FastAPI, Header, HTTPException, Request
|
|
7
|
+
except ImportError: # pragma: no cover
|
|
8
|
+
FastAPI = None
|
|
9
|
+
Header = None
|
|
10
|
+
HTTPException = None
|
|
11
|
+
Request = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WebhookRunner:
|
|
15
|
+
"""Отдельный runtime-класс для webhook."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
*,
|
|
20
|
+
bot,
|
|
21
|
+
dispatcher,
|
|
22
|
+
path: str = "/webhook",
|
|
23
|
+
secret: str | None = None,
|
|
24
|
+
host: str = "127.0.0.1",
|
|
25
|
+
port: int = 8080,
|
|
26
|
+
log_level: str = "info",
|
|
27
|
+
) -> None:
|
|
28
|
+
self.bot = bot
|
|
29
|
+
self.dispatcher = dispatcher
|
|
30
|
+
self.path = path
|
|
31
|
+
self.secret = secret
|
|
32
|
+
self.host = host
|
|
33
|
+
self.port = port
|
|
34
|
+
self.log_level = log_level
|
|
35
|
+
|
|
36
|
+
def create_app(self):
|
|
37
|
+
if FastAPI is None or Header is None or HTTPException is None or Request is None:
|
|
38
|
+
raise MaxConnection(
|
|
39
|
+
"Для webhook runtime требуется fastapi. Установите maxapi[webhook]."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
app = FastAPI()
|
|
43
|
+
|
|
44
|
+
@app.post(self.path)
|
|
45
|
+
async def handle_update(
|
|
46
|
+
request: Request,
|
|
47
|
+
x_max_bot_api_secret: str | None = Header(default=None),
|
|
48
|
+
) -> dict[str, Any]:
|
|
49
|
+
if self.secret is not None and x_max_bot_api_secret != self.secret:
|
|
50
|
+
raise HTTPException(status_code=403, detail="Invalid webhook secret")
|
|
51
|
+
payload = await request.json()
|
|
52
|
+
await self.dispatcher.process_update(payload, bot=self.bot)
|
|
53
|
+
return {"ok": True}
|
|
54
|
+
|
|
55
|
+
return app
|
|
56
|
+
|
|
57
|
+
async def start(self) -> None:
|
|
58
|
+
try:
|
|
59
|
+
import uvicorn
|
|
60
|
+
except ImportError as exc: # pragma: no cover
|
|
61
|
+
raise MaxConnection(
|
|
62
|
+
"Для webhook runtime требуется uvicorn. Установите maxapi[webhook]."
|
|
63
|
+
) from exc
|
|
64
|
+
app = self.create_app()
|
|
65
|
+
config = uvicorn.Config(
|
|
66
|
+
app=app,
|
|
67
|
+
host=self.host,
|
|
68
|
+
port=self.port,
|
|
69
|
+
log_level=self.log_level,
|
|
70
|
+
)
|
|
71
|
+
server = uvicorn.Server(config)
|
|
72
|
+
await server.serve()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .client import MaxApiTransport, TransportResult
|
|
2
|
+
from .config import RetryPolicy, TransportConfig
|
|
3
|
+
from .errors import RateLimitExceededError, ResponseDecodeError, ServerResponseError
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"MaxApiTransport",
|
|
7
|
+
"RateLimitExceededError",
|
|
8
|
+
"ResponseDecodeError",
|
|
9
|
+
"RetryPolicy",
|
|
10
|
+
"ServerResponseError",
|
|
11
|
+
"TransportConfig",
|
|
12
|
+
"TransportResult",
|
|
13
|
+
]
|