telekit 2.6.0a2__tar.gz → 2.6.0a4__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.
Files changed (89) hide show
  1. {telekit-2.6.0a2 → telekit-2.6.0a4}/PKG-INFO +23 -6
  2. {telekit-2.6.0a2 → telekit-2.6.0a4}/setup.py +2 -2
  3. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_buildtext/styles.py +484 -1
  4. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_chain_inline_keyboards_logic.py +4 -4
  5. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_handler.py +36 -1
  6. telekit-2.6.0a4/telekit/_rich.py +381 -0
  7. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_version.py +1 -1
  8. telekit-2.6.0a4/telekit/example/example_handlers/rich.py +555 -0
  9. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/start.py +3 -1
  10. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/senders.py +243 -47
  11. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/styles.py +31 -6
  12. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/traits/track_handoff_origin.py +63 -25
  13. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit.egg-info/PKG-INFO +23 -6
  14. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit.egg-info/SOURCES.txt +2 -0
  15. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit.egg-info/requires.txt +1 -1
  16. {telekit-2.6.0a2 → telekit-2.6.0a4}/LICENSE +0 -0
  17. {telekit-2.6.0a2 → telekit-2.6.0a4}/README.md +0 -0
  18. {telekit-2.6.0a2 → telekit-2.6.0a4}/setup.cfg +0 -0
  19. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/__init__.py +0 -0
  20. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_buildtext/__init__.py +0 -0
  21. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_buildtext/formatter.py +0 -0
  22. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_callback_query_handler.py +0 -0
  23. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_chain.py +0 -0
  24. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_chain_base.py +0 -0
  25. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_chain_entry_logic.py +0 -0
  26. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_chapters/__init__.py +0 -0
  27. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_chapters/chapters.py +0 -0
  28. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_init.py +0 -0
  29. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_inline_buttons.py +0 -0
  30. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_inline_keyboard.py +0 -0
  31. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_input_handler.py +0 -0
  32. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_logger.py +0 -0
  33. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_on.py +0 -0
  34. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_reply_keyboard.py +0 -0
  35. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_snapvault/__init__.py +0 -0
  36. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_snapvault/snapcode.py +0 -0
  37. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_snapvault/snapvault.py +0 -0
  38. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_state.py +0 -0
  39. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/__init__.py +0 -0
  40. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/mixin.py +0 -0
  41. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/parser/__init__.py +0 -0
  42. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/parser/builder.py +0 -0
  43. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/parser/canvas_parser.py +0 -0
  44. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/parser/lexer.py +0 -0
  45. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/parser/nodes.py +0 -0
  46. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/parser/parser.py +0 -0
  47. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/parser/token.py +0 -0
  48. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/telekit_dsl.py +0 -0
  49. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_telekit_dsl/telekit_orm.py +0 -0
  50. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_text_builder.py +0 -0
  51. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_timeout.py +0 -0
  52. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_trait.py +0 -0
  53. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/_user.py +0 -0
  54. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/chat.py +0 -0
  55. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/debug.py +0 -0
  56. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/dices.py +0 -0
  57. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/__init__.py +0 -0
  58. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/__init__.py +0 -0
  59. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/article.py +0 -0
  60. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/calendar.py +0 -0
  61. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/complete_hotel.py +0 -0
  62. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/counter.py +0 -0
  63. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/dsl.py +0 -0
  64. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/entry.py +0 -0
  65. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/faq.py +0 -0
  66. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/hotel.py +0 -0
  67. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/on_text.py +0 -0
  68. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/pages.py +0 -0
  69. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/pyapi.py +0 -0
  70. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/qr.py +0 -0
  71. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/quiz.py +0 -0
  72. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/spells.py +0 -0
  73. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/style.py +0 -0
  74. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_handlers/text_document.py +0 -0
  75. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/example/example_server.py +0 -0
  76. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/html_text.py +0 -0
  77. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/inline_buttons.py +0 -0
  78. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/parameters.py +0 -0
  79. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/reply_buttons.py +0 -0
  80. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/scheduler.py +0 -0
  81. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/server.py +0 -0
  82. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/traits/__init__.py +0 -0
  83. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/traits/calendar_pick.py +0 -0
  84. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/traits/paginated_choice.py +0 -0
  85. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/traits/paginated_text.py +0 -0
  86. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/types.py +0 -0
  87. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit/utils.py +0 -0
  88. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit.egg-info/dependency_links.txt +0 -0
  89. {telekit-2.6.0a2 → telekit-2.6.0a4}/telekit.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: telekit
3
- Version: 2.6.0a2
4
- Summary: Declarative, developer-friendly library for building Telegram bots
3
+ Version: 2.6.0a4
4
+ Summary: Declarative, developer-friendly framework for building Telegram bots
5
5
  Home-page: https://github.com/Romashkaa/telekit
6
6
  Author: romashka
7
7
  Author-email: notromashka@gmail.com
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: charset_normalizer==3.4.2
22
22
  Requires-Dist: Jinja2==3.1.6
23
- Requires-Dist: pyTelegramBotAPI==4.31.0
23
+ Requires-Dist: pyTelegramBotAPI==4.37.0
24
24
  Dynamic: author
25
25
  Dynamic: author-email
26
26
  Dynamic: classifier
@@ -471,13 +471,30 @@ Telekit focuses on one job: making Telegram bot development easier.
471
471
 
472
472
  ---
473
473
 
474
- # Changes in version 2.6.0a2
474
+ # Changes in version 2.6.0a4
475
+
476
+ ### v2.6.0 `a4`
477
+ - Added `handle_handoff()` to `Handler`: a shortcut for `handoff(handler).handle()` that takes the target handler as an argument, so it can be passed to `add_callback()` without an extra closure
478
+ - Added `handle_handoff_back_or()` to `TrackHandoffOrigin`: a closure-free counterpart of `handoff_back_or()`
479
+ - Reworked `handoff_back_or()` as a thin `functools.partial` wrapper around `handle_handoff_back_or()`
480
+ - Fixed the `handoff_back_or()` docstring: the returned callable forwards its arguments to `handle()` instead of being zero-argument
481
+ - Converted the `handoff()` docstring to reST
482
+
483
+ ### v2.6.0 `a3`
484
+ - Added support for Rich HTML messages (Bot API 10.1+)
485
+ - Added the `_rich.py` module for Rich HTML parsing and serialization
486
+ - Added new rich tags to `styles.py` for enhanced text formatting
487
+ - Added the `rich.py` example handler demonstrating Rich HTML: inline styles, blocks, lists, tables, media, and sender integration
475
488
 
476
489
  ### 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 `{}`
490
+ - Reworked `.env` and token/canvas file reading in `utils`
491
+ - Added full `.env` syntax support: comments, `export`, quotes and escapes, multi-line values, `$VAR` interpolation
492
+ - Added the `cache` parameter (default `True`) and `clear_cache()`
493
+ - Added detailed errors with fix suggestions and creation commands, and new `Env*Error` exceptions
494
+ - Changed `load_env()` to raise `EnvFileNotFoundError` for a missing file instead of returning `{}`
478
495
 
479
496
  ### v2.6.0 `a1`
480
- - Added a module-loading utility in `utils`
497
+ - Added a module-loading utility to `utils`
481
498
 
482
499
  ### v2.6.0 `a0`
483
500
  - Improved formatting in `_on.py`
@@ -45,7 +45,7 @@ def long_description():
45
45
  setup(
46
46
  name='telekit',
47
47
  version=version,
48
- description='Declarative, developer-friendly library for building Telegram bots',
48
+ description='Declarative, developer-friendly framework for building Telegram bots',
49
49
  long_description=long_description(),
50
50
  long_description_content_type='text/markdown',
51
51
  keywords='telegram bot api declarative tools bot-api',
@@ -70,4 +70,4 @@ setup(
70
70
  }
71
71
  )
72
72
 
73
- print(f"TELEKIT VERSION: {version}")
73
+ print(f"TELEKIT VERSION: {version}")
@@ -18,6 +18,7 @@
18
18
  #
19
19
  from typing import Union, Literal, TYPE_CHECKING, Union, Any
20
20
  from urllib.parse import quote
21
+ import html as _html
21
22
 
22
23
  if TYPE_CHECKING: # Union[str, "TextEntity", "Template"]
23
24
  from string.templatelib import Template # pyright: ignore[reportMissingImports]
@@ -728,6 +729,459 @@ class Stack(TextEntity):
728
729
  return f"\n{content}{end}\n"
729
730
 
730
731
 
732
+
733
+ # ---------------------------------------------------------------------------------
734
+ # Rich HTML styles (Bot API 10.1+). Use together with sender.set_rich_html(True)
735
+ # ---------------------------------------------------------------------------------
736
+
737
+ def _attrs(**attrs) -> str:
738
+ """Builds an HTML attribute string. `True` -> bare attribute, None/False -> skipped."""
739
+ parts = []
740
+ for key, value in attrs.items():
741
+ if value is None or value is False:
742
+ continue
743
+ key = key.replace("_", "-")
744
+ if value is True:
745
+ parts.append(key)
746
+ else:
747
+ parts.append(f'{key}="{_html.escape(str(value), quote=True)}"')
748
+ return (" " + " ".join(parts)) if parts else ""
749
+
750
+
751
+ class RichEntity(EasyTextEntity):
752
+ """
753
+ ⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
754
+
755
+ Base class for styles that exist only in Rich HTML.
756
+ Outside of rich mode the sender downgrades these tags to plain formatting
757
+ (or drops them) and prints a warning.
758
+ """
759
+
760
+
761
+ # -- inline
762
+
763
+ class Marked(RichEntity):
764
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
765
+
766
+ Highlighted text: ``<mark>``."""
767
+ def _render_html(self, content: str) -> str:
768
+ return f"<mark>{content}</mark>"
769
+
770
+
771
+ class Subscript(RichEntity):
772
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
773
+
774
+ Subscript: ``<sub>``."""
775
+ def _render_html(self, content: str) -> str:
776
+ return f"<sub>{content}</sub>"
777
+
778
+
779
+ class Superscript(RichEntity):
780
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
781
+
782
+ Superscript: ``<sup>``."""
783
+ def _render_html(self, content: str) -> str:
784
+ return f"<sup>{content}</sup>"
785
+
786
+
787
+ class CustomEmoji(EasyTextEntity):
788
+ """
789
+ Custom emoji: ``<tg-emoji emoji-id="...">fallback</tg-emoji>``.
790
+ Works both in regular HTML and in Rich HTML. The content is the fallback emoji.
791
+
792
+ Example::
793
+
794
+ CustomEmoji("👍", emoji_id=5368324170671202286)
795
+ """
796
+ def __init__(self, *content, emoji_id: int | str, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
797
+ self._emoji_id = emoji_id
798
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
799
+
800
+ def _render_html(self, content: str) -> str:
801
+ return f"<tg-emoji{_attrs(emoji_id=self._emoji_id)}>{content}</tg-emoji>"
802
+
803
+
804
+ class DateTime(RichEntity):
805
+ """
806
+ ⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
807
+
808
+ Formatted date and time: ``<tg-time unix="..." format="...">``.
809
+
810
+ Example::
811
+
812
+ DateTime("22:45 tomorrow", unix=1647531900, format="wDT")
813
+ """
814
+ def __init__(self, *content, unix: int, format: str | None = None, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
815
+ self._unix = unix
816
+ self._format = format
817
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
818
+
819
+ def _render_html(self, content: str) -> str:
820
+ return f"<tg-time{_attrs(unix=self._unix, format=self._format)}>{content}</tg-time>"
821
+
822
+
823
+ class Math(RichEntity):
824
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
825
+
826
+ Inline LaTeX formula: ``<tg-math>``. The content is treated as raw LaTeX."""
827
+ def _render_html(self, content: str) -> str:
828
+ return f"<tg-math>{content}</tg-math>"
829
+
830
+
831
+ class MathBlock(RichEntity):
832
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
833
+
834
+ Block LaTeX formula: ``<tg-math-block>``."""
835
+ def _render_html(self, content: str) -> str:
836
+ return f"<tg-math-block>{content}</tg-math-block>"
837
+
838
+
839
+ class Anchor(RichEntity):
840
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
841
+
842
+ Anchor definition: ``<a name="..."></a>``. Target for ``AnchorLink``."""
843
+ def __init__(self, name: str, *, enabled: bool | Any = True):
844
+ self._name = name
845
+ super().__init__(enabled=enabled)
846
+
847
+ def _render_html(self, content: str) -> str:
848
+ return f"<a{_attrs(name=self._name)}></a>"
849
+
850
+
851
+ class AnchorLink(RichEntity):
852
+ """
853
+ ⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
854
+
855
+ Link to an anchor or to a reference: ``<a href="#name">``.
856
+ An empty ``name`` links back to the top of the message.
857
+ """
858
+ def __init__(self, *content, name: str = "", escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
859
+ self._name = name
860
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
861
+
862
+ def _render_html(self, content: str) -> str:
863
+ return f"<a{_attrs(href='#' + self._name)}>{content}</a>"
864
+
865
+
866
+ class Reference(RichEntity):
867
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
868
+
869
+ Footnote definition: ``<tg-reference name="...">``. Link to it with ``AnchorLink``."""
870
+ def __init__(self, *content, name: str, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
871
+ self._name = name
872
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
873
+
874
+ def _render_html(self, content: str) -> str:
875
+ return f"<tg-reference{_attrs(name=self._name)}>{content}</tg-reference>"
876
+
877
+
878
+ # -- text blocks
879
+
880
+ class Heading(RichEntity):
881
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
882
+
883
+ Heading ``<h1>``..``<h6>``; ``level=1`` is the largest."""
884
+ def __init__(self, *content, level: int = 1, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
885
+ if not 1 <= level <= 6:
886
+ raise ValueError("Heading level must be between 1 and 6")
887
+ self._level = level
888
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
889
+
890
+ def _render_html(self, content: str) -> str:
891
+ return f"<h{self._level}>{content}</h{self._level}>"
892
+
893
+ def _render_markdown(self, content: str) -> str:
894
+ return telebot.formatting.mbold(content, escape=False) + "\n"
895
+
896
+
897
+ class Paragraph(RichEntity):
898
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
899
+
900
+ Paragraph: ``<p>``."""
901
+ def _render_html(self, content: str) -> str:
902
+ return f"<p>{content}</p>"
903
+
904
+
905
+ class Footer(RichEntity):
906
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
907
+
908
+ Footer: ``<footer>``."""
909
+ def _render_html(self, content: str) -> str:
910
+ return f"<footer>{content}</footer>"
911
+
912
+
913
+ class Divider(RichEntity):
914
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
915
+
916
+ Horizontal divider: ``<hr/>``."""
917
+ def __init__(self, *, enabled: bool | Any = True):
918
+ super().__init__(enabled=enabled)
919
+
920
+ def _render_html(self, content: str) -> str:
921
+ return "<hr/>"
922
+
923
+ def _render_markdown(self, content: str) -> str:
924
+ return "———\n"
925
+
926
+ def _render_none(self, content: str) -> str:
927
+ return "———\n"
928
+
929
+
930
+ class Cite(RichEntity):
931
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
932
+
933
+ Author/credit line: ``<cite>``. Use inside ``Quote`` or ``PullQuote``."""
934
+ def _render_html(self, content: str) -> str:
935
+ return f"<cite>{content}</cite>"
936
+
937
+
938
+ class PullQuote(RichEntity):
939
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
940
+
941
+ Centered pull quotation: ``<aside>`` with an optional credit."""
942
+ def __init__(self, *content, credit: Union[str, "TextEntity", "Template", None] = None, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
943
+ self._credit = credit
944
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
945
+
946
+ def _render_html(self, content: str) -> str:
947
+ credit = ""
948
+ if self._credit is not None:
949
+ credit = f"<cite>{self._render_item(self._credit, 'html')}</cite>"
950
+ return f"<aside>{content}{credit}</aside>"
951
+
952
+
953
+ class Details(RichEntity):
954
+ """
955
+ ⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
956
+
957
+ Collapsible block: ``<details><summary>``.
958
+
959
+ Example::
960
+
961
+ Details("Hidden text", summary="Click me", is_open=False)
962
+ """
963
+ def __init__(self, *content, summary: Union[str, "TextEntity", "Template"], is_open: bool = False, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
964
+ self._summary = summary
965
+ self._is_open = is_open
966
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
967
+
968
+ def _render_html(self, content: str) -> str:
969
+ summary = self._render_item(self._summary, "html")
970
+ return f"<details{_attrs(open=self._is_open)}><summary>{summary}</summary>{content}</details>"
971
+
972
+
973
+ # -- lists
974
+
975
+ class ListItem(RichEntity):
976
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
977
+
978
+ List item ``<li>`` with optional explicit ``value`` and ``type`` (ordered lists)."""
979
+ def __init__(self, *content, value: int | None = None, type: str | None = None, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
980
+ self._value = value
981
+ self._type = type
982
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
983
+
984
+ def _render_html(self, content: str) -> str:
985
+ return f"<li{_attrs(value=self._value, type=self._type)}>{content}</li>"
986
+
987
+
988
+ class _RichList(RichEntity):
989
+ _ordered = False
990
+
991
+ def _render_content(self, parse_mode):
992
+ if parse_mode == "html":
993
+ return "".join(
994
+ self._render_item(item, parse_mode) if isinstance(item, ListItem)
995
+ else f"<li>{self._render_item(item, parse_mode)}</li>"
996
+ for item in self._content
997
+ )
998
+ lines = []
999
+ for index, item in enumerate(self._content, start=1):
1000
+ marker = f"{index}. " if self._ordered else "• "
1001
+ lines.append(self._escape(marker, parse_mode) + self._render_item(item, parse_mode))
1002
+ return "\n".join(lines)
1003
+
1004
+
1005
+ class UnorderedList(_RichList):
1006
+ """
1007
+ ⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1008
+
1009
+ Bulleted list: ``<ul>``. Items are strings/entities or ``ListItem``.
1010
+
1011
+ Example::
1012
+
1013
+ UnorderedList("One", Bold("Two"), ListItem("Three"))
1014
+ """
1015
+ def _render_html(self, content: str) -> str:
1016
+ return f"<ul>{content}</ul>"
1017
+
1018
+
1019
+ class OrderedList(_RichList):
1020
+ """
1021
+ ⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1022
+
1023
+ Numbered list: ``<ol>``.
1024
+
1025
+ :param start: First number.
1026
+ :param type: ``"1"``, ``"a"``, ``"A"``, ``"i"`` or ``"I"``.
1027
+ :param reversed: Count backwards.
1028
+ """
1029
+ _ordered = True
1030
+
1031
+ def __init__(self, *items, start: int | None = None, type: str | None = None, reversed: bool = False, escape: bool = True, enabled: bool | Any = True):
1032
+ self._start = start
1033
+ self._type = type
1034
+ self._reversed = reversed
1035
+ super().__init__(*items, escape=escape, enabled=enabled)
1036
+
1037
+ def _render_html(self, content: str) -> str:
1038
+ attrs = _attrs(start=self._start, type=self._type, reversed=self._reversed)
1039
+ return f"<ol{attrs}>{content}</ol>"
1040
+
1041
+
1042
+ # -- tables
1043
+
1044
+ class TableCell(RichEntity):
1045
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1046
+
1047
+ Table cell ``<td>`` / ``<th>`` (``header=True``). Cells may contain only inline formatting."""
1048
+ def __init__(self, *content, header: bool = False, colspan: int | None = None, rowspan: int | None = None, align: Literal["left", "center", "right"] | None = None, valign: Literal["top", "middle", "bottom"] | None = None, escape: bool = True, sep: Union[str, "TextEntity", "Template"] = "", enabled: bool | Any = True):
1049
+ self._header = header
1050
+ self._span = {"colspan": colspan, "rowspan": rowspan, "align": align, "valign": valign}
1051
+ super().__init__(*content, escape=escape, sep=sep, enabled=enabled)
1052
+
1053
+ def _render_html(self, content: str) -> str:
1054
+ tag = "th" if self._header else "td"
1055
+ return f"<{tag}{_attrs(**self._span)}>{content}</{tag}>"
1056
+
1057
+
1058
+ class Table(RichEntity):
1059
+ """
1060
+ ⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1061
+
1062
+ Table: ``<table>``. Each row is a list/tuple of cells (strings, entities or ``TableCell``).
1063
+
1064
+ :param header: Render the first row as header cells.
1065
+
1066
+ Example::
1067
+
1068
+ Table(["Name", "Score"], ["Ann", "10"], ["Bob", "7"], header=True, bordered=True)
1069
+ """
1070
+ def __init__(self, *rows, header: bool = False, bordered: bool = False, striped: bool = False, caption: Union[str, "TextEntity", "Template", None] = None, escape: bool = True, enabled: bool | Any = True):
1071
+ self._header = header
1072
+ self._bordered = bordered
1073
+ self._striped = striped
1074
+ self._caption = caption
1075
+ super().__init__(*rows, escape=escape, enabled=enabled)
1076
+
1077
+ def _render_content(self, parse_mode):
1078
+ is_html = parse_mode == "html"
1079
+ lines = []
1080
+
1081
+ if self._caption is not None:
1082
+ caption = self._render_item(self._caption, parse_mode)
1083
+ lines.append(f"<caption>{caption}</caption>" if is_html else caption)
1084
+
1085
+ for row_index, row in enumerate(self._content):
1086
+ cells = []
1087
+ for cell in row:
1088
+ rendered = self._render_item(cell, parse_mode)
1089
+ if is_html and not isinstance(cell, TableCell):
1090
+ tag = "th" if (self._header and row_index == 0) else "td"
1091
+ rendered = f"<{tag}>{rendered}</{tag}>"
1092
+ cells.append(rendered)
1093
+
1094
+ if is_html:
1095
+ lines.append("<tr>" + "".join(cells) + "</tr>")
1096
+ else:
1097
+ lines.append(self._escape(" | ", parse_mode).join(cells))
1098
+
1099
+ return ("" if is_html else "\n").join(lines)
1100
+
1101
+ def _render_html(self, content: str) -> str:
1102
+ return f"<table{_attrs(bordered=self._bordered, striped=self._striped)}>{content}</table>"
1103
+
1104
+
1105
+ # -- media (http/https URLs only)
1106
+
1107
+ class _RichMedia(RichEntity):
1108
+ def __init__(self, url: str, *, caption: Union[str, "TextEntity", "Template", None] = None, credit: Union[str, "TextEntity", "Template", None] = None, spoiler: bool = False, enabled: bool | Any = True):
1109
+ self._url = url
1110
+ self._caption = caption
1111
+ self._credit = credit
1112
+ self._spoiler = spoiler
1113
+ super().__init__(enabled=enabled)
1114
+
1115
+ def _media(self) -> str:
1116
+ raise NotImplementedError
1117
+
1118
+ def _render_html(self, content: str) -> str:
1119
+ media = self._media()
1120
+ if self._caption is None and self._credit is None:
1121
+ return media
1122
+
1123
+ caption = self._render_item(self._caption, "html") if self._caption is not None else ""
1124
+ credit = f"<cite>{self._render_item(self._credit, 'html')}</cite>" if self._credit is not None else ""
1125
+ return f"<figure>{media}<figcaption>{caption}{credit}</figcaption></figure>"
1126
+
1127
+ def _media_attrs(self) -> str:
1128
+ return _attrs(src=self._url, **{"tg-spoiler": self._spoiler})
1129
+
1130
+
1131
+ class Image(_RichMedia):
1132
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1133
+
1134
+ Photo block: ``<img>``; with ``caption``/``credit`` it is wrapped into ``<figure>``."""
1135
+ def _media(self) -> str:
1136
+ return f"<img{self._media_attrs()}/>"
1137
+
1138
+
1139
+ class Video(_RichMedia):
1140
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1141
+
1142
+ Video block: ``<video>``."""
1143
+ def _media(self) -> str:
1144
+ return f"<video{self._media_attrs()}></video>"
1145
+
1146
+
1147
+ class Audio(_RichMedia):
1148
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1149
+
1150
+ Audio block: ``<audio>`` (.ogg is shown as a voice note)."""
1151
+ def _media(self) -> str:
1152
+ return f"<audio{self._media_attrs()}></audio>"
1153
+
1154
+
1155
+ class Map(RichEntity):
1156
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1157
+
1158
+ Map block: ``<tg-map>``. ``zoom`` must be 13–20."""
1159
+ def __init__(self, lat: float, long: float, *, zoom: int = 15, enabled: bool | Any = True):
1160
+ if not 13 <= zoom <= 20:
1161
+ raise ValueError("Map zoom must be between 13 and 20")
1162
+ self._lat, self._long, self._zoom = lat, long, zoom
1163
+ super().__init__(enabled=enabled)
1164
+
1165
+ def _render_html(self, content: str) -> str:
1166
+ return f"<tg-map{_attrs(lat=self._lat, long=self._long, zoom=self._zoom)}/>"
1167
+
1168
+
1169
+ class Collage(RichEntity):
1170
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1171
+
1172
+ Collage of media blocks: ``<tg-collage>``. Pass ``Image``/``Video`` entities."""
1173
+ def _render_html(self, content: str) -> str:
1174
+ return f"<tg-collage>{content}</tg-collage>"
1175
+
1176
+
1177
+ class Slideshow(RichEntity):
1178
+ """⚠️ Works only in Rich mode (``sender.set_rich_html(True)``).
1179
+
1180
+ Slideshow of media blocks: ``<tg-slideshow>``."""
1181
+ def _render_html(self, content: str) -> str:
1182
+ return f"<tg-slideshow>{content}</tg-slideshow>"
1183
+
1184
+
731
1185
  class Styles:
732
1186
  """
733
1187
  Namespace for message formatting styles:
@@ -757,4 +1211,33 @@ class Styles:
757
1211
  Mention: type[TextEntity] = Mention
758
1212
  UserLink: type[TextEntity] = UserLink
759
1213
  BotLink: type[TextEntity] = BotLink
760
- Group: type[TextEntity] = Group
1214
+ Group: type[TextEntity] = Group
1215
+ # Rich HTML
1216
+ Marked: type[TextEntity] = Marked
1217
+ Subscript: type[TextEntity] = Subscript
1218
+ Superscript: type[TextEntity] = Superscript
1219
+ CustomEmoji: type[TextEntity] = CustomEmoji
1220
+ DateTime: type[TextEntity] = DateTime
1221
+ Math: type[TextEntity] = Math
1222
+ MathBlock: type[TextEntity] = MathBlock
1223
+ Anchor: type[TextEntity] = Anchor
1224
+ AnchorLink: type[TextEntity] = AnchorLink
1225
+ Reference: type[TextEntity] = Reference
1226
+ Heading: type[TextEntity] = Heading
1227
+ Paragraph: type[TextEntity] = Paragraph
1228
+ Footer: type[TextEntity] = Footer
1229
+ Divider: type[TextEntity] = Divider
1230
+ Cite: type[TextEntity] = Cite
1231
+ PullQuote: type[TextEntity] = PullQuote
1232
+ Details: type[TextEntity] = Details
1233
+ ListItem: type[TextEntity] = ListItem
1234
+ UnorderedList: type[TextEntity] = UnorderedList
1235
+ OrderedList: type[TextEntity] = OrderedList
1236
+ Table: type[TextEntity] = Table
1237
+ TableCell: type[TextEntity] = TableCell
1238
+ Image: type[TextEntity] = Image
1239
+ Video: type[TextEntity] = Video
1240
+ Audio: type[TextEntity] = Audio
1241
+ Map: type[TextEntity] = Map
1242
+ Collage: type[TextEntity] = Collage
1243
+ Slideshow: type[TextEntity] = Slideshow
@@ -111,7 +111,7 @@ class ChainInlineKeyboardLogic(ChainBase):
111
111
  )
112
112
 
113
113
  markup = InlineKeyboardMarkup()
114
- markup.keyboard = self._build_keyboard_rows(buttons, row_width)
114
+ markup.inline_keyboard = self._build_keyboard_rows(buttons, row_width)
115
115
 
116
116
  self.sender.set_reply_markup(markup)
117
117
  self._handler.set_button_callbacks(button_callbacks)
@@ -173,7 +173,7 @@ class ChainInlineKeyboardLogic(ChainBase):
173
173
  )
174
174
 
175
175
  markup = InlineKeyboardMarkup()
176
- markup.keyboard = self._build_keyboard_rows(buttons, row_width)
176
+ markup.inline_keyboard = self._build_keyboard_rows(buttons, row_width)
177
177
 
178
178
  self.sender.set_reply_markup(markup)
179
179
  self._handler.set_button_callbacks(callback_functions)
@@ -281,7 +281,7 @@ class ChainInlineKeyboardLogic(ChainBase):
281
281
  )
282
282
 
283
283
  markup = InlineKeyboardMarkup()
284
- markup.keyboard = self._build_keyboard_rows(buttons, row_width)
284
+ markup.inline_keyboard = self._build_keyboard_rows(buttons, row_width)
285
285
 
286
286
  self.sender.set_reply_markup(markup)
287
287
  self._handler.set_button_callbacks(callback_functions)
@@ -333,7 +333,7 @@ class ChainInlineKeyboardLogic(ChainBase):
333
333
  buttons.append(InlineButton.Suggest(suggestion)._compile(caption))
334
334
 
335
335
  markup = InlineKeyboardMarkup()
336
- markup.keyboard = self._build_keyboard_rows(buttons, row_width)
336
+ markup.inline_keyboard = self._build_keyboard_rows(buttons, row_width)
337
337
 
338
338
  self.sender.set_reply_markup(markup)
339
339
 
@@ -242,10 +242,45 @@ class Handler:
242
242
  def _on_handoff(self, origin: "Handler") -> None:
243
243
  """Called when this handler is reached via handoff(). Override to customize."""
244
244
  pass
245
-
245
+
246
+ def handle_handoff(self, handler: type["Handler"] | str):
247
+ """
248
+ Transfers control to another handler and immediately runs its ``handle()``.
249
+
250
+ This is a shortcut for ``self.handoff(handler).handle()``. The target
251
+ handler is created via :meth:`handoff`, so it inherits the same initial
252
+ user message and the previous chain message (which allows editing it).
253
+
254
+ Unlike ``self.handoff(handler).handle``, it takes the target handler as
255
+ an argument, so it can be passed directly as a callback together with
256
+ its arguments, without creating an extra closure::
257
+
258
+ self.chain.add_callback(
259
+ "Settings",
260
+ self.handle_handoff, [SettingsHandler],
261
+ )
262
+
263
+ It can also be called directly::
264
+
265
+ self.handle_handoff(SettingsHandler)
266
+ self.handle_handoff("SettingsHandler")
267
+
268
+ :param handler: Target handler name or ``Handler`` subclass to transfer
269
+ control to.
270
+ :type handler: str | type[Handler]
271
+
272
+ :raises NameError: If a string name is provided but no registered
273
+ handler exists with that name.
274
+ :raises TypeError: If the provided value is not a ``Handler`` subclass.
275
+
276
+ .. seealso:: :meth:`handoff`
277
+ """
278
+ self.handoff(handler).handle()
246
279
 
247
280
  def freeze(self, func, *args):
248
281
  """
282
+ DEPRECATED
283
+
249
284
  Return a zero-argument callback that invokes the given function
250
285
  with the provided arguments.
251
286