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.
Files changed (168) hide show
  1. maxapi_python-2.0.0.dist-info/METADATA +217 -0
  2. maxapi_python-2.0.0.dist-info/RECORD +140 -0
  3. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/WHEEL +1 -1
  4. pymax/__init__.py +50 -105
  5. pymax/api/__init__.py +17 -0
  6. pymax/api/auth/__init__.py +1 -0
  7. pymax/api/auth/enums.py +17 -0
  8. pymax/api/auth/payloads.py +129 -0
  9. pymax/api/auth/service.py +313 -0
  10. pymax/api/auth/types.py +13 -0
  11. pymax/api/chats/__init__.py +8 -0
  12. pymax/api/chats/enums.py +27 -0
  13. pymax/api/chats/payloads.py +103 -0
  14. pymax/api/chats/service.py +277 -0
  15. pymax/api/facade.py +32 -0
  16. pymax/api/messages/__init__.py +1 -0
  17. pymax/api/messages/enums.py +17 -0
  18. pymax/api/messages/payloads.py +92 -0
  19. pymax/api/messages/service.py +337 -0
  20. pymax/api/models.py +13 -0
  21. pymax/api/response.py +123 -0
  22. pymax/api/self/__init__.py +2 -0
  23. pymax/api/self/enums.py +11 -0
  24. pymax/api/self/payloads.py +41 -0
  25. pymax/api/self/service.py +142 -0
  26. pymax/api/session/__init__.py +1 -0
  27. pymax/api/session/enums.py +10 -0
  28. pymax/api/session/payloads.py +76 -0
  29. pymax/api/session/service.py +72 -0
  30. pymax/api/uploads/__init__.py +1 -0
  31. pymax/api/uploads/models.py +49 -0
  32. pymax/api/uploads/payloads.py +25 -0
  33. pymax/api/uploads/service.py +458 -0
  34. pymax/api/users/__init__.py +2 -0
  35. pymax/api/users/enums.py +12 -0
  36. pymax/api/users/payloads.py +16 -0
  37. pymax/api/users/service.py +124 -0
  38. pymax/app.py +273 -0
  39. pymax/auth/__init__.py +25 -0
  40. pymax/auth/base.py +37 -0
  41. pymax/auth/email.py +0 -0
  42. pymax/auth/models.py +5 -0
  43. pymax/auth/providers.py +127 -0
  44. pymax/auth/qr.py +135 -0
  45. pymax/auth/service.py +25 -0
  46. pymax/auth/sms.py +122 -0
  47. pymax/base.py +204 -0
  48. pymax/client.py +106 -0
  49. pymax/client_web.py +83 -0
  50. pymax/config.py +215 -0
  51. pymax/connection/__init__.py +1 -0
  52. pymax/connection/connection.py +205 -0
  53. pymax/connection/pending.py +46 -0
  54. pymax/connection/readers/__init__.py +2 -0
  55. pymax/connection/readers/base.py +6 -0
  56. pymax/connection/readers/tcp.py +29 -0
  57. pymax/connection/readers/ws.py +14 -0
  58. pymax/dispatch/__init__.py +10 -0
  59. pymax/dispatch/dispatcher.py +222 -0
  60. pymax/dispatch/enums.py +12 -0
  61. pymax/dispatch/mapping.py +73 -0
  62. pymax/dispatch/resolvers.py +52 -0
  63. pymax/dispatch/router.py +216 -0
  64. pymax/exceptions.py +22 -89
  65. pymax/files/__init__.py +9 -0
  66. pymax/files/base.py +82 -0
  67. pymax/files/file.py +76 -0
  68. pymax/files/photo.py +108 -0
  69. pymax/files/static.py +10 -0
  70. pymax/files/video.py +74 -0
  71. pymax/formatting/__init__.py +0 -0
  72. pymax/formatting/markdown.py +217 -0
  73. pymax/infra/__init__.py +1 -0
  74. pymax/infra/auth.py +55 -0
  75. pymax/infra/base.py +15 -0
  76. pymax/infra/chat.py +240 -0
  77. pymax/infra/message.py +252 -0
  78. pymax/infra/protocol.py +9 -0
  79. pymax/infra/self.py +139 -0
  80. pymax/infra/user.py +107 -0
  81. pymax/logging.py +129 -0
  82. pymax/protocol/__init__.py +11 -0
  83. pymax/protocol/base.py +13 -0
  84. pymax/{static/enum.py → protocol/enums.py} +36 -79
  85. pymax/protocol/models.py +33 -0
  86. pymax/protocol/tcp/__init__.py +1 -0
  87. pymax/protocol/tcp/compression.py +97 -0
  88. pymax/protocol/tcp/framing.py +68 -0
  89. pymax/protocol/tcp/payload.py +127 -0
  90. pymax/protocol/tcp/protocol.py +68 -0
  91. pymax/protocol/ws/__init__.py +1 -0
  92. pymax/protocol/ws/protocol.py +27 -0
  93. pymax/py.typed +0 -0
  94. pymax/routers.py +8 -0
  95. pymax/session/__init__.py +3 -0
  96. pymax/session/models.py +11 -0
  97. pymax/session/protocol.py +14 -0
  98. pymax/session/store.py +232 -0
  99. pymax/telemetry/__init__.py +3 -0
  100. pymax/telemetry/navigation.py +181 -0
  101. pymax/telemetry/payloads.py +142 -0
  102. pymax/telemetry/service.py +225 -0
  103. pymax/transport/__init__.py +0 -0
  104. pymax/transport/base.py +14 -0
  105. pymax/transport/tcp.py +93 -0
  106. pymax/transport/websocket.py +50 -0
  107. pymax/types/__init__.py +2 -0
  108. pymax/types/domain/__init__.py +11 -0
  109. pymax/types/domain/attachments/__init__.py +11 -0
  110. pymax/types/domain/attachments/audio.py +35 -0
  111. pymax/types/domain/attachments/call.py +26 -0
  112. pymax/types/domain/attachments/contact.py +32 -0
  113. pymax/types/domain/attachments/control.py +20 -0
  114. pymax/types/domain/attachments/enums.py +27 -0
  115. pymax/types/domain/attachments/file.py +56 -0
  116. pymax/types/domain/attachments/keyboards/__init__.py +1 -0
  117. pymax/types/domain/attachments/keyboards/inline.py +19 -0
  118. pymax/types/domain/attachments/photo.py +45 -0
  119. pymax/types/domain/attachments/share.py +29 -0
  120. pymax/types/domain/attachments/sticker.py +50 -0
  121. pymax/types/domain/attachments/video.py +90 -0
  122. pymax/types/domain/auth.py +161 -0
  123. pymax/types/domain/base.py +17 -0
  124. pymax/types/domain/chat.py +426 -0
  125. pymax/types/domain/element.py +24 -0
  126. pymax/types/domain/enums.py +24 -0
  127. pymax/types/domain/error.py +20 -0
  128. pymax/types/domain/folder.py +74 -0
  129. pymax/types/domain/login.py +35 -0
  130. pymax/types/domain/message.py +378 -0
  131. pymax/types/domain/name.py +20 -0
  132. pymax/types/domain/profile.py +15 -0
  133. pymax/types/domain/session.py +52 -0
  134. pymax/types/domain/sync.py +80 -0
  135. pymax/types/domain/user.py +117 -0
  136. pymax/types/events/__init__.py +3 -0
  137. pymax/types/events/file.py +5 -0
  138. pymax/types/events/message.py +37 -0
  139. pymax/types/events/video.py +5 -0
  140. maxapi_python-1.2.4.dist-info/METADATA +0 -205
  141. maxapi_python-1.2.4.dist-info/RECORD +0 -33
  142. pymax/core.py +0 -390
  143. pymax/crud.py +0 -96
  144. pymax/files.py +0 -138
  145. pymax/filters.py +0 -164
  146. pymax/formatter.py +0 -31
  147. pymax/formatting.py +0 -74
  148. pymax/interfaces.py +0 -552
  149. pymax/mixins/__init__.py +0 -40
  150. pymax/mixins/auth.py +0 -368
  151. pymax/mixins/channel.py +0 -130
  152. pymax/mixins/group.py +0 -458
  153. pymax/mixins/handler.py +0 -285
  154. pymax/mixins/message.py +0 -879
  155. pymax/mixins/scheduler.py +0 -28
  156. pymax/mixins/self.py +0 -259
  157. pymax/mixins/socket.py +0 -297
  158. pymax/mixins/telemetry.py +0 -112
  159. pymax/mixins/user.py +0 -219
  160. pymax/mixins/websocket.py +0 -142
  161. pymax/models.py +0 -8
  162. pymax/navigation.py +0 -187
  163. pymax/payloads.py +0 -367
  164. pymax/protocols.py +0 -123
  165. pymax/static/constant.py +0 -89
  166. pymax/types.py +0 -1220
  167. pymax/utils.py +0 -90
  168. {maxapi_python-1.2.4.dist-info → maxapi_python-2.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,9 @@
1
+ from .file import File
2
+ from .photo import Photo
3
+ from .video import Video
4
+
5
+ __all__ = (
6
+ "File",
7
+ "Photo",
8
+ "Video",
9
+ )
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
@@ -0,0 +1,10 @@
1
+ from typing import Final
2
+
3
+ ALLOWED_EXTENSIONS: Final[set[str]] = {
4
+ ".jpg",
5
+ ".jpeg",
6
+ ".png",
7
+ ".gif",
8
+ ".webp",
9
+ ".bmp",
10
+ }
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
@@ -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-методы клиента."""