telegrinder 0.3.4.post1__py3-none-any.whl → 0.4.0__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/__init__.py +30 -31
- telegrinder/api/__init__.py +2 -1
- telegrinder/api/api.py +28 -20
- telegrinder/api/error.py +8 -4
- telegrinder/api/response.py +2 -2
- telegrinder/api/token.py +2 -2
- telegrinder/bot/__init__.py +6 -0
- telegrinder/bot/bot.py +38 -31
- telegrinder/bot/cute_types/__init__.py +2 -0
- telegrinder/bot/cute_types/base.py +54 -128
- telegrinder/bot/cute_types/callback_query.py +76 -61
- telegrinder/bot/cute_types/chat_join_request.py +4 -3
- telegrinder/bot/cute_types/chat_member_updated.py +28 -31
- telegrinder/bot/cute_types/inline_query.py +5 -4
- telegrinder/bot/cute_types/message.py +555 -602
- telegrinder/bot/cute_types/pre_checkout_query.py +42 -0
- telegrinder/bot/cute_types/update.py +20 -12
- telegrinder/bot/cute_types/utils.py +3 -36
- telegrinder/bot/dispatch/__init__.py +4 -0
- telegrinder/bot/dispatch/abc.py +8 -9
- telegrinder/bot/dispatch/context.py +5 -7
- telegrinder/bot/dispatch/dispatch.py +85 -33
- telegrinder/bot/dispatch/handler/abc.py +5 -6
- telegrinder/bot/dispatch/handler/audio_reply.py +2 -2
- telegrinder/bot/dispatch/handler/base.py +3 -3
- telegrinder/bot/dispatch/handler/document_reply.py +2 -2
- telegrinder/bot/dispatch/handler/func.py +36 -42
- telegrinder/bot/dispatch/handler/media_group_reply.py +5 -4
- telegrinder/bot/dispatch/handler/message_reply.py +2 -2
- telegrinder/bot/dispatch/handler/photo_reply.py +2 -2
- telegrinder/bot/dispatch/handler/sticker_reply.py +2 -2
- telegrinder/bot/dispatch/handler/video_reply.py +2 -2
- telegrinder/bot/dispatch/middleware/abc.py +83 -8
- telegrinder/bot/dispatch/middleware/global_middleware.py +70 -0
- telegrinder/bot/dispatch/process.py +44 -50
- telegrinder/bot/dispatch/return_manager/__init__.py +2 -0
- telegrinder/bot/dispatch/return_manager/abc.py +6 -10
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +20 -0
- telegrinder/bot/dispatch/view/__init__.py +2 -0
- telegrinder/bot/dispatch/view/abc.py +10 -6
- telegrinder/bot/dispatch/view/base.py +81 -50
- telegrinder/bot/dispatch/view/box.py +20 -9
- telegrinder/bot/dispatch/view/callback_query.py +3 -4
- telegrinder/bot/dispatch/view/chat_join_request.py +2 -7
- telegrinder/bot/dispatch/view/chat_member.py +3 -5
- telegrinder/bot/dispatch/view/inline_query.py +3 -4
- telegrinder/bot/dispatch/view/message.py +3 -4
- telegrinder/bot/dispatch/view/pre_checkout_query.py +16 -0
- telegrinder/bot/dispatch/view/raw.py +42 -40
- telegrinder/bot/dispatch/waiter_machine/actions.py +5 -4
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +9 -7
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +0 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/state.py +3 -2
- telegrinder/bot/dispatch/waiter_machine/machine.py +113 -34
- telegrinder/bot/dispatch/waiter_machine/middleware.py +15 -10
- telegrinder/bot/dispatch/waiter_machine/short_state.py +7 -18
- telegrinder/bot/polling/polling.py +62 -54
- telegrinder/bot/rules/__init__.py +24 -1
- telegrinder/bot/rules/abc.py +17 -10
- telegrinder/bot/rules/callback_data.py +20 -61
- telegrinder/bot/rules/chat_join.py +6 -4
- telegrinder/bot/rules/command.py +4 -4
- telegrinder/bot/rules/enum_text.py +1 -4
- telegrinder/bot/rules/func.py +5 -3
- telegrinder/bot/rules/fuzzy.py +1 -1
- telegrinder/bot/rules/id.py +24 -0
- telegrinder/bot/rules/inline.py +6 -4
- telegrinder/bot/rules/integer.py +2 -1
- telegrinder/bot/rules/logic.py +18 -0
- telegrinder/bot/rules/markup.py +5 -6
- telegrinder/bot/rules/message.py +2 -4
- telegrinder/bot/rules/message_entities.py +1 -3
- telegrinder/bot/rules/node.py +15 -9
- telegrinder/bot/rules/payload.py +81 -0
- telegrinder/bot/rules/payment_invoice.py +29 -0
- telegrinder/bot/rules/regex.py +5 -6
- telegrinder/bot/rules/state.py +1 -3
- telegrinder/bot/rules/text.py +10 -5
- telegrinder/bot/rules/update.py +0 -0
- telegrinder/bot/scenario/abc.py +2 -4
- telegrinder/bot/scenario/checkbox.py +12 -14
- telegrinder/bot/scenario/choice.py +6 -9
- telegrinder/client/__init__.py +9 -1
- telegrinder/client/abc.py +35 -10
- telegrinder/client/aiohttp.py +28 -24
- telegrinder/client/form_data.py +31 -0
- telegrinder/client/sonic.py +212 -0
- telegrinder/model.py +38 -145
- telegrinder/modules.py +3 -1
- telegrinder/msgspec_utils.py +136 -68
- telegrinder/node/__init__.py +74 -13
- telegrinder/node/attachment.py +92 -16
- telegrinder/node/base.py +196 -68
- telegrinder/node/callback_query.py +17 -16
- telegrinder/node/command.py +3 -2
- telegrinder/node/composer.py +40 -75
- telegrinder/node/container.py +13 -7
- telegrinder/node/either.py +82 -0
- telegrinder/node/event.py +20 -31
- telegrinder/node/file.py +51 -0
- telegrinder/node/me.py +4 -5
- telegrinder/node/payload.py +78 -0
- telegrinder/node/polymorphic.py +27 -8
- telegrinder/node/rule.py +2 -6
- telegrinder/node/scope.py +4 -6
- telegrinder/node/source.py +37 -21
- telegrinder/node/text.py +20 -8
- telegrinder/node/tools/generator.py +7 -11
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +0 -61
- telegrinder/tools/__init__.py +97 -38
- telegrinder/tools/adapter/__init__.py +19 -0
- telegrinder/tools/adapter/abc.py +49 -0
- telegrinder/tools/adapter/dataclass.py +56 -0
- telegrinder/{bot/rules → tools}/adapter/event.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/node.py +8 -10
- telegrinder/{bot/rules → tools}/adapter/raw_event.py +2 -2
- telegrinder/{bot/rules → tools}/adapter/raw_update.py +2 -2
- telegrinder/tools/buttons.py +52 -26
- telegrinder/tools/callback_data_serilization/__init__.py +5 -0
- telegrinder/tools/callback_data_serilization/abc.py +51 -0
- telegrinder/tools/callback_data_serilization/json_ser.py +60 -0
- telegrinder/tools/callback_data_serilization/msgpack_ser.py +172 -0
- telegrinder/tools/error_handler/abc.py +4 -7
- telegrinder/tools/error_handler/error.py +0 -0
- telegrinder/tools/error_handler/error_handler.py +34 -48
- telegrinder/tools/formatting/__init__.py +57 -37
- telegrinder/tools/formatting/deep_links.py +541 -0
- telegrinder/tools/formatting/{html.py → html_formatter.py} +51 -79
- telegrinder/tools/formatting/spec_html_formats.py +14 -60
- telegrinder/tools/functional.py +1 -5
- telegrinder/tools/global_context/global_context.py +26 -51
- telegrinder/tools/global_context/telegrinder_ctx.py +3 -3
- telegrinder/tools/i18n/abc.py +0 -0
- telegrinder/tools/i18n/middleware/abc.py +3 -6
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard.py +9 -9
- telegrinder/tools/lifespan.py +105 -0
- telegrinder/tools/limited_dict.py +5 -10
- telegrinder/tools/loop_wrapper/abc.py +7 -2
- telegrinder/tools/loop_wrapper/loop_wrapper.py +40 -95
- telegrinder/tools/magic.py +184 -34
- telegrinder/tools/state_storage/__init__.py +0 -0
- telegrinder/tools/state_storage/abc.py +5 -9
- telegrinder/tools/state_storage/memory.py +1 -1
- telegrinder/tools/strings.py +13 -0
- telegrinder/types/__init__.py +8 -0
- telegrinder/types/enums.py +31 -21
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +531 -109
- telegrinder/types/objects.py +934 -826
- telegrinder/verification_utils.py +0 -2
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/LICENSE +2 -2
- telegrinder-0.4.0.dist-info/METADATA +144 -0
- telegrinder-0.4.0.dist-info/RECORD +182 -0
- {telegrinder-0.3.4.post1.dist-info → telegrinder-0.4.0.dist-info}/WHEEL +1 -1
- telegrinder/bot/rules/adapter/__init__.py +0 -17
- telegrinder/bot/rules/adapter/abc.py +0 -31
- telegrinder/node/message.py +0 -14
- telegrinder/node/update.py +0 -15
- telegrinder/tools/formatting/links.py +0 -38
- telegrinder/tools/kb_set/__init__.py +0 -4
- telegrinder/tools/kb_set/base.py +0 -15
- telegrinder/tools/kb_set/yaml.py +0 -63
- telegrinder-0.3.4.post1.dist-info/METADATA +0 -110
- telegrinder-0.3.4.post1.dist-info/RECORD +0 -165
- /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,
|
|
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
|
-
|
|
32
|
-
|
|
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:
|
|
48
|
-
) -> Result[
|
|
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
|
|
53
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
145
|
-
return decoder.decode(
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
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")
|