telegrinder 0.1.dev169__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 +87 -47
- telegrinder/modules.py +3 -3
- telegrinder/msgspec_utils.py +94 -13
- 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.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/METADATA +2 -2
- telegrinder-0.1.dev171.dist-info/RECORD +145 -0
- telegrinder-0.1.dev169.dist-info/RECORD +0 -143
- {telegrinder-0.1.dev169.dist-info → telegrinder-0.1.dev171.dist-info}/LICENSE +0 -0
- {telegrinder-0.1.dev169.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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import enum
|
|
3
|
+
import keyword
|
|
3
4
|
import secrets
|
|
4
5
|
import typing
|
|
5
6
|
from datetime import datetime
|
|
@@ -8,18 +9,17 @@ from types import NoneType
|
|
|
8
9
|
import msgspec
|
|
9
10
|
from fntypes.co import Nothing, Result, Some
|
|
10
11
|
|
|
11
|
-
from .msgspec_utils import decoder, encoder, get_origin
|
|
12
|
+
from telegrinder.msgspec_utils import decoder, encoder, get_origin
|
|
12
13
|
|
|
13
14
|
if typing.TYPE_CHECKING:
|
|
14
15
|
from telegrinder.api.error import APIError
|
|
15
16
|
|
|
16
17
|
T = typing.TypeVar("T")
|
|
17
18
|
|
|
18
|
-
|
|
19
19
|
MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
|
|
20
20
|
"omit_defaults": True,
|
|
21
21
|
"dict": True,
|
|
22
|
-
"rename": {"
|
|
22
|
+
"rename": {kw + "_": kw for kw in keyword.kwlist},
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
|
|
@@ -59,48 +59,79 @@ def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
|
|
|
59
59
|
|
|
60
60
|
|
|
61
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
|
+
|
|
62
66
|
@classmethod
|
|
63
67
|
def from_bytes(cls, data: bytes) -> typing.Self:
|
|
64
68
|
return decoder.decode(data, type=cls)
|
|
65
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
|
+
|
|
66
88
|
def to_dict(
|
|
67
89
|
self,
|
|
68
90
|
*,
|
|
69
91
|
exclude_fields: set[str] | None = None,
|
|
70
92
|
) -> dict[str, typing.Any]:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
key: value for key, value in self.__dict__["model_as_dict"].items() if key not in exclude_fields
|
|
76
|
-
}
|
|
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
|
+
"""
|
|
77
97
|
|
|
98
|
+
return self._to_dict("model_as_dict", exclude_fields or set(), full=False)
|
|
78
99
|
|
|
79
|
-
|
|
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)
|
|
80
114
|
class DataConverter:
|
|
81
|
-
|
|
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: {})
|
|
82
120
|
|
|
83
121
|
def __repr__(self) -> str:
|
|
84
122
|
return "<{}: {}>".format(
|
|
85
123
|
self.__class__.__name__,
|
|
86
|
-
", ".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()),
|
|
87
125
|
)
|
|
88
126
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@staticmethod
|
|
98
|
-
def convert_enum(data: enum.Enum, _: bool = True) -> typing.Any:
|
|
99
|
-
return data.value
|
|
100
|
-
|
|
101
|
-
@staticmethod
|
|
102
|
-
def convert_datetime(data: datetime, _: bool = True) -> int:
|
|
103
|
-
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
|
+
)
|
|
104
135
|
|
|
105
136
|
def __call__(self, data: typing.Any, *, serialize: bool = True) -> typing.Any:
|
|
106
137
|
converter = self.get_converter(get_origin(type(data)))
|
|
@@ -110,9 +141,25 @@ class DataConverter:
|
|
|
110
141
|
return converter(self, data, serialize)
|
|
111
142
|
return data
|
|
112
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
|
+
|
|
113
160
|
def get_converter(self, t: type[typing.Any]):
|
|
114
|
-
for
|
|
115
|
-
if issubclass(t,
|
|
161
|
+
for type_, converter in self._converters.items():
|
|
162
|
+
if issubclass(t, type_):
|
|
116
163
|
return converter
|
|
117
164
|
return None
|
|
118
165
|
|
|
@@ -121,7 +168,7 @@ class DataConverter:
|
|
|
121
168
|
data: Model,
|
|
122
169
|
serialize: bool = True,
|
|
123
170
|
) -> str | dict[str, typing.Any]:
|
|
124
|
-
converted_dct = self(data.
|
|
171
|
+
converted_dct = self(data.to_full_dict(), serialize=False)
|
|
125
172
|
return encoder.encode(converted_dct) if serialize is True else converted_dct
|
|
126
173
|
|
|
127
174
|
def convert_dct(
|
|
@@ -141,25 +188,18 @@ class DataConverter:
|
|
|
141
188
|
converted_lst = [self(x, serialize=False) for x in data]
|
|
142
189
|
return encoder.encode(converted_lst) if serialize is True else converted_lst
|
|
143
190
|
|
|
144
|
-
def convert_tpl(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
and len(data) == 2
|
|
152
|
-
and isinstance(data[0], str)
|
|
153
|
-
and isinstance(data[1], bytes)
|
|
154
|
-
):
|
|
155
|
-
attach_name = secrets.token_urlsafe(16)
|
|
156
|
-
self.files[attach_name] = data
|
|
157
|
-
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
|
+
|
|
158
198
|
return data
|
|
159
199
|
|
|
160
200
|
|
|
161
201
|
class Proxy:
|
|
162
|
-
def __init__(self, cfg: "_ProxiedDict", key: str):
|
|
202
|
+
def __init__(self, cfg: "_ProxiedDict", key: str) -> None:
|
|
163
203
|
self.key = key
|
|
164
204
|
self.cfg = cfg
|
|
165
205
|
|
|
@@ -192,11 +232,11 @@ else:
|
|
|
192
232
|
|
|
193
233
|
|
|
194
234
|
__all__ = (
|
|
195
|
-
"Proxy",
|
|
196
235
|
"DataConverter",
|
|
197
|
-
"ProxiedDict",
|
|
198
236
|
"MODEL_CONFIG",
|
|
199
237
|
"Model",
|
|
238
|
+
"ProxiedDict",
|
|
239
|
+
"Proxy",
|
|
200
240
|
"full_result",
|
|
201
241
|
"get_params",
|
|
202
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
|
|