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
pymax/crud.py DELETED
@@ -1,96 +0,0 @@
1
- from typing import cast
2
- from uuid import UUID
3
-
4
- from sqlalchemy.engine.base import Engine
5
- from sqlmodel import Session, SQLModel, create_engine, select
6
-
7
- from .models import Auth
8
- from .static.enum import DeviceType
9
-
10
-
11
- class Database:
12
- def __init__(self, workdir: str) -> None:
13
- self.workdir = workdir
14
- self.engine = self.get_engine(workdir)
15
- self.create_all()
16
- self._ensure_single_auth()
17
-
18
- def create_all(self) -> None:
19
- SQLModel.metadata.create_all(self.engine)
20
-
21
- def get_engine(self, workdir: str) -> Engine:
22
- return create_engine(f"sqlite:///{workdir}/session.db")
23
-
24
- def get_session(self) -> Session:
25
- return Session(bind=self.engine)
26
-
27
- def get_auth_token(self) -> str | None:
28
- with self.get_session() as session:
29
- token = cast(str | None, session.exec(select(Auth.token)).first())
30
- return token
31
-
32
- def get_device_id(self) -> UUID:
33
- with self.get_session() as session:
34
- device_id = session.exec(select(Auth.device_id)).first()
35
-
36
- if device_id is None:
37
- auth = Auth()
38
- session.add(auth)
39
- session.commit()
40
- session.refresh(auth)
41
- return auth.device_id
42
- return device_id
43
-
44
- def insert_auth(self, auth: Auth) -> Auth:
45
- with self.get_session() as session:
46
- session.add(auth)
47
- session.commit()
48
- session.refresh(auth)
49
- return auth
50
-
51
- def update_auth_token(self, device_id: UUID, token: str) -> None:
52
- with self.get_session() as session:
53
- auth = session.exec(select(Auth).where(Auth.device_id == device_id)).first()
54
- if auth:
55
- auth.token = token
56
- session.add(auth)
57
- session.commit()
58
- session.refresh(auth)
59
- return
60
-
61
- existing = session.exec(select(Auth)).first()
62
- if existing:
63
- existing.device_id = device_id
64
- existing.token = token
65
- session.add(existing)
66
- session.commit()
67
- session.refresh(existing)
68
- return
69
-
70
- new_auth = Auth(device_id=device_id, token=token)
71
- session.add(new_auth)
72
- session.commit()
73
- session.refresh(new_auth)
74
-
75
- def update(self, auth: Auth) -> Auth:
76
- with self.get_session() as session:
77
- session.add(auth)
78
- session.commit()
79
- session.refresh(auth)
80
- return auth
81
-
82
- def _ensure_single_auth(self) -> None:
83
- with self.get_session() as session:
84
- rows = session.exec(select(Auth)).all()
85
- if not rows:
86
- auth = Auth(device_type=DeviceType.WEB.value)
87
- session.add(auth)
88
- session.commit()
89
- session.refresh(auth)
90
- return
91
-
92
- if len(rows) > 1:
93
- _ = rows[0]
94
- for extra in rows[1:]:
95
- session.delete(extra)
96
- session.commit()
pymax/files.py DELETED
@@ -1,138 +0,0 @@
1
- import mimetypes
2
- from abc import ABC, abstractmethod
3
- from pathlib import Path
4
- from typing import ClassVar
5
-
6
- from aiofiles import open as aio_open
7
- from aiohttp import ClientSession
8
- from typing_extensions import override
9
-
10
-
11
- class BaseFile(ABC):
12
- def __init__(
13
- self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
14
- ) -> None:
15
- self.raw = raw
16
- self.url = url
17
- self.path = path
18
-
19
- if self.url is None and self.path is None:
20
- raise ValueError("Either url or path must be provided.")
21
-
22
- if self.url and self.path:
23
- raise ValueError("Only one of url or path must be provided.")
24
-
25
- @abstractmethod
26
- async def read(self) -> bytes:
27
- if self.raw is not None:
28
- return self.raw
29
-
30
- if self.url:
31
- async with (
32
- ClientSession() as session,
33
- session.get(self.url) as response,
34
- ):
35
- response.raise_for_status()
36
- return await response.read()
37
- elif self.path:
38
- async with aio_open(self.path, "rb") as f:
39
- return await f.read()
40
- else:
41
- raise ValueError("Either url or path must be provided.")
42
-
43
-
44
- class Photo(BaseFile):
45
- ALLOWED_EXTENSIONS: ClassVar[set[str]] = {
46
- ".jpg",
47
- ".jpeg",
48
- ".png",
49
- ".gif",
50
- ".webp",
51
- ".bmp",
52
- } # FIXME: костыль ✅
53
-
54
- def __init__(
55
- self,
56
- raw: bytes | None = None,
57
- *,
58
- url: str | None = None,
59
- path: str | None = None,
60
- name: str | None = None,
61
- ) -> None:
62
- if path:
63
- self.file_name = Path(path).name
64
- elif url:
65
- self.file_name = Path(url).name
66
- elif name:
67
- self.file_name = name
68
- else:
69
- self.file_name = ""
70
-
71
- super().__init__(raw=raw, url=url, path=path)
72
-
73
- def validate_photo(self) -> tuple[str, str] | None:
74
- if self.path:
75
- extension = Path(self.path).suffix.lower()
76
- if extension not in self.ALLOWED_EXTENSIONS:
77
- raise ValueError(
78
- f"Invalid photo extension: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
79
- )
80
-
81
- return (extension[1:], ("image/" + extension[1:]).lower())
82
- elif self.url:
83
- extension = Path(self.url).suffix.lower()
84
- if extension not in self.ALLOWED_EXTENSIONS:
85
- raise ValueError(
86
- f"Invalid photo extension in URL: {extension}. Allowed: {self.ALLOWED_EXTENSIONS}"
87
- )
88
-
89
- mime_type = mimetypes.guess_type(self.url)[0]
90
-
91
- if not mime_type or not mime_type.startswith("image/"):
92
- raise ValueError(f"URL does not appear to be an image: {self.url}")
93
-
94
- return (extension[1:], mime_type)
95
- return None
96
-
97
- @override
98
- async def read(self) -> bytes:
99
- return await super().read()
100
-
101
-
102
- class Video(BaseFile):
103
- def __init__(
104
- self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
105
- ) -> None:
106
- self.file_name: str = ""
107
- if path:
108
- self.file_name = Path(path).name
109
- elif url:
110
- self.file_name = Path(url).name
111
-
112
- if not self.file_name:
113
- raise ValueError("Either url or path must be provided.")
114
- super().__init__(raw=raw, url=url, path=path)
115
-
116
- @override
117
- async def read(self) -> bytes:
118
- return await super().read()
119
-
120
-
121
- class File(BaseFile):
122
- def __init__(
123
- self, raw: bytes | None = None, *, url: str | None = None, path: str | None = None
124
- ) -> None:
125
- self.file_name: str = ""
126
- if path:
127
- self.file_name = Path(path).name
128
- elif url:
129
- self.file_name = Path(url).name
130
-
131
- if not self.file_name:
132
- raise ValueError("Either url or path must be provided.")
133
-
134
- super().__init__(raw=raw, url=url, path=path)
135
-
136
- @override
137
- async def read(self) -> bytes:
138
- return await super().read()
pymax/filters.py DELETED
@@ -1,164 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from abc import ABC, abstractmethod
5
- from typing import Generic, TypeVar
6
-
7
- from pymax.static.enum import AttachType, ChatType, MessageStatus
8
- from pymax.types import Message
9
-
10
- T_co = TypeVar("T_co")
11
-
12
-
13
- class BaseFilter(ABC, Generic[T_co]):
14
- event_type: type[T_co]
15
-
16
- @abstractmethod
17
- def __call__(self, event: T_co) -> bool: ...
18
-
19
- def __and__(self, other: BaseFilter[T_co]) -> BaseFilter[T_co]:
20
- return AndFilter(self, other)
21
-
22
- def __or__(self, other: BaseFilter[T_co]) -> BaseFilter[T_co]:
23
- return OrFilter(self, other)
24
-
25
- def __invert__(self) -> BaseFilter[T_co]:
26
- return NotFilter(self)
27
-
28
-
29
- class AndFilter(BaseFilter[T_co]):
30
- def __init__(self, *filters: BaseFilter[T_co]) -> None:
31
- self.filters = filters
32
- self.event_type = filters[0].event_type
33
-
34
- def __call__(self, event: T_co) -> bool:
35
- return all(f(event) for f in self.filters)
36
-
37
-
38
- class OrFilter(BaseFilter[T_co]):
39
- def __init__(self, *filters: BaseFilter[T_co]) -> None:
40
- self.filters = filters
41
- self.event_type = filters[0].event_type
42
-
43
- def __call__(self, event: T_co) -> bool:
44
- return any(f(event) for f in self.filters)
45
-
46
-
47
- class NotFilter(BaseFilter[T_co]):
48
- def __init__(self, base_filter: BaseFilter[T_co]) -> None:
49
- self.base_filter = base_filter
50
- self.event_type = base_filter.event_type
51
-
52
- def __call__(self, event: T_co) -> bool:
53
- return not self.base_filter(event)
54
-
55
-
56
- class ChatFilter(BaseFilter[Message]):
57
- event_type = Message
58
-
59
- def __init__(self, chat_id: int) -> None:
60
- self.chat_id = chat_id
61
-
62
- def __call__(self, message: Message) -> bool:
63
- return message.chat_id == self.chat_id
64
-
65
-
66
- class TextFilter(BaseFilter[Message]):
67
- event_type = Message
68
-
69
- def __init__(self, text: str) -> None:
70
- self.text = text
71
-
72
- def __call__(self, message: Message) -> bool:
73
- return self.text in message.text
74
-
75
-
76
- class SenderFilter(BaseFilter[Message]):
77
- event_type = Message
78
-
79
- def __init__(self, user_id: int) -> None:
80
- self.user_id = user_id
81
-
82
- def __call__(self, message: Message) -> bool:
83
- return message.sender == self.user_id
84
-
85
-
86
- class StatusFilter(BaseFilter[Message]):
87
- event_type = Message
88
-
89
- def __init__(self, status: MessageStatus) -> None:
90
- self.status = status
91
-
92
- def __call__(self, message: Message) -> bool:
93
- return message.status == self.status
94
-
95
-
96
- class TextContainsFilter(BaseFilter[Message]):
97
- event_type = Message
98
-
99
- def __init__(self, substring: str) -> None:
100
- self.substring = substring
101
-
102
- def __call__(self, message: Message) -> bool:
103
- return self.substring in message.text
104
-
105
-
106
- class RegexTextFilter(BaseFilter[Message]):
107
- event_type = Message
108
-
109
- def __init__(self, pattern: str) -> None:
110
- self.pattern = pattern
111
- self.regex = re.compile(pattern)
112
-
113
- def __call__(self, message: Message) -> bool:
114
- return bool(self.regex.search(message.text))
115
-
116
-
117
- class MediaFilter(BaseFilter[Message]):
118
- event_type = Message
119
-
120
- def __call__(self, message: Message) -> bool:
121
- return message.attaches is not None and len(message.attaches) > 0
122
-
123
-
124
- class FileFilter(BaseFilter[Message]):
125
- event_type = Message
126
-
127
- def __call__(self, message: Message) -> bool:
128
- if message.attaches is None:
129
- return False
130
- return any(attach.type == AttachType.FILE for attach in message.attaches)
131
-
132
-
133
- class Filters:
134
- @staticmethod
135
- def chat(chat_id: int) -> BaseFilter[Message]:
136
- return ChatFilter(chat_id)
137
-
138
- @staticmethod
139
- def text(text: str) -> BaseFilter[Message]:
140
- return TextFilter(text)
141
-
142
- @staticmethod
143
- def sender(user_id: int) -> BaseFilter[Message]:
144
- return SenderFilter(user_id)
145
-
146
- @staticmethod
147
- def status(status: MessageStatus) -> BaseFilter[Message]:
148
- return StatusFilter(status)
149
-
150
- @staticmethod
151
- def text_contains(substring: str) -> BaseFilter[Message]:
152
- return TextContainsFilter(substring)
153
-
154
- @staticmethod
155
- def text_matches(pattern: str) -> BaseFilter[Message]:
156
- return RegexTextFilter(pattern)
157
-
158
- @staticmethod
159
- def has_media() -> BaseFilter[Message]:
160
- return MediaFilter()
161
-
162
- @staticmethod
163
- def has_file() -> BaseFilter[Message]:
164
- return FileFilter()
pymax/formatter.py DELETED
@@ -1,31 +0,0 @@
1
- import logging
2
- from typing import ClassVar
3
-
4
-
5
- class ColoredFormatter(logging.Formatter):
6
- COLORS: ClassVar = {
7
- "DEBUG": "\033[37m",
8
- "INFO": "\033[36m",
9
- "WARNING": "\033[33m",
10
- "ERROR": "\033[31m",
11
- "CRITICAL": "\033[41m",
12
- }
13
-
14
- RESET = "\033[0m"
15
- DIM = "\033[2m"
16
- BOLD = "\033[1m"
17
-
18
- def format(self, record: logging.LogRecord) -> str:
19
- level_color = self.COLORS.get(record.levelname, self.RESET)
20
- time_color = self.DIM
21
- name_color = "\033[35m"
22
- message_color = self.RESET
23
-
24
- log = (
25
- f"{time_color}{self.formatTime(record, '%H:%M:%S')}{self.RESET} "
26
- f"[{level_color}{record.levelname}{self.RESET}] "
27
- f"{name_color}{record.name}{self.RESET}: "
28
- f"{message_color}{record.getMessage()}{self.RESET}"
29
- )
30
-
31
- return log
pymax/formatting.py DELETED
@@ -1,74 +0,0 @@
1
- import re
2
-
3
- from pymax.static.enum import FormattingType
4
- from pymax.types import Element
5
-
6
-
7
- class Formatting:
8
- MARKUP_BLOCK_PATTERN = re.compile(
9
- (
10
- r"\*\*(?P<strong>.+?)\*\*|"
11
- r"\*(?P<italic>.+?)\*|"
12
- r"__(?P<underline>.+?)__|"
13
- r"~~(?P<strike>.+?)~~"
14
- ),
15
- re.DOTALL,
16
- )
17
-
18
- @staticmethod
19
- def get_elements_from_markdown(text: str) -> tuple[list[Element], str]:
20
- text = text.strip("\n")
21
- elements: list[Element] = []
22
- clean_parts: list[str] = []
23
- current_pos = 0
24
-
25
- last_end = 0
26
- for match in Formatting.MARKUP_BLOCK_PATTERN.finditer(text):
27
- between = text[last_end : match.start()]
28
- if between:
29
- clean_parts.append(between)
30
- current_pos += len(between)
31
-
32
- inner_text = None
33
- fmt_type = None
34
- if match.group("strong") is not None:
35
- inner_text = match.group("strong")
36
- fmt_type = FormattingType.STRONG
37
- elif match.group("italic") is not None:
38
- inner_text = match.group("italic")
39
- fmt_type = FormattingType.EMPHASIZED
40
- elif match.group("underline") is not None:
41
- inner_text = match.group("underline")
42
- fmt_type = FormattingType.UNDERLINE
43
- elif match.group("strike") is not None:
44
- inner_text = match.group("strike")
45
- fmt_type = FormattingType.STRIKETHROUGH
46
-
47
- if inner_text is not None and fmt_type is not None:
48
- next_pos = match.end()
49
- has_newline = (next_pos < len(text) and text[next_pos] == "\n") or (
50
- next_pos == len(text)
51
- )
52
-
53
- length = len(inner_text) + (1 if has_newline else 0)
54
- elements.append(Element(type=fmt_type, from_=current_pos, length=length))
55
-
56
- clean_parts.append(inner_text)
57
- if has_newline:
58
- clean_parts.append("\n")
59
-
60
- current_pos += length
61
-
62
- if next_pos < len(text) and text[next_pos] == "\n":
63
- last_end = match.end() + 1
64
- else:
65
- last_end = match.end()
66
- else:
67
- last_end = match.end()
68
-
69
- tail = text[last_end:]
70
- if tail:
71
- clean_parts.append(tail)
72
-
73
- clean_text = "".join(clean_parts)
74
- return elements, clean_text