telegrinder 0.1.dev170__py3-none-any.whl → 0.1.dev171__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/api/abc.py +7 -1
- telegrinder/api/api.py +12 -3
- telegrinder/api/error.py +2 -1
- telegrinder/bot/bot.py +6 -1
- telegrinder/bot/cute_types/base.py +144 -17
- telegrinder/bot/cute_types/callback_query.py +6 -1
- telegrinder/bot/cute_types/chat_member_updated.py +1 -2
- telegrinder/bot/cute_types/message.py +23 -11
- telegrinder/bot/cute_types/update.py +48 -0
- telegrinder/bot/cute_types/utils.py +2 -465
- telegrinder/bot/dispatch/__init__.py +2 -3
- telegrinder/bot/dispatch/abc.py +6 -3
- telegrinder/bot/dispatch/context.py +6 -6
- telegrinder/bot/dispatch/dispatch.py +61 -23
- telegrinder/bot/dispatch/handler/abc.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -17
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +2 -2
- telegrinder/bot/dispatch/process.py +10 -10
- telegrinder/bot/dispatch/return_manager/abc.py +3 -3
- telegrinder/bot/dispatch/view/abc.py +12 -15
- telegrinder/bot/dispatch/view/box.py +73 -62
- telegrinder/bot/dispatch/view/message.py +11 -3
- telegrinder/bot/dispatch/view/raw.py +3 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +2 -2
- telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
- telegrinder/bot/dispatch/waiter_machine/short_state.py +2 -1
- telegrinder/bot/polling/polling.py +3 -3
- telegrinder/bot/rules/abc.py +11 -7
- telegrinder/bot/rules/adapter/event.py +7 -4
- telegrinder/bot/rules/adapter/node.py +1 -1
- telegrinder/bot/rules/command.py +5 -7
- telegrinder/bot/rules/func.py +1 -1
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/integer.py +1 -2
- telegrinder/bot/rules/markup.py +3 -3
- telegrinder/bot/rules/message_entities.py +1 -1
- telegrinder/bot/rules/node.py +2 -2
- telegrinder/bot/rules/regex.py +1 -1
- telegrinder/bot/rules/rule_enum.py +1 -1
- telegrinder/bot/scenario/checkbox.py +2 -2
- telegrinder/model.py +85 -46
- telegrinder/modules.py +3 -3
- telegrinder/msgspec_utils.py +33 -5
- telegrinder/node/__init__.py +20 -11
- telegrinder/node/attachment.py +19 -16
- telegrinder/node/base.py +120 -24
- telegrinder/node/callback_query.py +5 -9
- telegrinder/node/command.py +6 -2
- telegrinder/node/composer.py +82 -54
- telegrinder/node/container.py +4 -4
- telegrinder/node/event.py +59 -0
- telegrinder/node/me.py +3 -0
- telegrinder/node/message.py +6 -10
- telegrinder/node/polymorphic.py +11 -12
- telegrinder/node/rule.py +27 -5
- telegrinder/node/source.py +10 -11
- telegrinder/node/text.py +4 -4
- telegrinder/node/update.py +1 -2
- telegrinder/py.typed +0 -0
- telegrinder/tools/__init__.py +2 -2
- telegrinder/tools/buttons.py +5 -10
- telegrinder/tools/error_handler/error.py +2 -0
- telegrinder/tools/error_handler/error_handler.py +1 -1
- telegrinder/tools/formatting/spec_html_formats.py +10 -10
- telegrinder/tools/global_context/__init__.py +2 -2
- telegrinder/tools/global_context/global_context.py +2 -2
- telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
- telegrinder/tools/keyboard.py +2 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +39 -5
- telegrinder/tools/magic.py +48 -15
- telegrinder/types/enums.py +1 -0
- telegrinder/types/methods.py +14 -5
- telegrinder/types/objects.py +3 -0
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/METADATA +2 -2
- telegrinder-0.1.dev171.dist-info/RECORD +145 -0
- telegrinder-0.1.dev170.dist-info/RECORD +0 -143
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/LICENSE +0 -0
- {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/WHEEL +0 -0
|
@@ -103,7 +103,10 @@ class RawEventView(BaseView[UpdateCute]):
|
|
|
103
103
|
return False
|
|
104
104
|
|
|
105
105
|
async def process(self, event: Update, api: ABCAPI) -> bool:
|
|
106
|
+
if not self.handlers or not self.middlewares:
|
|
107
|
+
return False
|
|
106
108
|
return await process_inner(
|
|
109
|
+
api,
|
|
107
110
|
UpdateCute.from_update(event, bound_api=api),
|
|
108
111
|
event,
|
|
109
112
|
self.middlewares,
|
|
@@ -123,7 +123,7 @@ class WaiterMachine:
|
|
|
123
123
|
|
|
124
124
|
ctx = Context(**context)
|
|
125
125
|
if await behaviour.check(event.api, update, ctx):
|
|
126
|
-
await behaviour.run(event, ctx)
|
|
126
|
+
await behaviour.run(event.api, event, ctx)
|
|
127
127
|
return True
|
|
128
128
|
|
|
129
129
|
return False
|
|
@@ -132,7 +132,7 @@ class WaiterMachine:
|
|
|
132
132
|
self,
|
|
133
133
|
views: typing.Iterable[ABCStateView[EventModel]],
|
|
134
134
|
absolutely_dead_time: datetime.timedelta = WEEK,
|
|
135
|
-
):
|
|
135
|
+
) -> None:
|
|
136
136
|
"""Clears storage.
|
|
137
137
|
|
|
138
138
|
:param absolutely_dead_time: timedelta when state can be forgotten.
|
|
@@ -83,7 +83,7 @@ class WaiterMiddleware(ABCMiddleware[EventType]):
|
|
|
83
83
|
result = await handler.check(event.ctx_api, ctx.raw_update, ctx)
|
|
84
84
|
|
|
85
85
|
if result is True:
|
|
86
|
-
await handler.run(event, ctx)
|
|
86
|
+
await handler.run(event.api, event, ctx)
|
|
87
87
|
|
|
88
88
|
elif short_state.default_behaviour is not None:
|
|
89
89
|
await self.machine.call_behaviour(
|
|
@@ -24,7 +24,7 @@ class ShortStateContext(typing.Generic[EventModel], typing.NamedTuple):
|
|
|
24
24
|
context: Context
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
@dataclasses.dataclass
|
|
27
|
+
@dataclasses.dataclass(slots=True)
|
|
28
28
|
class ShortState(typing.Generic[EventModel]):
|
|
29
29
|
key: "Identificator"
|
|
30
30
|
ctx_api: ABCAPI
|
|
@@ -38,6 +38,7 @@ class ShortState(typing.Generic[EventModel]):
|
|
|
38
38
|
on_drop_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None, kw_only=True)
|
|
39
39
|
exit_behaviour: Behaviour[EventModel] | None = dataclasses.field(default=None, kw_only=True)
|
|
40
40
|
expiration_date: datetime.datetime | None = dataclasses.field(init=False, kw_only=True)
|
|
41
|
+
creation_date: datetime.datetime = dataclasses.field(init=False)
|
|
41
42
|
context: ShortStateContext[EventModel] | None = dataclasses.field(default=None, init=False, kw_only=True)
|
|
42
43
|
|
|
43
44
|
def __post_init__(self, expiration: datetime.timedelta | None = None) -> None:
|
|
@@ -23,7 +23,7 @@ class Polling(ABCPolling):
|
|
|
23
23
|
max_reconnetions: int = 10,
|
|
24
24
|
include_updates: set[str | UpdateType] | None = None,
|
|
25
25
|
exclude_updates: set[str | UpdateType] | None = None,
|
|
26
|
-
):
|
|
26
|
+
) -> None:
|
|
27
27
|
self.api = api
|
|
28
28
|
self.allowed_updates = self.get_allowed_updates(
|
|
29
29
|
include_updates=include_updates,
|
|
@@ -48,8 +48,8 @@ class Polling(ABCPolling):
|
|
|
48
48
|
self.reconnection_timeout,
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
@staticmethod
|
|
51
52
|
def get_allowed_updates(
|
|
52
|
-
self,
|
|
53
53
|
*,
|
|
54
54
|
include_updates: set[str | UpdateType] | None = None,
|
|
55
55
|
exclude_updates: set[str | UpdateType] | None = None,
|
|
@@ -111,7 +111,7 @@ class Polling(ABCPolling):
|
|
|
111
111
|
exit(6)
|
|
112
112
|
else:
|
|
113
113
|
logger.warning(
|
|
114
|
-
"Server disconnected, waiting 5 seconds to
|
|
114
|
+
"Server disconnected, waiting 5 seconds to reconnect...",
|
|
115
115
|
)
|
|
116
116
|
reconn_counter += 1
|
|
117
117
|
await asyncio.sleep(self.reconnection_timeout)
|
telegrinder/bot/rules/abc.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
+
from functools import cached_property
|
|
3
4
|
|
|
4
5
|
import typing_extensions as typing
|
|
5
6
|
|
|
@@ -8,12 +9,14 @@ from telegrinder.bot.dispatch.context import Context
|
|
|
8
9
|
from telegrinder.bot.dispatch.process import check_rule
|
|
9
10
|
from telegrinder.bot.rules.adapter import ABCAdapter, RawUpdateAdapter
|
|
10
11
|
from telegrinder.bot.rules.adapter.node import Event
|
|
11
|
-
from telegrinder.node.base import Node, is_node
|
|
12
|
-
from telegrinder.node.composer import NodeCollection
|
|
12
|
+
from telegrinder.node.base import Node, get_nodes, is_node
|
|
13
13
|
from telegrinder.tools.i18n.base import ABCTranslator
|
|
14
14
|
from telegrinder.tools.magic import cache_translation, get_annotations, get_cached_translation
|
|
15
15
|
from telegrinder.types.objects import Update as UpdateObject
|
|
16
16
|
|
|
17
|
+
if typing.TYPE_CHECKING:
|
|
18
|
+
from telegrinder.node.composer import NodeCollection
|
|
19
|
+
|
|
17
20
|
AdaptTo = typing.TypeVar("AdaptTo", default=typing.Any)
|
|
18
21
|
|
|
19
22
|
Message: typing.TypeAlias = MessageCute
|
|
@@ -41,14 +44,15 @@ class ABCRule(ABC, typing.Generic[AdaptTo]):
|
|
|
41
44
|
async def check(self, event: AdaptTo, *, ctx: Context) -> bool:
|
|
42
45
|
pass
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
@cached_property
|
|
48
|
+
def required_nodes(self) -> dict[str, type[Node]]:
|
|
49
|
+
return get_nodes(self.check)
|
|
46
50
|
|
|
47
51
|
async def bounding_check(
|
|
48
52
|
self,
|
|
49
53
|
adapted_value: AdaptTo,
|
|
50
54
|
ctx: Context,
|
|
51
|
-
node_col: NodeCollection | None = None,
|
|
55
|
+
node_col: "NodeCollection | None" = None,
|
|
52
56
|
) -> bool:
|
|
53
57
|
kw = {}
|
|
54
58
|
node_col_values = node_col.values() if node_col is not None else {}
|
|
@@ -179,10 +183,10 @@ class Always(ABCRule):
|
|
|
179
183
|
|
|
180
184
|
__all__ = (
|
|
181
185
|
"ABCRule",
|
|
186
|
+
"Always",
|
|
182
187
|
"AndRule",
|
|
188
|
+
"Never",
|
|
183
189
|
"NotRule",
|
|
184
190
|
"OrRule",
|
|
185
191
|
"with_caching_translations",
|
|
186
|
-
"Never",
|
|
187
|
-
"Always",
|
|
188
192
|
)
|
|
@@ -3,7 +3,7 @@ import typing
|
|
|
3
3
|
from fntypes.result import Error, Ok, Result
|
|
4
4
|
|
|
5
5
|
from telegrinder.api.abc import ABCAPI
|
|
6
|
-
from telegrinder.bot.cute_types import BaseCute
|
|
6
|
+
from telegrinder.bot.cute_types.base import BaseCute
|
|
7
7
|
from telegrinder.bot.rules.adapter.abc import ABCAdapter
|
|
8
8
|
from telegrinder.bot.rules.adapter.errors import AdapterError
|
|
9
9
|
from telegrinder.msgspec_utils import Nothing
|
|
@@ -43,16 +43,19 @@ class EventAdapter(ABCAdapter[Update, ToCute]):
|
|
|
43
43
|
AdapterError(f"Update is not of event type {self.event!r}."),
|
|
44
44
|
)
|
|
45
45
|
|
|
46
|
-
if
|
|
46
|
+
if (event := getattr(update, self.event.value, Nothing)) is Nothing:
|
|
47
47
|
return Error(
|
|
48
48
|
AdapterError(f"Update is not an {self.event!r}."),
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
return Ok(
|
|
52
|
-
self.cute_model.from_update(
|
|
52
|
+
self.cute_model.from_update(
|
|
53
|
+
getattr(update, self.event.value).unwrap(),
|
|
54
|
+
bound_api=api,
|
|
55
|
+
),
|
|
53
56
|
)
|
|
54
57
|
|
|
55
|
-
event =
|
|
58
|
+
event = getattr(update, update.update_type.value).unwrap()
|
|
56
59
|
if not update.update_type or not issubclass(event.__class__, self.event):
|
|
57
60
|
return Error(AdapterError(f"Update is not an {self.event.__name__!r}."))
|
|
58
61
|
return Ok(self.cute_model.from_update(event, bound_api=api))
|
|
@@ -31,7 +31,7 @@ class NodeAdapter(typing.Generic[*Ts], ABCAdapter[Update, Event[tuple[*Ts]]]):
|
|
|
31
31
|
for node_t in self.nodes:
|
|
32
32
|
try:
|
|
33
33
|
# FIXME: adapters should have context
|
|
34
|
-
node_sessions.append(await compose_node(node_t, update_cute, Context())) # type: ignore
|
|
34
|
+
node_sessions.append(await compose_node(node_t, update_cute, Context(raw_update=update))) # type: ignore
|
|
35
35
|
except ComposeError:
|
|
36
36
|
for session in node_sessions:
|
|
37
37
|
await session.close(with_value=None)
|
telegrinder/bot/rules/command.py
CHANGED
|
@@ -2,24 +2,22 @@ import dataclasses
|
|
|
2
2
|
import typing
|
|
3
3
|
|
|
4
4
|
from telegrinder.bot.dispatch.context import Context
|
|
5
|
-
from telegrinder.node import Source
|
|
6
5
|
from telegrinder.node.command import CommandInfo, single_split
|
|
7
6
|
from telegrinder.node.me import Me
|
|
7
|
+
from telegrinder.node.source import Source
|
|
8
|
+
from telegrinder.types.enums import ChatType
|
|
8
9
|
|
|
9
|
-
from ...types import ChatType
|
|
10
10
|
from .abc import ABCRule
|
|
11
11
|
|
|
12
|
-
Validator = typing.Callable[[str], typing.Any | None]
|
|
12
|
+
Validator: typing.TypeAlias = typing.Callable[[str], typing.Any | None]
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
@dataclasses.dataclass(frozen=True)
|
|
15
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
16
16
|
class Argument:
|
|
17
17
|
name: str
|
|
18
18
|
validators: list[Validator] = dataclasses.field(default_factory=lambda: [])
|
|
19
19
|
optional: bool = dataclasses.field(default=False, kw_only=True)
|
|
20
20
|
|
|
21
|
-
# NOTE: add optional param `description`
|
|
22
|
-
|
|
23
21
|
def check(self, data: str) -> typing.Any | None:
|
|
24
22
|
for validator in self.validators:
|
|
25
23
|
data = validator(data) # type: ignore
|
|
@@ -79,7 +77,7 @@ class Command(ABCRule):
|
|
|
79
77
|
|
|
80
78
|
return self.parse_arguments(arguments[1:], s)
|
|
81
79
|
|
|
82
|
-
def parse_arguments(self, arguments: list[Argument], s: str) -> dict | None:
|
|
80
|
+
def parse_arguments(self, arguments: list[Argument], s: str) -> dict[str, typing.Any] | None:
|
|
83
81
|
if not arguments:
|
|
84
82
|
return {} if not s else None
|
|
85
83
|
|
telegrinder/bot/rules/func.py
CHANGED
telegrinder/bot/rules/fuzzy.py
CHANGED
|
@@ -7,7 +7,7 @@ from .abc import ABCRule
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class FuzzyText(ABCRule):
|
|
10
|
-
def __init__(self, texts: str | list[str], min_ratio: float = 0.7):
|
|
10
|
+
def __init__(self, texts: str | list[str], min_ratio: float = 0.7) -> None:
|
|
11
11
|
if isinstance(texts, str):
|
|
12
12
|
texts = [texts]
|
|
13
13
|
self.texts = texts
|
telegrinder/bot/rules/integer.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from telegrinder.bot.dispatch.context import Context
|
|
2
1
|
from telegrinder.node.text import TextInteger
|
|
3
2
|
|
|
4
3
|
from .abc import ABCRule
|
|
@@ -11,7 +10,7 @@ class IsInteger(NodeRule):
|
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class IntegerInRange(ABCRule):
|
|
14
|
-
def __init__(self, rng: range):
|
|
13
|
+
def __init__(self, rng: range) -> None:
|
|
15
14
|
self.rng = rng
|
|
16
15
|
|
|
17
16
|
async def check(self, integer: TextInteger) -> bool:
|
telegrinder/bot/rules/markup.py
CHANGED
|
@@ -4,12 +4,12 @@ import vbml
|
|
|
4
4
|
|
|
5
5
|
from telegrinder.bot.dispatch.context import Context
|
|
6
6
|
from telegrinder.node.text import Text
|
|
7
|
-
from telegrinder.tools.global_context import
|
|
7
|
+
from telegrinder.tools.global_context.telegrinder_ctx import TelegrinderContext
|
|
8
8
|
|
|
9
9
|
from .abc import ABCRule
|
|
10
10
|
|
|
11
11
|
PatternLike: typing.TypeAlias = str | vbml.Pattern
|
|
12
|
-
global_ctx =
|
|
12
|
+
global_ctx = TelegrinderContext()
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def check_string(patterns: list[vbml.Pattern], s: str, ctx: Context) -> bool:
|
|
@@ -24,7 +24,7 @@ def check_string(patterns: list[vbml.Pattern], s: str, ctx: Context) -> bool:
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class Markup(ABCRule):
|
|
27
|
-
def __init__(self, patterns: PatternLike | list[PatternLike], /):
|
|
27
|
+
def __init__(self, patterns: PatternLike | list[PatternLike], /) -> None:
|
|
28
28
|
if not isinstance(patterns, list):
|
|
29
29
|
patterns = [patterns]
|
|
30
30
|
self.patterns = [
|
|
@@ -15,7 +15,7 @@ class HasEntities(MessageRule):
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class MessageEntities(MessageRule, requires=[HasEntities()]):
|
|
18
|
-
def __init__(self, entities: Entity | list[Entity]):
|
|
18
|
+
def __init__(self, entities: Entity | list[Entity], /) -> None:
|
|
19
19
|
self.entities = [entities] if not isinstance(entities, list) else entities
|
|
20
20
|
|
|
21
21
|
async def check(self, message: Message, ctx: Context) -> bool:
|
telegrinder/bot/rules/node.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import typing
|
|
2
2
|
|
|
3
3
|
from telegrinder.bot.dispatch.context import Context
|
|
4
|
-
from telegrinder.node import Node
|
|
4
|
+
from telegrinder.node.base import Node
|
|
5
5
|
|
|
6
6
|
from .abc import ABCRule
|
|
7
7
|
from .adapter.node import NodeAdapter
|
|
@@ -15,7 +15,7 @@ class NodeRule(ABCRule[tuple[Node, ...]]):
|
|
|
15
15
|
|
|
16
16
|
@property
|
|
17
17
|
def adapter(self) -> NodeAdapter:
|
|
18
|
-
return NodeAdapter(*self.nodes)
|
|
18
|
+
return NodeAdapter(*self.nodes) # type: ignore
|
|
19
19
|
|
|
20
20
|
async def check(self, resolved_nodes: tuple[Node, ...], ctx: Context) -> typing.Literal[True]:
|
|
21
21
|
for i, node in enumerate(resolved_nodes):
|
telegrinder/bot/rules/regex.py
CHANGED
|
@@ -10,7 +10,7 @@ PatternLike: typing.TypeAlias = str | typing.Pattern[str]
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Regex(ABCRule):
|
|
13
|
-
def __init__(self, regexp: PatternLike | list[PatternLike]):
|
|
13
|
+
def __init__(self, regexp: PatternLike | list[PatternLike]) -> None:
|
|
14
14
|
self.regexp: list[re.Pattern[str]] = []
|
|
15
15
|
match regexp:
|
|
16
16
|
case re.Pattern() as pattern:
|
|
@@ -15,7 +15,7 @@ if typing.TYPE_CHECKING:
|
|
|
15
15
|
from telegrinder.bot.dispatch.view.abc import BaseStateView
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
@dataclasses.dataclass
|
|
18
|
+
@dataclasses.dataclass(slots=True)
|
|
19
19
|
class Choice:
|
|
20
20
|
name: str
|
|
21
21
|
is_picked: bool
|
|
@@ -114,7 +114,7 @@ class Checkbox(ABCScenario[CallbackQueryCute]):
|
|
|
114
114
|
api: "API",
|
|
115
115
|
view: "BaseStateView[CallbackQueryCute]",
|
|
116
116
|
) -> tuple[dict[str, bool], int]:
|
|
117
|
-
assert len(self.choices) >
|
|
117
|
+
assert len(self.choices) > 0
|
|
118
118
|
message = (
|
|
119
119
|
await api.send_message(
|
|
120
120
|
chat_id=self.chat_id,
|
telegrinder/model.py
CHANGED
|
@@ -9,14 +9,13 @@ from types import NoneType
|
|
|
9
9
|
import msgspec
|
|
10
10
|
from fntypes.co import Nothing, Result, Some
|
|
11
11
|
|
|
12
|
-
from .msgspec_utils import decoder, encoder, get_origin
|
|
12
|
+
from telegrinder.msgspec_utils import decoder, encoder, get_origin
|
|
13
13
|
|
|
14
14
|
if typing.TYPE_CHECKING:
|
|
15
15
|
from telegrinder.api.error import APIError
|
|
16
16
|
|
|
17
17
|
T = typing.TypeVar("T")
|
|
18
18
|
|
|
19
|
-
|
|
20
19
|
MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
|
|
21
20
|
"omit_defaults": True,
|
|
22
21
|
"dict": True,
|
|
@@ -60,48 +59,79 @@ def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
|
60
59
|
|
|
61
60
|
|
|
62
61
|
class Model(msgspec.Struct, **MODEL_CONFIG):
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_data(cls, data: dict[str, typing.Any]) -> typing.Self:
|
|
64
|
+
return decoder.convert(data, type=cls)
|
|
65
|
+
|
|
63
66
|
@classmethod
|
|
64
67
|
def from_bytes(cls, data: bytes) -> typing.Self:
|
|
65
68
|
return decoder.decode(data, type=cls)
|
|
66
69
|
|
|
70
|
+
def _to_dict(
|
|
71
|
+
self,
|
|
72
|
+
dct_name: str,
|
|
73
|
+
exclude_fields: set[str],
|
|
74
|
+
full: bool,
|
|
75
|
+
) -> dict[str, typing.Any]:
|
|
76
|
+
if dct_name not in self.__dict__:
|
|
77
|
+
self.__dict__[dct_name] = (
|
|
78
|
+
msgspec.structs.asdict(self)
|
|
79
|
+
if not full
|
|
80
|
+
else encoder.to_builtins(self.to_dict(exclude_fields=exclude_fields), order="deterministic")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not exclude_fields:
|
|
84
|
+
return self.__dict__[dct_name]
|
|
85
|
+
|
|
86
|
+
return {key: value for key, value in self.__dict__[dct_name].items() if key not in exclude_fields}
|
|
87
|
+
|
|
67
88
|
def to_dict(
|
|
68
89
|
self,
|
|
69
90
|
*,
|
|
70
91
|
exclude_fields: set[str] | None = None,
|
|
71
92
|
) -> dict[str, typing.Any]:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
key: value for key, value in self.__dict__["model_as_dict"].items() if key not in exclude_fields
|
|
77
|
-
}
|
|
93
|
+
"""
|
|
94
|
+
:param exclude_fields: Model field names to exclude from the dictionary representation of this model.
|
|
95
|
+
:return: A dictionary representation of this model.
|
|
96
|
+
"""
|
|
78
97
|
|
|
98
|
+
return self._to_dict("model_as_dict", exclude_fields or set(), full=False)
|
|
79
99
|
|
|
80
|
-
|
|
100
|
+
def to_full_dict(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
exclude_fields: set[str] | None = None,
|
|
104
|
+
) -> dict[str, typing.Any]:
|
|
105
|
+
"""
|
|
106
|
+
:param exclude_fields: Model field names to exclude from the dictionary representation of this model.
|
|
107
|
+
:return: A dictionary representation of this model including all models, structs, custom types.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
return self._to_dict("model_as_full_dict", exclude_fields or set(), full=True)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True, repr=False)
|
|
81
114
|
class DataConverter:
|
|
82
|
-
|
|
115
|
+
_converters: dict[type[typing.Any], typing.Callable[..., typing.Any]] = dataclasses.field(
|
|
116
|
+
init=False,
|
|
117
|
+
default_factory=lambda: {},
|
|
118
|
+
)
|
|
119
|
+
_files: dict[str, tuple[str, bytes]] = dataclasses.field(default_factory=lambda: {})
|
|
83
120
|
|
|
84
121
|
def __repr__(self) -> str:
|
|
85
122
|
return "<{}: {}>".format(
|
|
86
123
|
self.__class__.__name__,
|
|
87
|
-
", ".join(f"{k}={v.__name__!r}" for k, v in self.
|
|
124
|
+
", ".join(f"{k}={v.__name__!r}" for k, v in self._converters.items()),
|
|
88
125
|
)
|
|
89
126
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@staticmethod
|
|
99
|
-
def convert_enum(data: enum.Enum, _: bool = True) -> typing.Any:
|
|
100
|
-
return data.value
|
|
101
|
-
|
|
102
|
-
@staticmethod
|
|
103
|
-
def convert_datetime(data: datetime, _: bool = True) -> int:
|
|
104
|
-
return int(data.timestamp())
|
|
127
|
+
def __post_init__(self) -> None:
|
|
128
|
+
self._converters.update(
|
|
129
|
+
{
|
|
130
|
+
get_origin(value.__annotations__["data"]): value
|
|
131
|
+
for key, value in vars(self.__class__).items()
|
|
132
|
+
if key.startswith("convert_") and callable(value)
|
|
133
|
+
}
|
|
134
|
+
)
|
|
105
135
|
|
|
106
136
|
def __call__(self, data: typing.Any, *, serialize: bool = True) -> typing.Any:
|
|
107
137
|
converter = self.get_converter(get_origin(type(data)))
|
|
@@ -111,9 +141,25 @@ class DataConverter:
|
|
|
111
141
|
return converter(self, data, serialize)
|
|
112
142
|
return data
|
|
113
143
|
|
|
144
|
+
@property
|
|
145
|
+
def converters(self) -> dict[type[typing.Any], typing.Callable[..., typing.Any]]:
|
|
146
|
+
return self._converters.copy()
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def files(self) -> dict[str, tuple[str, bytes]]:
|
|
150
|
+
return self._files.copy()
|
|
151
|
+
|
|
152
|
+
@staticmethod
|
|
153
|
+
def convert_enum(data: enum.Enum, _: bool = False) -> typing.Any:
|
|
154
|
+
return data.value
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def convert_datetime(data: datetime, _: bool = False) -> int:
|
|
158
|
+
return int(data.timestamp())
|
|
159
|
+
|
|
114
160
|
def get_converter(self, t: type[typing.Any]):
|
|
115
|
-
for
|
|
116
|
-
if issubclass(t,
|
|
161
|
+
for type_, converter in self._converters.items():
|
|
162
|
+
if issubclass(t, type_):
|
|
117
163
|
return converter
|
|
118
164
|
return None
|
|
119
165
|
|
|
@@ -122,7 +168,7 @@ class DataConverter:
|
|
|
122
168
|
data: Model,
|
|
123
169
|
serialize: bool = True,
|
|
124
170
|
) -> str | dict[str, typing.Any]:
|
|
125
|
-
converted_dct = self(data.
|
|
171
|
+
converted_dct = self(data.to_full_dict(), serialize=False)
|
|
126
172
|
return encoder.encode(converted_dct) if serialize is True else converted_dct
|
|
127
173
|
|
|
128
174
|
def convert_dct(
|
|
@@ -142,25 +188,18 @@ class DataConverter:
|
|
|
142
188
|
converted_lst = [self(x, serialize=False) for x in data]
|
|
143
189
|
return encoder.encode(converted_lst) if serialize is True else converted_lst
|
|
144
190
|
|
|
145
|
-
def convert_tpl(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
and len(data) == 2
|
|
153
|
-
and isinstance(data[0], str)
|
|
154
|
-
and isinstance(data[1], bytes)
|
|
155
|
-
):
|
|
156
|
-
attach_name = secrets.token_urlsafe(16)
|
|
157
|
-
self.files[attach_name] = data
|
|
158
|
-
return "attach://{}".format(attach_name)
|
|
191
|
+
def convert_tpl(self, data: tuple[typing.Any, ...], _: bool = False) -> str | tuple[typing.Any, ...]:
|
|
192
|
+
match data:
|
|
193
|
+
case (str(filename), bytes(content)):
|
|
194
|
+
attach_name = secrets.token_urlsafe(16)
|
|
195
|
+
self._files[attach_name] = (filename, content)
|
|
196
|
+
return "attach://{}".format(attach_name)
|
|
197
|
+
|
|
159
198
|
return data
|
|
160
199
|
|
|
161
200
|
|
|
162
201
|
class Proxy:
|
|
163
|
-
def __init__(self, cfg: "_ProxiedDict", key: str):
|
|
202
|
+
def __init__(self, cfg: "_ProxiedDict", key: str) -> None:
|
|
164
203
|
self.key = key
|
|
165
204
|
self.cfg = cfg
|
|
166
205
|
|
|
@@ -193,11 +232,11 @@ else:
|
|
|
193
232
|
|
|
194
233
|
|
|
195
234
|
__all__ = (
|
|
196
|
-
"Proxy",
|
|
197
235
|
"DataConverter",
|
|
198
|
-
"ProxiedDict",
|
|
199
236
|
"MODEL_CONFIG",
|
|
200
237
|
"Model",
|
|
238
|
+
"ProxiedDict",
|
|
239
|
+
"Proxy",
|
|
201
240
|
"full_result",
|
|
202
241
|
"get_params",
|
|
203
242
|
)
|
telegrinder/modules.py
CHANGED
|
@@ -108,8 +108,8 @@ elif logging_module == "logging":
|
|
|
108
108
|
"level": "green",
|
|
109
109
|
"level_module": "blue",
|
|
110
110
|
"level_func": "cyan",
|
|
111
|
-
"level_lineno": "
|
|
112
|
-
"level_message": "
|
|
111
|
+
"level_lineno": "white",
|
|
112
|
+
"level_message": "green",
|
|
113
113
|
},
|
|
114
114
|
"DEBUG": {
|
|
115
115
|
"level": "blue",
|
|
@@ -232,7 +232,7 @@ def _set_logger_level(level):
|
|
|
232
232
|
if logging_module == "logging":
|
|
233
233
|
import logging
|
|
234
234
|
|
|
235
|
-
logging.getLogger("telegrinder").setLevel(
|
|
235
|
+
logging.getLogger("telegrinder").setLevel(level)
|
|
236
236
|
elif logging_module == "loguru":
|
|
237
237
|
import loguru # type: ignore
|
|
238
238
|
|
telegrinder/msgspec_utils.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import dataclasses
|
|
1
2
|
import typing
|
|
2
3
|
|
|
3
4
|
import fntypes.option
|
|
@@ -34,8 +35,6 @@ else:
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
T = typing.TypeVar("T")
|
|
37
|
-
Type = typing.TypeVar("Type", bound=type | typing.Any)
|
|
38
|
-
Ts = typing.TypeVarTuple("Ts")
|
|
39
38
|
|
|
40
39
|
DecHook: typing.TypeAlias = typing.Callable[[type[T], typing.Any], typing.Any]
|
|
41
40
|
EncHook: typing.TypeAlias = typing.Callable[[T], typing.Any]
|
|
@@ -63,6 +62,16 @@ def msgspec_convert(obj: typing.Any, t: type[T]) -> Result[T, str]:
|
|
|
63
62
|
)
|
|
64
63
|
|
|
65
64
|
|
|
65
|
+
def msgspec_to_builtins(
|
|
66
|
+
obj: typing.Any,
|
|
67
|
+
*,
|
|
68
|
+
str_keys: bool = False,
|
|
69
|
+
builtin_types: typing.Iterable[type[typing.Any]] | None = None,
|
|
70
|
+
order: typing.Literal["deterministic", "sorted"] | None = None,
|
|
71
|
+
) -> typing.Any:
|
|
72
|
+
return encoder.to_builtins(**locals())
|
|
73
|
+
|
|
74
|
+
|
|
66
75
|
def option_dec_hook(tp: type[Option[typing.Any]], obj: typing.Any) -> Option[typing.Any]:
|
|
67
76
|
orig_type = get_origin(tp)
|
|
68
77
|
(value_type,) = typing.get_args(tp) or (typing.Any,)
|
|
@@ -137,6 +146,11 @@ def variative_dec_hook(tp: type[Variative], obj: typing.Any) -> Variative:
|
|
|
137
146
|
)
|
|
138
147
|
|
|
139
148
|
|
|
149
|
+
@typing.runtime_checkable
|
|
150
|
+
class DataclassInstance(typing.Protocol):
|
|
151
|
+
__dataclass_fields__: typing.ClassVar[dict[str, dataclasses.Field[typing.Any]]]
|
|
152
|
+
|
|
153
|
+
|
|
140
154
|
class Decoder:
|
|
141
155
|
"""Class `Decoder` for `msgspec` module with decode hook
|
|
142
156
|
for objects with the specified type.
|
|
@@ -155,7 +169,6 @@ class Decoder:
|
|
|
155
169
|
decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
|
|
156
170
|
|
|
157
171
|
decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
|
|
158
|
-
decoder.dec_hook(int, "123") #> TypeError: Unknown type `int`. You can implement decode hook for this type.
|
|
159
172
|
|
|
160
173
|
decoder.convert("123", type=int, strict=False) #> 123
|
|
161
174
|
decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
|
|
@@ -250,8 +263,6 @@ class Encoder:
|
|
|
250
263
|
encoder.enc_hooks[dt] = lambda d: int(d.timestamp())
|
|
251
264
|
|
|
252
265
|
encoder.enc_hook(dt.now()) #> 1713354732
|
|
253
|
-
encoder.enc_hook(123) #> NotImplementedError: Not implemented encode hook for object of type `int`.
|
|
254
|
-
|
|
255
266
|
encoder.encode({'digit': Digit.ONE}) #> '{"digit":1}'
|
|
256
267
|
```
|
|
257
268
|
"""
|
|
@@ -302,6 +313,22 @@ class Encoder:
|
|
|
302
313
|
buf = msgspec.json.encode(obj, enc_hook=self.enc_hook)
|
|
303
314
|
return buf.decode() if as_str else buf
|
|
304
315
|
|
|
316
|
+
def to_builtins(
|
|
317
|
+
self,
|
|
318
|
+
obj: typing.Any,
|
|
319
|
+
*,
|
|
320
|
+
str_keys: bool = False,
|
|
321
|
+
builtin_types: typing.Iterable[type[typing.Any]] | None = None,
|
|
322
|
+
order: typing.Literal["deterministic", "sorted"] | None = None,
|
|
323
|
+
) -> typing.Any:
|
|
324
|
+
return msgspec.to_builtins(
|
|
325
|
+
obj,
|
|
326
|
+
str_keys=str_keys,
|
|
327
|
+
builtin_types=builtin_types,
|
|
328
|
+
enc_hook=self.enc_hook,
|
|
329
|
+
order=order,
|
|
330
|
+
)
|
|
331
|
+
|
|
305
332
|
|
|
306
333
|
decoder: typing.Final[Decoder] = Decoder()
|
|
307
334
|
encoder: typing.Final[Encoder] = Encoder()
|
|
@@ -317,6 +344,7 @@ __all__ = (
|
|
|
317
344
|
"encoder",
|
|
318
345
|
"get_origin",
|
|
319
346
|
"msgspec_convert",
|
|
347
|
+
"msgspec_to_builtins",
|
|
320
348
|
"option_dec_hook",
|
|
321
349
|
"repr_type",
|
|
322
350
|
"variative_dec_hook",
|