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
6 changes: 5 additions & 1 deletion pybotx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@
FinalRecipientsListEmptyError,
StealthModeDisabledError,
)
from pybotx.client.exceptions.users import UserNotFoundError
from pybotx.client.exceptions.users import (
UserNotFoundError,
UserProfileUpdateUnavailableError,
)
from pybotx.client.smartapps_api.exceptions import SyncSmartAppEventHandlerNotFoundError
from pybotx.client.smartapps_api.smartapp_manifest import (
SmartappManifest,
Expand Down Expand Up @@ -301,6 +304,7 @@
"UserFromSearch",
"UserKinds",
"UserNotFoundError",
"UserProfileUpdateUnavailableError",
"UserSender",
"Video",
"Voice",
Expand Down
20 changes: 18 additions & 2 deletions pybotx/bot/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,11 +1421,15 @@ async def search_user_by_emails(
*,
bot_id: UUID,
emails: list[str],
trusts_search: bool = False,
partial_response: bool = False,
) -> list[UserFromSearch]:
"""Search user by emails for search.

:param bot_id: Bot which should perform the request.
:param emails: User emails.
:param trusts_search: Search users on trusted servers.
:param partial_response: Return local results if trusted server lookup fails.

:return: Search result with user information.
"""
Expand All @@ -1435,7 +1439,11 @@ async def search_user_by_emails(
self._httpx_client,
self._bot_accounts_storage,
)
payload = BotXAPISearchUserByEmailsRequestPayload.from_domain(emails=emails)
payload = BotXAPISearchUserByEmailsRequestPayload.from_domain(
emails=emails,
trusts_search=trusts_search,
partial_response=partial_response,
)

botx_api_users_from_search = await method.execute(payload)

Expand All @@ -1447,6 +1455,8 @@ async def search_user_by_email_post(
*,
bot_id: UUID,
email: str,
trusts_search: bool = False,
partial_response: bool = False,
) -> UserFromSearch:
"""Search user by email for search.

Expand All @@ -1455,6 +1465,8 @@ async def search_user_by_email_post(

:param bot_id: Bot which should perform the request.
:param email: User email.
:param trusts_search: Search users on trusted servers.
:param partial_response: Return local results if trusted server lookup fails.

:return: User information.
"""
Expand All @@ -1464,7 +1476,11 @@ async def search_user_by_email_post(
self._httpx_client,
self._bot_accounts_storage,
)
payload = BotXAPISearchUserByEmailRequestPayload.from_domain(email=email)
payload = BotXAPISearchUserByEmailRequestPayload.from_domain(
email=email,
trusts_search=trusts_search,
partial_response=partial_response,
)

botx_api_user_from_search = await method.execute(payload)

Expand Down
4 changes: 4 additions & 0 deletions pybotx/client/exceptions/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ class InvalidProfileDataError(BaseClientError):

class NoUserKindSelectedError(BaseClientError):
"""No user kind selected."""


class UserProfileUpdateUnavailableError(BaseClientError):
"""User profile update service is unavailable."""
25 changes: 22 additions & 3 deletions pybotx/client/users_api/search_user_by_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,27 @@
BotXAPISearchUserResponsePayload,
)
from pybotx.logger import logger
from pybotx.missing import Missing, Undefined
from pybotx.models.api_base import UnverifiedPayloadBaseModel


class BotXAPISearchUserByEmailRequestPayload(UnverifiedPayloadBaseModel):
email: str
trusts_search: Missing[bool] = Undefined
partial_response: Missing[bool] = Undefined

@classmethod
def from_domain(cls, email: str) -> "BotXAPISearchUserByEmailRequestPayload":
return cls(email=email)
def from_domain(
cls,
email: str,
trusts_search: bool = False,
partial_response: bool = False,
) -> "BotXAPISearchUserByEmailRequestPayload":
return cls(
email=email,
trusts_search=trusts_search or Undefined,
partial_response=partial_response or Undefined,
)


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

email = payload.email
request_json = {"emails": [email]}
request_json = {
"emails": [email],
"trusts_search": payload.trusts_search,
"partial_response": payload.partial_response,
}
request_json = {
key: value for key, value in request_json.items() if value is not Undefined
}

response = await self._botx_method_call(
"POST",
Expand Down
11 changes: 10 additions & 1 deletion pybotx/client/users_api/search_user_by_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@
from pybotx.client.users_api.user_from_search import (
BotXAPISearchUserByEmailsResponsePayload,
)
from pybotx.missing import Missing, Undefined
from pybotx.models.api_base import UnverifiedPayloadBaseModel


class BotXAPISearchUserByEmailsRequestPayload(UnverifiedPayloadBaseModel):
emails: list[str]
trusts_search: Missing[bool] = Undefined
partial_response: Missing[bool] = Undefined

@classmethod
def from_domain(
cls,
emails: list[str],
trusts_search: bool = False,
partial_response: bool = False,
) -> "BotXAPISearchUserByEmailsRequestPayload":
return cls(emails=emails)
return cls(
emails=emails,
trusts_search=trusts_search or Undefined,
partial_response=partial_response or Undefined,
)


class SearchUserByEmailsMethod(AuthorizedBotXMethod):
Expand Down
7 changes: 6 additions & 1 deletion pybotx/client/users_api/update_user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

from pybotx.client.authorized_botx_method import AuthorizedBotXMethod
from pybotx.client.botx_method import response_exception_thrower
from pybotx.client.exceptions.users import InvalidProfileDataError, UserNotFoundError
from pybotx.client.exceptions.users import (
InvalidProfileDataError,
UserNotFoundError,
UserProfileUpdateUnavailableError,
)
from pybotx.missing import Missing, Undefined
from pybotx.models.api_base import UnverifiedPayloadBaseModel, VerifiedPayloadBaseModel
from pybotx.models.attachments import (
Expand Down Expand Up @@ -67,6 +71,7 @@ class UpdateUsersProfileMethod(AuthorizedBotXMethod):
**AuthorizedBotXMethod.status_handlers,
400: response_exception_thrower(InvalidProfileDataError),
404: response_exception_thrower(UserNotFoundError),
503: response_exception_thrower(UserProfileUpdateUnavailableError),
}

async def execute(
Expand Down
18 changes: 9 additions & 9 deletions pybotx/client/users_api/user_from_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,17 @@ class BotXAPIUserFromCSVResult(VerifiedPayloadBaseModel):
company: str | None = Field(alias="Company")
department: str | None = Field(alias="Department")
position: str | None = Field(alias="Position")
avatar: str | None = Field(alias="Avatar")
avatar_preview: str | None = Field(alias="Avatar preview")
office: str | None = Field(alias="Office")
avatar: str | None = Field(default=None, alias="Avatar")
avatar_preview: str | None = Field(default=None, alias="Avatar preview")
office: str | None = Field(default=None, alias="Office")
manager: str | None = Field(alias="Manager")
manager_huid: UUID | None = Field(alias="Manager HUID")
description: str | None = Field(alias="Description")
phone: str | None = Field(alias="Phone")
other_phone: str | None = Field(alias="Other phone")
ip_phone: str | None = Field(alias="IP phone")
other_ip_phone: str | None = Field(alias="Other IP phone")
personnel_number: str | None = Field(alias="Personnel number")
description: str | None = Field(default=None, alias="Description")
phone: str | None = Field(default=None, alias="Phone")
other_phone: str | None = Field(default=None, alias="Other phone")
ip_phone: str | None = Field(default=None, alias="IP phone")
other_ip_phone: str | None = Field(default=None, alias="Other IP phone")
personnel_number: str | None = Field(default=None, alias="Personnel number")

@field_validator(
"email",
Expand Down
10 changes: 9 additions & 1 deletion pybotx/client/users_api/user_from_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from pybotx.models.api_base import VerifiedPayloadBaseModel
from pybotx.models.enums import APIUserKinds, convert_user_kind_to_domain
from pybotx.models.users import UserFromSearch
from pydantic import Field
from pydantic import Field, field_validator


class BotXAPISearchUserResult(VerifiedPayloadBaseModel):
Expand All @@ -32,6 +32,14 @@ class BotXAPISearchUserResult(VerifiedPayloadBaseModel):
created_at: datetime | None = None
updated_at: datetime | None = None

@field_validator("ip_phone", "other_ip_phone", "other_phone", mode="before")
@classmethod
def convert_phone_to_string(cls, value: str | int | None) -> str | None:
if value is None:
return None

return str(value)


class BotXAPISearchUserResponsePayload(VerifiedPayloadBaseModel):
status: Literal["ok"]
Expand Down
42 changes: 42 additions & 0 deletions tests/client/users_api/test_search_user_by_emails.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,45 @@ async def test__search_user_by_email_without_data__succeed(
# - Assert -
assert_deep_equal(users, [user_from_search_without_data])
assert endpoint.called


async def test__search_user_by_email_with_trusts_search__succeed(
respx_mock: MockRouter,
host: str,
bot_id: UUID,
user_from_search_with_data: UserFromSearch,
user_from_search_with_data_json: dict[str, Any],
bot_factory: Any,
) -> None:
# - Arrange -
user_emails = ["ad_user@cts.com"]

request = BotXRequest(
method="POST",
path="/api/v3/botx/users/by_email",
json={
"emails": user_emails,
"trusts_search": True,
"partial_response": True,
},
)
endpoint = mock_botx(
respx_mock,
host,
request,
ok_payload([user_from_search_with_data_json]),
HTTPStatus.OK,
)

# - Act -
async with bot_factory() as bot:
users = await bot.search_user_by_emails(
bot_id=bot_id,
emails=user_emails,
trusts_search=True,
partial_response=True,
)

# - Assert -
assert_deep_equal(users, [user_from_search_with_data])
assert endpoint.called
37 changes: 37 additions & 0 deletions tests/client/users_api/test_search_user_by_huid.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,43 @@ async def test__search_user_by_huid__succeed(
assert endpoint.called


async def test__search_user_by_huid_with_numeric_ip_phone__succeed(
respx_mock: MockRouter,
host: str,
bot_id: UUID,
user_from_search_with_data: UserFromSearch,
user_from_search_with_data_json: dict[str, Any],
bot_factory: Any,
) -> None:
# - Arrange -
user_from_search_with_data_json["ip_phone"] = 1271020

request = BotXRequest(
method="GET",
path="/api/v3/botx/users/by_huid",
params={"user_huid": "f837dff4-d3ad-4b8d-a0a3-5c6ca9c747d1"},
)
endpoint = mock_botx(
respx_mock,
host,
request,
ok_payload(user_from_search_with_data_json),
HTTPStatus.OK,
)

# - Act -
async with bot_factory() as bot:
user = await bot.search_user_by_huid(
bot_id=bot_id,
huid=UUID("f837dff4-d3ad-4b8d-a0a3-5c6ca9c747d1"),
)

# - Assert -
assert user.ip_phone == "1271020"
assert_deep_equal(user, user_from_search_with_data)
assert endpoint.called


async def test__search_user_by_huid_without_data__succeed(
respx_mock: MockRouter,
host: str,
Expand Down
48 changes: 47 additions & 1 deletion tests/client/users_api/test_update_user_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
import pytest
from respx.router import MockRouter

from pybotx.client.exceptions.users import InvalidProfileDataError
from pybotx.client.exceptions.users import (
InvalidProfileDataError,
UserProfileUpdateUnavailableError,
)
from pybotx.models.attachments import AttachmentImage
from pybotx.models.enums import AttachmentTypes
from tests.testkit import BotXRequest, error_payload, mock_botx, ok_payload
Expand Down Expand Up @@ -168,3 +171,46 @@ async def test__update_user_profile__invalid_profile_data_error(

# - Assert -
assert endpoint.called


@pytest.mark.parametrize(
"reason",
[
"error_from_ad_phonebook_service",
"unexpected_error",
],
)
async def test__update_user_profile__service_unavailable_error(
respx_mock: MockRouter,
host: str,
bot_id: UUID,
bot_factory: Any,
reason: str,
) -> None:
# - Arrange -
request = BotXRequest(
method="PUT",
path="/api/v3/botx/users/update_profile",
json={
"user_huid": "6fafda2c-6505-57a5-a088-25ea5d1d0364",
},
)
endpoint = mock_botx(
respx_mock,
host,
request,
error_payload(reason),
HTTPStatus.SERVICE_UNAVAILABLE,
)

# - Act -
async with bot_factory() as bot:
with pytest.raises(UserProfileUpdateUnavailableError) as exc:
await bot.update_user_profile(
bot_id=bot_id,
user_huid=UUID("6fafda2c-6505-57a5-a088-25ea5d1d0364"),
)

# - Assert -
assert reason in str(exc.value)
assert endpoint.called
Loading
Loading