maxapi-python 1.2.4__py3-none-any.whl → 2.0.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_python-2.0.0.dist-info/METADATA +217 -0
- maxapi_python-2.0.0.dist-info/RECORD +140 -0
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
- pymax/__init__.py +50 -105
- pymax/api/__init__.py +17 -0
- pymax/api/auth/__init__.py +1 -0
- pymax/api/auth/enums.py +17 -0
- pymax/api/auth/payloads.py +129 -0
- pymax/api/auth/service.py +313 -0
- pymax/api/auth/types.py +13 -0
- pymax/api/chats/__init__.py +8 -0
- pymax/api/chats/enums.py +27 -0
- pymax/api/chats/payloads.py +103 -0
- pymax/api/chats/service.py +277 -0
- pymax/api/facade.py +32 -0
- pymax/api/messages/__init__.py +1 -0
- pymax/api/messages/enums.py +17 -0
- pymax/api/messages/payloads.py +92 -0
- pymax/api/messages/service.py +337 -0
- pymax/api/models.py +13 -0
- pymax/api/response.py +123 -0
- pymax/api/self/__init__.py +2 -0
- pymax/api/self/enums.py +11 -0
- pymax/api/self/payloads.py +41 -0
- pymax/api/self/service.py +142 -0
- pymax/api/session/__init__.py +1 -0
- pymax/api/session/enums.py +10 -0
- pymax/api/session/payloads.py +76 -0
- pymax/api/session/service.py +72 -0
- pymax/api/uploads/__init__.py +1 -0
- pymax/api/uploads/models.py +49 -0
- pymax/api/uploads/payloads.py +25 -0
- pymax/api/uploads/service.py +458 -0
- pymax/api/users/__init__.py +2 -0
- pymax/api/users/enums.py +12 -0
- pymax/api/users/payloads.py +16 -0
- pymax/api/users/service.py +124 -0
- pymax/app.py +273 -0
- pymax/auth/__init__.py +25 -0
- pymax/auth/base.py +37 -0
- pymax/auth/email.py +0 -0
- pymax/auth/models.py +5 -0
- pymax/auth/providers.py +127 -0
- pymax/auth/qr.py +135 -0
- pymax/auth/service.py +25 -0
- pymax/auth/sms.py +122 -0
- pymax/base.py +204 -0
- pymax/client.py +106 -0
- pymax/client_web.py +83 -0
- pymax/config.py +215 -0
- pymax/connection/__init__.py +1 -0
- pymax/connection/connection.py +205 -0
- pymax/connection/pending.py +46 -0
- pymax/connection/readers/__init__.py +2 -0
- pymax/connection/readers/base.py +6 -0
- pymax/connection/readers/tcp.py +29 -0
- pymax/connection/readers/ws.py +14 -0
- pymax/dispatch/__init__.py +10 -0
- pymax/dispatch/dispatcher.py +222 -0
- pymax/dispatch/enums.py +12 -0
- pymax/dispatch/mapping.py +73 -0
- pymax/dispatch/resolvers.py +52 -0
- pymax/dispatch/router.py +216 -0
- pymax/exceptions.py +22 -89
- pymax/files/__init__.py +9 -0
- pymax/files/base.py +82 -0
- pymax/files/file.py +76 -0
- pymax/files/photo.py +108 -0
- pymax/files/static.py +10 -0
- pymax/files/video.py +74 -0
- pymax/formatting/__init__.py +0 -0
- pymax/formatting/markdown.py +217 -0
- pymax/infra/__init__.py +1 -0
- pymax/infra/auth.py +55 -0
- pymax/infra/base.py +15 -0
- pymax/infra/chat.py +240 -0
- pymax/infra/message.py +252 -0
- pymax/infra/protocol.py +9 -0
- pymax/infra/self.py +139 -0
- pymax/infra/user.py +107 -0
- pymax/logging.py +129 -0
- pymax/protocol/__init__.py +11 -0
- pymax/protocol/base.py +13 -0
- pymax/{static/enum.py → protocol/enums.py} +36 -79
- pymax/protocol/models.py +33 -0
- pymax/protocol/tcp/__init__.py +1 -0
- pymax/protocol/tcp/compression.py +97 -0
- pymax/protocol/tcp/framing.py +68 -0
- pymax/protocol/tcp/payload.py +127 -0
- pymax/protocol/tcp/protocol.py +68 -0
- pymax/protocol/ws/__init__.py +1 -0
- pymax/protocol/ws/protocol.py +27 -0
- pymax/py.typed +0 -0
- pymax/routers.py +8 -0
- pymax/session/__init__.py +3 -0
- pymax/session/models.py +11 -0
- pymax/session/protocol.py +14 -0
- pymax/session/store.py +232 -0
- pymax/telemetry/__init__.py +3 -0
- pymax/telemetry/navigation.py +181 -0
- pymax/telemetry/payloads.py +142 -0
- pymax/telemetry/service.py +225 -0
- pymax/transport/__init__.py +0 -0
- pymax/transport/base.py +14 -0
- pymax/transport/tcp.py +93 -0
- pymax/transport/websocket.py +50 -0
- pymax/types/__init__.py +2 -0
- pymax/types/domain/__init__.py +11 -0
- pymax/types/domain/attachments/__init__.py +11 -0
- pymax/types/domain/attachments/audio.py +35 -0
- pymax/types/domain/attachments/call.py +26 -0
- pymax/types/domain/attachments/contact.py +32 -0
- pymax/types/domain/attachments/control.py +20 -0
- pymax/types/domain/attachments/enums.py +27 -0
- pymax/types/domain/attachments/file.py +56 -0
- pymax/types/domain/attachments/keyboards/__init__.py +1 -0
- pymax/types/domain/attachments/keyboards/inline.py +19 -0
- pymax/types/domain/attachments/photo.py +45 -0
- pymax/types/domain/attachments/share.py +29 -0
- pymax/types/domain/attachments/sticker.py +50 -0
- pymax/types/domain/attachments/video.py +90 -0
- pymax/types/domain/auth.py +161 -0
- pymax/types/domain/base.py +17 -0
- pymax/types/domain/chat.py +426 -0
- pymax/types/domain/element.py +24 -0
- pymax/types/domain/enums.py +24 -0
- pymax/types/domain/error.py +20 -0
- pymax/types/domain/folder.py +74 -0
- pymax/types/domain/login.py +35 -0
- pymax/types/domain/message.py +378 -0
- pymax/types/domain/name.py +20 -0
- pymax/types/domain/profile.py +15 -0
- pymax/types/domain/session.py +52 -0
- pymax/types/domain/sync.py +80 -0
- pymax/types/domain/user.py +117 -0
- pymax/types/events/__init__.py +3 -0
- pymax/types/events/file.py +5 -0
- pymax/types/events/message.py +37 -0
- pymax/types/events/video.py +5 -0
- maxapi_python-1.2.4.dist-info/METADATA +0 -205
- maxapi_python-1.2.4.dist-info/RECORD +0 -33
- pymax/core.py +0 -390
- pymax/crud.py +0 -96
- pymax/files.py +0 -138
- pymax/filters.py +0 -164
- pymax/formatter.py +0 -31
- pymax/formatting.py +0 -74
- pymax/interfaces.py +0 -552
- pymax/mixins/__init__.py +0 -40
- pymax/mixins/auth.py +0 -368
- pymax/mixins/channel.py +0 -130
- pymax/mixins/group.py +0 -458
- pymax/mixins/handler.py +0 -285
- pymax/mixins/message.py +0 -879
- pymax/mixins/scheduler.py +0 -28
- pymax/mixins/self.py +0 -259
- pymax/mixins/socket.py +0 -297
- pymax/mixins/telemetry.py +0 -112
- pymax/mixins/user.py +0 -219
- pymax/mixins/websocket.py +0 -142
- pymax/models.py +0 -8
- pymax/navigation.py +0 -187
- pymax/payloads.py +0 -367
- pymax/protocols.py +0 -123
- pymax/static/constant.py +0 -89
- pymax/types.py +0 -1220
- pymax/utils.py +0 -90
- {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
pymax/files/__init__.py
ADDED
pymax/files/base.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections.abc import AsyncGenerator
|
|
4
|
+
|
|
5
|
+
import aiofiles
|
|
6
|
+
import aiohttp
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseFile(ABC):
|
|
10
|
+
def __init__(
|
|
11
|
+
self, raw: bytes | None = None, *, path: str | None, url: str | None, name: str | None
|
|
12
|
+
) -> None:
|
|
13
|
+
self.path = path
|
|
14
|
+
self.url = url
|
|
15
|
+
self.raw = raw
|
|
16
|
+
self.name = name
|
|
17
|
+
|
|
18
|
+
if raw is None and not url and not path:
|
|
19
|
+
raise ValueError("Path or Url or Raw must be provided")
|
|
20
|
+
|
|
21
|
+
if raw is not None and not name:
|
|
22
|
+
raise ValueError("Name must be provided for raw data")
|
|
23
|
+
|
|
24
|
+
sources = sum(source is not None for source in (raw, url, path))
|
|
25
|
+
if sources > 1:
|
|
26
|
+
raise ValueError("Only one of raw, url or path must be provided.")
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def read(self) -> bytes:
|
|
30
|
+
if self.raw:
|
|
31
|
+
return self.raw
|
|
32
|
+
|
|
33
|
+
if self.path:
|
|
34
|
+
async with aiofiles.open(self.path, "rb") as f:
|
|
35
|
+
return await f.read()
|
|
36
|
+
elif self.url:
|
|
37
|
+
async with aiohttp.ClientSession() as session: # noqa: SIM117
|
|
38
|
+
async with session.get(self.url) as resp:
|
|
39
|
+
resp.raise_for_status()
|
|
40
|
+
return await resp.read()
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError("Path or Url must be provided")
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
async def size(self) -> int:
|
|
46
|
+
if self.raw:
|
|
47
|
+
return len(self.raw)
|
|
48
|
+
|
|
49
|
+
if self.path:
|
|
50
|
+
return os.path.getsize(self.path)
|
|
51
|
+
|
|
52
|
+
if self.url:
|
|
53
|
+
async with aiohttp.ClientSession() as session: # noqa: SIM117
|
|
54
|
+
async with session.head(self.url) as resp:
|
|
55
|
+
return int(resp.headers["Content-Length"])
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError("Path or Url must be provided")
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
async def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
|
|
61
|
+
if size <= 0:
|
|
62
|
+
raise ValueError("size must be greater than zero")
|
|
63
|
+
|
|
64
|
+
if self.raw:
|
|
65
|
+
for i in range(0, len(self.raw), size):
|
|
66
|
+
yield self.raw[i : i + size]
|
|
67
|
+
|
|
68
|
+
if self.path:
|
|
69
|
+
async with aiofiles.open(self.path, "rb") as f:
|
|
70
|
+
while True:
|
|
71
|
+
data = await f.read(size)
|
|
72
|
+
|
|
73
|
+
if not data:
|
|
74
|
+
break
|
|
75
|
+
yield data
|
|
76
|
+
|
|
77
|
+
if self.url:
|
|
78
|
+
async with aiohttp.ClientSession() as session: # noqa: SIM117
|
|
79
|
+
async with session.get(self.url) as resp:
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
async for chunk in resp.content.iter_chunked(size):
|
|
82
|
+
yield chunk
|
pymax/files/file.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .base import BaseFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class File(BaseFile):
|
|
8
|
+
"""Обычный файл для отправки в сообщение.
|
|
9
|
+
|
|
10
|
+
Используйте ``File`` в ``attachments`` у ``send_message``, ``Message.answer``
|
|
11
|
+
или ``Chat.answer``. Источником может быть ``path``, ``url`` или ``raw``;
|
|
12
|
+
передавайте только один источник. Для ``raw`` обязательно укажите ``name``.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
raw: Байты файла.
|
|
16
|
+
url: URL, откуда PyMax скачает файл перед upload.
|
|
17
|
+
path: Локальный путь к файлу.
|
|
18
|
+
name: Имя файла, которое будет отправлено Max.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
.. code-block:: python
|
|
22
|
+
|
|
23
|
+
from pymax import File
|
|
24
|
+
|
|
25
|
+
await client.send_message(
|
|
26
|
+
chat_id=123,
|
|
27
|
+
text="Документ",
|
|
28
|
+
attachments=[File(path="report.pdf")],
|
|
29
|
+
)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
raw: bytes | None = None,
|
|
35
|
+
*,
|
|
36
|
+
url: str | None = None,
|
|
37
|
+
path: str | None = None,
|
|
38
|
+
name: str | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.name: str = name or ""
|
|
41
|
+
if not self.name and path:
|
|
42
|
+
self.name = Path(path).name
|
|
43
|
+
elif not self.name and url:
|
|
44
|
+
self.name = Path(url).name
|
|
45
|
+
|
|
46
|
+
if not self.name:
|
|
47
|
+
raise ValueError("Either name, url or path must be provided.")
|
|
48
|
+
|
|
49
|
+
super().__init__(raw=raw, url=url, path=path, name=self.name)
|
|
50
|
+
|
|
51
|
+
async def read(self) -> bytes:
|
|
52
|
+
"""Читает файл целиком в память.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Байты файла.
|
|
56
|
+
"""
|
|
57
|
+
return await super().read()
|
|
58
|
+
|
|
59
|
+
async def size(self) -> int:
|
|
60
|
+
"""Возвращает размер файла в байтах.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Размер файла.
|
|
64
|
+
"""
|
|
65
|
+
return await super().size()
|
|
66
|
+
|
|
67
|
+
def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
|
|
68
|
+
"""Итерирует файл чанками.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
size: Размер чанка в байтах.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Async generator с байтами.
|
|
75
|
+
"""
|
|
76
|
+
return super().iter_chunks(size)
|
pymax/files/photo.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import mimetypes
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .base import BaseFile
|
|
6
|
+
from .static import ALLOWED_EXTENSIONS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Photo(BaseFile):
|
|
10
|
+
"""Фото для отправки в сообщение.
|
|
11
|
+
|
|
12
|
+
``Photo`` принимает ``path``, ``url`` или ``raw`` и проверяет расширение:
|
|
13
|
+
``.jpg``, ``.jpeg``, ``.png``, ``.gif``, ``.webp`` или ``.bmp``.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
raw: Байты изображения.
|
|
17
|
+
url: URL изображения.
|
|
18
|
+
path: Локальный путь к изображению.
|
|
19
|
+
name: Имя файла. Обязательно для ``raw``.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
.. code-block:: python
|
|
23
|
+
|
|
24
|
+
from pymax import Photo
|
|
25
|
+
|
|
26
|
+
await client.send_message(
|
|
27
|
+
chat_id=123,
|
|
28
|
+
text="Фото",
|
|
29
|
+
attachments=[Photo(path="image.jpg")],
|
|
30
|
+
)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
raw: bytes | None = None,
|
|
36
|
+
*,
|
|
37
|
+
url: str | None = None,
|
|
38
|
+
path: str | None = None,
|
|
39
|
+
name: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
if path:
|
|
42
|
+
self.file_name = Path(path).name
|
|
43
|
+
elif url:
|
|
44
|
+
self.file_name = Path(url).name
|
|
45
|
+
elif name:
|
|
46
|
+
self.file_name = name
|
|
47
|
+
else:
|
|
48
|
+
self.file_name = ""
|
|
49
|
+
|
|
50
|
+
super().__init__(raw=raw, url=url, path=path, name=name)
|
|
51
|
+
|
|
52
|
+
def validate_photo(self) -> tuple[str, str] | None:
|
|
53
|
+
"""Проверяет расширение и MIME-тип фото.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
``(extension, mime_type)`` или ``None``, если источник не задан.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: Если расширение или MIME-тип не похожи на изображение.
|
|
60
|
+
"""
|
|
61
|
+
if self.path or self.raw is not None:
|
|
62
|
+
source_name = self.path or self.file_name
|
|
63
|
+
extension = Path(source_name).suffix.lower()
|
|
64
|
+
if extension not in ALLOWED_EXTENSIONS:
|
|
65
|
+
msg = f"Invalid photo extension: {extension}. Allowed: {ALLOWED_EXTENSIONS}"
|
|
66
|
+
raise ValueError(msg)
|
|
67
|
+
return (extension[1:], ("image/" + extension[1:]).lower())
|
|
68
|
+
if self.url:
|
|
69
|
+
extension = Path(self.url).suffix.lower()
|
|
70
|
+
if extension not in ALLOWED_EXTENSIONS:
|
|
71
|
+
msg = f"Invalid photo extension: {extension}. Allowed: {ALLOWED_EXTENSIONS}"
|
|
72
|
+
raise ValueError(msg)
|
|
73
|
+
|
|
74
|
+
mime_type = mimetypes.guess_type(self.url)[0]
|
|
75
|
+
|
|
76
|
+
if not mime_type or not mime_type.startswith("image/"):
|
|
77
|
+
msg = f"URL does not appear to be an image: {self.url}"
|
|
78
|
+
raise ValueError(msg)
|
|
79
|
+
|
|
80
|
+
return (extension[1:], mime_type)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
async def read(self) -> bytes:
|
|
84
|
+
"""Читает фото целиком в память.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Байты изображения.
|
|
88
|
+
"""
|
|
89
|
+
return await super().read()
|
|
90
|
+
|
|
91
|
+
async def size(self) -> int:
|
|
92
|
+
"""Возвращает размер фото в байтах.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Размер изображения.
|
|
96
|
+
"""
|
|
97
|
+
return await super().size()
|
|
98
|
+
|
|
99
|
+
def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
|
|
100
|
+
"""Итерирует фото чанками.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
size: Размер чанка в байтах.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Async generator с байтами.
|
|
107
|
+
"""
|
|
108
|
+
return super().iter_chunks(size)
|
pymax/files/static.py
ADDED
pymax/files/video.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .base import BaseFile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Video(BaseFile):
|
|
8
|
+
"""Видео для отправки в сообщение.
|
|
9
|
+
|
|
10
|
+
``Video`` принимает ``path``, ``url`` или ``raw``. При отправке PyMax
|
|
11
|
+
загружает видео чанками и ждет от Max событие готовности обработки.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
raw: Байты видео.
|
|
15
|
+
url: URL видео.
|
|
16
|
+
path: Локальный путь к видео.
|
|
17
|
+
name: Имя файла. Обязательно для ``raw``.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
.. code-block:: python
|
|
21
|
+
|
|
22
|
+
from pymax import Video
|
|
23
|
+
|
|
24
|
+
await client.send_message(
|
|
25
|
+
chat_id=123,
|
|
26
|
+
text="Видео",
|
|
27
|
+
attachments=[Video(path="clip.mp4")],
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
raw: bytes | None = None,
|
|
34
|
+
*,
|
|
35
|
+
url: str | None = None,
|
|
36
|
+
path: str | None = None,
|
|
37
|
+
name: str | None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.name: str = name or ""
|
|
40
|
+
if not self.name and path:
|
|
41
|
+
self.name = Path(path).name
|
|
42
|
+
elif not self.name and url:
|
|
43
|
+
self.name = Path(url).name
|
|
44
|
+
|
|
45
|
+
if not self.name:
|
|
46
|
+
raise ValueError("Either name, url or path must be provided.")
|
|
47
|
+
super().__init__(raw=raw, url=url, path=path, name=self.name)
|
|
48
|
+
|
|
49
|
+
async def read(self) -> bytes:
|
|
50
|
+
"""Читает видео целиком в память.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Байты видео.
|
|
54
|
+
"""
|
|
55
|
+
return await super().read()
|
|
56
|
+
|
|
57
|
+
async def size(self) -> int:
|
|
58
|
+
"""Возвращает размер видео в байтах.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Размер видео.
|
|
62
|
+
"""
|
|
63
|
+
return await super().size()
|
|
64
|
+
|
|
65
|
+
def iter_chunks(self, size: int) -> AsyncGenerator[bytes, None]:
|
|
66
|
+
"""Итерирует видео чанками.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
size: Размер чанка в байтах.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Async generator с байтами.
|
|
73
|
+
"""
|
|
74
|
+
return super().iter_chunks(size)
|
|
File without changes
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from pymax.types.domain.element import Element, ElementAttributes
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Formatter:
|
|
5
|
+
MARKERS = {
|
|
6
|
+
"```": "CODE",
|
|
7
|
+
"**": "STRONG",
|
|
8
|
+
"__": "UNDERLINE",
|
|
9
|
+
"~~": "STRIKETHROUGH",
|
|
10
|
+
"`": "MONOSPACED",
|
|
11
|
+
"_": "EMPHASIZED",
|
|
12
|
+
"*": "EMPHASIZED",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
MARKER_ORDER = ["```", "**", "__", "~~", "`", "_", "*"]
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _parse_link(
|
|
19
|
+
text: str,
|
|
20
|
+
i: int,
|
|
21
|
+
clean_pos: int,
|
|
22
|
+
) -> tuple[str, str, int] | None:
|
|
23
|
+
if not text.startswith("[", i):
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
label_end = text.find("]", i + 1)
|
|
27
|
+
if label_end == -1:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
if label_end + 1 >= len(text) or text[label_end + 1] != "(":
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
url_start = label_end + 2
|
|
34
|
+
url_end = text.find(")", url_start)
|
|
35
|
+
if url_end == -1:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
label = text[i + 1 : label_end]
|
|
39
|
+
url = text[url_start:url_end]
|
|
40
|
+
|
|
41
|
+
if not label or not url:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
return label, url, url_end + 1
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def format_markdown(text: str) -> tuple[str, list[Element]]:
|
|
48
|
+
clean_text = ""
|
|
49
|
+
entities: list[Element] = []
|
|
50
|
+
|
|
51
|
+
i = 0
|
|
52
|
+
clean_pos = 0
|
|
53
|
+
active: dict[str, int] = {}
|
|
54
|
+
|
|
55
|
+
line_start = True
|
|
56
|
+
|
|
57
|
+
while i < len(text):
|
|
58
|
+
handled = False
|
|
59
|
+
|
|
60
|
+
# LINK: [text](url)
|
|
61
|
+
parsed_link = Formatter._parse_link(text, i, clean_pos)
|
|
62
|
+
|
|
63
|
+
if parsed_link is not None:
|
|
64
|
+
label, url, next_i = parsed_link
|
|
65
|
+
|
|
66
|
+
start = clean_pos
|
|
67
|
+
|
|
68
|
+
clean_text += label
|
|
69
|
+
clean_pos += len(label)
|
|
70
|
+
|
|
71
|
+
entities.append(
|
|
72
|
+
Element(
|
|
73
|
+
type="LINK",
|
|
74
|
+
from_=start,
|
|
75
|
+
length=len(label),
|
|
76
|
+
attributes=ElementAttributes(url=url),
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
i = next_i
|
|
81
|
+
line_start = False
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# HEADING: # Title
|
|
85
|
+
if line_start and text[i] == "#":
|
|
86
|
+
start_i = i
|
|
87
|
+
|
|
88
|
+
while i < len(text) and text[i] == "#":
|
|
89
|
+
i += 1
|
|
90
|
+
|
|
91
|
+
if i < len(text) and text[i] == " ":
|
|
92
|
+
i += 1
|
|
93
|
+
start = clean_pos
|
|
94
|
+
|
|
95
|
+
while i < len(text) and text[i] != "\n":
|
|
96
|
+
clean_text += text[i]
|
|
97
|
+
i += 1
|
|
98
|
+
clean_pos += 1
|
|
99
|
+
|
|
100
|
+
length = clean_pos - start
|
|
101
|
+
|
|
102
|
+
if length > 0:
|
|
103
|
+
entities.append(
|
|
104
|
+
Element(
|
|
105
|
+
type="HEADING",
|
|
106
|
+
from_=start,
|
|
107
|
+
length=length,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
line_start = False
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
i = start_i
|
|
115
|
+
|
|
116
|
+
# QUOTE: > text
|
|
117
|
+
if line_start and text[i] == ">":
|
|
118
|
+
i += 1
|
|
119
|
+
|
|
120
|
+
if i < len(text) and text[i] == " ":
|
|
121
|
+
i += 1
|
|
122
|
+
|
|
123
|
+
start = clean_pos
|
|
124
|
+
|
|
125
|
+
while i < len(text) and text[i] != "\n":
|
|
126
|
+
clean_text += text[i]
|
|
127
|
+
i += 1
|
|
128
|
+
clean_pos += 1
|
|
129
|
+
|
|
130
|
+
length = clean_pos - start
|
|
131
|
+
|
|
132
|
+
if length > 0:
|
|
133
|
+
entities.append(
|
|
134
|
+
Element(
|
|
135
|
+
type="QUOTE",
|
|
136
|
+
from_=start,
|
|
137
|
+
length=length,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
line_start = False
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
for marker in Formatter.MARKER_ORDER:
|
|
145
|
+
if not text.startswith(marker, i):
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
marker_len = len(marker)
|
|
149
|
+
|
|
150
|
+
if marker not in active:
|
|
151
|
+
if marker == "```":
|
|
152
|
+
closing_index = text.find(marker, i + marker_len)
|
|
153
|
+
|
|
154
|
+
if closing_index == -1 or closing_index == i + marker_len:
|
|
155
|
+
clean_text += marker
|
|
156
|
+
clean_pos += marker_len
|
|
157
|
+
i += marker_len
|
|
158
|
+
handled = True
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
active[marker] = clean_pos
|
|
162
|
+
i += marker_len
|
|
163
|
+
|
|
164
|
+
line_end = text.find("\n", i)
|
|
165
|
+
if line_end != -1 and line_end < closing_index:
|
|
166
|
+
i = line_end + 1
|
|
167
|
+
|
|
168
|
+
handled = True
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
end = text.find("\n", i + marker_len)
|
|
172
|
+
closing_index = text.find(
|
|
173
|
+
marker,
|
|
174
|
+
i + marker_len,
|
|
175
|
+
None if end == -1 else end,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if closing_index == -1 or closing_index == i + marker_len:
|
|
179
|
+
clean_text += marker
|
|
180
|
+
clean_pos += marker_len
|
|
181
|
+
i += marker_len
|
|
182
|
+
handled = True
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
active[marker] = clean_pos
|
|
186
|
+
i += marker_len
|
|
187
|
+
handled = True
|
|
188
|
+
break
|
|
189
|
+
|
|
190
|
+
start = active[marker]
|
|
191
|
+
length = clean_pos - start
|
|
192
|
+
|
|
193
|
+
if length > 0:
|
|
194
|
+
entities.append(
|
|
195
|
+
Element(
|
|
196
|
+
type=Formatter.MARKERS[marker],
|
|
197
|
+
from_=start,
|
|
198
|
+
length=length,
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
del active[marker]
|
|
203
|
+
i += marker_len
|
|
204
|
+
handled = True
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
if handled:
|
|
208
|
+
line_start = False
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
clean_text += text[i]
|
|
212
|
+
line_start = text[i] == "\n"
|
|
213
|
+
|
|
214
|
+
i += 1
|
|
215
|
+
clean_pos += 1
|
|
216
|
+
|
|
217
|
+
return clean_text, entities
|
pymax/infra/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .base import BaseMixin
|
pymax/infra/auth.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from pymax.api.auth.types import MISSING, Missing
|
|
2
|
+
from pymax.auth.providers import EmailCodeProvider
|
|
3
|
+
|
|
4
|
+
from .protocol import IClientProtocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthMixin(IClientProtocol):
|
|
8
|
+
"""Методы клиента для управления 2FA текущего аккаунта."""
|
|
9
|
+
|
|
10
|
+
async def set_2fa(
|
|
11
|
+
self,
|
|
12
|
+
password: str,
|
|
13
|
+
email: str | Missing = MISSING,
|
|
14
|
+
hint: str | Missing = MISSING,
|
|
15
|
+
email_code_provider: EmailCodeProvider | None = None,
|
|
16
|
+
) -> bool:
|
|
17
|
+
"""Устанавливает пароль 2FA для текущей учетной записи.
|
|
18
|
+
|
|
19
|
+
Если ``email`` или ``hint`` не переданы, PyMax не добавляет
|
|
20
|
+
соответствующую настройку. Если ``email`` передан без
|
|
21
|
+
``email_code_provider``, код подтверждения будет запрошен в консоли.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
password: Новый пароль 2FA.
|
|
25
|
+
email: Адрес электронной почты для 2FA, если требуется.
|
|
26
|
+
hint: Подсказка пароля, если требуется.
|
|
27
|
+
email_code_provider: Провайдер кода из электронной почты, если
|
|
28
|
+
требуется.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
``True``, если пароль успешно установлен.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
RuntimeError: Если установка пароля не удалась.
|
|
35
|
+
"""
|
|
36
|
+
return await self._app.api.auth.set_2fa(
|
|
37
|
+
password=password,
|
|
38
|
+
email=email,
|
|
39
|
+
hint=hint,
|
|
40
|
+
email_code_provider=email_code_provider,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async def remove_2fa(self, password: str) -> bool:
|
|
44
|
+
"""Удаляет пароль 2FA для текущей учетной записи.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
password: Текущий пароль 2FA.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
``True``, если пароль успешно удален.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
RuntimeError: Если удаление пароля не удалось.
|
|
54
|
+
"""
|
|
55
|
+
return await self._app.api.auth.remove_2fa(password=password)
|
pymax/infra/base.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .auth import AuthMixin
|
|
2
|
+
from .chat import ChatMixin
|
|
3
|
+
from .message import MessageMixin
|
|
4
|
+
from .self import SelfMixin
|
|
5
|
+
from .user import UserMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseMixin(
|
|
9
|
+
SelfMixin,
|
|
10
|
+
UserMixin,
|
|
11
|
+
ChatMixin,
|
|
12
|
+
MessageMixin,
|
|
13
|
+
AuthMixin,
|
|
14
|
+
):
|
|
15
|
+
"""Собирает публичные API-методы клиента."""
|