Skip to content

Commit 0059a7b

Browse files
0.76.4
## Added - Добавлен VoEx API: - `Bot.get_call(bot_id, call_id)` для получения данных звонка. - `Bot.get_conference(bot_id, call_id)` для получения данных конференции. - Новые доменные модели `Call` и `Conference`. - Новые исключения `CallNotFoundError` и `ConferenceNotFoundError` для ответов `404`. - В `search_user_by_email` и `search_user_by_emails` добавлены параметры: - `trusts_search` - `partial_response` По умолчанию параметры отключены и не отправляются в запросе, что сохраняет обратную совместимость. ## Fixed - Исправлено определение MIME type для вложений с расширениями в любом регистре. Например, `.PNG`, `.JPG`, `.PDF` теперь корректно мапятся на ожидаемый MIME type. - Исправлена сериализация markup link button без `command`. Поле `command` теперь может отсутствовать, что нужно для ссылочных кнопок. - Актуализирован Users API: - `update_user_profile` теперь мапит `503 Service Unavailable` в отдельное исключение `UserProfileUpdateUnavailableError`. - `UserProfileUpdateUnavailableError` экспортируется из публичного API `pybotx`. - Числовые значения `ip_phone`, `other_ip_phone`, `other_phone` в ответах поиска пользователей приводятся к строке. - CSV-парсинг пользователей стал устойчивее к отсутствию части необязательных колонок: avatar, office, phones, description, personnel number и др.
1 parent 0dcb0c5 commit 0059a7b

26 files changed

Lines changed: 765 additions & 25 deletions

‎pybotx/__init__.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@
6464
FinalRecipientsListEmptyError,
6565
StealthModeDisabledError,
6666
)
67-
from pybotx.client.exceptions.users import UserNotFoundError
67+
from pybotx.client.exceptions.users import (
68+
UserNotFoundError,
69+
UserProfileUpdateUnavailableError,
70+
)
6871
from pybotx.client.smartapps_api.exceptions import SyncSmartAppEventHandlerNotFoundError
6972
from pybotx.client.smartapps_api.smartapp_manifest import (
7073
SmartappManifest,
@@ -301,6 +304,7 @@
301304
"UserFromSearch",
302305
"UserKinds",
303306
"UserNotFoundError",
307+
"UserProfileUpdateUnavailableError",
304308
"UserSender",
305309
"Video",
306310
"Voice",

‎pybotx/bot/bot.py‎

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@
224224
BotXAPIUsersAsCSVRequestPayload,
225225
UsersAsCSVMethod,
226226
)
227+
from pybotx.client.voex_api.get_call import (
228+
BotXAPIGetCallRequestPayload,
229+
GetCallMethod,
230+
)
231+
from pybotx.client.voex_api.get_conference import (
232+
BotXAPIGetConferenceRequestPayload,
233+
GetConferenceMethod,
234+
)
227235
from pybotx.constants import BOTX_DEFAULT_TIMEOUT, STICKER_PACKS_PER_PAGE
228236
from pybotx.converters import optional_sequence_to_list
229237
from pybotx.image_validators import (
@@ -236,12 +244,14 @@
236244
from pybotx.models.attachments import IncomingFileAttachment, OutgoingAttachment
237245
from pybotx.models.bot_account import BotAccountWithSecret
238246
from pybotx.models.bot_catalog import BotsListItem
247+
from pybotx.models.call import Call
239248
from pybotx.models.chats import ChatInfo, ChatLink, ChatListItem
240249
from pybotx.models.commands import (
241250
BotAPISystemEvent,
242251
BotAPIIncomingMessage,
243252
BotCommand,
244253
)
254+
from pybotx.models.conference import Conference
245255
from pybotx.models.enums import BotAPICommandTypes, ChatLinkTypes, ChatTypes
246256
from pybotx.models.message.edit_message import EditMessage
247257
from pybotx.models.message.markup import BubbleMarkup, KeyboardMarkup
@@ -1395,6 +1405,56 @@ async def pin_message(
13951405

13961406
await method.execute(payload)
13971407

1408+
async def get_call(
1409+
self,
1410+
*,
1411+
bot_id: UUID,
1412+
call_id: UUID,
1413+
) -> Call:
1414+
"""Get call.
1415+
1416+
:param bot_id: Bot which should perform the request.
1417+
:param call_id: Call id.
1418+
1419+
:return: Call.
1420+
"""
1421+
method = GetCallMethod(
1422+
bot_id,
1423+
self._httpx_client,
1424+
self._bot_accounts_storage,
1425+
)
1426+
payload = BotXAPIGetCallRequestPayload.from_domain(
1427+
call_id=call_id,
1428+
)
1429+
botx_call = await method.execute(payload)
1430+
1431+
return botx_call.to_domain()
1432+
1433+
async def get_conference(
1434+
self,
1435+
*,
1436+
bot_id: UUID,
1437+
call_id: UUID,
1438+
) -> Conference:
1439+
"""Get Conference.
1440+
1441+
:param bot_id: Bot which should perform the request.
1442+
:param call_id: Call id.
1443+
1444+
:return: Conference.
1445+
"""
1446+
method = GetConferenceMethod(
1447+
bot_id,
1448+
self._httpx_client,
1449+
self._bot_accounts_storage,
1450+
)
1451+
payload = BotXAPIGetConferenceRequestPayload.from_domain(
1452+
call_id=call_id,
1453+
)
1454+
botx_conference = await method.execute(payload)
1455+
1456+
return botx_conference.to_domain()
1457+
13981458
async def unpin_message(
13991459
self,
14001460
*,
@@ -1421,11 +1481,15 @@ async def search_user_by_emails(
14211481
*,
14221482
bot_id: UUID,
14231483
emails: list[str],
1484+
trusts_search: bool = False,
1485+
partial_response: bool = False,
14241486
) -> list[UserFromSearch]:
14251487
"""Search user by emails for search.
14261488
14271489
:param bot_id: Bot which should perform the request.
14281490
:param emails: User emails.
1491+
:param trusts_search: Search users on trusted servers.
1492+
:param partial_response: Return local results if trusted server lookup fails.
14291493
14301494
:return: Search result with user information.
14311495
"""
@@ -1435,7 +1499,11 @@ async def search_user_by_emails(
14351499
self._httpx_client,
14361500
self._bot_accounts_storage,
14371501
)
1438-
payload = BotXAPISearchUserByEmailsRequestPayload.from_domain(emails=emails)
1502+
payload = BotXAPISearchUserByEmailsRequestPayload.from_domain(
1503+
emails=emails,
1504+
trusts_search=trusts_search,
1505+
partial_response=partial_response,
1506+
)
14391507

14401508
botx_api_users_from_search = await method.execute(payload)
14411509

@@ -1447,6 +1515,8 @@ async def search_user_by_email_post(
14471515
*,
14481516
bot_id: UUID,
14491517
email: str,
1518+
trusts_search: bool = False,
1519+
partial_response: bool = False,
14501520
) -> UserFromSearch:
14511521
"""Search user by email for search.
14521522
@@ -1455,6 +1525,8 @@ async def search_user_by_email_post(
14551525
14561526
:param bot_id: Bot which should perform the request.
14571527
:param email: User email.
1528+
:param trusts_search: Search users on trusted servers.
1529+
:param partial_response: Return local results if trusted server lookup fails.
14581530
14591531
:return: User information.
14601532
"""
@@ -1464,7 +1536,11 @@ async def search_user_by_email_post(
14641536
self._httpx_client,
14651537
self._bot_accounts_storage,
14661538
)
1467-
payload = BotXAPISearchUserByEmailRequestPayload.from_domain(email=email)
1539+
payload = BotXAPISearchUserByEmailRequestPayload.from_domain(
1540+
email=email,
1541+
trusts_search=trusts_search,
1542+
partial_response=partial_response,
1543+
)
14681544

14691545
botx_api_user_from_search = await method.execute(payload)
14701546

‎pybotx/client/exceptions/users.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ class InvalidProfileDataError(BaseClientError):
1111

1212
class NoUserKindSelectedError(BaseClientError):
1313
"""No user kind selected."""
14+
15+
16+
class UserProfileUpdateUnavailableError(BaseClientError):
17+
"""User profile update service is unavailable."""

‎pybotx/client/users_api/search_user_by_email.py‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,27 @@
99
BotXAPISearchUserResponsePayload,
1010
)
1111
from pybotx.logger import logger
12+
from pybotx.missing import Missing, Undefined
1213
from pybotx.models.api_base import UnverifiedPayloadBaseModel
1314

1415

1516
class BotXAPISearchUserByEmailRequestPayload(UnverifiedPayloadBaseModel):
1617
email: str
18+
trusts_search: Missing[bool] = Undefined
19+
partial_response: Missing[bool] = Undefined
1720

1821
@classmethod
19-
def from_domain(cls, email: str) -> "BotXAPISearchUserByEmailRequestPayload":
20-
return cls(email=email)
22+
def from_domain(
23+
cls,
24+
email: str,
25+
trusts_search: bool = False,
26+
partial_response: bool = False,
27+
) -> "BotXAPISearchUserByEmailRequestPayload":
28+
return cls(
29+
email=email,
30+
trusts_search=trusts_search or Undefined,
31+
partial_response=partial_response or Undefined,
32+
)
2133

2234

2335
class SearchUserByEmailMethod(AuthorizedBotXMethod):
@@ -67,7 +79,14 @@ async def execute(
6779
path = "/api/v3/botx/users/by_email"
6880

6981
email = payload.email
70-
request_json = {"emails": [email]}
82+
request_json = {
83+
"emails": [email],
84+
"trusts_search": payload.trusts_search,
85+
"partial_response": payload.partial_response,
86+
}
87+
request_json = {
88+
key: value for key, value in request_json.items() if value is not Undefined
89+
}
7190

7291
response = await self._botx_method_call(
7392
"POST",

‎pybotx/client/users_api/search_user_by_emails.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,27 @@
33
from pybotx.client.users_api.user_from_search import (
44
BotXAPISearchUserByEmailsResponsePayload,
55
)
6+
from pybotx.missing import Missing, Undefined
67
from pybotx.models.api_base import UnverifiedPayloadBaseModel
78

89

910
class BotXAPISearchUserByEmailsRequestPayload(UnverifiedPayloadBaseModel):
1011
emails: list[str]
12+
trusts_search: Missing[bool] = Undefined
13+
partial_response: Missing[bool] = Undefined
1114

1215
@classmethod
1316
def from_domain(
1417
cls,
1518
emails: list[str],
19+
trusts_search: bool = False,
20+
partial_response: bool = False,
1621
) -> "BotXAPISearchUserByEmailsRequestPayload":
17-
return cls(emails=emails)
22+
return cls(
23+
emails=emails,
24+
trusts_search=trusts_search or Undefined,
25+
partial_response=partial_response or Undefined,
26+
)
1827

1928

2029
class SearchUserByEmailsMethod(AuthorizedBotXMethod):

‎pybotx/client/users_api/update_user_profile.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33

44
from pybotx.client.authorized_botx_method import AuthorizedBotXMethod
55
from pybotx.client.botx_method import response_exception_thrower
6-
from pybotx.client.exceptions.users import InvalidProfileDataError, UserNotFoundError
6+
from pybotx.client.exceptions.users import (
7+
InvalidProfileDataError,
8+
UserNotFoundError,
9+
UserProfileUpdateUnavailableError,
10+
)
711
from pybotx.missing import Missing, Undefined
812
from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel
913
from pybotx.models.attachments import (
@@ -67,6 +71,7 @@ class UpdateUsersProfileMethod(AuthorizedBotXMethod):
6771
**AuthorizedBotXMethod.status_handlers,
6872
400: response_exception_thrower(InvalidProfileDataError),
6973
404: response_exception_thrower(UserNotFoundError),
74+
503: response_exception_thrower(UserProfileUpdateUnavailableError),
7075
}
7176

7277
async def execute(

‎pybotx/client/users_api/user_from_csv.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ class BotXAPIUserFromCSVResult(VerifiedPayloadBaseModel):
2323
company: str | None = Field(alias="Company")
2424
department: str | None = Field(alias="Department")
2525
position: str | None = Field(alias="Position")
26-
avatar: str | None = Field(alias="Avatar")
27-
avatar_preview: str | None = Field(alias="Avatar preview")
28-
office: str | None = Field(alias="Office")
26+
avatar: str | None = Field(default=None, alias="Avatar")
27+
avatar_preview: str | None = Field(default=None, alias="Avatar preview")
28+
office: str | None = Field(default=None, alias="Office")
2929
manager: str | None = Field(alias="Manager")
3030
manager_huid: UUID | None = Field(alias="Manager HUID")
31-
description: str | None = Field(alias="Description")
32-
phone: str | None = Field(alias="Phone")
33-
other_phone: str | None = Field(alias="Other phone")
34-
ip_phone: str | None = Field(alias="IP phone")
35-
other_ip_phone: str | None = Field(alias="Other IP phone")
36-
personnel_number: str | None = Field(alias="Personnel number")
31+
description: str | None = Field(default=None, alias="Description")
32+
phone: str | None = Field(default=None, alias="Phone")
33+
other_phone: str | None = Field(default=None, alias="Other phone")
34+
ip_phone: str | None = Field(default=None, alias="IP phone")
35+
other_ip_phone: str | None = Field(default=None, alias="Other IP phone")
36+
personnel_number: str | None = Field(default=None, alias="Personnel number")
3737

3838
@field_validator(
3939
"email",

‎pybotx/client/users_api/user_from_search.py‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pybotx.models.api_base import VerifiedPayloadBaseModel
66
from pybotx.models.enums import APIUserKinds, convert_user_kind_to_domain
77
from pybotx.models.users import UserFromSearch
8-
from pydantic import Field
8+
from pydantic import Field, field_validator
99

1010

1111
class BotXAPISearchUserResult(VerifiedPayloadBaseModel):
@@ -32,6 +32,14 @@ class BotXAPISearchUserResult(VerifiedPayloadBaseModel):
3232
created_at: datetime | None = None
3333
updated_at: datetime | None = None
3434

35+
@field_validator("ip_phone", "other_ip_phone", "other_phone", mode="before")
36+
@classmethod
37+
def convert_phone_to_string(cls, value: str | int | None) -> str | None:
38+
if value is None:
39+
return None
40+
41+
return str(value)
42+
3543

3644
class BotXAPISearchUserResponsePayload(VerifiedPayloadBaseModel):
3745
status: Literal["ok"]

‎pybotx/client/voex_api/__init__.py‎

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from pybotx.client.exceptions.base import BaseClientError
2+
3+
4+
class ConferenceNotFoundError(BaseClientError):
5+
"""Conference with specified call_id not found."""
6+
7+
8+
class CallNotFoundError(BaseClientError):
9+
"""Call with specified call_id not found."""

0 commit comments

Comments
 (0)