Skip to content

Commit cf8e856

Browse files
committed
- file updates
- some method fix
1 parent e29f18c commit cf8e856

10 files changed

Lines changed: 245 additions & 31 deletions

File tree

pybotx/bot/bot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,7 @@ async def search_user_by_email_post(
14501450
) -> UserFromSearch:
14511451
"""Search user by email for search.
14521452
1453+
Wraps the single email into a list payload and returns the first result.
14531454
For multiple emails use `search_user_by_emails`.
14541455
14551456
:param bot_id: Bot which should perform the request.
@@ -2171,13 +2172,15 @@ async def download_file(
21712172
chat_id: UUID,
21722173
file_id: UUID,
21732174
async_buffer: AsyncBufferWritable,
2175+
is_preview: bool = False,
21742176
) -> None:
21752177
"""Download file form file service.
21762178
21772179
:param bot_id: Bot which should perform the request.
21782180
:param chat_id: Target chat id.
21792181
:param file_id: Async file id.
21802182
:param async_buffer: Buffer to write downloaded file.
2183+
:param is_preview: If true and file has preview, return it instead of original.
21812184
"""
21822185

21832186
method = DownloadFileMethod(
@@ -2188,6 +2191,7 @@ async def download_file(
21882191
payload = BotXAPIDownloadFileRequestPayload.from_domain(
21892192
chat_id=chat_id,
21902193
file_id=file_id,
2194+
is_preview=is_preview,
21912195
)
21922196

21932197
await method.execute(payload, async_buffer)

pybotx/client/files_api/download_file.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ def from_domain(
2222
cls,
2323
chat_id: UUID,
2424
file_id: UUID,
25+
is_preview: bool = False,
2526
) -> "BotXAPIDownloadFileRequestPayload":
2627
return cls(
2728
group_chat_id=chat_id,
2829
file_id=file_id,
29-
is_preview=False,
30+
is_preview=is_preview,
3031
)
3132

3233

pybotx/client/users_api/search_user_by_email.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
from pybotx.client.authorized_botx_method import AuthorizedBotXMethod
44
from pybotx.client.botx_method import response_exception_thrower
5+
from pybotx.client.exceptions.http import InvalidBotXResponsePayloadError
56
from pybotx.client.exceptions.users import UserNotFoundError
6-
from pybotx.client.users_api.user_from_search import BotXAPISearchUserResponsePayload
7+
from pybotx.client.users_api.user_from_search import (
8+
BotXAPISearchUserByEmailsResponsePayload,
9+
BotXAPISearchUserResponsePayload,
10+
)
11+
from pybotx.logger import logger
712
from pybotx.models.api_base import UnverifiedPayloadBaseModel
813

914

@@ -61,13 +66,32 @@ async def execute(
6166
) -> BotXAPISearchUserResponsePayload:
6267
path = "/api/v3/botx/users/by_email"
6368

69+
email = payload.email
70+
request_json = {"emails": [email]}
71+
6472
response = await self._botx_method_call(
6573
"POST",
6674
self._build_url(path),
67-
json=payload.jsonable_dict(),
75+
json=request_json,
6876
)
6977

70-
return self._verify_and_extract_api_model(
71-
BotXAPISearchUserResponsePayload,
72-
response,
78+
try:
79+
list_payload = self._verify_and_extract_api_model(
80+
BotXAPISearchUserByEmailsResponsePayload,
81+
response,
82+
)
83+
except InvalidBotXResponsePayloadError as exc:
84+
raise exc
85+
86+
if not list_payload.result:
87+
raise UserNotFoundError("User not found")
88+
89+
if len(list_payload.result) > 1:
90+
logger.warning(
91+
"Search by email returned multiple users; taking the first result"
92+
)
93+
94+
return BotXAPISearchUserResponsePayload(
95+
status="ok",
96+
result=list_payload.result[0],
7397
)

pybotx/models/async_files.py

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from contextlib import asynccontextmanager
22
from dataclasses import dataclass
3-
from typing import AsyncGenerator, Literal, Union, cast
3+
from typing import AsyncGenerator, Literal, Optional, Union, cast
44
from uuid import UUID
55

66
from aiofiles.tempfile import SpooledTemporaryFile
77

88
from pybotx.bot.contextvars import bot_id_var, bot_var, chat_id_var
99
from pybotx.constants import CHUNK_SIZE
10+
from pybotx.missing import MissingOptional, Undefined
1011
from pybotx.models.api_base import VerifiedPayloadBaseModel
1112
from pydantic import ConfigDict
1213
from pybotx.models.enums import (
@@ -29,9 +30,27 @@ class AsyncFileBase:
2930
_file_url: str
3031
_file_mimetype: str
3132
_file_hash: str
33+
file_preview: Optional[str] = None
34+
file_preview_height: Optional[int] = None
35+
file_preview_width: Optional[int] = None
36+
file_encryption_algo: Optional[str] = None
37+
chunk_size: Optional[int] = None
38+
caption: Optional[str] = None
39+
40+
@property
41+
def file_url(self) -> str:
42+
return self._file_url
43+
44+
@property
45+
def file_mimetype(self) -> str:
46+
return self._file_mimetype
47+
48+
@property
49+
def file_hash(self) -> str:
50+
return self._file_hash
3251

3352
@asynccontextmanager
34-
async def open(self) -> AsyncGenerator[SpooledTemporaryFile, None]:
53+
async def open(self, *, is_preview: bool = False) -> AsyncGenerator[SpooledTemporaryFile, None]:
3554
bot = bot_var.get()
3655

3756
async with SpooledTemporaryFile(max_size=CHUNK_SIZE) as tmp_file:
@@ -40,6 +59,7 @@ async def open(self) -> AsyncGenerator[SpooledTemporaryFile, None]:
4059
chat_id=chat_id_var.get(),
4160
file_id=self._file_id,
4261
async_buffer=tmp_file,
62+
is_preview=is_preview,
4363
)
4464

4565
yield tmp_file
@@ -53,7 +73,7 @@ class Image(AsyncFileBase):
5373
@dataclass
5474
class Video(AsyncFileBase):
5575
type: Literal[AttachmentTypes.VIDEO]
56-
duration: int
76+
duration: int = 0
5777

5878

5979
@dataclass
@@ -64,7 +84,7 @@ class Document(AsyncFileBase):
6484
@dataclass
6585
class Voice(AsyncFileBase):
6686
type: Literal[AttachmentTypes.VOICE]
67-
duration: int
87+
duration: int = 0
6888

6989

7090
class APIAsyncFileBase(VerifiedPayloadBaseModel):
@@ -75,8 +95,14 @@ class APIAsyncFileBase(VerifiedPayloadBaseModel):
7595
file_name: str
7696
file_size: int
7797
file_hash: str
98+
file_preview: MissingOptional[str] = Undefined
99+
file_preview_height: MissingOptional[int] = Undefined
100+
file_preview_width: MissingOptional[int] = Undefined
101+
file_encryption_algo: MissingOptional[str] = Undefined
102+
chunk_size: MissingOptional[int] = Undefined
103+
caption: MissingOptional[str] = Undefined
78104

79-
model_config = ConfigDict(extra="allow")
105+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
80106

81107

82108
class ApiAsyncFileImage(APIAsyncFileBase):
@@ -107,6 +133,14 @@ class ApiAsyncFileVoice(APIAsyncFileBase):
107133
File = Union[Image, Video, Document, Voice]
108134

109135

136+
def _to_optional(value: MissingOptional[Union[str, int]]) -> Optional[Union[str, int]]:
137+
return None if value is Undefined else value
138+
139+
140+
def _to_missing(value: Optional[Union[str, int]]) -> MissingOptional[Union[str, int]]:
141+
return Undefined if value is None else value
142+
143+
110144
def convert_async_file_from_domain(file: File) -> APIAsyncFile:
111145
attachment_type = convert_attachment_type_from_domain(file.type)
112146

@@ -121,6 +155,12 @@ def convert_async_file_from_domain(file: File) -> APIAsyncFile:
121155
file=file._file_url,
122156
file_mime_type=file._file_mimetype,
123157
file_hash=file._file_hash,
158+
file_preview=_to_missing(file.file_preview),
159+
file_preview_height=_to_missing(file.file_preview_height),
160+
file_preview_width=_to_missing(file.file_preview_width),
161+
file_encryption_algo=_to_missing(file.file_encryption_algo),
162+
chunk_size=_to_missing(file.chunk_size),
163+
caption=_to_missing(file.caption),
124164
)
125165

126166
if attachment_type == APIAttachmentTypes.VIDEO:
@@ -135,6 +175,12 @@ def convert_async_file_from_domain(file: File) -> APIAsyncFile:
135175
file=file._file_url,
136176
file_mime_type=file._file_mimetype,
137177
file_hash=file._file_hash,
178+
file_preview=_to_missing(file.file_preview),
179+
file_preview_height=_to_missing(file.file_preview_height),
180+
file_preview_width=_to_missing(file.file_preview_width),
181+
file_encryption_algo=_to_missing(file.file_encryption_algo),
182+
chunk_size=_to_missing(file.chunk_size),
183+
caption=_to_missing(file.caption),
138184
)
139185

140186
if attachment_type == APIAttachmentTypes.DOCUMENT:
@@ -148,6 +194,12 @@ def convert_async_file_from_domain(file: File) -> APIAsyncFile:
148194
file=file._file_url,
149195
file_mime_type=file._file_mimetype,
150196
file_hash=file._file_hash,
197+
file_preview=_to_missing(file.file_preview),
198+
file_preview_height=_to_missing(file.file_preview_height),
199+
file_preview_width=_to_missing(file.file_preview_width),
200+
file_encryption_algo=_to_missing(file.file_encryption_algo),
201+
chunk_size=_to_missing(file.chunk_size),
202+
caption=_to_missing(file.caption),
151203
)
152204

153205
if attachment_type == APIAttachmentTypes.VOICE:
@@ -162,6 +214,12 @@ def convert_async_file_from_domain(file: File) -> APIAsyncFile:
162214
file=file._file_url,
163215
file_mime_type=file._file_mimetype,
164216
file_hash=file._file_hash,
217+
file_preview=_to_missing(file.file_preview),
218+
file_preview_height=_to_missing(file.file_preview_height),
219+
file_preview_width=_to_missing(file.file_preview_width),
220+
file_encryption_algo=_to_missing(file.file_encryption_algo),
221+
chunk_size=_to_missing(file.chunk_size),
222+
caption=_to_missing(file.caption),
165223
)
166224

167225
raise NotImplementedError(f"Unsupported attachment type: {attachment_type}")
@@ -182,6 +240,21 @@ def convert_async_file_to_domain(async_file: APIAsyncFile) -> File:
182240
_file_mimetype=async_file.file_mime_type,
183241
_file_url=async_file.file,
184242
_file_hash=async_file.file_hash,
243+
file_preview=cast(Optional[str], _to_optional(async_file.file_preview)),
244+
file_preview_height=cast(
245+
Optional[int],
246+
_to_optional(async_file.file_preview_height),
247+
),
248+
file_preview_width=cast(
249+
Optional[int],
250+
_to_optional(async_file.file_preview_width),
251+
),
252+
file_encryption_algo=cast(
253+
Optional[str],
254+
_to_optional(async_file.file_encryption_algo),
255+
),
256+
chunk_size=cast(Optional[int], _to_optional(async_file.chunk_size)),
257+
caption=cast(Optional[str], _to_optional(async_file.caption)),
185258
)
186259

187260
if attachment_type == AttachmentTypes.VIDEO:
@@ -197,6 +270,21 @@ def convert_async_file_to_domain(async_file: APIAsyncFile) -> File:
197270
_file_mimetype=async_file.file_mime_type,
198271
_file_url=async_file.file,
199272
_file_hash=async_file.file_hash,
273+
file_preview=cast(Optional[str], _to_optional(async_file.file_preview)),
274+
file_preview_height=cast(
275+
Optional[int],
276+
_to_optional(async_file.file_preview_height),
277+
),
278+
file_preview_width=cast(
279+
Optional[int],
280+
_to_optional(async_file.file_preview_width),
281+
),
282+
file_encryption_algo=cast(
283+
Optional[str],
284+
_to_optional(async_file.file_encryption_algo),
285+
),
286+
chunk_size=cast(Optional[int], _to_optional(async_file.chunk_size)),
287+
caption=cast(Optional[str], _to_optional(async_file.caption)),
200288
)
201289

202290
if attachment_type == AttachmentTypes.DOCUMENT:
@@ -211,6 +299,21 @@ def convert_async_file_to_domain(async_file: APIAsyncFile) -> File:
211299
_file_mimetype=async_file.file_mime_type,
212300
_file_url=async_file.file,
213301
_file_hash=async_file.file_hash,
302+
file_preview=cast(Optional[str], _to_optional(async_file.file_preview)),
303+
file_preview_height=cast(
304+
Optional[int],
305+
_to_optional(async_file.file_preview_height),
306+
),
307+
file_preview_width=cast(
308+
Optional[int],
309+
_to_optional(async_file.file_preview_width),
310+
),
311+
file_encryption_algo=cast(
312+
Optional[str],
313+
_to_optional(async_file.file_encryption_algo),
314+
),
315+
chunk_size=cast(Optional[int], _to_optional(async_file.chunk_size)),
316+
caption=cast(Optional[str], _to_optional(async_file.caption)),
214317
)
215318

216319
if attachment_type == AttachmentTypes.VOICE:
@@ -226,6 +329,21 @@ def convert_async_file_to_domain(async_file: APIAsyncFile) -> File:
226329
_file_mimetype=async_file.file_mime_type,
227330
_file_url=async_file.file,
228331
_file_hash=async_file.file_hash,
332+
file_preview=cast(Optional[str], _to_optional(async_file.file_preview)),
333+
file_preview_height=cast(
334+
Optional[int],
335+
_to_optional(async_file.file_preview_height),
336+
),
337+
file_preview_width=cast(
338+
Optional[int],
339+
_to_optional(async_file.file_preview_width),
340+
),
341+
file_encryption_algo=cast(
342+
Optional[str],
343+
_to_optional(async_file.file_encryption_algo),
344+
),
345+
chunk_size=cast(Optional[int], _to_optional(async_file.chunk_size)),
346+
caption=cast(Optional[str], _to_optional(async_file.caption)),
229347
)
230348

231349
raise NotImplementedError(f"Unsupported attachment type: {attachment_type}")

tests/client/files_api/test_download_file.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ async def test__download_file__unexpected_not_found_error_raised(
3737
params={
3838
"group_chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa",
3939
"file_id": "c3b9def2-b2c8-4732-b61f-99b9b110fa80",
40+
"is_preview": False,
4041
},
4142
headers={"Authorization": "Bearer token"},
4243
).mock(
@@ -84,6 +85,7 @@ async def test__download_file__file_metadata_not_found_error_raised(
8485
params={
8586
"group_chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa",
8687
"file_id": "c3b9def2-b2c8-4732-b61f-99b9b110fa80",
88+
"is_preview": False,
8789
},
8890
headers={"Authorization": "Bearer token"},
8991
).mock(
@@ -132,6 +134,7 @@ async def test__download_file__file_deleted_error_raised(
132134
params={
133135
"group_chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa",
134136
"file_id": "c3b9def2-b2c8-4732-b61f-99b9b110fa80",
137+
"is_preview": False,
135138
},
136139
headers={"Authorization": "Bearer token"},
137140
).mock(
@@ -179,6 +182,7 @@ async def test__download_file__chat_not_found_error_raised(
179182
params={
180183
"group_chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa",
181184
"file_id": "c3b9def2-b2c8-4732-b61f-99b9b110fa80",
185+
"is_preview": False,
182186
},
183187
headers={"Authorization": "Bearer token"},
184188
).mock(
@@ -226,6 +230,7 @@ async def test__download_file__succeed(
226230
params={
227231
"group_chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa",
228232
"file_id": "c3b9def2-b2c8-4732-b61f-99b9b110fa80",
233+
"is_preview": False,
229234
},
230235
headers={"Authorization": "Bearer token"},
231236
).mock(

tests/client/files_api/test_upload_file.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,18 @@ async def test__download_file__succeed(
114114

115115
# - Act -
116116
async with lifespan_wrapper(built_bot) as bot:
117-
await bot.upload_file(
117+
uploaded_file = await bot.upload_file(
118118
bot_id=bot_id,
119119
chat_id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"),
120120
async_buffer=async_buffer,
121121
filename="test.txt",
122122
)
123123

124124
# - Assert -
125+
assert uploaded_file.file_preview == "https://link.to/preview"
126+
assert uploaded_file.file_preview_height == 300
127+
assert uploaded_file.file_preview_width == 300
128+
assert uploaded_file.file_encryption_algo == "stream"
129+
assert uploaded_file.chunk_size == 2097152
130+
assert uploaded_file.caption == "текст"
125131
assert endpoint.called

0 commit comments

Comments
 (0)