telekit 2.6.0a1__tar.gz → 2.6.0a2__tar.gz
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.
- {telekit-2.6.0a1/telekit.egg-info → telekit-2.6.0a2}/PKG-INFO +11 -50
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_version.py +1 -1
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/utils.py +448 -55
- {telekit-2.6.0a1 → telekit-2.6.0a2/telekit.egg-info}/PKG-INFO +11 -50
- {telekit-2.6.0a1 → telekit-2.6.0a2}/LICENSE +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/README.md +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/setup.cfg +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/setup.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_buildtext/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_buildtext/formatter.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_buildtext/styles.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_callback_query_handler.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_chain.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_chain_base.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_chain_entry_logic.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_chain_inline_keyboards_logic.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_chapters/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_chapters/chapters.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_handler.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_init.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_inline_buttons.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_inline_keyboard.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_input_handler.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_logger.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_on.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_reply_keyboard.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_snapvault/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_snapvault/snapcode.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_snapvault/snapvault.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_state.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/mixin.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/parser/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/parser/builder.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/parser/lexer.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/parser/nodes.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/parser/parser.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/parser/token.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_telekit_dsl/telekit_orm.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_text_builder.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_timeout.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_trait.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/_user.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/chat.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/debug.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/dices.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/article.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/calendar.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/complete_hotel.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/counter.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/dsl.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/entry.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/faq.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/hotel.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/on_text.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/pages.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/pyapi.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/qr.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/quiz.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/spells.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/start.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/style.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_handlers/text_document.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/example/example_server.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/html_text.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/inline_buttons.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/parameters.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/reply_buttons.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/scheduler.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/senders.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/server.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/styles.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/traits/__init__.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/traits/calendar_pick.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/traits/paginated_choice.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/traits/paginated_text.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/traits/track_handoff_origin.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit/types.py +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit.egg-info/SOURCES.txt +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit.egg-info/dependency_links.txt +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit.egg-info/requires.txt +0 -0
- {telekit-2.6.0a1 → telekit-2.6.0a2}/telekit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telekit
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.0a2
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -471,52 +471,13 @@ Telekit focuses on one job: making Telegram bot development easier.
|
|
|
471
471
|
|
|
472
472
|
---
|
|
473
473
|
|
|
474
|
-
# Changes in version 2.6.
|
|
475
|
-
|
|
476
|
-
### v2.
|
|
477
|
-
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
- Implement `TelegramMarkdownV2Sanitizer` for improved `MarkdownV2` handling
|
|
485
|
-
|
|
486
|
-
### v2.5.0 `(final)`
|
|
487
|
-
- Refactor `CalendarPick` trait to use `set_keyboard`
|
|
488
|
-
|
|
489
|
-
### v2.5.0`b3`
|
|
490
|
-
- Added `utils.Markers` class
|
|
491
|
-
- Added `HTMLText` class for handling Telegram HTML strings with tag-aware indexing and slicing.
|
|
492
|
-
- Added `PaginatedText` trait for displaying long HTML text in a paginated format, supporting navigation and smart splitting.
|
|
493
|
-
- Added `__radd__` to `TextEntity`: `"Regular" + Bold(" and Bold")`
|
|
494
|
-
- Added `__mul__` to `TextEntity`: `Bold("Text") * 3`
|
|
495
|
-
- Added `enabled=` parameter to `TextEntity`: `Bold("bold text", enabled=is_text_bold)`
|
|
496
|
-
- Added `TextBuilder` class – a fluent message composition API mirroring `InlineKeyboard`'s builder pattern
|
|
497
|
-
- Added styles to `telekit.types`
|
|
498
|
-
- Added `utils.CyclicList`
|
|
499
|
-
- Fixed `_answer_callback_query` to always call `bot.answer_callback_query()`, even without a popup text
|
|
500
|
-
|
|
501
|
-
### v2.5.0`b2`
|
|
502
|
-
- Added the `escape` parameter to `telekit.utils.*`:
|
|
503
|
-
- `make_user_link`
|
|
504
|
-
- `make_bot_link`
|
|
505
|
-
- `Handler.handlers_dict` now excludes private handlers (classes whose names start with `_`).
|
|
506
|
-
- Added `Debug.duplicate_handler_warnings` to warn about duplicate handler names during initialization.
|
|
507
|
-
- Added `Handler.chat` object (BETA)
|
|
508
|
-
|
|
509
|
-
### v2.5.0`b1`
|
|
510
|
-
- Added `Sender.send_message` method.
|
|
511
|
-
- Added `utils.make_mention` utility for generating `tg://user?id=` mention links.
|
|
512
|
-
- Added new inline button types to `inline_buttons`:
|
|
513
|
-
- `ContactButton` — mentions a user by Telegram ID via `tg://user?id=`.
|
|
514
|
-
- `UserLinkButton` — opens a user profile by username; supports pre-filled message text.
|
|
515
|
-
- `BotLinkButton` — opens a bot by username; supports deep-link `?start=` payload.
|
|
516
|
-
- Added new methods to `InlineKeyboard`:
|
|
517
|
-
- `add_contact` — adds a `ContactButton`.
|
|
518
|
-
- `add_user_link` — adds a `UserLinkButton`.
|
|
519
|
-
- `add_bot_link` — adds a `BotLinkButton`.
|
|
520
|
-
|
|
521
|
-
### v2.5.0`b0`
|
|
522
|
-
- Added support for t-strings (PEP 750, Python 3.14+) in `TextEntity`.
|
|
474
|
+
# Changes in version 2.6.0a2
|
|
475
|
+
|
|
476
|
+
### v2.6.0 `a2`
|
|
477
|
+
- Reworked `.env` and token/canvas file reading in `utils`: full `.env` syntax support (comments, `export`, quotes and escapes, multi-line values, `$VAR` interpolation), a `cache` parameter (default `True`) with `clear_cache()`, detailed errors with fix suggestions and creation commands, and new `Env*Error` exceptions; `load_env` now raises `EnvFileNotFoundError` for a missing file instead of returning `{}`
|
|
478
|
+
|
|
479
|
+
### v2.6.0 `a1`
|
|
480
|
+
- Added a module-loading utility in `utils`
|
|
481
|
+
|
|
482
|
+
### v2.6.0 `a0`
|
|
483
|
+
- Improved formatting in `_on.py`
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
+
import difflib
|
|
4
|
+
import shlex
|
|
3
5
|
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from urllib.parse import urlencode, quote
|
|
@@ -116,48 +118,352 @@ def format_file_size(size: int, precision: int = 1) -> str:
|
|
|
116
118
|
return f"{formatted} {units[index]}"
|
|
117
119
|
|
|
118
120
|
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
119
|
-
# Environment
|
|
121
|
+
# Environment: errors
|
|
120
122
|
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
121
123
|
|
|
124
|
+
class EnvError(Exception):
|
|
125
|
+
"""Base class for all errors raised while reading env / token / canvas files."""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class EnvFileNotFoundError(EnvError, FileNotFoundError):
|
|
129
|
+
"""The file does not exist. The message contains commands to create it."""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class EnvPermissionError(EnvError, PermissionError):
|
|
133
|
+
"""The file cannot be read because of missing permissions."""
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class EnvKeyError(EnvError, KeyError):
|
|
137
|
+
"""The requested variable is missing in the ``.env`` file."""
|
|
138
|
+
|
|
139
|
+
def __str__(self) -> str:
|
|
140
|
+
return str(self.args[0]) if self.args else ""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class EnvSyntaxError(EnvError, ValueError):
|
|
144
|
+
"""The ``.env`` file contains a line that cannot be parsed."""
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class EnvValueError(EnvError, ValueError):
|
|
148
|
+
"""The file exists, but its content is empty, invalid or not UTF-8."""
|
|
149
|
+
|
|
150
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
151
|
+
# Environment: cache
|
|
152
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
153
|
+
|
|
154
|
+
# Parsed ``.env`` files: absolute path -> {key: value}
|
|
155
|
+
_ENV_CACHE: dict[str, dict[str, str]] = {}
|
|
156
|
+
# Meaningful first line of plain text files (token.txt, canvas_path.txt): absolute path -> line
|
|
157
|
+
_TEXT_CACHE: dict[str, str] = {}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def clear_cache() -> None:
|
|
161
|
+
"""Drop everything cached by ``cache=True`` calls."""
|
|
162
|
+
_ENV_CACHE.clear()
|
|
163
|
+
_TEXT_CACHE.clear()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _cache_key(path: str) -> str:
|
|
167
|
+
return os.path.abspath(os.path.expanduser(path))
|
|
168
|
+
|
|
169
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
170
|
+
# Environment: error helpers
|
|
171
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
172
|
+
|
|
173
|
+
def _create_commands(path: str, line: str) -> str:
|
|
174
|
+
"""Shell commands that append ``line`` to ``path`` (creating the file if needed)."""
|
|
175
|
+
return "\n".join([
|
|
176
|
+
f" Linux/macOS: printf '%s\\n' {shlex.quote(line)} >> {shlex.quote(path)}",
|
|
177
|
+
f' Win: echo {line} >> "{path}"',
|
|
178
|
+
])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _find_similar_files(path: str) -> list[str]:
|
|
182
|
+
"""Look for files with a similar name next to ``path`` and in the current directory."""
|
|
183
|
+
p = Path(path)
|
|
184
|
+
found: list[str] = []
|
|
185
|
+
|
|
186
|
+
for folder in {p.parent, Path.cwd()}:
|
|
187
|
+
try:
|
|
188
|
+
if not folder.is_dir():
|
|
189
|
+
continue
|
|
190
|
+
names = [e.name for e in folder.iterdir() if e.is_file()]
|
|
191
|
+
except OSError:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
close = set(difflib.get_close_matches(p.name, names, n=5, cutoff=0.6))
|
|
195
|
+
if p.name.startswith(".env") or p.suffix == ".env":
|
|
196
|
+
close |= {n for n in names if n.startswith(".env") or n.endswith(".env")}
|
|
197
|
+
|
|
198
|
+
for name in sorted(close):
|
|
199
|
+
candidate = str(folder / name)
|
|
200
|
+
if _cache_key(candidate) != _cache_key(path) and candidate not in found:
|
|
201
|
+
found.append(candidate)
|
|
202
|
+
|
|
203
|
+
return found[:5]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _missing_file_message(path: str, example: str, kind: str) -> str:
|
|
207
|
+
p = Path(path)
|
|
208
|
+
lines = [
|
|
209
|
+
f"{kind} file not found: '{path}'",
|
|
210
|
+
f" Resolved to: {_cache_key(path)}",
|
|
211
|
+
f" Current working directory: {os.getcwd()}",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
if not p.parent.exists():
|
|
215
|
+
lines += [
|
|
216
|
+
f" The directory '{p.parent}' does not exist either. Create it first:",
|
|
217
|
+
f" mkdir -p {shlex.quote(str(p.parent))} (Windows: mkdir \"{p.parent}\")",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
similar = _find_similar_files(path)
|
|
221
|
+
if similar:
|
|
222
|
+
lines.append(" Similar files found - maybe you meant one of these:")
|
|
223
|
+
lines += [f" {s}" for s in similar]
|
|
224
|
+
|
|
225
|
+
lines += [
|
|
226
|
+
" To create the file, run one of the commands below",
|
|
227
|
+
f" (replace the example value with your own):",
|
|
228
|
+
_create_commands(path, example),
|
|
229
|
+
" Or pass the correct location, e.g. read_*(\"path/to/file\") "
|
|
230
|
+
"(relative paths are resolved from the current working directory).",
|
|
231
|
+
]
|
|
232
|
+
return "\n".join(lines)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _syntax_error(source: str, lineno: int, line: str, problem: str, hint: str) -> EnvSyntaxError:
|
|
236
|
+
return EnvSyntaxError(
|
|
237
|
+
f"Invalid syntax in '{source}' at line {lineno}: {problem}\n"
|
|
238
|
+
f" > {line.strip()}\n"
|
|
239
|
+
f" Hint: {hint}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _read_text(path: str, example: str, kind: str) -> str:
|
|
244
|
+
"""Read a text file as UTF-8 (BOM tolerated) and turn I/O errors into helpful ones."""
|
|
245
|
+
try:
|
|
246
|
+
return Path(path).read_text(encoding="utf-8-sig")
|
|
247
|
+
except FileNotFoundError:
|
|
248
|
+
raise EnvFileNotFoundError(_missing_file_message(path, example, kind)) from None
|
|
249
|
+
except IsADirectoryError:
|
|
250
|
+
raise EnvError(
|
|
251
|
+
f"Expected a file but '{path}' is a directory.\n"
|
|
252
|
+
f" Hint: pass the path to the file itself, e.g. '{os.path.join(path, '.env')}'."
|
|
253
|
+
) from None
|
|
254
|
+
except PermissionError:
|
|
255
|
+
raise EnvPermissionError(
|
|
256
|
+
f"No permission to read '{path}'.\n"
|
|
257
|
+
f" Hint: check the access rights - Linux/macOS: `chmod u+r {shlex.quote(path)}`; "
|
|
258
|
+
f"Windows: file Properties -> Security. Also make sure no other program locks the file."
|
|
259
|
+
) from None
|
|
260
|
+
except UnicodeDecodeError as e:
|
|
261
|
+
raise EnvValueError(
|
|
262
|
+
f"'{path}' is not valid UTF-8 ({e.reason} at byte {e.start}).\n"
|
|
263
|
+
f" Hint: re-save the file in UTF-8 encoding (in most editors: "
|
|
264
|
+
f"'Save with encoding' -> 'UTF-8')."
|
|
265
|
+
) from None
|
|
266
|
+
except OSError as e:
|
|
267
|
+
raise EnvError(
|
|
268
|
+
f"Cannot read '{path}': {e.strerror or e}.\n"
|
|
269
|
+
f" Hint: check that the path is correct and the file is accessible."
|
|
270
|
+
) from None
|
|
271
|
+
|
|
272
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
273
|
+
# Environment: .env parser
|
|
274
|
+
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
275
|
+
|
|
276
|
+
# Syntax supported:
|
|
277
|
+
# # full-line comment
|
|
278
|
+
# KEY=value unquoted, inline comment after " #"
|
|
279
|
+
# export KEY=value "export" prefix
|
|
280
|
+
# KEY = value spaces around "="
|
|
281
|
+
# KEY= empty value
|
|
282
|
+
# KEY='literal $value\n' single quotes: no escapes, no interpolation
|
|
283
|
+
# KEY="line1\nline2 \"q\"" double quotes: \n \r \t \" \\ \$ escapes + interpolation
|
|
284
|
+
# KEY="multi quoted values may span several lines
|
|
285
|
+
# line"
|
|
286
|
+
# B=${A}/x B=$A B=${A:-d} interpolation (earlier keys of the file, then os.environ);
|
|
287
|
+
# unknown variables without a default are left untouched
|
|
288
|
+
# UTF-8 BOM and CRLF line endings are handled.
|
|
289
|
+
|
|
290
|
+
_KEY_RE = re.compile(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.\-]*)\s*=(.*)$")
|
|
291
|
+
_AFTER_QUOTE_RE = re.compile(r"^\s*(?:#.*)?$")
|
|
292
|
+
_INLINE_COMMENT_RE = re.compile(r"\s+#.*$")
|
|
293
|
+
_VAR_RE = re.compile(
|
|
294
|
+
r"\$\{([A-Za-z_][A-Za-z0-9_]*)(?:(:?-)([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)"
|
|
295
|
+
)
|
|
296
|
+
_ESCAPES = {"n": "\n", "r": "\r", "t": "\t", '"': '"', "\\": "\\"}
|
|
297
|
+
_DOLLAR = "\x00" # placeholder for an escaped "\$" until interpolation is done
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _scan_double_quoted(s: str) -> tuple[str, int] | None:
|
|
301
|
+
"""Scan ``s`` (text after an opening ``"``). Return (value, index of closing quote) or None."""
|
|
302
|
+
out: list[str] = []
|
|
303
|
+
i = 0
|
|
304
|
+
while i < len(s):
|
|
305
|
+
c = s[i]
|
|
306
|
+
if c == "\\" and i + 1 < len(s):
|
|
307
|
+
nxt = s[i + 1]
|
|
308
|
+
if nxt in _ESCAPES:
|
|
309
|
+
out.append(_ESCAPES[nxt])
|
|
310
|
+
elif nxt == "$":
|
|
311
|
+
out.append(_DOLLAR)
|
|
312
|
+
else:
|
|
313
|
+
out.append("\\" + nxt) # unknown escape stays as is
|
|
314
|
+
i += 2
|
|
315
|
+
continue
|
|
316
|
+
if c == '"':
|
|
317
|
+
return "".join(out), i
|
|
318
|
+
out.append(c)
|
|
319
|
+
i += 1
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _interpolate(value: str, env: dict[str, str]) -> str:
|
|
324
|
+
def repl(m: re.Match) -> str:
|
|
325
|
+
name = m.group(1) or m.group(4)
|
|
326
|
+
op, default = m.group(2), m.group(3)
|
|
327
|
+
found = env.get(name, os.environ.get(name))
|
|
328
|
+
|
|
329
|
+
if op == ":-" and not found:
|
|
330
|
+
return default or ""
|
|
331
|
+
if op == "-" and found is None:
|
|
332
|
+
return default or ""
|
|
333
|
+
return m.group(0) if found is None else found
|
|
334
|
+
|
|
335
|
+
return _VAR_RE.sub(repl, value).replace(_DOLLAR, "$")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _parse_env(text: str, source: str) -> dict[str, str]:
|
|
339
|
+
"""Parse the content of a ``.env`` file. ``source`` is only used in error messages."""
|
|
340
|
+
env: dict[str, str] = {}
|
|
341
|
+
lines = text.lstrip("\ufeff").splitlines()
|
|
342
|
+
i = 0
|
|
343
|
+
|
|
344
|
+
while i < len(lines):
|
|
345
|
+
raw = lines[i]
|
|
346
|
+
lineno = i + 1
|
|
347
|
+
stripped = raw.strip()
|
|
348
|
+
|
|
349
|
+
if not stripped or stripped.startswith("#"):
|
|
350
|
+
i += 1
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
m = _KEY_RE.match(stripped)
|
|
354
|
+
if not m:
|
|
355
|
+
if "=" not in stripped:
|
|
356
|
+
raise _syntax_error(
|
|
357
|
+
source, lineno, raw, "missing '='",
|
|
358
|
+
f"write the line as KEY=value, or start it with '#' to make it a comment, "
|
|
359
|
+
f"e.g. '{stripped.split()[0]}=your_value'.",
|
|
360
|
+
)
|
|
361
|
+
raise _syntax_error(
|
|
362
|
+
source, lineno, raw, "invalid variable name",
|
|
363
|
+
"names may contain letters, digits, '_', '.', '-' and must not start with a digit "
|
|
364
|
+
"or contain spaces, e.g. MY_TOKEN=value.",
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
key, rest = m.group(1), m.group(2).strip()
|
|
368
|
+
|
|
369
|
+
if rest[:1] in ('"', "'"):
|
|
370
|
+
quote_char = rest[0]
|
|
371
|
+
body = rest[1:]
|
|
372
|
+
start_line = lineno
|
|
373
|
+
|
|
374
|
+
while True:
|
|
375
|
+
if quote_char == '"':
|
|
376
|
+
scanned = _scan_double_quoted(body)
|
|
377
|
+
else:
|
|
378
|
+
end = body.find("'")
|
|
379
|
+
scanned = (body[:end], end) if end != -1 else None
|
|
380
|
+
|
|
381
|
+
if scanned is not None:
|
|
382
|
+
break
|
|
383
|
+
i += 1
|
|
384
|
+
if i >= len(lines):
|
|
385
|
+
raise _syntax_error(
|
|
386
|
+
source, start_line, raw, f"unclosed {quote_char} quote",
|
|
387
|
+
f"add the closing {quote_char} at the end of the value. For text that "
|
|
388
|
+
f"contains the same quote, use the other quote type"
|
|
389
|
+
+ (' or escape it as \\"' if quote_char == '"' else "") + ".",
|
|
390
|
+
)
|
|
391
|
+
body += "\n" + lines[i]
|
|
392
|
+
|
|
393
|
+
value, end = scanned
|
|
394
|
+
tail = body[end + 1:]
|
|
395
|
+
if not _AFTER_QUOTE_RE.match(tail):
|
|
396
|
+
raise _syntax_error(
|
|
397
|
+
source, start_line, raw, f"unexpected text after closing quote: {tail.strip()!r}",
|
|
398
|
+
"put comments after whitespace and '#', or quote the whole value "
|
|
399
|
+
"(escape inner double quotes as \\\").",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
if quote_char == '"':
|
|
403
|
+
value = _interpolate(value, env)
|
|
404
|
+
else:
|
|
405
|
+
if rest.startswith("#"):
|
|
406
|
+
rest = ""
|
|
407
|
+
value = _interpolate(_INLINE_COMMENT_RE.sub("", rest).strip(), env)
|
|
408
|
+
|
|
409
|
+
env[key] = value
|
|
410
|
+
i += 1
|
|
411
|
+
|
|
412
|
+
return env
|
|
413
|
+
|
|
414
|
+
|
|
122
415
|
def _split_env_path(path: str, default: str) -> tuple[str, str]:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
416
|
+
"""Split ``"file.env:KEY"`` into ``("file.env", "KEY")``. Windows drive letters are safe."""
|
|
417
|
+
m = re.match(r"^(.+):([A-Za-z_][A-Za-z0-9_.\-]*)$", path)
|
|
418
|
+
if m:
|
|
419
|
+
return m.group(1), m.group(2)
|
|
420
|
+
return path, default
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _is_env_path(path: str) -> bool:
|
|
424
|
+
base = os.path.basename(_split_env_path(path, "")[0])
|
|
425
|
+
return base.endswith(".env") or base.startswith(".env")
|
|
426
|
+
|
|
127
427
|
|
|
128
|
-
|
|
428
|
+
def _load_env(path: str, cache: bool, example: str) -> dict[str, str]:
|
|
429
|
+
key = _cache_key(path)
|
|
129
430
|
|
|
431
|
+
if not (cache and key in _ENV_CACHE):
|
|
432
|
+
text = _read_text(path, example, ".env")
|
|
433
|
+
_ENV_CACHE[key] = _parse_env(text, path)
|
|
130
434
|
|
|
131
|
-
|
|
435
|
+
return dict(_ENV_CACHE[key])
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def load_env(path=".env", *, cache: bool = True) -> dict[str, str]:
|
|
132
439
|
"""
|
|
133
440
|
Load all key-value pairs from a ``.env`` file.
|
|
134
441
|
|
|
135
|
-
|
|
442
|
+
Supported syntax: ``KEY=value``, ``export KEY=value``, spaces around ``=``,
|
|
443
|
+
full-line and inline (`` # ...``) comments, empty values, single-quoted
|
|
444
|
+
(literal) and double-quoted values with ``\\n \\r \\t \\" \\\\ \\$`` escapes,
|
|
445
|
+
multi-line quoted values, ``$VAR`` / ``${VAR}`` / ``${VAR:-default}``
|
|
446
|
+
interpolation, UTF-8 BOM and CRLF line endings.
|
|
136
447
|
|
|
137
448
|
:param path: Path to the ``.env`` file. Defaults to ``".env"``.
|
|
138
449
|
:type path: ``str``
|
|
139
|
-
:
|
|
140
|
-
|
|
450
|
+
:param cache: If ``True`` (default) and the file was already parsed, return
|
|
451
|
+
the in-memory result without touching the disk. If ``False``, the file
|
|
452
|
+
is always read and parsed again, and the cache is updated.
|
|
453
|
+
:type cache: ``bool``
|
|
454
|
+
:return: Dictionary of all key-value pairs found in the file.
|
|
141
455
|
:rtype: ``dict[str, str]``
|
|
456
|
+
:raises EnvFileNotFoundError: If the file does not exist (the message
|
|
457
|
+
contains shell commands to create it). Subclass of ``FileNotFoundError``.
|
|
458
|
+
:raises EnvSyntaxError: If a line cannot be parsed (the message contains
|
|
459
|
+
the line number and a hint). Subclass of ``ValueError``.
|
|
460
|
+
:raises EnvPermissionError: If the file cannot be read.
|
|
461
|
+
:raises EnvValueError: If the file is not UTF-8.
|
|
142
462
|
"""
|
|
143
|
-
|
|
144
|
-
return {}
|
|
145
|
-
|
|
146
|
-
env: dict[str, str] = {}
|
|
147
|
-
|
|
148
|
-
with open(path, "r") as f:
|
|
149
|
-
for line in f:
|
|
150
|
-
line = line.strip()
|
|
151
|
-
if not line or line.startswith("#"):
|
|
152
|
-
continue
|
|
153
|
-
|
|
154
|
-
key, value = line.split("=", 1)
|
|
155
|
-
env[key.strip()] = value.strip()
|
|
156
|
-
|
|
157
|
-
return env
|
|
463
|
+
return _load_env(path, cache, "KEY=value")
|
|
158
464
|
|
|
159
465
|
|
|
160
|
-
def read_envar(path: str, name: str) -> str:
|
|
466
|
+
def read_envar(path: str, name: str, *, cache: bool = True) -> str:
|
|
161
467
|
"""
|
|
162
468
|
Read a single environment variable from a ``.env`` file.
|
|
163
469
|
|
|
@@ -165,19 +471,75 @@ def read_envar(path: str, name: str) -> str:
|
|
|
165
471
|
:type path: ``str``
|
|
166
472
|
:param name: Name of the key to read.
|
|
167
473
|
:type name: ``str``
|
|
168
|
-
:
|
|
474
|
+
:param cache: Use the in-memory copy of the file if available
|
|
475
|
+
(see :func:`load_env`). Default is ``True``.
|
|
476
|
+
:type cache: ``bool``
|
|
477
|
+
:return: Value of the key (may be an empty string if it is declared empty).
|
|
169
478
|
:rtype: ``str``
|
|
170
|
-
:raises
|
|
479
|
+
:raises EnvKeyError: If ``name`` is not found in the file. The message
|
|
480
|
+
lists available keys, close matches and the command to add the key.
|
|
481
|
+
Subclass of ``KeyError``.
|
|
482
|
+
:raises EnvFileNotFoundError: See :func:`load_env`.
|
|
171
483
|
"""
|
|
172
|
-
|
|
484
|
+
example = f"{name}=your_value"
|
|
485
|
+
env = _load_env(path, cache, example)
|
|
486
|
+
|
|
487
|
+
if name in env:
|
|
488
|
+
return env[name]
|
|
489
|
+
|
|
490
|
+
lines = [f"Variable '{name}' not found in '{path}'."]
|
|
491
|
+
|
|
492
|
+
lower = {k.lower(): k for k in env}
|
|
493
|
+
if name.lower() in lower:
|
|
494
|
+
lines.append(
|
|
495
|
+
f" Names are case-sensitive: the file has '{lower[name.lower()]}', not '{name}'."
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
close = difflib.get_close_matches(name, list(env), n=3, cutoff=0.6)
|
|
499
|
+
close = [c for c in close if c != lower.get(name.lower())]
|
|
500
|
+
if close:
|
|
501
|
+
lines.append(" Did you mean: " + ", ".join(f"'{c}'" for c in close) + "?")
|
|
502
|
+
|
|
503
|
+
if env:
|
|
504
|
+
lines.append(" Variables in this file: " + ", ".join(env))
|
|
505
|
+
else:
|
|
506
|
+
lines.append(" The file contains no variables (it is empty or has only comments).")
|
|
507
|
+
|
|
508
|
+
lines += [
|
|
509
|
+
f" To fix it, add the line `{example}` to '{path}':",
|
|
510
|
+
_create_commands(path, example),
|
|
511
|
+
" Check also that the line is not commented out with '#' "
|
|
512
|
+
"and that you are reading the right file.",
|
|
513
|
+
]
|
|
514
|
+
if cache:
|
|
515
|
+
lines.append(" If you edited the file after the first read, call again with cache=False (or clear_cache()).")
|
|
516
|
+
|
|
517
|
+
raise EnvKeyError("\n".join(lines))
|
|
173
518
|
|
|
174
|
-
if name not in env:
|
|
175
|
-
raise KeyError(f"{name} not found in {path}")
|
|
176
519
|
|
|
177
|
-
|
|
520
|
+
def _read_first_value(path: str, example: str, kind: str, cache: bool) -> str:
|
|
521
|
+
"""First meaningful (non-empty, non-comment) line of a plain text file."""
|
|
522
|
+
key = _cache_key(path)
|
|
523
|
+
if cache and key in _TEXT_CACHE:
|
|
524
|
+
return _TEXT_CACHE[key]
|
|
178
525
|
|
|
526
|
+
text = _read_text(path, example, kind)
|
|
179
527
|
|
|
180
|
-
|
|
528
|
+
for line in text.splitlines():
|
|
529
|
+
line = line.strip()
|
|
530
|
+
if line and not line.startswith("#"):
|
|
531
|
+
_TEXT_CACHE[key] = line
|
|
532
|
+
return line
|
|
533
|
+
|
|
534
|
+
_TEXT_CACHE.pop(key, None)
|
|
535
|
+
raise EnvValueError(
|
|
536
|
+
f"{kind} file '{path}' is empty (no lines other than blanks and '#' comments).\n"
|
|
537
|
+
f" Write the value on the first line, e.g. `{example}`:\n"
|
|
538
|
+
f"{_create_commands(path, example)}"
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def read_token(path: str = "token.txt", *, cache: bool = True) -> str:
|
|
181
543
|
"""
|
|
182
544
|
Read the bot token from a file or ``.env``.
|
|
183
545
|
|
|
@@ -187,9 +549,9 @@ def read_token(path: str = "token.txt") -> str:
|
|
|
187
549
|
123456789:BotSecretToken Main production bot
|
|
188
550
|
987654321:AnotherToken Backup bot
|
|
189
551
|
|
|
190
|
-
Reads only the first line
|
|
191
|
-
|
|
192
|
-
reordering lines.
|
|
552
|
+
Reads only the first meaningful line (blank lines and ``#`` comments are
|
|
553
|
+
skipped). Inline comments (everything after the first whitespace) are
|
|
554
|
+
ignored. Multiple tokens can be stored and swapped by reordering lines.
|
|
193
555
|
|
|
194
556
|
**Environment file** (``.env``)::
|
|
195
557
|
|
|
@@ -200,21 +562,35 @@ def read_token(path: str = "token.txt") -> str:
|
|
|
200
562
|
:param path: Path to a token file, or ``".env"`` / ``".env:KEY"`` for
|
|
201
563
|
environment files. Defaults to ``"token.txt"``.
|
|
202
564
|
:type path: ``str``
|
|
565
|
+
:param cache: If ``True`` (default), use the in-memory copy if available; if
|
|
566
|
+
``False``, re-read the file and refresh the cache.
|
|
567
|
+
:type cache: ``bool``
|
|
203
568
|
:return: Bot token string.
|
|
204
569
|
:rtype: ``str``
|
|
205
|
-
:raises
|
|
206
|
-
:raises
|
|
570
|
+
:raises EnvKeyError: If the key is not found in the ``.env`` file.
|
|
571
|
+
:raises EnvFileNotFoundError: If the file does not exist (message contains
|
|
572
|
+
commands to create it).
|
|
573
|
+
:raises EnvValueError: If the file or the value is empty.
|
|
207
574
|
"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
575
|
+
example = "123456789:YourBotToken"
|
|
576
|
+
|
|
577
|
+
if _is_env_path(path):
|
|
578
|
+
file_path, name = _split_env_path(path, "TOKEN")
|
|
579
|
+
token = read_envar(file_path, name, cache=cache).strip()
|
|
580
|
+
|
|
581
|
+
if not token:
|
|
582
|
+
raise EnvValueError(
|
|
583
|
+
f"Variable '{name}' in '{file_path}' is empty.\n"
|
|
584
|
+
f" Hint: set the token after '=', e.g. `{name}={example}` "
|
|
585
|
+
f"(get one from @BotFather in Telegram)."
|
|
586
|
+
)
|
|
214
587
|
return token
|
|
215
|
-
|
|
216
588
|
|
|
217
|
-
|
|
589
|
+
line = _read_first_value(path, example, "Token", cache)
|
|
590
|
+
return line.split()[0]
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def read_canvas_path(path: str = "canvas_path.txt", *, cache: bool = True) -> str:
|
|
218
594
|
"""
|
|
219
595
|
Read the ``.canvas`` file path from a file or ``.env``.
|
|
220
596
|
|
|
@@ -224,8 +600,10 @@ def read_canvas_path(path: str = "canvas_path.txt") -> str:
|
|
|
224
600
|
/home/user/project/main.canvas Production canvas
|
|
225
601
|
/home/user/project/test.canvas Test canvas
|
|
226
602
|
|
|
227
|
-
Reads only the first line
|
|
228
|
-
|
|
603
|
+
Reads only the first meaningful line (blank lines and ``#`` comments are
|
|
604
|
+
skipped). A comment may follow the path after two or more spaces or a tab
|
|
605
|
+
(single spaces are part of the path). Multiple paths can be stored and
|
|
606
|
+
swapped by reordering lines.
|
|
229
607
|
|
|
230
608
|
**Environment file** (``.env``)::
|
|
231
609
|
|
|
@@ -236,16 +614,31 @@ def read_canvas_path(path: str = "canvas_path.txt") -> str:
|
|
|
236
614
|
:param path: Path to a canvas path file, or ``".env"`` / ``".env:KEY"`` for
|
|
237
615
|
environment files. Defaults to ``"canvas_path.txt"``.
|
|
238
616
|
:type path: ``str``
|
|
617
|
+
:param cache: If ``True`` (default), use the in-memory copy if available; if
|
|
618
|
+
``False``, re-read the file and refresh the cache.
|
|
619
|
+
:type cache: ``bool``
|
|
239
620
|
:return: Path to the ``.canvas`` file.
|
|
240
621
|
:rtype: ``str``
|
|
241
|
-
:raises
|
|
242
|
-
:raises
|
|
622
|
+
:raises EnvKeyError: If the key is not found in the ``.env`` file.
|
|
623
|
+
:raises EnvFileNotFoundError: If the file does not exist (message contains
|
|
624
|
+
commands to create it).
|
|
625
|
+
:raises EnvValueError: If the file or the value is empty.
|
|
243
626
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
627
|
+
example = "/path/to/project/main.canvas"
|
|
628
|
+
|
|
629
|
+
if _is_env_path(path):
|
|
630
|
+
file_path, name = _split_env_path(path, "CANVAS_PATH")
|
|
631
|
+
value = read_envar(file_path, name, cache=cache).strip()
|
|
632
|
+
|
|
633
|
+
if not value:
|
|
634
|
+
raise EnvValueError(
|
|
635
|
+
f"Variable '{name}' in '{file_path}' is empty.\n"
|
|
636
|
+
f" Hint: set the path after '=', e.g. `{name}={example}`."
|
|
637
|
+
)
|
|
638
|
+
return value
|
|
639
|
+
|
|
640
|
+
line = _read_first_value(path, example, "Canvas path", cache)
|
|
641
|
+
return re.split(r"\s{2,}|\t", line, maxsplit=1)[0]
|
|
249
642
|
|
|
250
643
|
# ––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––––
|
|
251
644
|
# Inline Keyboards
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telekit
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.0a2
|
|
4
4
|
Summary: Declarative, developer-friendly library for building Telegram bots
|
|
5
5
|
Home-page: https://github.com/Romashkaa/telekit
|
|
6
6
|
Author: romashka
|
|
@@ -471,52 +471,13 @@ Telekit focuses on one job: making Telegram bot development easier.
|
|
|
471
471
|
|
|
472
472
|
---
|
|
473
473
|
|
|
474
|
-
# Changes in version 2.6.
|
|
475
|
-
|
|
476
|
-
### v2.
|
|
477
|
-
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
- Implement `TelegramMarkdownV2Sanitizer` for improved `MarkdownV2` handling
|
|
485
|
-
|
|
486
|
-
### v2.5.0 `(final)`
|
|
487
|
-
- Refactor `CalendarPick` trait to use `set_keyboard`
|
|
488
|
-
|
|
489
|
-
### v2.5.0`b3`
|
|
490
|
-
- Added `utils.Markers` class
|
|
491
|
-
- Added `HTMLText` class for handling Telegram HTML strings with tag-aware indexing and slicing.
|
|
492
|
-
- Added `PaginatedText` trait for displaying long HTML text in a paginated format, supporting navigation and smart splitting.
|
|
493
|
-
- Added `__radd__` to `TextEntity`: `"Regular" + Bold(" and Bold")`
|
|
494
|
-
- Added `__mul__` to `TextEntity`: `Bold("Text") * 3`
|
|
495
|
-
- Added `enabled=` parameter to `TextEntity`: `Bold("bold text", enabled=is_text_bold)`
|
|
496
|
-
- Added `TextBuilder` class – a fluent message composition API mirroring `InlineKeyboard`'s builder pattern
|
|
497
|
-
- Added styles to `telekit.types`
|
|
498
|
-
- Added `utils.CyclicList`
|
|
499
|
-
- Fixed `_answer_callback_query` to always call `bot.answer_callback_query()`, even without a popup text
|
|
500
|
-
|
|
501
|
-
### v2.5.0`b2`
|
|
502
|
-
- Added the `escape` parameter to `telekit.utils.*`:
|
|
503
|
-
- `make_user_link`
|
|
504
|
-
- `make_bot_link`
|
|
505
|
-
- `Handler.handlers_dict` now excludes private handlers (classes whose names start with `_`).
|
|
506
|
-
- Added `Debug.duplicate_handler_warnings` to warn about duplicate handler names during initialization.
|
|
507
|
-
- Added `Handler.chat` object (BETA)
|
|
508
|
-
|
|
509
|
-
### v2.5.0`b1`
|
|
510
|
-
- Added `Sender.send_message` method.
|
|
511
|
-
- Added `utils.make_mention` utility for generating `tg://user?id=` mention links.
|
|
512
|
-
- Added new inline button types to `inline_buttons`:
|
|
513
|
-
- `ContactButton` — mentions a user by Telegram ID via `tg://user?id=`.
|
|
514
|
-
- `UserLinkButton` — opens a user profile by username; supports pre-filled message text.
|
|
515
|
-
- `BotLinkButton` — opens a bot by username; supports deep-link `?start=` payload.
|
|
516
|
-
- Added new methods to `InlineKeyboard`:
|
|
517
|
-
- `add_contact` — adds a `ContactButton`.
|
|
518
|
-
- `add_user_link` — adds a `UserLinkButton`.
|
|
519
|
-
- `add_bot_link` — adds a `BotLinkButton`.
|
|
520
|
-
|
|
521
|
-
### v2.5.0`b0`
|
|
522
|
-
- Added support for t-strings (PEP 750, Python 3.14+) in `TextEntity`.
|
|
474
|
+
# Changes in version 2.6.0a2
|
|
475
|
+
|
|
476
|
+
### v2.6.0 `a2`
|
|
477
|
+
- Reworked `.env` and token/canvas file reading in `utils`: full `.env` syntax support (comments, `export`, quotes and escapes, multi-line values, `$VAR` interpolation), a `cache` parameter (default `True`) with `clear_cache()`, detailed errors with fix suggestions and creation commands, and new `Env*Error` exceptions; `load_env` now raises `EnvFileNotFoundError` for a missing file instead of returning `{}`
|
|
478
|
+
|
|
479
|
+
### v2.6.0 `a1`
|
|
480
|
+
- Added a module-loading utility in `utils`
|
|
481
|
+
|
|
482
|
+
### v2.6.0 `a0`
|
|
483
|
+
- Improved formatting in `_on.py`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|