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.

Files changed (79) hide show
  1. telegrinder/api/abc.py +7 -1
  2. telegrinder/api/api.py +12 -3
  3. telegrinder/api/error.py +2 -1
  4. telegrinder/bot/bot.py +6 -1
  5. telegrinder/bot/cute_types/base.py +144 -17
  6. telegrinder/bot/cute_types/callback_query.py +6 -1
  7. telegrinder/bot/cute_types/chat_member_updated.py +1 -2
  8. telegrinder/bot/cute_types/message.py +23 -11
  9. telegrinder/bot/cute_types/update.py +48 -0
  10. telegrinder/bot/cute_types/utils.py +2 -465
  11. telegrinder/bot/dispatch/__init__.py +2 -3
  12. telegrinder/bot/dispatch/abc.py +6 -3
  13. telegrinder/bot/dispatch/context.py +6 -6
  14. telegrinder/bot/dispatch/dispatch.py +61 -23
  15. telegrinder/bot/dispatch/handler/abc.py +2 -2
  16. telegrinder/bot/dispatch/handler/func.py +36 -17
  17. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  18. telegrinder/bot/dispatch/middleware/abc.py +2 -2
  19. telegrinder/bot/dispatch/process.py +10 -10
  20. telegrinder/bot/dispatch/return_manager/abc.py +3 -3
  21. telegrinder/bot/dispatch/view/abc.py +12 -15
  22. telegrinder/bot/dispatch/view/box.py +73 -62
  23. telegrinder/bot/dispatch/view/message.py +11 -3
  24. telegrinder/bot/dispatch/view/raw.py +3 -0
  25. telegrinder/bot/dispatch/waiter_machine/machine.py +2 -2
  26. telegrinder/bot/dispatch/waiter_machine/middleware.py +1 -1
  27. telegrinder/bot/dispatch/waiter_machine/short_state.py +2 -1
  28. telegrinder/bot/polling/polling.py +3 -3
  29. telegrinder/bot/rules/abc.py +11 -7
  30. telegrinder/bot/rules/adapter/event.py +7 -4
  31. telegrinder/bot/rules/adapter/node.py +1 -1
  32. telegrinder/bot/rules/command.py +5 -7
  33. telegrinder/bot/rules/func.py +1 -1
  34. telegrinder/bot/rules/fuzzy.py +1 -1
  35. telegrinder/bot/rules/integer.py +1 -2
  36. telegrinder/bot/rules/markup.py +3 -3
  37. telegrinder/bot/rules/message_entities.py +1 -1
  38. telegrinder/bot/rules/node.py +2 -2
  39. telegrinder/bot/rules/regex.py +1 -1
  40. telegrinder/bot/rules/rule_enum.py +1 -1
  41. telegrinder/bot/scenario/checkbox.py +2 -2
  42. telegrinder/model.py +85 -46
  43. telegrinder/modules.py +3 -3
  44. telegrinder/msgspec_utils.py +33 -5
  45. telegrinder/node/__init__.py +20 -11
  46. telegrinder/node/attachment.py +19 -16
  47. telegrinder/node/base.py +120 -24
  48. telegrinder/node/callback_query.py +5 -9
  49. telegrinder/node/command.py +6 -2
  50. telegrinder/node/composer.py +82 -54
  51. telegrinder/node/container.py +4 -4
  52. telegrinder/node/event.py +59 -0
  53. telegrinder/node/me.py +3 -0
  54. telegrinder/node/message.py +6 -10
  55. telegrinder/node/polymorphic.py +11 -12
  56. telegrinder/node/rule.py +27 -5
  57. telegrinder/node/source.py +10 -11
  58. telegrinder/node/text.py +4 -4
  59. telegrinder/node/update.py +1 -2
  60. telegrinder/py.typed +0 -0
  61. telegrinder/tools/__init__.py +2 -2
  62. telegrinder/tools/buttons.py +5 -10
  63. telegrinder/tools/error_handler/error.py +2 -0
  64. telegrinder/tools/error_handler/error_handler.py +1 -1
  65. telegrinder/tools/formatting/spec_html_formats.py +10 -10
  66. telegrinder/tools/global_context/__init__.py +2 -2
  67. telegrinder/tools/global_context/global_context.py +2 -2
  68. telegrinder/tools/global_context/telegrinder_ctx.py +4 -4
  69. telegrinder/tools/keyboard.py +2 -2
  70. telegrinder/tools/loop_wrapper/loop_wrapper.py +39 -5
  71. telegrinder/tools/magic.py +48 -15
  72. telegrinder/types/enums.py +1 -0
  73. telegrinder/types/methods.py +14 -5
  74. telegrinder/types/objects.py +3 -0
  75. {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/METADATA +2 -2
  76. telegrinder-0.1.dev171.dist-info/RECORD +145 -0
  77. telegrinder-0.1.dev170.dist-info/RECORD +0 -143
  78. {telegrinder-0.1.dev170.dist-info → telegrinder-0.1.dev171.dist-info}/LICENSE +0 -0
  79. {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 reconnet...",
114
+ "Server disconnected, waiting 5 seconds to reconnect...",
115
115
  )
116
116
  reconn_counter += 1
117
117
  await asyncio.sleep(self.reconnection_timeout)
@@ -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
- def get_required_nodes(self) -> dict[str, type[Node]]:
45
- return {k: v for k, v in get_annotations(self.check).items() if is_node(v)}
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 update_dct[self.event.value] is Nothing:
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(update_dct[self.event.value].unwrap(), bound_api=api),
52
+ self.cute_model.from_update(
53
+ getattr(update, self.event.value).unwrap(),
54
+ bound_api=api,
55
+ ),
53
56
  )
54
57
 
55
- event = update_dct[update.update_type.value].unwrap()
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)
@@ -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
 
@@ -2,7 +2,7 @@ import inspect
2
2
  import typing
3
3
 
4
4
  from telegrinder.bot.dispatch.context import Context
5
- from telegrinder.types import Update
5
+ from telegrinder.types.objects import Update
6
6
 
7
7
  from .abc import ABCAdapter, ABCRule, AdaptTo, RawUpdateAdapter
8
8
 
@@ -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
@@ -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:
@@ -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 TelegrinderCtx
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 = TelegrinderCtx()
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:
@@ -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):
@@ -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:
@@ -7,7 +7,7 @@ from .abc import ABCRule, Update, check_rule
7
7
  from .func import FuncRule
8
8
 
9
9
 
10
- @dataclasses.dataclass
10
+ @dataclasses.dataclass(slots=True)
11
11
  class RuleEnumState:
12
12
  name: str
13
13
  rule: ABCRule
@@ -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) > 1
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
- exclude_fields = exclude_fields or set()
73
- if "model_as_dict" not in self.__dict__:
74
- self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
75
- return {
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
- @dataclasses.dataclass(kw_only=True)
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
- files: dict[str, tuple[str, bytes]] = dataclasses.field(default_factory=lambda: {})
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.converters.items()),
124
+ ", ".join(f"{k}={v.__name__!r}" for k, v in self._converters.items()),
88
125
  )
89
126
 
90
- @property
91
- def converters(self) -> dict[type[typing.Any], typing.Callable[..., typing.Any]]:
92
- return {
93
- get_origin(value.__annotations__["data"]): value
94
- for key, value in vars(self.__class__).items()
95
- if key.startswith("convert_") and callable(value)
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 type, converter in self.converters.items():
116
- if issubclass(t, type):
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.to_dict(), serialize=False)
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
- self,
147
- data: tuple[typing.Any, ...],
148
- _: bool = True,
149
- ) -> str | tuple[typing.Any, ...]:
150
- if (
151
- isinstance(data, tuple)
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": "green",
112
- "level_message": "white",
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(logging.getLevelName(level))
235
+ logging.getLogger("telegrinder").setLevel(level)
236
236
  elif logging_module == "loguru":
237
237
  import loguru # type: ignore
238
238
 
@@ -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",