Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:

- name: Run linters
run: |
poetry run ./scripts/docs-lint
poetry run bash ./scripts/docs-lint
771 changes: 173 additions & 598 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions pybotx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
ChatCreationError,
ChatCreationProhibitedError,
InvalidUsersListError,
ThreadCreationError,
ThreadCreationProhibitedError,
)
from pybotx.client.exceptions.common import (
ChatNotFoundError,
Expand Down Expand Up @@ -260,6 +262,9 @@
"SyncSmartAppEventHandlerFunc",
"SyncSmartAppEventHandlerNotFoundError",
"SyncSourceTypes",
"ThreadCreationError",
"ThreadCreationEventNotFoundError",
"ThreadCreationProhibitedError",
"UnknownBotAccountError",
"UnknownSystemEventError",
"UnsupportedBotAPIVersionError",
Expand Down
10 changes: 5 additions & 5 deletions pybotx/async_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,30 @@
try:
from typing import Protocol
except ImportError:
from typing_extensions import Protocol # type: ignore # noqa: WPS440
from typing_extensions import Protocol # type: ignore


class AsyncBufferBase(Protocol):
async def seek(
self,
cursor: int,
whence: int = os.SEEK_SET,
) -> int: ...
) -> int: ... # pragma: no cover

async def tell(self) -> int: ...
async def tell(self) -> int: ... # pragma: no cover


class AsyncBufferWritable(AsyncBufferBase):
@abc.abstractmethod
async def write(self, content: bytes) -> int: ...
async def write(self, content: bytes) -> int: ... # pragma: no cover


class AsyncBufferReadable(AsyncBufferBase):
@abc.abstractmethod
async def read(
self,
bytes_to_read: Optional[int] = None,
) -> bytes: ...
) -> bytes: ... # pragma: no cover


async def get_file_size(async_buffer: AsyncBufferReadable) -> int:
Expand Down
105 changes: 81 additions & 24 deletions pybotx/bot/bot.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
from asyncio import Task
from collections.abc import AsyncIterable, AsyncIterator, Iterator, Mapping, Sequence
from contextlib import asynccontextmanager
from datetime import datetime
from types import SimpleNamespace
from typing import (
Any,
AsyncIterable,
AsyncIterator,
Dict,
Iterator,
List,
Mapping,
Optional,
Sequence,
Set,
Tuple,
Union,
Expand All @@ -23,11 +19,11 @@
import jwt
from aiocsv.readers import AsyncDictReader
from aiofiles.tempfile import NamedTemporaryFile, TemporaryDirectory
from pydantic import ValidationError, parse_obj_as

from pybotx.async_buffer import AsyncBufferReadable, AsyncBufferWritable
from pybotx.bot.bot_accounts_storage import BotAccountsStorage
from pybotx.bot.callbacks.callback_manager import CallbackManager
from pydantic import TypeAdapter
from pybotx.bot.callbacks.callback_memory_repo import CallbackMemoryRepo
from pybotx.bot.callbacks.callback_repo_proto import CallbackRepoProto
from pybotx.bot.contextvars import bot_id_var, chat_id_var
Expand All @@ -53,10 +49,18 @@
BotXAPIChatInfoRequestPayload,
ChatInfoMethod,
)
from pybotx.client.chats_api.personal_chat import (
BotXAPIPersonalChatRequestPayload,
PersonalChatMethod,
)
from pybotx.client.chats_api.create_chat import (
BotXAPICreateChatRequestPayload,
CreateChatMethod,
)
from pybotx.client.chats_api.create_thread import (
BotXAPICreateThreadRequestPayload,
CreateThreadMethod,
)
from pybotx.client.chats_api.disable_stealth import (
BotXAPIDisableStealthRequestPayload,
DisableStealthMethod,
Expand Down Expand Up @@ -234,8 +238,12 @@
from pybotx.models.bot_account import BotAccountWithSecret
from pybotx.models.bot_catalog import BotsListItem
from pybotx.models.chats import ChatInfo, ChatListItem
from pybotx.models.commands import BotAPICommand, BotCommand
from pybotx.models.enums import ChatTypes
from pybotx.models.commands import (
BotAPISystemEvent,
BotAPIIncomingMessage,
BotCommand,
)
from pybotx.models.enums import BotAPICommandTypes, ChatTypes
from pybotx.models.message.edit_message import EditMessage
from pybotx.models.message.markup import BubbleMarkup, KeyboardMarkup
from pybotx.models.message.message_status import MessageStatus
Expand All @@ -256,6 +264,7 @@
)
from pybotx.models.system_events.smartapp_event import SmartAppEvent
from pybotx.models.users import UserFromCSV, UserFromSearch
from pydantic import ValidationError

MissingOptionalAttachment = MissingOptional[
Union[IncomingFileAttachment, OutgoingAttachment]
Expand Down Expand Up @@ -312,11 +321,13 @@ def async_execute_raw_bot_command(
self._verify_request(request_headers, trusted_issuers=trusted_issuers)

try:
bot_api_command: BotAPICommand = parse_obj_as(
# Same ignore as in pydantic
BotAPICommand, # type: ignore[arg-type]
raw_bot_command,
)
command_type = raw_bot_command.get("command", {}).get("command_type")
if command_type == BotAPICommandTypes.USER:
bot_api_command = BotAPIIncomingMessage.model_validate(raw_bot_command)
else:
bot_api_command = TypeAdapter(BotAPISystemEvent).validate_python(
raw_bot_command
)
except ValidationError as validation_exc:
raise ValueError("Bot command validation error") from validation_exc

Expand Down Expand Up @@ -350,9 +361,8 @@ async def sync_execute_raw_smartapp_event(
self._verify_request(request_headers, trusted_issuers=trusted_issuers)

try:
bot_api_smartapp_event: BotAPISyncSmartAppEvent = parse_obj_as(
BotAPISyncSmartAppEvent,
raw_smartapp_event,
bot_api_smartapp_event = BotAPISyncSmartAppEvent.model_validate(
raw_smartapp_event
)
except ValidationError as validation_exc:
raise ValueError(
Expand Down Expand Up @@ -388,7 +398,9 @@ async def raw_get_status(
self._verify_request(request_headers, trusted_issuers=trusted_issuers)

try:
bot_api_status_recipient = BotAPIStatusRecipient.parse_obj(query_params)
bot_api_status_recipient = BotAPIStatusRecipient.model_validate(
query_params
)
except ValidationError as exc:
raise ValueError("Status request validation error") from exc

Expand All @@ -415,9 +427,7 @@ async def set_raw_botx_method_result(
if verify_request:
self._verify_request(request_headers, trusted_issuers=trusted_issuers)

callback: BotXMethodCallback = parse_obj_as(
# Same ignore as in pydantic
BotXMethodCallback, # type: ignore[arg-type]
callback: BotXMethodCallback = TypeAdapter(BotXMethodCallback).validate_python(
raw_botx_method_result,
)

Expand Down Expand Up @@ -548,7 +558,7 @@ async def answer_message(
:return: Notification sync_id.
"""

try: # noqa: WPS229
try:
bot_id = bot_id_var.get()
chat_id = chat_id_var.get()
except LookupError as exc:
Expand Down Expand Up @@ -1007,6 +1017,29 @@ async def chat_info(

return botx_api_chat_info.to_domain()

async def personal_chat(
self,
*,
bot_id: UUID,
user_huid: UUID,
) -> ChatInfo:
"""Get personal chat between bot and user.

:param bot_id: Bot which should perform the request.
:param user_huid: User identifier.

:return: Chat information.
"""

method = PersonalChatMethod(
bot_id, self._httpx_client, self._bot_accounts_storage
)

payload = BotXAPIPersonalChatRequestPayload.from_domain(user_huid=user_huid)
botx_api_personal_chat = await method.execute(payload)

return botx_api_personal_chat.to_domain()

async def add_users_to_chat(
self,
*,
Expand Down Expand Up @@ -1145,6 +1178,7 @@ async def create_chat(
huids: List[UUID],
description: Optional[str] = None,
shared_history: Missing[bool] = Undefined,
avatar: Optional[str] = None,
) -> UUID:
"""Create chat.

Expand All @@ -1155,6 +1189,7 @@ async def create_chat(
:param description: Chat description.
:param shared_history: (BotX default: False) Open old chat history for
new added users.
:param avatar: Chat avatar in data URL format (RFC 2397).

:return: Created chat uuid.
"""
Expand All @@ -1165,17 +1200,39 @@ async def create_chat(
self._bot_accounts_storage,
)

payload = BotXAPICreateChatRequestPayload.from_domain(
payload = BotXAPICreateChatRequestPayload(
name=name,
chat_type=chat_type,
huids=huids,
members=huids,
shared_history=shared_history,
description=description,
avatar=avatar,
)
botx_api_chat_id = await method.execute(payload)

return botx_api_chat_id.to_domain()

async def create_thread(self, bot_id: UUID, sync_id: UUID) -> UUID:
"""
Create thread.

:param bot_id: Bot which should perform the request.
:param sync_id: Message for which thread should be created

:return: Created thread uuid.
"""

method = CreateThreadMethod(
bot_id,
self._httpx_client,
self._bot_accounts_storage,
)

payload = BotXAPICreateThreadRequestPayload.from_domain(sync_id=sync_id)
botx_api_thread_id = await method.execute(payload)

return botx_api_thread_id.to_domain()

async def pin_message(
self,
*,
Expand Down Expand Up @@ -2066,7 +2123,7 @@ async def collect_metric(
)
await method.execute(payload)

def _verify_request( # noqa: WPS231, WPS238
def _verify_request(
self,
headers: Optional[Mapping[str, str]],
*,
Expand Down
2 changes: 1 addition & 1 deletion pybotx/bot/bot_accounts_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def iter_bot_accounts(self) -> Iterator[BotAccountWithSecret]:

def get_cts_url(self, bot_id: UUID) -> str:
bot_account = self.get_bot_account(bot_id)
return bot_account.cts_url
return str(bot_account.cts_url)

def set_token(self, bot_id: UUID, token: str) -> None:
self._auth_tokens[bot_id] = token
Expand Down
2 changes: 1 addition & 1 deletion pybotx/bot/handler_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async def handle_bot_command(self, bot_command: BotCommand, bot: "Bot") -> None:

elif isinstance(
bot_command,
SystemEvent.__args__, # type: ignore [attr-defined] # noqa: WPS609
SystemEvent.__args__, # type: ignore [attr-defined]
):
event_handler = self._get_system_event_handler_or_none(bot_command)
if event_handler:
Expand Down
2 changes: 1 addition & 1 deletion pybotx/bot/middlewares/exception_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ async def dispatch(
if exception_handler is None:
raise message_handler_exc

try: # noqa: WPS505
try:
await exception_handler(message, bot, message_handler_exc)
except Exception as error_handler_exc:
exc_name = type(message_handler_exc).__name__
Expand Down
10 changes: 5 additions & 5 deletions pybotx/client/botx_method.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import httpx
from mypy_extensions import Arg
from pydantic import ValidationError, parse_obj_as

from pybotx.bot.bot_accounts_storage import BotAccountsStorage
from pybotx.bot.callbacks.callback_manager import CallbackManager
Expand All @@ -32,6 +31,7 @@
BotAPIMethodFailedCallback,
BotXMethodCallback,
)
from pydantic import ValidationError

StatusHandler = Callable[[Arg(httpx.Response, "response")], NoReturn] # noqa: F821
StatusHandlers = Mapping[int, StatusHandler]
Expand Down Expand Up @@ -106,7 +106,7 @@ def _verify_and_extract_api_model(
)

try:
api_model = parse_obj_as(model_cls, raw_model)
api_model = model_cls.model_validate(raw_model)
except ValidationError as validation_exc:
raise InvalidBotXResponsePayloadError(response) from validation_exc

Expand Down Expand Up @@ -155,9 +155,9 @@ async def _process_callback(
callback_timeout: Optional[float],
default_callback_timeout: float,
) -> Optional[BotXMethodCallback]:
assert (
self._callbacks_manager is not None
), "CallbackManager hasn't been passed to this method"
assert self._callbacks_manager is not None, (
"CallbackManager hasn't been passed to this method"
)

await self._callbacks_manager.create_botx_method_callback(sync_id)

Expand Down
Loading
Loading