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.
- telegrinder/__init__.py +129 -22
- telegrinder/api/__init__.py +11 -2
- telegrinder/api/abc.py +25 -9
- telegrinder/api/api.py +47 -28
- telegrinder/api/error.py +14 -4
- telegrinder/api/response.py +11 -7
- telegrinder/bot/__init__.py +68 -7
- telegrinder/bot/bot.py +30 -24
- telegrinder/bot/cute_types/__init__.py +11 -1
- telegrinder/bot/cute_types/base.py +138 -0
- telegrinder/bot/cute_types/callback_query.py +458 -15
- telegrinder/bot/cute_types/inline_query.py +30 -24
- telegrinder/bot/cute_types/message.py +2982 -78
- telegrinder/bot/cute_types/update.py +30 -0
- telegrinder/bot/cute_types/utils.py +794 -0
- telegrinder/bot/dispatch/__init__.py +56 -3
- telegrinder/bot/dispatch/abc.py +9 -7
- telegrinder/bot/dispatch/composition.py +74 -0
- telegrinder/bot/dispatch/context.py +71 -0
- telegrinder/bot/dispatch/dispatch.py +86 -49
- telegrinder/bot/dispatch/handler/__init__.py +3 -0
- telegrinder/bot/dispatch/handler/abc.py +11 -5
- telegrinder/bot/dispatch/handler/func.py +41 -32
- telegrinder/bot/dispatch/handler/message_reply.py +46 -0
- telegrinder/bot/dispatch/middleware/__init__.py +2 -0
- telegrinder/bot/dispatch/middleware/abc.py +10 -4
- telegrinder/bot/dispatch/process.py +53 -49
- telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
- telegrinder/bot/dispatch/return_manager/abc.py +95 -0
- telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
- telegrinder/bot/dispatch/return_manager/message.py +25 -0
- telegrinder/bot/dispatch/view/__init__.py +14 -2
- telegrinder/bot/dispatch/view/abc.py +128 -2
- telegrinder/bot/dispatch/view/box.py +38 -0
- telegrinder/bot/dispatch/view/callback_query.py +13 -39
- telegrinder/bot/dispatch/view/inline_query.py +11 -39
- telegrinder/bot/dispatch/view/message.py +11 -47
- telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
- telegrinder/bot/polling/__init__.py +2 -0
- telegrinder/bot/polling/abc.py +11 -4
- telegrinder/bot/polling/polling.py +89 -40
- telegrinder/bot/rules/__init__.py +91 -5
- telegrinder/bot/rules/abc.py +81 -63
- telegrinder/bot/rules/adapter/__init__.py +11 -0
- telegrinder/bot/rules/adapter/abc.py +21 -0
- telegrinder/bot/rules/adapter/errors.py +5 -0
- telegrinder/bot/rules/adapter/event.py +49 -0
- telegrinder/bot/rules/adapter/raw_update.py +24 -0
- telegrinder/bot/rules/callback_data.py +159 -38
- telegrinder/bot/rules/command.py +116 -0
- telegrinder/bot/rules/enum_text.py +28 -0
- telegrinder/bot/rules/func.py +17 -17
- telegrinder/bot/rules/fuzzy.py +13 -10
- telegrinder/bot/rules/inline.py +61 -0
- telegrinder/bot/rules/integer.py +12 -7
- telegrinder/bot/rules/is_from.py +148 -7
- telegrinder/bot/rules/markup.py +21 -18
- telegrinder/bot/rules/mention.py +17 -0
- telegrinder/bot/rules/message_entities.py +33 -0
- telegrinder/bot/rules/regex.py +27 -19
- telegrinder/bot/rules/rule_enum.py +74 -0
- telegrinder/bot/rules/start.py +25 -13
- telegrinder/bot/rules/text.py +23 -14
- telegrinder/bot/scenario/__init__.py +2 -0
- telegrinder/bot/scenario/abc.py +12 -5
- telegrinder/bot/scenario/checkbox.py +48 -30
- telegrinder/bot/scenario/choice.py +16 -10
- telegrinder/client/__init__.py +3 -1
- telegrinder/client/abc.py +26 -16
- telegrinder/client/aiohttp.py +54 -32
- telegrinder/model.py +119 -40
- telegrinder/modules.py +189 -21
- telegrinder/msgspec_json.py +14 -0
- telegrinder/msgspec_utils.py +227 -0
- telegrinder/node/__init__.py +31 -0
- telegrinder/node/attachment.py +71 -0
- telegrinder/node/base.py +93 -0
- telegrinder/node/composer.py +71 -0
- telegrinder/node/container.py +22 -0
- telegrinder/node/message.py +18 -0
- telegrinder/node/rule.py +56 -0
- telegrinder/node/source.py +31 -0
- telegrinder/node/text.py +13 -0
- telegrinder/node/tools/__init__.py +3 -0
- telegrinder/node/tools/generator.py +40 -0
- telegrinder/node/update.py +12 -0
- telegrinder/rules.py +1 -1
- telegrinder/tools/__init__.py +138 -4
- telegrinder/tools/buttons.py +89 -51
- telegrinder/tools/error_handler/__init__.py +8 -0
- telegrinder/tools/error_handler/abc.py +30 -0
- telegrinder/tools/error_handler/error_handler.py +156 -0
- telegrinder/tools/formatting/__init__.py +81 -3
- telegrinder/tools/formatting/html.py +283 -37
- telegrinder/tools/formatting/links.py +32 -0
- telegrinder/tools/formatting/spec_html_formats.py +121 -0
- telegrinder/tools/global_context/__init__.py +12 -0
- telegrinder/tools/global_context/abc.py +66 -0
- telegrinder/tools/global_context/global_context.py +451 -0
- telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
- telegrinder/tools/i18n/__init__.py +12 -0
- telegrinder/tools/i18n/base.py +31 -0
- telegrinder/tools/i18n/middleware/__init__.py +3 -0
- telegrinder/tools/i18n/middleware/base.py +26 -0
- telegrinder/tools/i18n/simple.py +48 -0
- telegrinder/tools/kb_set/__init__.py +2 -0
- telegrinder/tools/kb_set/base.py +3 -0
- telegrinder/tools/kb_set/yaml.py +28 -17
- telegrinder/tools/keyboard.py +84 -62
- telegrinder/tools/loop_wrapper/__init__.py +4 -0
- telegrinder/tools/loop_wrapper/abc.py +18 -0
- telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
- telegrinder/tools/magic.py +48 -23
- telegrinder/tools/parse_mode.py +1 -2
- telegrinder/types/__init__.py +1 -0
- telegrinder/types/enums.py +653 -0
- telegrinder/types/methods.py +4107 -1279
- telegrinder/types/objects.py +4771 -1745
- {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/LICENSE +2 -1
- telegrinder-0.1.dev159.dist-info/METADATA +109 -0
- telegrinder-0.1.dev159.dist-info/RECORD +126 -0
- {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/WHEEL +1 -1
- telegrinder/bot/dispatch/waiter.py +0 -38
- telegrinder/result.py +0 -38
- telegrinder/tools/formatting/abc.py +0 -52
- telegrinder/tools/formatting/markdown.py +0 -57
- telegrinder-0.1.dev20.dist-info/METADATA +0 -22
- telegrinder-0.1.dev20.dist-info/RECORD +0 -71
telegrinder/modules.py
CHANGED
|
@@ -1,39 +1,78 @@
|
|
|
1
|
-
import
|
|
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) ->
|
|
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:
|
|
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="
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
)
|
telegrinder/node/base.py
ADDED
|
@@ -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
|
+
)
|