telegrinder 0.1.dev20__py3-none-any.whl → 0.1.dev159__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.

Potentially problematic release.


This version of telegrinder might be problematic. Click here for more details.

Files changed (132) hide show
  1. telegrinder/__init__.py +129 -22
  2. telegrinder/api/__init__.py +11 -2
  3. telegrinder/api/abc.py +25 -9
  4. telegrinder/api/api.py +47 -28
  5. telegrinder/api/error.py +14 -4
  6. telegrinder/api/response.py +11 -7
  7. telegrinder/bot/__init__.py +68 -7
  8. telegrinder/bot/bot.py +30 -24
  9. telegrinder/bot/cute_types/__init__.py +11 -1
  10. telegrinder/bot/cute_types/base.py +138 -0
  11. telegrinder/bot/cute_types/callback_query.py +458 -15
  12. telegrinder/bot/cute_types/inline_query.py +30 -24
  13. telegrinder/bot/cute_types/message.py +2982 -78
  14. telegrinder/bot/cute_types/update.py +30 -0
  15. telegrinder/bot/cute_types/utils.py +794 -0
  16. telegrinder/bot/dispatch/__init__.py +56 -3
  17. telegrinder/bot/dispatch/abc.py +9 -7
  18. telegrinder/bot/dispatch/composition.py +74 -0
  19. telegrinder/bot/dispatch/context.py +71 -0
  20. telegrinder/bot/dispatch/dispatch.py +86 -49
  21. telegrinder/bot/dispatch/handler/__init__.py +3 -0
  22. telegrinder/bot/dispatch/handler/abc.py +11 -5
  23. telegrinder/bot/dispatch/handler/func.py +41 -32
  24. telegrinder/bot/dispatch/handler/message_reply.py +46 -0
  25. telegrinder/bot/dispatch/middleware/__init__.py +2 -0
  26. telegrinder/bot/dispatch/middleware/abc.py +10 -4
  27. telegrinder/bot/dispatch/process.py +53 -49
  28. telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
  29. telegrinder/bot/dispatch/return_manager/abc.py +95 -0
  30. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  31. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  32. telegrinder/bot/dispatch/return_manager/message.py +25 -0
  33. telegrinder/bot/dispatch/view/__init__.py +14 -2
  34. telegrinder/bot/dispatch/view/abc.py +128 -2
  35. telegrinder/bot/dispatch/view/box.py +38 -0
  36. telegrinder/bot/dispatch/view/callback_query.py +13 -39
  37. telegrinder/bot/dispatch/view/inline_query.py +11 -39
  38. telegrinder/bot/dispatch/view/message.py +11 -47
  39. telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
  40. telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
  41. telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
  42. telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
  43. telegrinder/bot/polling/__init__.py +2 -0
  44. telegrinder/bot/polling/abc.py +11 -4
  45. telegrinder/bot/polling/polling.py +89 -40
  46. telegrinder/bot/rules/__init__.py +91 -5
  47. telegrinder/bot/rules/abc.py +81 -63
  48. telegrinder/bot/rules/adapter/__init__.py +11 -0
  49. telegrinder/bot/rules/adapter/abc.py +21 -0
  50. telegrinder/bot/rules/adapter/errors.py +5 -0
  51. telegrinder/bot/rules/adapter/event.py +49 -0
  52. telegrinder/bot/rules/adapter/raw_update.py +24 -0
  53. telegrinder/bot/rules/callback_data.py +159 -38
  54. telegrinder/bot/rules/command.py +116 -0
  55. telegrinder/bot/rules/enum_text.py +28 -0
  56. telegrinder/bot/rules/func.py +17 -17
  57. telegrinder/bot/rules/fuzzy.py +13 -10
  58. telegrinder/bot/rules/inline.py +61 -0
  59. telegrinder/bot/rules/integer.py +12 -7
  60. telegrinder/bot/rules/is_from.py +148 -7
  61. telegrinder/bot/rules/markup.py +21 -18
  62. telegrinder/bot/rules/mention.py +17 -0
  63. telegrinder/bot/rules/message_entities.py +33 -0
  64. telegrinder/bot/rules/regex.py +27 -19
  65. telegrinder/bot/rules/rule_enum.py +74 -0
  66. telegrinder/bot/rules/start.py +25 -13
  67. telegrinder/bot/rules/text.py +23 -14
  68. telegrinder/bot/scenario/__init__.py +2 -0
  69. telegrinder/bot/scenario/abc.py +12 -5
  70. telegrinder/bot/scenario/checkbox.py +48 -30
  71. telegrinder/bot/scenario/choice.py +16 -10
  72. telegrinder/client/__init__.py +3 -1
  73. telegrinder/client/abc.py +26 -16
  74. telegrinder/client/aiohttp.py +54 -32
  75. telegrinder/model.py +119 -40
  76. telegrinder/modules.py +189 -21
  77. telegrinder/msgspec_json.py +14 -0
  78. telegrinder/msgspec_utils.py +227 -0
  79. telegrinder/node/__init__.py +31 -0
  80. telegrinder/node/attachment.py +71 -0
  81. telegrinder/node/base.py +93 -0
  82. telegrinder/node/composer.py +71 -0
  83. telegrinder/node/container.py +22 -0
  84. telegrinder/node/message.py +18 -0
  85. telegrinder/node/rule.py +56 -0
  86. telegrinder/node/source.py +31 -0
  87. telegrinder/node/text.py +13 -0
  88. telegrinder/node/tools/__init__.py +3 -0
  89. telegrinder/node/tools/generator.py +40 -0
  90. telegrinder/node/update.py +12 -0
  91. telegrinder/rules.py +1 -1
  92. telegrinder/tools/__init__.py +138 -4
  93. telegrinder/tools/buttons.py +89 -51
  94. telegrinder/tools/error_handler/__init__.py +8 -0
  95. telegrinder/tools/error_handler/abc.py +30 -0
  96. telegrinder/tools/error_handler/error_handler.py +156 -0
  97. telegrinder/tools/formatting/__init__.py +81 -3
  98. telegrinder/tools/formatting/html.py +283 -37
  99. telegrinder/tools/formatting/links.py +32 -0
  100. telegrinder/tools/formatting/spec_html_formats.py +121 -0
  101. telegrinder/tools/global_context/__init__.py +12 -0
  102. telegrinder/tools/global_context/abc.py +66 -0
  103. telegrinder/tools/global_context/global_context.py +451 -0
  104. telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
  105. telegrinder/tools/i18n/__init__.py +12 -0
  106. telegrinder/tools/i18n/base.py +31 -0
  107. telegrinder/tools/i18n/middleware/__init__.py +3 -0
  108. telegrinder/tools/i18n/middleware/base.py +26 -0
  109. telegrinder/tools/i18n/simple.py +48 -0
  110. telegrinder/tools/kb_set/__init__.py +2 -0
  111. telegrinder/tools/kb_set/base.py +3 -0
  112. telegrinder/tools/kb_set/yaml.py +28 -17
  113. telegrinder/tools/keyboard.py +84 -62
  114. telegrinder/tools/loop_wrapper/__init__.py +4 -0
  115. telegrinder/tools/loop_wrapper/abc.py +18 -0
  116. telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
  117. telegrinder/tools/magic.py +48 -23
  118. telegrinder/tools/parse_mode.py +1 -2
  119. telegrinder/types/__init__.py +1 -0
  120. telegrinder/types/enums.py +653 -0
  121. telegrinder/types/methods.py +4107 -1279
  122. telegrinder/types/objects.py +4771 -1745
  123. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/LICENSE +2 -1
  124. telegrinder-0.1.dev159.dist-info/METADATA +109 -0
  125. telegrinder-0.1.dev159.dist-info/RECORD +126 -0
  126. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/WHEEL +1 -1
  127. telegrinder/bot/dispatch/waiter.py +0 -38
  128. telegrinder/result.py +0 -38
  129. telegrinder/tools/formatting/abc.py +0 -52
  130. telegrinder/tools/formatting/markdown.py +0 -57
  131. telegrinder-0.1.dev20.dist-info/METADATA +0 -22
  132. telegrinder-0.1.dev20.dist-info/RECORD +0 -71
telegrinder/modules.py CHANGED
@@ -1,39 +1,78 @@
1
- import logging
1
+ import os
2
2
  import typing
3
3
 
4
4
  from choicelib import choice_in_order
5
- from typing_extensions import Protocol
6
5
 
7
6
 
8
- class JSONModule(Protocol):
9
- def loads(self, s: str) -> typing.Union[dict, list]:
7
+ class JSONModule(typing.Protocol):
8
+ def loads(self, s: str) -> dict[str, typing.Any] | list[typing.Any]:
10
9
  ...
11
10
 
12
- def dumps(self, o: typing.Union[dict, list]) -> str:
11
+ def dumps(self, o: dict[str, typing.Any] | list[typing.Any]) -> str:
13
12
  ...
14
13
 
15
14
 
15
+ class LoggerModule(typing.Protocol):
16
+ def debug(self, __msg: object, *args: object, **kwargs: object):
17
+ ...
18
+
19
+ def info(self, __msg: object, *args: object, **kwargs: object):
20
+ ...
21
+
22
+ def warning(self, __msg: object, *args: object, **kwargs: object):
23
+ ...
24
+
25
+ def error(self, __msg: object, *args: object, **kwargs: object):
26
+ ...
27
+
28
+ def critical(self, __msg: object, *args: object, **kwargs: object):
29
+ ...
30
+
31
+ def exception(self, __msg: object, *args: object, **kwargs: object):
32
+ ...
33
+
34
+ def set_level(
35
+ self,
36
+ level: typing.Literal[
37
+ "DEBUG",
38
+ "INFO",
39
+ "WARNING",
40
+ "ERROR",
41
+ "CRITICAL",
42
+ "EXCEPTION",
43
+ ],
44
+ ) -> None:
45
+ ...
46
+
47
+
48
+ logger: LoggerModule
16
49
  json: JSONModule = choice_in_order(
17
- ["ujson", "hyperjson", "orjson"], do_import=True, default="json"
50
+ ["ujson", "hyperjson", "orjson"], do_import=True, default="telegrinder.msgspec_json"
18
51
  )
19
-
20
52
  logging_module = choice_in_order(["loguru"], default="logging")
53
+ logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
21
54
 
22
55
  if logging_module == "loguru":
23
56
  import os
24
57
  import sys
25
58
 
26
- if not os.environ.get("LOGURU_AUTOINIT"):
27
- os.environ["LOGURU_AUTOINIT"] = "0"
28
59
  from loguru import logger # type: ignore
29
60
 
30
- if not logger._core.handlers: # type: ignore
31
- log_format = (
32
- "<level>{level: <8}</level> | "
33
- "{time:YYYY-MM-DD HH:mm:ss} | "
34
- "{name}:{function}:{line} > <level>{message}</level>"
35
- )
36
- logger.add(sys.stderr, format=log_format, enqueue=True, colorize=True)
61
+ os.environ.setdefault("LOGURU_AUTOINIT", "0")
62
+ log_format = (
63
+ "<level>{level: <8}</level> | "
64
+ "<lg>{time:YYYY-MM-DD HH:mm:ss}</lg> | "
65
+ "<le>{name}</le>:<le>{function}</le>:"
66
+ "<le>{line}</le> > <lw>{message}</lw>"
67
+ )
68
+ logger.remove() # type: ignore
69
+ handler_id = logger.add( # type: ignore
70
+ sink=sys.stderr,
71
+ format=log_format,
72
+ enqueue=True,
73
+ colorize=True,
74
+ level=logging_level,
75
+ )
37
76
 
38
77
  elif logging_module == "logging":
39
78
  """
@@ -41,8 +80,111 @@ elif logging_module == "logging":
41
80
  About:
42
81
  https://docs.python.org/3/howto/logging-cookbook.html#use-of-alternative-formatting-styles
43
82
  """
83
+
44
84
  import inspect
45
85
  import logging
86
+ import sys
87
+
88
+ import colorama
89
+
90
+ colorama.just_fix_windows_console() # init & fix console
91
+
92
+ FORMAT = (
93
+ "<white>{name: <4} |</white> <level>{levelname: <8}</level>"
94
+ " <white>|</white> <green>{asctime}</green> <white>|</white> <level_module>"
95
+ "{module}</level_module><white>:</white><level_func>"
96
+ "{funcName}</level_func><white>:</white><level_lineno>"
97
+ "{lineno}</level_lineno><white> > </white><level_message>"
98
+ "{message}</level_message>"
99
+ )
100
+ COLORS = {
101
+ "red": colorama.Fore.LIGHTRED_EX,
102
+ "green": colorama.Fore.LIGHTGREEN_EX,
103
+ "blue": colorama.Fore.LIGHTBLUE_EX,
104
+ "white": colorama.Fore.LIGHTWHITE_EX,
105
+ "yellow": colorama.Fore.LIGHTYELLOW_EX,
106
+ "magenta": colorama.Fore.LIGHTMAGENTA_EX,
107
+ "cyan": colorama.Fore.LIGHTCYAN_EX,
108
+ "reset": colorama.Style.RESET_ALL,
109
+ }
110
+ LEVEL_SETTINGS = {
111
+ "INFO": {
112
+ "level": "green",
113
+ "level_module": "blue",
114
+ "level_func": "cyan",
115
+ "level_lineno": "green",
116
+ "level_message": "white",
117
+ },
118
+ "DEBUG": {
119
+ "level": "blue",
120
+ "level_module": "yellow",
121
+ "level_func": "green",
122
+ "level_lineno": "cyan",
123
+ "level_message": "blue",
124
+ },
125
+ "WARNING": {
126
+ "level": "yellow",
127
+ "level_module": "red",
128
+ "level_func": "green",
129
+ "level_lineno": "red",
130
+ "level_message": "yellow",
131
+ },
132
+ "ERROR": {
133
+ "level": "red",
134
+ "level_module": "magenta",
135
+ "level_func": "yellow",
136
+ "level_lineno": "green",
137
+ "level_message": "red",
138
+ },
139
+ "CRITICAL": {
140
+ "level": "cyan",
141
+ "level_module": "yellow",
142
+ "level_func": "yellow",
143
+ "level_lineno": "yellow",
144
+ "level_message": "cyan",
145
+ },
146
+ }
147
+ FORMAT = (
148
+ FORMAT
149
+ .replace("<white>", COLORS["white"])
150
+ .replace("</white>", COLORS["reset"])
151
+ .replace("<green>", COLORS["green"])
152
+ .replace("</green>", COLORS["reset"])
153
+ )
154
+ LEVEL_FORMATS: dict[str, str] = {}
155
+ for level, settings in LEVEL_SETTINGS.items():
156
+ fmt = FORMAT
157
+ for name, color in settings.items():
158
+ fmt = (
159
+ fmt
160
+ .replace(f"<{name}>", COLORS[color])
161
+ .replace(f"</{name}>", COLORS["reset"])
162
+ )
163
+ LEVEL_FORMATS[level] = fmt
164
+
165
+
166
+ class TelegrinderLoggingFormatter(logging.Formatter):
167
+ def format(self, record: logging.LogRecord) -> str:
168
+ if not record.funcName or record.funcName == "<module>":
169
+ record.funcName = "\b"
170
+ frame = next(
171
+ (
172
+ frame
173
+ for frame in inspect.stack()
174
+ if frame.filename == record.pathname
175
+ and frame.lineno == record.lineno
176
+ ),
177
+ None,
178
+ )
179
+ if frame:
180
+ module = inspect.getmodule(frame.frame)
181
+ record.module = module.__name__ if module else "<module>"
182
+ return logging.Formatter(
183
+ LEVEL_FORMATS.get(record.levelname),
184
+ datefmt="%Y-%m-%d %H:%M:%S",
185
+ style="{",
186
+ ).format(record)
187
+
46
188
 
47
189
  class LogMessage:
48
190
  def __init__(self, fmt, args, kwargs):
@@ -50,15 +192,16 @@ elif logging_module == "logging":
50
192
  self.args = args
51
193
  self.kwargs = kwargs
52
194
 
53
- def __str__(self):
54
- return self.fmt.format(*self.args)
195
+ def __str__(self) -> str:
196
+ return self.fmt.format(*self.args, **self.kwargs)
55
197
 
56
- class StyleAdapter(logging.LoggerAdapter):
198
+ class TelegrinderLoggingStyleAdapter(logging.LoggerAdapter):
57
199
  def __init__(self, logger, extra=None):
58
200
  super().__init__(logger, extra or {})
59
201
 
60
202
  def log(self, level, msg, *args, **kwargs):
61
203
  if self.isEnabledFor(level):
204
+ kwargs.setdefault("stacklevel", 2)
62
205
  msg, args, kwargs = self.proc(msg, args, kwargs)
63
206
  self.logger._log(level, msg, args, **kwargs)
64
207
 
@@ -68,9 +211,34 @@ elif logging_module == "logging":
68
211
  for key in inspect.getfullargspec(self.logger._log).args[1:]
69
212
  if key in kwargs
70
213
  }
214
+
71
215
  if isinstance(msg, str):
72
216
  msg = LogMessage(msg, args, kwargs)
73
- args = ()
217
+ args = tuple()
74
218
  return msg, args, log_kwargs
75
219
 
76
- logger = StyleAdapter(logging.getLogger("telegrinder")) # type: ignore
220
+ handler = logging.StreamHandler(sys.stderr)
221
+ handler.setFormatter(TelegrinderLoggingFormatter())
222
+ logger = logging.getLogger("telegrinder") # type: ignore
223
+ logger.setLevel(logging.getLevelName(logging_level)) # type: ignore
224
+ logger.addHandler(handler) # type: ignore
225
+ logger = TelegrinderLoggingStyleAdapter(logger) # type: ignore
226
+
227
+
228
+ def _set_logger_level(level):
229
+ level = level.upper()
230
+ if logging_module == "logging":
231
+ import logging
232
+
233
+ logging.getLogger("telegrinder").setLevel(logging.getLevelName(level))
234
+ elif logging_module == "loguru":
235
+ import loguru # type: ignore
236
+
237
+ if handler_id in loguru.logger._core.handlers: # type: ignore
238
+ loguru.logger._core.handlers[handler_id]._levelno = loguru.logger.level(level).no # type: ignore
239
+
240
+
241
+ setattr(logger, "set_level", staticmethod(_set_logger_level)) # type: ignore
242
+
243
+
244
+ __all__ = ("json", "logger")
@@ -0,0 +1,14 @@
1
+ import typing
2
+
3
+ from .msgspec_utils import decoder, encoder
4
+
5
+
6
+ def loads(s: str | bytes) -> dict[str, typing.Any] | list[typing.Any]:
7
+ return decoder.decode(s, type=dict[str, typing.Any] | list[typing.Any]) # type: ignore
8
+
9
+
10
+ def dumps(o: dict[str, typing.Any] | list[typing.Any]) -> str:
11
+ return encoder.encode(o)
12
+
13
+
14
+ __all__ = ("dumps", "loads")
@@ -0,0 +1,227 @@
1
+ import typing
2
+
3
+ import fntypes.option
4
+ import msgspec
5
+ from fntypes.co import Error, Ok, Result, Variative
6
+
7
+ T = typing.TypeVar("T")
8
+ Ts = typing.TypeVarTuple("Ts")
9
+
10
+ if typing.TYPE_CHECKING:
11
+ import types
12
+ from datetime import datetime
13
+
14
+ from fntypes.option import Option
15
+ else:
16
+ from datetime import datetime as dt
17
+
18
+ Value = typing.TypeVar("Value")
19
+
20
+ datetime = type("datetime", (dt,), {})
21
+
22
+ class OptionMeta(type):
23
+ def __instancecheck__(cls, __instance: typing.Any) -> bool:
24
+ return isinstance(__instance, fntypes.option.Some | fntypes.option.Nothing)
25
+
26
+
27
+ class Option(typing.Generic[Value], metaclass=OptionMeta):
28
+ pass
29
+
30
+ DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], object]
31
+ EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
32
+
33
+ Nothing: typing.Final[fntypes.option.Nothing] = fntypes.option.Nothing()
34
+
35
+
36
+ def get_origin(t: type[T]) -> type[T]:
37
+ return typing.cast(T, typing.get_origin(t)) or t
38
+
39
+
40
+ def repr_type(t: type) -> str:
41
+ return getattr(t, "__name__", repr(get_origin(t)))
42
+
43
+
44
+ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, msgspec.ValidationError]:
45
+ try:
46
+ return Ok(decoder.convert(obj, type=t, strict=True))
47
+ except msgspec.ValidationError as exc:
48
+ return Error(exc)
49
+
50
+
51
+ def datetime_dec_hook(tp: type[datetime], obj: int | float) -> datetime:
52
+ return tp.fromtimestamp(obj)
53
+
54
+
55
+ def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
56
+ if obj is None:
57
+ return Nothing
58
+ generic_args = typing.get_args(tp)
59
+ value_type: typing.Any | type[typing.Any] = typing.Any if not generic_args else generic_args[0]
60
+ return msgspec_convert({"value": obj}, fntypes.option.Some[value_type]).unwrap()
61
+
62
+
63
+ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
64
+ union_types = typing.get_args(tp)
65
+
66
+ if isinstance(obj, dict):
67
+ struct_fields_match_sums: dict[type[msgspec.Struct], int] = {
68
+ m: sum(1 for k in obj if k in m.__struct_fields__)
69
+ for m in union_types
70
+ if issubclass(get_origin(m), msgspec.Struct)
71
+ }
72
+ union_types = tuple(t for t in union_types if t not in struct_fields_match_sums)
73
+ reverse = False
74
+
75
+ if len(set(struct_fields_match_sums.values())) != len(struct_fields_match_sums.values()):
76
+ struct_fields_match_sums = {m: len(m.__struct_fields__) for m in struct_fields_match_sums}
77
+ reverse = True
78
+
79
+ union_types = (
80
+ *sorted(struct_fields_match_sums, key=lambda k: struct_fields_match_sums[k], reverse=reverse),
81
+ *union_types,
82
+ )
83
+
84
+ for t in union_types:
85
+ match msgspec_convert(obj, t):
86
+ case Ok(value):
87
+ return tp(value)
88
+
89
+ raise TypeError(
90
+ "Object of type `{}` does not belong to types `{}`".format(
91
+ repr_type(type(obj)),
92
+ " | ".join(map(repr_type, union_types)),
93
+ )
94
+ )
95
+
96
+
97
+ def datetime_enc_hook(obj: datetime) -> int:
98
+ return int(obj.timestamp())
99
+
100
+
101
+ def option_enc_hook(obj: Option[typing.Any]) -> typing.Any | None:
102
+ return obj.value if isinstance(obj, fntypes.option.Some) else None
103
+
104
+
105
+ def variative_enc_hook(obj: Variative[typing.Any]) -> typing.Any:
106
+ return obj.v
107
+
108
+
109
+ class Decoder:
110
+ def __init__(self) -> None:
111
+ self.dec_hooks: dict[
112
+ typing.Union[type[typing.Any], "types.UnionType"], DecHook[typing.Any]
113
+ ] = {
114
+ Option: option_dec_hook,
115
+ Variative: variative_dec_hook,
116
+ datetime: datetime_dec_hook,
117
+ }
118
+
119
+ def add_dec_hook(self, tp: type[T]):
120
+ def decorator(func: DecHook[T]) -> DecHook[T]:
121
+ return self.dec_hooks.setdefault(get_origin(tp), func)
122
+
123
+ return decorator
124
+
125
+ def dec_hook(self, tp: type[typing.Any], obj: object) -> object:
126
+ origin_type = t if isinstance((t := get_origin(tp)), type) else type(t)
127
+ if origin_type not in self.dec_hooks:
128
+ raise TypeError(
129
+ f"Unknown type `{repr_type(origin_type)}`. "
130
+ "You can implement decode hook for this type."
131
+ )
132
+ return self.dec_hooks[origin_type](tp, obj)
133
+
134
+ def convert(
135
+ self,
136
+ obj: object,
137
+ *,
138
+ type: type[T] = dict,
139
+ strict: bool = True,
140
+ from_attributes: bool = False,
141
+ builtin_types: typing.Iterable[type] | None = None,
142
+ str_keys: bool = False,
143
+ ) -> T:
144
+ return msgspec.convert(
145
+ obj,
146
+ type,
147
+ strict=strict,
148
+ from_attributes=from_attributes,
149
+ dec_hook=self.dec_hook,
150
+ builtin_types=builtin_types,
151
+ str_keys=str_keys,
152
+ )
153
+
154
+ def decode(
155
+ self,
156
+ buf: str | bytes,
157
+ *,
158
+ type: type[T] = dict,
159
+ strict: bool = True,
160
+ ) -> T:
161
+ return msgspec.json.decode(
162
+ buf,
163
+ type=type,
164
+ strict=strict,
165
+ dec_hook=self.dec_hook,
166
+ )
167
+
168
+
169
+ class Encoder:
170
+ def __init__(self) -> None:
171
+ self.enc_hooks: dict[type, EncHook] = {
172
+ fntypes.option.Some: option_enc_hook,
173
+ fntypes.option.Nothing: option_enc_hook,
174
+ Variative: variative_enc_hook,
175
+ datetime: datetime_enc_hook,
176
+ }
177
+
178
+ def add_dec_hook(self, tp: type[T]):
179
+ def decorator(func: EncHook[T]) -> EncHook[T]:
180
+ return self.enc_hooks.setdefault(get_origin(tp), func)
181
+
182
+ return decorator
183
+
184
+ def enc_hook(self, obj: object) -> object:
185
+ origin_type = get_origin(type(obj))
186
+ if origin_type not in self.enc_hooks:
187
+ raise NotImplementedError(
188
+ "Not implemented encode hook for "
189
+ f"object of type `{repr_type(origin_type)}`."
190
+ )
191
+ return self.enc_hooks[origin_type](obj)
192
+
193
+ @typing.overload
194
+ def encode(self, obj: typing.Any) -> str:
195
+ ...
196
+
197
+ @typing.overload
198
+ def encode(self, obj: typing.Any, *, as_str: bool = False) -> bytes:
199
+ ...
200
+
201
+ def encode(self, obj: typing.Any, *, as_str: bool = True) -> str | bytes:
202
+ buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
203
+ return buf.decode() if as_str else buf
204
+
205
+
206
+ decoder: typing.Final[Decoder] = Decoder()
207
+ encoder: typing.Final[Encoder] = Encoder()
208
+
209
+
210
+ __all__ = (
211
+ "Decoder",
212
+ "Encoder",
213
+ "Option",
214
+ "Nothing",
215
+ "get_origin",
216
+ "repr_type",
217
+ "msgspec_convert",
218
+ "datetime_dec_hook",
219
+ "datetime_enc_hook",
220
+ "option_dec_hook",
221
+ "option_enc_hook",
222
+ "variative_dec_hook",
223
+ "variative_enc_hook",
224
+ "datetime",
225
+ "decoder",
226
+ "encoder",
227
+ )
@@ -0,0 +1,31 @@
1
+ from .attachment import Attachment, Audio, Photo, Video
2
+ from .base import ComposeError, DataNode, Node, ScalarNode
3
+ from .composer import NodeCollection, NodeSession, compose_node
4
+ from .container import ContainerNode
5
+ from .message import MessageNode
6
+ from .rule import RuleContext
7
+ from .source import Source
8
+ from .text import Text
9
+ from .tools import generate
10
+ from .update import UpdateNode
11
+
12
+ __all__ = (
13
+ "Node",
14
+ "DataNode",
15
+ "ScalarNode",
16
+ "Attachment",
17
+ "Photo",
18
+ "Video",
19
+ "Text",
20
+ "Audio",
21
+ "UpdateNode",
22
+ "compose_node",
23
+ "ComposeError",
24
+ "MessageNode",
25
+ "Source",
26
+ "NodeSession",
27
+ "NodeCollection",
28
+ "ContainerNode",
29
+ "generate",
30
+ "RuleContext",
31
+ )
@@ -0,0 +1,71 @@
1
+ import dataclasses
2
+ import typing
3
+
4
+ from fntypes.option import Nothing
5
+
6
+ import telegrinder.types
7
+ from telegrinder.msgspec_utils import Option
8
+
9
+ from .base import ComposeError, DataNode, ScalarNode
10
+ from .message import MessageNode
11
+
12
+
13
+ @dataclasses.dataclass
14
+ class Attachment(DataNode):
15
+ attachment_type: typing.Literal["audio", "document", "photo", "poll", "video"]
16
+ _: dataclasses.KW_ONLY
17
+ audio: Option[telegrinder.types.Audio] = dataclasses.field(default_factory=lambda: Nothing())
18
+ document: Option[telegrinder.types.Document] = dataclasses.field(default_factory=lambda: Nothing())
19
+ photo: Option[list[telegrinder.types.PhotoSize]] = dataclasses.field(default_factory=lambda: Nothing())
20
+ poll: Option[telegrinder.types.Poll] = dataclasses.field(default_factory=lambda: Nothing())
21
+ video: Option[telegrinder.types.Video] = dataclasses.field(default_factory=lambda: Nothing())
22
+
23
+ @classmethod
24
+ async def compose(cls, message: MessageNode) -> "Attachment":
25
+ for attachment_type in ("audio", "document", "photo", "poll", "video"):
26
+ if (attachment := getattr(message, attachment_type, None)) is not None:
27
+ return cls(attachment_type, **{attachment_type: attachment})
28
+ return cls.compose_error("No attachment found in message")
29
+
30
+
31
+ @dataclasses.dataclass
32
+ class Photo(DataNode):
33
+ sizes: list[telegrinder.types.PhotoSize]
34
+
35
+ @classmethod
36
+ async def compose(cls, attachment: Attachment) -> typing.Self:
37
+ return cls(attachment.photo.expect(ComposeError("Attachment is not an photo")))
38
+
39
+
40
+ class Video(ScalarNode, telegrinder.types.Video):
41
+ @classmethod
42
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Video:
43
+ return attachment.video.expect(ComposeError("Attachment is not an video"))
44
+
45
+
46
+ class Audio(ScalarNode, telegrinder.types.Audio):
47
+ @classmethod
48
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Audio:
49
+ return attachment.audio.expect(ComposeError("Attachment is not an audio"))
50
+
51
+
52
+ class Document(ScalarNode, telegrinder.types.Document):
53
+ @classmethod
54
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Document:
55
+ return attachment.document.expect(ComposeError("Attachment is not an document"))
56
+
57
+
58
+ class Poll(ScalarNode, telegrinder.types.Poll):
59
+ @classmethod
60
+ async def compose(cls, attachment: Attachment) -> telegrinder.types.Poll:
61
+ return attachment.poll.expect(ComposeError("Attachment is not an poll"))
62
+
63
+
64
+ __all__ = (
65
+ "Attachment",
66
+ "Audio",
67
+ "Document",
68
+ "Photo",
69
+ "Poll",
70
+ "Video",
71
+ )
@@ -0,0 +1,93 @@
1
+ import abc
2
+ import inspect
3
+ import typing
4
+
5
+ ComposeResult: typing.TypeAlias = typing.Coroutine[typing.Any, typing.Any, typing.Any] | typing.AsyncGenerator[typing.Any, None]
6
+
7
+
8
+ class ComposeError(BaseException):
9
+ pass
10
+
11
+
12
+ class Node(abc.ABC):
13
+ node: str = "node"
14
+
15
+ @classmethod
16
+ @abc.abstractmethod
17
+ def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
18
+ pass
19
+
20
+ @classmethod
21
+ def compose_error(cls, error: str | None = None) -> typing.NoReturn:
22
+ raise ComposeError(error)
23
+
24
+ @classmethod
25
+ def get_sub_nodes(cls) -> dict[str, type[typing.Self]]:
26
+ parameters = inspect.signature(cls.compose).parameters
27
+
28
+ sub_nodes = {}
29
+ for name, param in parameters.items():
30
+ if param.annotation is inspect._empty:
31
+ continue
32
+ node = param.annotation
33
+ sub_nodes[name] = node
34
+ return sub_nodes
35
+
36
+ @classmethod
37
+ def as_node(cls) -> type[typing.Self]:
38
+ return cls
39
+
40
+ @classmethod
41
+ def is_generator(cls) -> bool:
42
+ return inspect.isasyncgenfunction(cls.compose)
43
+
44
+
45
+ class DataNode(Node, abc.ABC):
46
+ node = "data"
47
+
48
+ @typing.dataclass_transform()
49
+ @classmethod
50
+ @abc.abstractmethod
51
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
52
+ pass
53
+
54
+
55
+ class ScalarNodeProto(Node, abc.ABC):
56
+ @classmethod
57
+ @abc.abstractmethod
58
+ async def compose(cls, *args: tuple[typing.Any, ...], **kwargs: typing.Any) -> ComposeResult:
59
+ pass
60
+
61
+
62
+ SCALAR_NODE = type("ScalarNode", (), {"node": "scalar"})
63
+
64
+
65
+ if typing.TYPE_CHECKING:
66
+
67
+ class ScalarNode(ScalarNodeProto, abc.ABC):
68
+ pass
69
+
70
+ else:
71
+
72
+ def create_node(cls, bases, dct):
73
+ dct.update(cls.__dict__)
74
+ return type(cls.__name__, bases, dct)
75
+
76
+ def create_class(name, bases, dct):
77
+ return type(
78
+ "Scalar",
79
+ (SCALAR_NODE,),
80
+ {"as_node": classmethod(lambda cls: create_node(cls, bases, dct))},
81
+ )
82
+
83
+ class ScalarNode(ScalarNodeProto, abc.ABC, metaclass=create_class):
84
+ pass
85
+
86
+
87
+ __all__ = (
88
+ "ScalarNode",
89
+ "SCALAR_NODE",
90
+ "DataNode",
91
+ "Node",
92
+ "ComposeError",
93
+ )