telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.1__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 (169) hide show
  1. telegrinder/__init__.py +30 -31
  2. telegrinder/api/__init__.py +2 -1
  3. telegrinder/api/api.py +28 -20
  4. telegrinder/api/error.py +8 -4
  5. telegrinder/api/response.py +2 -2
  6. telegrinder/api/token.py +2 -2
  7. telegrinder/bot/__init__.py +6 -0
  8. telegrinder/bot/bot.py +38 -31
  9. telegrinder/bot/cute_types/__init__.py +2 -0
  10. telegrinder/bot/cute_types/base.py +55 -129
  11. telegrinder/bot/cute_types/callback_query.py +76 -61
  12. telegrinder/bot/cute_types/chat_join_request.py +4 -3
  13. telegrinder/bot/cute_types/chat_member_updated.py +28 -31
  14. telegrinder/bot/cute_types/inline_query.py +5 -4
  15. telegrinder/bot/cute_types/message.py +555 -602
  16. telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
  17. telegrinder/bot/cute_types/update.py +20 -12
  18. telegrinder/bot/cute_types/utils.py +3 -36
  19. telegrinder/bot/dispatch/__init__.py +4 -0
  20. telegrinder/bot/dispatch/abc.py +8 -9
  21. telegrinder/bot/dispatch/context.py +5 -7
  22. telegrinder/bot/dispatch/dispatch.py +85 -33
  23. telegrinder/bot/dispatch/handler/abc.py +5 -6
  24. telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
  25. telegrinder/bot/dispatch/handler/base.py +3 -3
  26. telegrinder/bot/dispatch/handler/document_reply.py +2 -2
  27. telegrinder/bot/dispatch/handler/func.py +36 -42
  28. telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
  29. telegrinder/bot/dispatch/handler/message_reply.py +2 -2
  30. telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
  31. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
  32. telegrinder/bot/dispatch/handler/video_reply.py +2 -2
  33. telegrinder/bot/dispatch/middleware/abc.py +83 -8
  34. telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
  35. telegrinder/bot/dispatch/process.py +44 -50
  36. telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
  37. telegrinder/bot/dispatch/return_manager/abc.py +6 -10
  38. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
  39. telegrinder/bot/dispatch/view/__init__.py +2 -0
  40. telegrinder/bot/dispatch/view/abc.py +10 -6
  41. telegrinder/bot/dispatch/view/base.py +81 -50
  42. telegrinder/bot/dispatch/view/box.py +20 -9
  43. telegrinder/bot/dispatch/view/callback_query.py +3 -4
  44. telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
  45. telegrinder/bot/dispatch/view/chat_member.py +3 -5
  46. telegrinder/bot/dispatch/view/inline_query.py +3 -4
  47. telegrinder/bot/dispatch/view/message.py +3 -4
  48. telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
  49. telegrinder/bot/dispatch/view/raw.py +42 -40
  50. telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
  51. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
  52. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
  53. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
  54. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  55. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
  56. telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
  57. telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
  58. telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
  59. telegrinder/bot/polling/polling.py +62 -54
  60. telegrinder/bot/rules/__init__.py +24 -1
  61. telegrinder/bot/rules/abc.py +17 -10
  62. telegrinder/bot/rules/callback_data.py +20 -61
  63. telegrinder/bot/rules/chat_join.py +6 -4
  64. telegrinder/bot/rules/command.py +4 -4
  65. telegrinder/bot/rules/enum_text.py +1 -4
  66. telegrinder/bot/rules/func.py +5 -3
  67. telegrinder/bot/rules/fuzzy.py +1 -1
  68. telegrinder/bot/rules/id.py +24 -0
  69. telegrinder/bot/rules/inline.py +6 -4
  70. telegrinder/bot/rules/integer.py +2 -1
  71. telegrinder/bot/rules/logic.py +18 -0
  72. telegrinder/bot/rules/markup.py +5 -6
  73. telegrinder/bot/rules/message.py +2 -4
  74. telegrinder/bot/rules/message_entities.py +1 -3
  75. telegrinder/bot/rules/node.py +15 -9
  76. telegrinder/bot/rules/payload.py +81 -0
  77. telegrinder/bot/rules/payment_invoice.py +29 -0
  78. telegrinder/bot/rules/regex.py +5 -6
  79. telegrinder/bot/rules/state.py +1 -3
  80. telegrinder/bot/rules/text.py +10 -5
  81. telegrinder/bot/rules/update.py +0 -0
  82. telegrinder/bot/scenario/abc.py +2 -4
  83. telegrinder/bot/scenario/checkbox.py +12 -14
  84. telegrinder/bot/scenario/choice.py +6 -9
  85. telegrinder/client/__init__.py +9 -1
  86. telegrinder/client/abc.py +35 -10
  87. telegrinder/client/aiohttp.py +28 -24
  88. telegrinder/client/form_data.py +31 -0
  89. telegrinder/client/sonic.py +212 -0
  90. telegrinder/model.py +38 -145
  91. telegrinder/modules.py +3 -1
  92. telegrinder/msgspec_utils.py +136 -68
  93. telegrinder/node/__init__.py +74 -13
  94. telegrinder/node/attachment.py +92 -16
  95. telegrinder/node/base.py +196 -68
  96. telegrinder/node/callback_query.py +17 -16
  97. telegrinder/node/command.py +3 -2
  98. telegrinder/node/composer.py +40 -75
  99. telegrinder/node/container.py +13 -7
  100. telegrinder/node/either.py +82 -0
  101. telegrinder/node/event.py +20 -31
  102. telegrinder/node/file.py +51 -0
  103. telegrinder/node/me.py +4 -5
  104. telegrinder/node/payload.py +78 -0
  105. telegrinder/node/polymorphic.py +28 -9
  106. telegrinder/node/rule.py +2 -6
  107. telegrinder/node/scope.py +4 -6
  108. telegrinder/node/source.py +37 -21
  109. telegrinder/node/text.py +20 -8
  110. telegrinder/node/tools/generator.py +7 -11
  111. telegrinder/py.typed +0 -0
  112. telegrinder/rules.py +0 -61
  113. telegrinder/tools/__init__.py +97 -38
  114. telegrinder/tools/adapter/__init__.py +19 -0
  115. telegrinder/tools/adapter/abc.py +49 -0
  116. telegrinder/tools/adapter/dataclass.py +56 -0
  117. telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
  118. telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
  119. telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
  120. telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
  121. telegrinder/tools/buttons.py +52 -26
  122. telegrinder/tools/callback_data_serilization/__init__.py +5 -0
  123. telegrinder/tools/callback_data_serilization/abc.py +51 -0
  124. telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
  125. telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
  126. telegrinder/tools/error_handler/abc.py +4 -7
  127. telegrinder/tools/error_handler/error.py +0 -0
  128. telegrinder/tools/error_handler/error_handler.py +34 -48
  129. telegrinder/tools/formatting/__init__.py +57 -37
  130. telegrinder/tools/formatting/deep_links.py +541 -0
  131. telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
  132. telegrinder/tools/formatting/spec_html_formats.py +14 -60
  133. telegrinder/tools/functional.py +1 -5
  134. telegrinder/tools/global_context/global_context.py +26 -51
  135. telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
  136. telegrinder/tools/i18n/abc.py +0 -0
  137. telegrinder/tools/i18n/middleware/abc.py +3 -6
  138. telegrinder/tools/input_file_directory.py +30 -0
  139. telegrinder/tools/keyboard.py +9 -9
  140. telegrinder/tools/lifespan.py +105 -0
  141. telegrinder/tools/limited_dict.py +5 -10
  142. telegrinder/tools/loop_wrapper/abc.py +7 -2
  143. telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
  144. telegrinder/tools/magic.py +236 -60
  145. telegrinder/tools/state_storage/__init__.py +0 -0
  146. telegrinder/tools/state_storage/abc.py +5 -9
  147. telegrinder/tools/state_storage/memory.py +1 -1
  148. telegrinder/tools/strings.py +13 -0
  149. telegrinder/types/__init__.py +8 -0
  150. telegrinder/types/enums.py +31 -21
  151. telegrinder/types/input_file.py +51 -0
  152. telegrinder/types/methods.py +531 -109
  153. telegrinder/types/objects.py +934 -826
  154. telegrinder/verification_utils.py +0 -2
  155. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.1.dist-info}/LICENSE +2 -2
  156. telegrinder-0.4.1.dist-info/METADATA +143 -0
  157. telegrinder-0.4.1.dist-info/RECORD +182 -0
  158. {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.1.dist-info}/WHEEL +1 -1
  159. telegrinder/bot/rules/adapter/__init__.py +0 -17
  160. telegrinder/bot/rules/adapter/abc.py +0 -31
  161. telegrinder/node/message.py +0 -14
  162. telegrinder/node/update.py +0 -15
  163. telegrinder/tools/formatting/links.py +0 -38
  164. telegrinder/tools/kb_set/__init__.py +0 -4
  165. telegrinder/tools/kb_set/base.py +0 -15
  166. telegrinder/tools/kb_set/yaml.py +0 -63
  167. telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
  168. telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
  169. /telegrinder/{bot/rules → tools}/adapter/errors.py +0 -0
@@ -0,0 +1,31 @@
1
+ import typing
2
+
3
+ from telegrinder.msgspec_utils import encoder
4
+
5
+
6
+ def encode_form_data(
7
+ data: dict[str, typing.Any],
8
+ files: dict[str, tuple[str, typing.Any]],
9
+ /,
10
+ ) -> dict[str, str]:
11
+ context = dict(files=files)
12
+ return {
13
+ k: encoder.encode(v, context=context).removeprefix('"').removesuffix('"') # Remove quoted strings
14
+ if not isinstance(v, str)
15
+ else v
16
+ for k, v in data.items()
17
+ }
18
+
19
+
20
+ class MultipartFormProto(typing.Protocol):
21
+ def add_field(
22
+ self,
23
+ name: str,
24
+ value: typing.Any,
25
+ /,
26
+ *,
27
+ filename: str | None = None,
28
+ ) -> None: ...
29
+
30
+
31
+ __all__ = ("MultipartFormProto", "encode_form_data")
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import ssl
5
+ import typing
6
+
7
+ import certifi
8
+
9
+ import telegrinder.msgspec_json as json
10
+ from telegrinder.client.abc import ABCClient
11
+
12
+ if typing.TYPE_CHECKING:
13
+ from aiosonic import Connection, HTTPClient, HttpResponse, MultipartForm, Proxy, TCPConnector, Timeouts
14
+
15
+ type Data = dict[str, typing.Any] | MultipartForm
16
+ type Response = HttpResponse
17
+
18
+ AIOSONIC_OBJECTS = (
19
+ "Connection",
20
+ "HTTPClient",
21
+ "HttpResponse",
22
+ "Proxy",
23
+ "TCPConnector",
24
+ "Timeouts",
25
+ )
26
+
27
+
28
+ def init_aiosonic_module(client_class: type[ABCClient[typing.Any]], /) -> None:
29
+ try:
30
+ import aiosonic
31
+ import aiosonic.exceptions
32
+ except ImportError:
33
+ raise ImportError(
34
+ "Module 'aiosonic' is not installed. You can install as follows: pip install aiosonic"
35
+ ) from None
36
+
37
+ globalns = globals()
38
+ for name in AIOSONIC_OBJECTS:
39
+ globalns.setdefault(name, getattr(aiosonic, name))
40
+
41
+ if "MultipartForm" not in globalns:
42
+ globalns["MultipartForm"] = type("MultiPartForm", (_MultipartForm, aiosonic.MultipartForm), {})
43
+
44
+ if not client_class.CONNECTION_TIMEOUT_ERRORS:
45
+ client_class.CONNECTION_TIMEOUT_ERRORS = (
46
+ aiosonic.exceptions.ConnectTimeout,
47
+ aiosonic.exceptions.RequestTimeout,
48
+ TimeoutError,
49
+ )
50
+
51
+ if not client_class.CLIENT_CONNECTION_ERRORS:
52
+ client_class.CLIENT_CONNECTION_ERRORS = (
53
+ aiosonic.exceptions.ConnectionDisconnected,
54
+ aiosonic.exceptions.ConnectionPoolAcquireTimeout,
55
+ )
56
+
57
+
58
+ class _MultipartForm(MultipartForm if typing.TYPE_CHECKING else object):
59
+ async def _generate_chunks(self) -> typing.AsyncGenerator[bytes, None]:
60
+ for field in self.fields:
61
+ yield (f"--{self.boundary}\r\n").encode()
62
+
63
+ if isinstance(field[1], io.IOBase):
64
+ yield (
65
+ "Content-Type: application/octet-stream\r\n"
66
+ "Content-Disposition: form-data; " + f'name="{field[0]}"; filename="{field[2]}"\r\n\r\n'
67
+ ).encode()
68
+
69
+ async for data in self._read_file(field[1]):
70
+ yield data + b"\r\n"
71
+
72
+ field[1].close()
73
+ else:
74
+ yield (
75
+ "Content-Type: text/plain; charset=utf-8\r\n"
76
+ f'Content-Disposition: form-data; name="{field[0]}"\r\n\r\n'
77
+ ).encode()
78
+ yield field[1].encode() + b"\r\n"
79
+
80
+ yield (f"--{self.boundary}--").encode()
81
+
82
+ async def get_body_size(self) -> tuple[bytes, int]:
83
+ if not self.fields:
84
+ return b"", 0
85
+ return await super().get_body_size()
86
+
87
+ def get_headers(self, size: int | None = None) -> dict[str, str]:
88
+ if not self.fields:
89
+ return {"Content-Type": "application/x-www-form-urlencoded"}
90
+ return super().get_headers(size)
91
+
92
+
93
+ class AiosonicClient(ABCClient["MultipartForm"]):
94
+ """HTTP client based on `aiosonic` module."""
95
+
96
+ def __init_subclass__(cls, *args: typing.Any, **kwargs: typing.Any) -> None:
97
+ init_aiosonic_module(cls)
98
+ return super().__init_subclass__(*args, **kwargs)
99
+
100
+ def __init__(
101
+ self,
102
+ *,
103
+ verify_ssl: bool = True,
104
+ tpc_pool_size: int = 25,
105
+ tpc_timeouts: Timeouts | None = None,
106
+ proxy: Proxy | None = None,
107
+ conn_max_requests: int = 100,
108
+ use_dns_cache: bool = True,
109
+ handle_cookies: bool = False,
110
+ ttl_dns_cache: int = 10000,
111
+ **kwargs: typing.Any,
112
+ ) -> None:
113
+ init_aiosonic_module(self.__class__)
114
+ self.ssl = ssl.create_default_context(cafile=certifi.where())
115
+ self.proxy = proxy
116
+ self.verify_ssl = verify_ssl
117
+ self.handle_cookies = handle_cookies
118
+ self.tpc_timeouts = tpc_timeouts or Timeouts()
119
+ self.tcp_connector = TCPConnector(
120
+ pool_size=tpc_pool_size,
121
+ timeouts=self.tpc_timeouts,
122
+ connection_cls=Connection,
123
+ conn_max_requests=conn_max_requests,
124
+ use_dns_cache=use_dns_cache,
125
+ ttl_dns_cache=ttl_dns_cache,
126
+ **kwargs,
127
+ )
128
+ self.client: HTTPClient | None = None
129
+
130
+ def __repr__(self) -> str:
131
+ return "<{}: proxy={!r}, tpc_timeouts={!r}, tcp_connector={!r}, client={!r}>".format(
132
+ self.__class__.__name__,
133
+ self.proxy,
134
+ self.tpc_timeouts,
135
+ self.tcp_connector,
136
+ self.client,
137
+ )
138
+
139
+ @classmethod
140
+ def multipart_form_factory(cls) -> MultipartForm:
141
+ return MultipartForm()
142
+
143
+ async def request_raw(
144
+ self,
145
+ url: str,
146
+ method: str = "GET",
147
+ data: Data | None = None,
148
+ **kwargs: typing.Any,
149
+ ) -> Response:
150
+ if self.client is None:
151
+ self.client = HTTPClient(
152
+ connector=self.tcp_connector,
153
+ handle_cookies=self.handle_cookies,
154
+ verify_ssl=self.verify_ssl,
155
+ proxy=self.proxy,
156
+ )
157
+
158
+ return await self.client.request(
159
+ url=url,
160
+ method=method,
161
+ data=data,
162
+ json_serializer=json.dumps,
163
+ ssl=self.ssl,
164
+ **kwargs,
165
+ )
166
+
167
+ async def request_text(
168
+ self,
169
+ url: str,
170
+ method: str = "GET",
171
+ data: Data | None = None,
172
+ **kwargs: typing.Any,
173
+ ) -> str:
174
+ response = await self.request_raw(url, method, data, **kwargs)
175
+ return await response.text()
176
+
177
+ async def request_json(
178
+ self,
179
+ url: str,
180
+ method: str = "GET",
181
+ data: Data | None = None,
182
+ **kwargs: typing.Any,
183
+ ) -> dict[str, typing.Any]:
184
+ return json.loads(await self.request_content(url, method, data, **kwargs))
185
+
186
+ async def request_bytes(
187
+ self,
188
+ url: str,
189
+ method: str = "GET",
190
+ data: Data | None = None,
191
+ **kwargs: typing.Any,
192
+ ) -> bytes:
193
+ response = await self.request_raw(url, method, data, **kwargs)
194
+ return response.body
195
+
196
+ async def request_content(
197
+ self,
198
+ url: str,
199
+ method: str = "GET",
200
+ data: Data | None = None,
201
+ **kwargs: typing.Any,
202
+ ) -> bytes:
203
+ response = await self.request_raw(url, method, data, **kwargs)
204
+ return await response.content()
205
+
206
+ async def close(self) -> None:
207
+ if self.client is not None:
208
+ await self.client.connector.cleanup()
209
+ self.client = None
210
+
211
+
212
+ __all__ = ("AiosonicClient",)
telegrinder/model.py CHANGED
@@ -1,61 +1,35 @@
1
- import base64
2
- import dataclasses
3
- import enum
4
1
  import keyword
5
- import os
6
- import secrets
7
2
  import typing
8
- from datetime import datetime
9
- from types import NoneType
10
3
 
11
4
  import msgspec
12
5
  from fntypes.co import Nothing, Result, Some
13
6
 
14
- from telegrinder.msgspec_utils import decoder, encoder, get_origin
7
+ from telegrinder.msgspec_utils import decoder, encoder, struct_as_dict
15
8
 
16
9
  if typing.TYPE_CHECKING:
17
10
  from telegrinder.api.error import APIError
18
11
 
19
- T = typing.TypeVar("T")
20
- P = typing.ParamSpec("P")
21
-
22
- UnionType: typing.TypeAlias = typing.Annotated[tuple[T, ...], ...]
23
-
24
12
  MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
25
- "omit_defaults": True,
26
13
  "dict": True,
27
14
  "rename": {kw + "_": kw for kw in keyword.kwlist},
28
15
  }
16
+ UNSET = typing.cast(typing.Any, msgspec.UNSET)
17
+ """Docs: https://jcristharif.com/msgspec/api.html#unset
29
18
 
30
-
31
- @typing.overload
32
- def full_result(
33
- result: Result[msgspec.Raw, "APIError"],
34
- full_t: type[T],
35
- ) -> Result[T, "APIError"]: ...
36
-
37
-
38
- @typing.overload
39
- def full_result(
40
- result: Result[msgspec.Raw, "APIError"],
41
- full_t: UnionType[T],
42
- ) -> Result[T, "APIError"]: ...
19
+ During decoding, if a field isn't explicitly set in the model,
20
+ the default value of `UNSET` will be set instead. This lets downstream
21
+ consumers determine whether a field was left unset, or explicitly set a value."""
43
22
 
44
23
 
45
- def full_result(
24
+ def full_result[T](
46
25
  result: Result[msgspec.Raw, "APIError"],
47
- full_t: typing.Any,
48
- ) -> Result[typing.Any, "APIError"]:
26
+ full_t: type[T],
27
+ ) -> Result[T, "APIError"]:
49
28
  return result.map(lambda v: decoder.decode(v, type=full_t))
50
29
 
51
30
 
52
- def generate_random_id(length_bytes: int) -> str:
53
- if length_bytes < 1 or length_bytes > 64:
54
- raise ValueError("Length of bytes must be between 1 and 64.")
55
-
56
- random_bytes = os.urandom(length_bytes)
57
- random_id = base64.urlsafe_b64encode(random_bytes).rstrip(b"=").decode("utf-8")
58
- return random_id
31
+ def is_none(value: typing.Any, /) -> typing.TypeGuard[None | Nothing]:
32
+ return value is None or isinstance(value, Nothing)
59
33
 
60
34
 
61
35
  def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
@@ -66,7 +40,7 @@ def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
66
40
  ):
67
41
  if isinstance(v, Proxy):
68
42
  v = v.get()
69
- if k == "self" or type(v) in (NoneType, Nothing):
43
+ if k == "self" or is_none(v):
70
44
  continue
71
45
  validated_params[k] = v.unwrap() if isinstance(v, Some) else v
72
46
  return validated_params
@@ -118,12 +92,12 @@ if typing.TYPE_CHECKING:
118
92
  converter=...,
119
93
  ) -> typing.Any: ...
120
94
 
121
- class From(typing.Generic[T]):
95
+ class From[T]:
122
96
  def __new__(cls, _: T, /) -> typing.Any: ...
123
97
  else:
124
98
  from msgspec import field as _field
125
99
 
126
- From = typing.Annotated[T, ...]
100
+ type From[T] = T
127
101
 
128
102
  def field(**kwargs):
129
103
  kwargs.pop("converter", None)
@@ -132,8 +106,19 @@ else:
132
106
 
133
107
  @typing.dataclass_transform(field_specifiers=(field,))
134
108
  class Model(msgspec.Struct, **MODEL_CONFIG):
109
+ if not typing.TYPE_CHECKING:
110
+
111
+ def __post_init__(self):
112
+ for field in self.__struct_fields__:
113
+ if is_none(getattr(self, field)):
114
+ setattr(self, field, UNSET)
115
+
116
+ def __getattribute__(self, name, /):
117
+ val = super().__getattribute__(name)
118
+ return Nothing() if val is UNSET else val
119
+
135
120
  @classmethod
136
- def from_data(cls: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
121
+ def from_data[**P, T](cls: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
137
122
  return decoder.convert(msgspec.structs.asdict(cls(*args, **kwargs)), type=cls) # type: ignore
138
123
 
139
124
  @classmethod
@@ -141,8 +126,8 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
141
126
  return decoder.convert(obj, type=cls)
142
127
 
143
128
  @classmethod
144
- def from_bytes(cls, obj: bytes, /) -> typing.Self:
145
- return decoder.decode(obj, type=cls)
129
+ def from_raw(cls, raw: str | bytes, /) -> typing.Self:
130
+ return decoder.decode(raw, type=cls)
146
131
 
147
132
  def _to_dict(
148
133
  self,
@@ -152,7 +137,7 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
152
137
  ) -> dict[str, typing.Any]:
153
138
  if dct_name not in self.__dict__:
154
139
  self.__dict__[dct_name] = (
155
- msgspec.structs.asdict(self)
140
+ struct_as_dict(self)
156
141
  if not full
157
142
  else encoder.to_builtins(self.to_dict(exclude_fields=exclude_fields), order="deterministic")
158
143
  )
@@ -162,16 +147,17 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
162
147
 
163
148
  return {key: value for key, value in self.__dict__[dct_name].items() if key not in exclude_fields}
164
149
 
150
+ def to_raw(self) -> str:
151
+ return encoder.encode(self)
152
+
165
153
  def to_dict(
166
154
  self,
167
155
  *,
168
156
  exclude_fields: set[str] | None = None,
169
157
  ) -> dict[str, typing.Any]:
170
- """
171
- :param exclude_fields: Model field names to exclude from the dictionary representation of this model.
158
+ """:param exclude_fields: Model field names to exclude from the dictionary representation of this model.
172
159
  :return: A dictionary representation of this model.
173
160
  """
174
-
175
161
  return self._to_dict("model_as_dict", exclude_fields or set(), full=False)
176
162
 
177
163
  def to_full_dict(
@@ -179,104 +165,14 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
179
165
  *,
180
166
  exclude_fields: set[str] | None = None,
181
167
  ) -> dict[str, typing.Any]:
182
- """
183
- :param exclude_fields: Model field names to exclude from the dictionary representation of this model.
168
+ """:param exclude_fields: Model field names to exclude from the dictionary representation of this model.
184
169
  :return: A dictionary representation of this model including all models, structs, custom types.
185
170
  """
186
-
187
171
  return self._to_dict("model_as_full_dict", exclude_fields or set(), full=True)
188
172
 
189
173
 
190
- @dataclasses.dataclass(kw_only=True, frozen=True, slots=True, repr=False)
191
- class DataConverter:
192
- _converters: dict[type[typing.Any], typing.Callable[..., typing.Any]] = dataclasses.field(
193
- init=False,
194
- default_factory=lambda: {},
195
- )
196
- _files: dict[str, tuple[str, bytes]] = dataclasses.field(default_factory=lambda: {})
197
-
198
- def __repr__(self) -> str:
199
- return "<{}: {}>".format(
200
- self.__class__.__name__,
201
- ", ".join(f"{k}={v.__name__!r}" for k, v in self._converters.items()),
202
- )
203
-
204
- def __post_init__(self) -> None:
205
- self._converters.update(
206
- {
207
- get_origin(value.__annotations__["data"]): value
208
- for key, value in vars(self.__class__).items()
209
- if key.startswith("convert_") and callable(value)
210
- }
211
- )
212
-
213
- def __call__(self, data: typing.Any, *, serialize: bool = True) -> typing.Any:
214
- converter = self.get_converter(get_origin(type(data)))
215
- if converter is not None:
216
- if isinstance(converter, staticmethod):
217
- return converter(data, serialize)
218
- return converter(self, data, serialize)
219
- return data
220
-
221
- @property
222
- def converters(self) -> dict[type[typing.Any], typing.Callable[..., typing.Any]]:
223
- return self._converters.copy()
224
-
225
- @property
226
- def files(self) -> dict[str, tuple[str, bytes]]:
227
- return self._files.copy()
228
-
229
- @staticmethod
230
- def convert_enum(data: enum.Enum, _: bool = False) -> typing.Any:
231
- return data.value
232
-
233
- @staticmethod
234
- def convert_datetime(data: datetime, _: bool = False) -> int:
235
- return int(data.timestamp())
236
-
237
- def get_converter(self, t: type[typing.Any]):
238
- for type_, converter in self._converters.items():
239
- if issubclass(t, type_):
240
- return converter
241
- return None
242
-
243
- def convert_model(
244
- self,
245
- data: Model,
246
- serialize: bool = True,
247
- ) -> str | dict[str, typing.Any]:
248
- converted_dct = self(data.to_dict(), serialize=False)
249
- return encoder.encode(converted_dct) if serialize is True else converted_dct
250
-
251
- def convert_dct(
252
- self,
253
- data: dict[str, typing.Any],
254
- serialize: bool = True,
255
- ) -> dict[str, typing.Any]:
256
- return {
257
- k: self(v, serialize=serialize) for k, v in data.items() if type(v) not in (NoneType, Nothing)
258
- }
259
-
260
- def convert_lst(
261
- self,
262
- data: list[typing.Any],
263
- serialize: bool = True,
264
- ) -> str | list[typing.Any]:
265
- converted_lst = [self(x, serialize=False) for x in data]
266
- return encoder.encode(converted_lst) if serialize is True else converted_lst
267
-
268
- def convert_tpl(self, data: tuple[typing.Any, ...], _: bool = False) -> str | tuple[typing.Any, ...]:
269
- match data:
270
- case (str(filename), bytes(content)):
271
- attach_name = secrets.token_urlsafe(16)
272
- self._files[attach_name] = (filename, content)
273
- return "attach://{}".format(attach_name)
274
-
275
- return data
276
-
277
-
278
- class Proxy:
279
- def __init__(self, cfg: "_ProxiedDict", key: str) -> None:
174
+ class Proxy[T]:
175
+ def __init__(self, cfg: "_ProxiedDict[T]", key: str) -> None:
280
176
  self.key = key
281
177
  self.cfg = cfg
282
178
 
@@ -284,7 +180,7 @@ class Proxy:
284
180
  return self.cfg._defaults.get(self.key)
285
181
 
286
182
 
287
- class _ProxiedDict(typing.Generic[T]):
183
+ class _ProxiedDict[T]:
288
184
  def __init__(self, tp: type[T]) -> None:
289
185
  self.type = tp
290
186
  self._defaults = {}
@@ -301,20 +197,17 @@ class _ProxiedDict(typing.Generic[T]):
301
197
 
302
198
  if typing.TYPE_CHECKING:
303
199
 
304
- def ProxiedDict(typed_dct: type[T]) -> T | _ProxiedDict[T]: # noqa: N802
200
+ def ProxiedDict[T](typed_dct: type[T]) -> T | _ProxiedDict[T]: # noqa: N802
305
201
  ...
306
-
307
202
  else:
308
203
  ProxiedDict = _ProxiedDict
309
204
 
310
205
 
311
206
  __all__ = (
312
- "DataConverter",
313
207
  "MODEL_CONFIG",
314
208
  "Model",
315
209
  "ProxiedDict",
316
210
  "Proxy",
317
211
  "full_result",
318
- "generate_random_id",
319
212
  "get_params",
320
213
  )
telegrinder/modules.py CHANGED
@@ -3,6 +3,8 @@ import typing
3
3
 
4
4
  from choicelib import choice_in_order
5
5
 
6
+ import telegrinder.msgspec_json as json
7
+
6
8
 
7
9
  @typing.runtime_checkable
8
10
  class LoggerModule(typing.Protocol):
@@ -234,4 +236,4 @@ def _set_logger_level(level, /):
234
236
  setattr(logger, "set_level", staticmethod(_set_logger_level)) # type: ignore
235
237
 
236
238
 
237
- __all__ = ("LoggerModule", "logger")
239
+ __all__ = ("LoggerModule", "json", "logger")