telegrinder 0.4.2__py3-none-any.whl → 0.5.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 (233) hide show
  1. telegrinder/__init__.py +37 -55
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +6 -4
  4. telegrinder/api/api.py +100 -26
  5. telegrinder/api/error.py +42 -8
  6. telegrinder/api/response.py +4 -1
  7. telegrinder/api/token.py +2 -2
  8. telegrinder/bot/__init__.py +9 -25
  9. telegrinder/bot/bot.py +31 -25
  10. telegrinder/bot/cute_types/__init__.py +0 -0
  11. telegrinder/bot/cute_types/base.py +103 -61
  12. telegrinder/bot/cute_types/callback_query.py +447 -400
  13. telegrinder/bot/cute_types/chat_join_request.py +59 -62
  14. telegrinder/bot/cute_types/chat_member_updated.py +154 -157
  15. telegrinder/bot/cute_types/inline_query.py +41 -44
  16. telegrinder/bot/cute_types/message.py +98 -67
  17. telegrinder/bot/cute_types/pre_checkout_query.py +38 -42
  18. telegrinder/bot/cute_types/update.py +1 -8
  19. telegrinder/bot/cute_types/utils.py +1 -1
  20. telegrinder/bot/dispatch/__init__.py +10 -15
  21. telegrinder/bot/dispatch/abc.py +12 -11
  22. telegrinder/bot/dispatch/action.py +104 -0
  23. telegrinder/bot/dispatch/context.py +32 -26
  24. telegrinder/bot/dispatch/dispatch.py +61 -134
  25. telegrinder/bot/dispatch/handler/__init__.py +2 -0
  26. telegrinder/bot/dispatch/handler/abc.py +10 -8
  27. telegrinder/bot/dispatch/handler/audio_reply.py +2 -3
  28. telegrinder/bot/dispatch/handler/base.py +10 -33
  29. telegrinder/bot/dispatch/handler/document_reply.py +2 -3
  30. telegrinder/bot/dispatch/handler/func.py +55 -87
  31. telegrinder/bot/dispatch/handler/media_group_reply.py +2 -3
  32. telegrinder/bot/dispatch/handler/message_reply.py +2 -3
  33. telegrinder/bot/dispatch/handler/photo_reply.py +2 -3
  34. telegrinder/bot/dispatch/handler/sticker_reply.py +2 -3
  35. telegrinder/bot/dispatch/handler/video_reply.py +2 -3
  36. telegrinder/bot/dispatch/middleware/__init__.py +0 -0
  37. telegrinder/bot/dispatch/middleware/abc.py +79 -55
  38. telegrinder/bot/dispatch/middleware/global_middleware.py +18 -33
  39. telegrinder/bot/dispatch/process.py +84 -105
  40. telegrinder/bot/dispatch/return_manager/__init__.py +0 -0
  41. telegrinder/bot/dispatch/return_manager/abc.py +102 -65
  42. telegrinder/bot/dispatch/return_manager/callback_query.py +4 -5
  43. telegrinder/bot/dispatch/return_manager/inline_query.py +3 -4
  44. telegrinder/bot/dispatch/return_manager/message.py +8 -10
  45. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +4 -5
  46. telegrinder/bot/dispatch/view/__init__.py +4 -4
  47. telegrinder/bot/dispatch/view/abc.py +6 -16
  48. telegrinder/bot/dispatch/view/base.py +54 -178
  49. telegrinder/bot/dispatch/view/box.py +19 -18
  50. telegrinder/bot/dispatch/view/callback_query.py +4 -8
  51. telegrinder/bot/dispatch/view/chat_join_request.py +5 -6
  52. telegrinder/bot/dispatch/view/chat_member.py +5 -25
  53. telegrinder/bot/dispatch/view/error.py +9 -0
  54. telegrinder/bot/dispatch/view/inline_query.py +4 -8
  55. telegrinder/bot/dispatch/view/message.py +5 -25
  56. telegrinder/bot/dispatch/view/pre_checkout_query.py +4 -8
  57. telegrinder/bot/dispatch/view/raw.py +3 -109
  58. telegrinder/bot/dispatch/waiter_machine/__init__.py +2 -5
  59. telegrinder/bot/dispatch/waiter_machine/actions.py +6 -4
  60. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +1 -3
  61. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +1 -1
  62. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +11 -7
  63. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
  64. telegrinder/bot/dispatch/waiter_machine/machine.py +43 -60
  65. telegrinder/bot/dispatch/waiter_machine/middleware.py +19 -23
  66. telegrinder/bot/dispatch/waiter_machine/short_state.py +6 -5
  67. telegrinder/bot/polling/__init__.py +0 -0
  68. telegrinder/bot/polling/abc.py +0 -0
  69. telegrinder/bot/polling/polling.py +209 -88
  70. telegrinder/bot/rules/__init__.py +3 -16
  71. telegrinder/bot/rules/abc.py +42 -122
  72. telegrinder/bot/rules/callback_data.py +29 -49
  73. telegrinder/bot/rules/chat_join.py +5 -23
  74. telegrinder/bot/rules/command.py +8 -4
  75. telegrinder/bot/rules/enum_text.py +3 -4
  76. telegrinder/bot/rules/func.py +7 -14
  77. telegrinder/bot/rules/fuzzy.py +3 -4
  78. telegrinder/bot/rules/inline.py +8 -20
  79. telegrinder/bot/rules/integer.py +2 -3
  80. telegrinder/bot/rules/is_from.py +12 -11
  81. telegrinder/bot/rules/logic.py +11 -5
  82. telegrinder/bot/rules/markup.py +22 -14
  83. telegrinder/bot/rules/mention.py +8 -7
  84. telegrinder/bot/rules/message_entities.py +8 -4
  85. telegrinder/bot/rules/node.py +23 -12
  86. telegrinder/bot/rules/payload.py +5 -4
  87. telegrinder/bot/rules/payment_invoice.py +6 -21
  88. telegrinder/bot/rules/regex.py +2 -4
  89. telegrinder/bot/rules/rule_enum.py +8 -7
  90. telegrinder/bot/rules/start.py +5 -6
  91. telegrinder/bot/rules/state.py +1 -1
  92. telegrinder/bot/rules/text.py +4 -15
  93. telegrinder/bot/rules/update.py +3 -4
  94. telegrinder/bot/scenario/__init__.py +0 -0
  95. telegrinder/bot/scenario/abc.py +6 -5
  96. telegrinder/bot/scenario/checkbox.py +1 -1
  97. telegrinder/bot/scenario/choice.py +30 -39
  98. telegrinder/client/__init__.py +3 -5
  99. telegrinder/client/abc.py +11 -6
  100. telegrinder/client/aiohttp.py +141 -27
  101. telegrinder/client/form_data.py +1 -1
  102. telegrinder/model.py +61 -89
  103. telegrinder/modules.py +325 -102
  104. telegrinder/msgspec_utils/__init__.py +40 -0
  105. telegrinder/msgspec_utils/abc.py +18 -0
  106. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  107. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  108. telegrinder/msgspec_utils/custom_types/enum_meta.py +43 -0
  109. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  110. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  111. telegrinder/msgspec_utils/decoder.py +389 -0
  112. telegrinder/msgspec_utils/encoder.py +206 -0
  113. telegrinder/{msgspec_json.py → msgspec_utils/json.py} +6 -5
  114. telegrinder/msgspec_utils/tools.py +75 -0
  115. telegrinder/node/__init__.py +24 -7
  116. telegrinder/node/attachment.py +1 -0
  117. telegrinder/node/base.py +154 -72
  118. telegrinder/node/callback_query.py +5 -5
  119. telegrinder/node/collection.py +39 -0
  120. telegrinder/node/command.py +1 -2
  121. telegrinder/node/composer.py +121 -72
  122. telegrinder/node/container.py +11 -8
  123. telegrinder/node/context.py +48 -0
  124. telegrinder/node/either.py +27 -40
  125. telegrinder/node/error.py +41 -0
  126. telegrinder/node/event.py +37 -11
  127. telegrinder/node/exceptions.py +7 -0
  128. telegrinder/node/file.py +0 -0
  129. telegrinder/node/i18n.py +108 -0
  130. telegrinder/node/me.py +3 -2
  131. telegrinder/node/payload.py +1 -1
  132. telegrinder/node/polymorphic.py +63 -28
  133. telegrinder/node/reply_message.py +12 -0
  134. telegrinder/node/rule.py +6 -13
  135. telegrinder/node/scope.py +14 -5
  136. telegrinder/node/session.py +53 -0
  137. telegrinder/node/source.py +41 -9
  138. telegrinder/node/text.py +1 -2
  139. telegrinder/node/tools/__init__.py +0 -0
  140. telegrinder/node/tools/generator.py +3 -5
  141. telegrinder/node/utility.py +16 -0
  142. telegrinder/py.typed +0 -0
  143. telegrinder/rules.py +0 -0
  144. telegrinder/tools/__init__.py +48 -88
  145. telegrinder/tools/aio.py +103 -0
  146. telegrinder/tools/callback_data_serialization/__init__.py +5 -0
  147. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/abc.py +0 -0
  148. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/json_ser.py +2 -3
  149. telegrinder/tools/{callback_data_serilization → callback_data_serialization}/msgpack_ser.py +45 -27
  150. telegrinder/tools/final.py +21 -0
  151. telegrinder/tools/formatting/__init__.py +2 -18
  152. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  153. telegrinder/tools/formatting/{deep_links.py → deep_links/links.py} +12 -85
  154. telegrinder/tools/formatting/deep_links/parsing.py +90 -0
  155. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  156. telegrinder/tools/formatting/html_formatter.py +18 -45
  157. telegrinder/tools/fullname.py +83 -0
  158. telegrinder/tools/global_context/__init__.py +4 -3
  159. telegrinder/tools/global_context/abc.py +17 -14
  160. telegrinder/tools/global_context/builtin_context.py +39 -0
  161. telegrinder/tools/global_context/global_context.py +138 -39
  162. telegrinder/tools/input_file_directory.py +0 -0
  163. telegrinder/tools/keyboard/__init__.py +39 -0
  164. telegrinder/tools/keyboard/abc.py +159 -0
  165. telegrinder/tools/keyboard/base.py +77 -0
  166. telegrinder/tools/keyboard/buttons/__init__.py +14 -0
  167. telegrinder/tools/keyboard/buttons/base.py +18 -0
  168. telegrinder/tools/{buttons.py → keyboard/buttons/buttons.py} +71 -23
  169. telegrinder/tools/keyboard/buttons/static_buttons.py +56 -0
  170. telegrinder/tools/keyboard/buttons/tools.py +18 -0
  171. telegrinder/tools/keyboard/data.py +20 -0
  172. telegrinder/tools/keyboard/keyboard.py +131 -0
  173. telegrinder/tools/keyboard/static_keyboard.py +83 -0
  174. telegrinder/tools/lifespan.py +87 -51
  175. telegrinder/tools/limited_dict.py +4 -1
  176. telegrinder/tools/loop_wrapper.py +332 -0
  177. telegrinder/tools/magic/__init__.py +32 -0
  178. telegrinder/tools/magic/annotations.py +165 -0
  179. telegrinder/tools/magic/dictionary.py +20 -0
  180. telegrinder/tools/magic/function.py +246 -0
  181. telegrinder/tools/magic/shortcut.py +111 -0
  182. telegrinder/tools/parse_mode.py +9 -3
  183. telegrinder/tools/singleton/__init__.py +4 -0
  184. telegrinder/tools/singleton/abc.py +14 -0
  185. telegrinder/tools/singleton/singleton.py +18 -0
  186. telegrinder/tools/state_storage/__init__.py +0 -0
  187. telegrinder/tools/state_storage/abc.py +6 -1
  188. telegrinder/tools/state_storage/memory.py +1 -1
  189. telegrinder/tools/strings.py +0 -0
  190. telegrinder/types/__init__.py +307 -268
  191. telegrinder/types/enums.py +68 -37
  192. telegrinder/types/input_file.py +3 -3
  193. telegrinder/types/methods.py +5699 -5055
  194. telegrinder/types/methods_utils.py +62 -0
  195. telegrinder/types/objects.py +1782 -994
  196. telegrinder/verification_utils.py +3 -1
  197. telegrinder-0.5.1.dist-info/METADATA +162 -0
  198. telegrinder-0.5.1.dist-info/RECORD +200 -0
  199. {telegrinder-0.4.2.dist-info → telegrinder-0.5.1.dist-info}/licenses/LICENSE +2 -2
  200. telegrinder/bot/dispatch/waiter_machine/hasher/state.py +0 -20
  201. telegrinder/bot/rules/id.py +0 -24
  202. telegrinder/bot/rules/message.py +0 -15
  203. telegrinder/client/sonic.py +0 -212
  204. telegrinder/msgspec_utils.py +0 -478
  205. telegrinder/tools/adapter/__init__.py +0 -19
  206. telegrinder/tools/adapter/abc.py +0 -49
  207. telegrinder/tools/adapter/dataclass.py +0 -56
  208. telegrinder/tools/adapter/errors.py +0 -5
  209. telegrinder/tools/adapter/event.py +0 -61
  210. telegrinder/tools/adapter/node.py +0 -46
  211. telegrinder/tools/adapter/raw_event.py +0 -27
  212. telegrinder/tools/adapter/raw_update.py +0 -30
  213. telegrinder/tools/callback_data_serilization/__init__.py +0 -5
  214. telegrinder/tools/error_handler/__init__.py +0 -10
  215. telegrinder/tools/error_handler/abc.py +0 -30
  216. telegrinder/tools/error_handler/error.py +0 -9
  217. telegrinder/tools/error_handler/error_handler.py +0 -179
  218. telegrinder/tools/formatting/spec_html_formats.py +0 -75
  219. telegrinder/tools/functional.py +0 -8
  220. telegrinder/tools/global_context/telegrinder_ctx.py +0 -27
  221. telegrinder/tools/i18n/__init__.py +0 -12
  222. telegrinder/tools/i18n/abc.py +0 -32
  223. telegrinder/tools/i18n/middleware/__init__.py +0 -3
  224. telegrinder/tools/i18n/middleware/abc.py +0 -22
  225. telegrinder/tools/i18n/simple.py +0 -43
  226. telegrinder/tools/keyboard.py +0 -132
  227. telegrinder/tools/loop_wrapper/__init__.py +0 -4
  228. telegrinder/tools/loop_wrapper/abc.py +0 -20
  229. telegrinder/tools/loop_wrapper/loop_wrapper.py +0 -169
  230. telegrinder/tools/magic.py +0 -344
  231. telegrinder-0.4.2.dist-info/METADATA +0 -151
  232. telegrinder-0.4.2.dist-info/RECORD +0 -182
  233. {telegrinder-0.4.2.dist-info → telegrinder-0.5.1.dist-info}/WHEEL +0 -0
telegrinder/client/abc.py CHANGED
@@ -5,9 +5,10 @@ from abc import ABC, abstractmethod
5
5
  from telegrinder.client.form_data import MultipartFormProto, encode_form_data
6
6
 
7
7
  type Data = dict[str, typing.Any] | MultipartFormProto
8
+ type Files = dict[str, tuple[str, typing.Any]]
8
9
 
9
10
 
10
- class ABCClient[MultipartForm: MultipartFormProto](ABC):
11
+ class ABCClient(ABC):
11
12
  CONNECTION_TIMEOUT_ERRORS: tuple[type[BaseException], ...] = ()
12
13
  CLIENT_CONNECTION_ERRORS: tuple[type[BaseException], ...] = ()
13
14
 
@@ -15,6 +16,11 @@ class ABCClient[MultipartForm: MultipartFormProto](ABC):
15
16
  def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
16
17
  pass
17
18
 
19
+ @property
20
+ @abstractmethod
21
+ def timeout(self) -> float:
22
+ pass
23
+
18
24
  @abstractmethod
19
25
  async def request_text(
20
26
  self,
@@ -61,7 +67,7 @@ class ABCClient[MultipartForm: MultipartFormProto](ABC):
61
67
 
62
68
  @classmethod
63
69
  @abstractmethod
64
- def multipart_form_factory(cls) -> MultipartForm:
70
+ def multipart_form_factory(cls) -> MultipartFormProto:
65
71
  pass
66
72
 
67
73
  @classmethod
@@ -69,8 +75,8 @@ class ABCClient[MultipartForm: MultipartFormProto](ABC):
69
75
  cls,
70
76
  *,
71
77
  data: dict[str, typing.Any],
72
- files: dict[str, tuple[str, typing.Any]] | None = None,
73
- ) -> MultipartForm:
78
+ files: Files | None = None,
79
+ ) -> MultipartFormProto:
74
80
  multipart_form = cls.multipart_form_factory()
75
81
  files = files or {}
76
82
 
@@ -92,9 +98,8 @@ class ABCClient[MultipartForm: MultipartFormProto](ABC):
92
98
  exc_type: type[BaseException],
93
99
  exc_val: typing.Any,
94
100
  exc_tb: typing.Any,
95
- ) -> bool:
101
+ ) -> None:
96
102
  await self.close()
97
- return not bool(exc_val)
98
103
 
99
104
 
100
105
  __all__ = ("ABCClient",)
@@ -1,70 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
1
4
  import ssl
2
5
  import typing
3
6
 
4
7
  import aiohttp
5
8
  import certifi
6
- from aiohttp import ClientSession, TCPConnector
9
+ from aiohttp import BasicAuth, ClientSession, TCPConnector
10
+ from aiohttp.hdrs import USER_AGENT
11
+ from aiohttp.http import SERVER_SOFTWARE
7
12
 
8
- import telegrinder.msgspec_json as json
13
+ from telegrinder.__meta__ import __version__
9
14
  from telegrinder.client.abc import ABCClient
15
+ from telegrinder.msgspec_utils import json
10
16
 
11
17
  if typing.TYPE_CHECKING:
12
18
  from aiohttp import ClientResponse
13
19
 
14
20
  type Data = dict[str, typing.Any] | aiohttp.formdata.FormData
15
21
  type Response = ClientResponse
22
+ type Timeout = int | float | aiohttp.ClientTimeout
23
+ type Proxy = str | tuple[str, BasicAuth]
24
+ type ProxyChain = typing.Iterable[Proxy]
25
+ type ProxyType = Proxy | ProxyChain
26
+
27
+ DEFAULT_TIMEOUT: typing.Final[float] = 30.0
28
+ DEFAULT_LIMIT_SIMULTANEOUS_CONNECTIONS: typing.Final[int] = 100
29
+ DEFAULT_TTL_DNS_CACHE: typing.Final[int] = 3600
30
+ DEFAULT_HEADERS: typing.Final[dict[str, str]] = {USER_AGENT: f"{SERVER_SOFTWARE} telegrinder/{__version__}"}
31
+
32
+
33
+ def _get_client_timeout(timeout: Timeout, /) -> aiohttp.ClientTimeout:
34
+ return timeout if isinstance(timeout, aiohttp.ClientTimeout) else aiohttp.ClientTimeout(total=float(timeout))
35
+
36
+
37
+ def _prepare_proxy_connector(
38
+ proxy: ProxyType,
39
+ /,
40
+ ) -> tuple[type[TCPConnector], dict[str, typing.Any] | list[typing.Any]]:
41
+ try:
42
+ from aiohttp_socks import ChainProxyConnector, ProxyConnector, ProxyInfo # type: ignore
43
+ from aiohttp_socks.utils import parse_proxy_url # type: ignore
44
+ except ImportError:
45
+ raise ImportError(
46
+ "Module `aiohttp-socks` is not installed. You can install as follows: pip install aiohttp-socks "
47
+ 'or pip install "telegrinder[socks]"',
48
+ ) from None
49
+
50
+ match proxy:
51
+ case str() | (str(), BasicAuth()):
52
+ proxy_chain = (proxy,)
53
+ case _:
54
+ proxy_chain = proxy
55
+
56
+ proxy_infos = list() # type: ignore
57
+
58
+ for _proxy in proxy_chain:
59
+ url, basic = (_proxy, None) if isinstance(_proxy, str) else _proxy
60
+ proxy_type, host, port, username, password = parse_proxy_url(url) # type: ignore
61
+
62
+ if basic is not None:
63
+ username, password = basic.login, basic.password # type: ignore
64
+
65
+ proxy_infos.append(
66
+ ProxyInfo( # type: ignore
67
+ proxy_type,
68
+ host,
69
+ port,
70
+ username,
71
+ password,
72
+ rdns=True,
73
+ ),
74
+ )
75
+
76
+ return (
77
+ (
78
+ ProxyConnector, # type: ignore
79
+ proxy_infos[0]._asdict(), # type: ignore
80
+ )
81
+ if len(proxy_infos) == 1
82
+ else (ChainProxyConnector, proxy_infos)
83
+ ) # type: ignore
16
84
 
17
85
 
18
- class AiohttpClient(ABCClient[aiohttp.formdata.FormData]):
86
+ class AiohttpClient(ABCClient):
19
87
  """HTTP client based on `aiohttp` module."""
20
88
 
21
- CONNECTION_TIMEOUT_ERRORS = (
22
- aiohttp.client.ServerConnectionError,
23
- TimeoutError,
24
- )
89
+ CONNECTION_TIMEOUT_ERRORS = (aiohttp.client.ServerConnectionError,)
25
90
  CLIENT_CONNECTION_ERRORS = (
26
91
  aiohttp.client.ClientConnectionError,
27
92
  aiohttp.client.ClientConnectorError,
28
93
  aiohttp.ClientOSError,
29
94
  )
30
95
 
96
+ __slots__ = (
97
+ "session",
98
+ "limit",
99
+ "ttl_dns_cache",
100
+ "session_params",
101
+ "_proxy",
102
+ "_tcp_connector_kwargs",
103
+ "_tcp_connector_class",
104
+ )
105
+
31
106
  def __init__(
32
107
  self,
33
108
  session: ClientSession | None = None,
34
- timeout: aiohttp.ClientTimeout | None = None,
109
+ proxy: ProxyType | None = None,
110
+ timeout: Timeout = DEFAULT_TIMEOUT,
111
+ limit: int = DEFAULT_LIMIT_SIMULTANEOUS_CONNECTIONS,
112
+ ttl_dns_cache: int = DEFAULT_TTL_DNS_CACHE,
35
113
  **session_params: typing.Any,
36
114
  ) -> None:
37
115
  self.session = session
116
+ self.limit = limit
117
+ self.ttl_dns_cache = ttl_dns_cache
38
118
  self.session_params = session_params
39
- self.timeout = timeout or aiohttp.ClientTimeout(total=0)
119
+ self._timeout = _get_client_timeout(timeout)
120
+ self._proxy = proxy
121
+ self._tcp_connector_kwargs = dict[str, typing.Any](
122
+ ssl=ssl.create_default_context(cafile=certifi.where()),
123
+ limit=limit,
124
+ ttl_dns_cache=ttl_dns_cache,
125
+ )
126
+ self._tcp_connector_class: type[TCPConnector] = TCPConnector
127
+
128
+ self.session_params.setdefault("headers", DEFAULT_HEADERS)
129
+ self._setup_proxy()
40
130
 
41
131
  def __repr__(self) -> str:
42
132
  return "<{}: session={!r}, timeout={}, closed={}>".format(
43
- self.__class__.__name__,
133
+ type(self).__name__,
44
134
  self.session,
45
- self.timeout,
135
+ self._timeout,
46
136
  True if self.session is None else self.session.closed,
47
137
  )
48
138
 
139
+ def __del__(self) -> None:
140
+ if self.session and not self.session.closed:
141
+ if self.session._connector is not None and self.session._connector_owner:
142
+ self.session._connector._close()
143
+ self.session._connector = None
144
+
145
+ @classmethod
146
+ def multipart_form_factory(cls) -> aiohttp.formdata.FormData:
147
+ return aiohttp.formdata.FormData(quote_fields=False)
148
+
149
+ @property
150
+ def timeout(self) -> float:
151
+ return float(self._timeout.total or 0.0)
152
+
49
153
  async def request_raw(
50
154
  self,
51
155
  url: str,
52
156
  method: str = "GET",
53
157
  data: Data | None = None,
158
+ *,
159
+ timeout: Timeout | None = None,
54
160
  **kwargs: typing.Any,
55
161
  ) -> Response:
56
162
  if not self.session:
57
163
  self.session = ClientSession(
58
- connector=TCPConnector(ssl=ssl.create_default_context(cafile=certifi.where())),
164
+ connector=self._tcp_connector_class(**self._tcp_connector_kwargs),
59
165
  json_serialize=json.dumps,
60
166
  **self.session_params,
61
167
  )
62
168
 
63
169
  async with self.session.request(
64
- url=url,
65
170
  method=method,
171
+ url=url,
66
172
  data=data,
67
- timeout=self.timeout,
173
+ timeout=self._timeout if timeout is None else _get_client_timeout(timeout),
68
174
  **kwargs,
69
175
  ) as response:
70
176
  await response.read()
@@ -75,9 +181,11 @@ class AiohttpClient(ABCClient[aiohttp.formdata.FormData]):
75
181
  url: str,
76
182
  method: str = "GET",
77
183
  data: Data | None = None,
184
+ *,
185
+ timeout: Timeout | None = None,
78
186
  **kwargs: typing.Any,
79
187
  ) -> dict[str, typing.Any]:
80
- response = await self.request_raw(url, method, data, **kwargs)
188
+ response = await self.request_raw(url, method, data, timeout=timeout, **kwargs)
81
189
  return await response.json(
82
190
  encoding="UTF-8",
83
191
  loads=json.loads,
@@ -89,9 +197,11 @@ class AiohttpClient(ABCClient[aiohttp.formdata.FormData]):
89
197
  url: str,
90
198
  method: str = "GET",
91
199
  data: Data | None = None,
200
+ *,
201
+ timeout: Timeout | None = None,
92
202
  **kwargs: typing.Any,
93
203
  ) -> str:
94
- response = await self.request_raw(url, method, data, **kwargs) # type: ignore
204
+ response = await self.request_raw(url, method, data, timeout=timeout, **kwargs)
95
205
  return await response.text(encoding="UTF-8")
96
206
 
97
207
  async def request_bytes(
@@ -99,9 +209,11 @@ class AiohttpClient(ABCClient[aiohttp.formdata.FormData]):
99
209
  url: str,
100
210
  method: str = "GET",
101
211
  data: Data | None = None,
212
+ *,
213
+ timeout: Timeout | None = None,
102
214
  **kwargs: typing.Any,
103
215
  ) -> bytes:
104
- response = await self.request_raw(url, method, data, **kwargs) # type: ignore
216
+ response = await self.request_raw(url, method, data, timeout=timeout, **kwargs)
105
217
  if response._body is None:
106
218
  await response.read()
107
219
  return response._body or bytes()
@@ -111,24 +223,26 @@ class AiohttpClient(ABCClient[aiohttp.formdata.FormData]):
111
223
  url: str,
112
224
  method: str = "GET",
113
225
  data: Data | None = None,
226
+ *,
227
+ timeout: Timeout | None = None,
114
228
  **kwargs: typing.Any,
115
229
  ) -> bytes:
116
- response = await self.request_raw(url, method, data, **kwargs)
230
+ response = await self.request_raw(url, method, data, timeout=timeout, **kwargs)
117
231
  return response._body or bytes()
118
232
 
119
- async def close(self) -> None:
233
+ async def close(self, *, gracefully: bool = True) -> None:
120
234
  if self.session and not self.session.closed:
121
235
  await self.session.close()
122
236
 
123
- @classmethod
124
- def multipart_form_factory(cls) -> aiohttp.formdata.FormData:
125
- return aiohttp.formdata.FormData(quote_fields=False)
237
+ if gracefully:
238
+ # Wait 250 ms for graceful shutdown SSL connections
239
+ await asyncio.sleep(0.250)
126
240
 
127
- def __del__(self) -> None:
128
- if self.session and not self.session.closed:
129
- if self.session._connector is not None and self.session._connector_owner:
130
- self.session._connector._close()
131
- self.session._connector = None
241
+ def _setup_proxy(self) -> None:
242
+ if self._proxy is not None:
243
+ tcp_connector_class, data = _prepare_proxy_connector(self._proxy)
244
+ self._tcp_connector_class = tcp_connector_class
245
+ self._tcp_connector_kwargs.update(data if isinstance(data, dict) else dict(proxy_infos=data))
132
246
 
133
247
 
134
248
  __all__ = ("AiohttpClient",)
@@ -10,7 +10,7 @@ def encode_form_data(
10
10
  ) -> dict[str, str]:
11
11
  context = dict(files=files)
12
12
  return {
13
- k: encoder.encode(v, context=context).removeprefix('"').removesuffix('"') # Remove quoted strings
13
+ k: encoder.encode(v, context=context).strip('"') # Remove quoted string
14
14
  if not isinstance(v, str)
15
15
  else v
16
16
  for k, v in data.items()
telegrinder/model.py CHANGED
@@ -1,53 +1,31 @@
1
1
  import keyword
2
- import typing
2
+ import types
3
+ from reprlib import recursive_repr
3
4
 
4
5
  import msgspec
5
- from fntypes.co import Nothing, Result, Some
6
+ import typing_extensions as typing
7
+ from fntypes.co import Nothing
6
8
 
7
- from telegrinder.msgspec_utils import decoder, encoder, struct_as_dict
8
-
9
- if typing.TYPE_CHECKING:
10
- from telegrinder.api.error import APIError
9
+ from telegrinder.msgspec_utils import Option, decoder, encoder, struct_asdict
11
10
 
11
+ UNSET: typing.Final[typing.Any] = typing.cast("typing.Any", msgspec.UNSET)
12
+ """See [DOCS](https://jcristharif.com/msgspec/api.html#unset) about `msgspec.UNSET`."""
12
13
  MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
13
14
  "dict": True,
14
15
  "rename": {kw + "_": kw for kw in keyword.kwlist},
15
16
  }
16
- UNSET = typing.cast(typing.Any, msgspec.UNSET)
17
- """Docs: https://jcristharif.com/msgspec/api.html#unset
18
-
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."""
22
-
23
-
24
- def full_result[T](
25
- result: Result[msgspec.Raw, "APIError"],
26
- full_t: type[T],
27
- ) -> Result[T, "APIError"]:
28
- return result.map(lambda v: decoder.decode(v, type=full_t))
29
-
30
-
31
- def is_none(value: typing.Any, /) -> typing.TypeGuard[None | Nothing]:
32
- return value is None or isinstance(value, Nothing)
17
+ INSPECTED_MODEL_FIELDS_KEY: typing.Final[str] = "__inspected_struct_fields__"
33
18
 
34
19
 
35
- def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
36
- validated_params = {}
37
- for k, v in (
38
- *params.pop("other", {}).items(),
39
- *params.items(),
40
- ):
41
- if isinstance(v, Proxy):
42
- v = v.get()
43
- if k == "self" or is_none(v):
44
- continue
45
- validated_params[k] = v.unwrap() if isinstance(v, Some) else v
46
- return validated_params
20
+ def is_none(obj: typing.Any, /) -> typing.TypeIs[Nothing | None]:
21
+ return isinstance(obj, Nothing | types.NoneType)
47
22
 
48
23
 
49
24
  if typing.TYPE_CHECKING:
50
25
 
26
+ @typing.overload
27
+ def field() -> typing.Any: ...
28
+
51
29
  @typing.overload
52
30
  def field(*, name: str | None = ...) -> typing.Any: ...
53
31
 
@@ -100,6 +78,9 @@ else:
100
78
  type From[T] = T
101
79
 
102
80
  def field(**kwargs):
81
+ if (default := kwargs.get("default")) is Ellipsis:
82
+ kwargs["default"] = UNSET
83
+
103
84
  kwargs.pop("converter", None)
104
85
  return _field(**kwargs)
105
86
 
@@ -108,14 +89,50 @@ else:
108
89
  class Model(msgspec.Struct, **MODEL_CONFIG):
109
90
  if not typing.TYPE_CHECKING:
110
91
 
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
92
  def __getattribute__(self, name, /):
117
- val = super().__getattribute__(name)
118
- return Nothing() if val is UNSET else val
93
+ class_ = type(self)
94
+ val = object.__getattribute__(self, name)
95
+
96
+ if name not in class_.__struct_fields__:
97
+ return val
98
+
99
+ if (
100
+ (field_info := class_.get_fields().get(name)) is not None
101
+ and isinstance(field_info.type, msgspec.inspect.CustomType)
102
+ and issubclass(field_info.type.cls, Option)
103
+ ):
104
+ return Nothing() if val is UNSET else val
105
+
106
+ if val is UNSET:
107
+ raise AttributeError(f"{class_.__name__!r} object has no attribute {name!r}")
108
+
109
+ return val
110
+
111
+ def __post_init__(self) -> None:
112
+ for field, value in struct_asdict(self, exclude_unset=False).items():
113
+ if is_none(value):
114
+ setattr(self, field, UNSET)
115
+
116
+ @recursive_repr()
117
+ def __repr__(self) -> str:
118
+ return "{}({})".format(
119
+ type(self).__name__,
120
+ ", ".join(
121
+ f"{f}={val!r}"
122
+ for f, val in struct_asdict(self, exclude_unset=False, unset_as_nothing=True).items()
123
+ ),
124
+ )
125
+
126
+ @classmethod
127
+ def get_fields(cls) -> types.MappingProxyType[str, msgspec.inspect.Field]:
128
+ if (model_fields := getattr(cls, INSPECTED_MODEL_FIELDS_KEY, None)) is not None:
129
+ return model_fields
130
+
131
+ model_fields = types.MappingProxyType[str, msgspec.inspect.Field](
132
+ {f.name: f for f in msgspec.inspect.type_info(cls).fields}, # type: ignore
133
+ )
134
+ setattr(cls, INSPECTED_MODEL_FIELDS_KEY, model_fields)
135
+ return model_fields
119
136
 
120
137
  @classmethod
121
138
  def from_data[**P, T](cls: typing.Callable[P, T], *args: P.args, **kwargs: P.kwargs) -> T:
@@ -137,7 +154,7 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
137
154
  ) -> dict[str, typing.Any]:
138
155
  if dct_name not in self.__dict__:
139
156
  self.__dict__[dct_name] = (
140
- struct_as_dict(self)
157
+ struct_asdict(self)
141
158
  if not full
142
159
  else encoder.to_builtins(self.to_dict(exclude_fields=exclude_fields), order="deterministic")
143
160
  )
@@ -155,9 +172,6 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
155
172
  *,
156
173
  exclude_fields: set[str] | None = None,
157
174
  ) -> dict[str, typing.Any]:
158
- """:param exclude_fields: Model field names to exclude from the dictionary representation of this model.
159
- :return: A dictionary representation of this model.
160
- """
161
175
  return self._to_dict("model_as_dict", exclude_fields or set(), full=False)
162
176
 
163
177
  def to_full_dict(
@@ -165,49 +179,7 @@ class Model(msgspec.Struct, **MODEL_CONFIG):
165
179
  *,
166
180
  exclude_fields: set[str] | None = None,
167
181
  ) -> dict[str, typing.Any]:
168
- """:param exclude_fields: Model field names to exclude from the dictionary representation of this model.
169
- :return: A dictionary representation of this model including all models, structs, custom types.
170
- """
171
182
  return self._to_dict("model_as_full_dict", exclude_fields or set(), full=True)
172
183
 
173
184
 
174
- class Proxy[T]:
175
- def __init__(self, cfg: "_ProxiedDict[T]", key: str) -> None:
176
- self.key = key
177
- self.cfg = cfg
178
-
179
- def get(self) -> typing.Any | None:
180
- return self.cfg._defaults.get(self.key)
181
-
182
-
183
- class _ProxiedDict[T]:
184
- def __init__(self, tp: type[T]) -> None:
185
- self.type = tp
186
- self._defaults = {}
187
-
188
- def __setattribute__(self, name: str, value: typing.Any, /) -> None:
189
- self._defaults[name] = value
190
-
191
- def __getitem__(self, key: str, /) -> None:
192
- return Proxy(self, key) # type: ignore
193
-
194
- def __setitem__(self, key: str, value: typing.Any, /) -> None:
195
- self._defaults[key] = value
196
-
197
-
198
- if typing.TYPE_CHECKING:
199
-
200
- def ProxiedDict[T](typed_dct: type[T]) -> T | _ProxiedDict[T]: # noqa: N802
201
- ...
202
- else:
203
- ProxiedDict = _ProxiedDict
204
-
205
-
206
- __all__ = (
207
- "MODEL_CONFIG",
208
- "Model",
209
- "ProxiedDict",
210
- "Proxy",
211
- "full_result",
212
- "get_params",
213
- )
185
+ __all__ = ("MODEL_CONFIG", "UNSET", "Model", "field", "is_none")