telegrinder 0.1.dev20__py3-none-any.whl → 0.1.dev159__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 (132) hide show
  1. telegrinder/__init__.py +129 -22
  2. telegrinder/api/__init__.py +11 -2
  3. telegrinder/api/abc.py +25 -9
  4. telegrinder/api/api.py +47 -28
  5. telegrinder/api/error.py +14 -4
  6. telegrinder/api/response.py +11 -7
  7. telegrinder/bot/__init__.py +68 -7
  8. telegrinder/bot/bot.py +30 -24
  9. telegrinder/bot/cute_types/__init__.py +11 -1
  10. telegrinder/bot/cute_types/base.py +138 -0
  11. telegrinder/bot/cute_types/callback_query.py +458 -15
  12. telegrinder/bot/cute_types/inline_query.py +30 -24
  13. telegrinder/bot/cute_types/message.py +2982 -78
  14. telegrinder/bot/cute_types/update.py +30 -0
  15. telegrinder/bot/cute_types/utils.py +794 -0
  16. telegrinder/bot/dispatch/__init__.py +56 -3
  17. telegrinder/bot/dispatch/abc.py +9 -7
  18. telegrinder/bot/dispatch/composition.py +74 -0
  19. telegrinder/bot/dispatch/context.py +71 -0
  20. telegrinder/bot/dispatch/dispatch.py +86 -49
  21. telegrinder/bot/dispatch/handler/__init__.py +3 -0
  22. telegrinder/bot/dispatch/handler/abc.py +11 -5
  23. telegrinder/bot/dispatch/handler/func.py +41 -32
  24. telegrinder/bot/dispatch/handler/message_reply.py +46 -0
  25. telegrinder/bot/dispatch/middleware/__init__.py +2 -0
  26. telegrinder/bot/dispatch/middleware/abc.py +10 -4
  27. telegrinder/bot/dispatch/process.py +53 -49
  28. telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
  29. telegrinder/bot/dispatch/return_manager/abc.py +95 -0
  30. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  31. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  32. telegrinder/bot/dispatch/return_manager/message.py +25 -0
  33. telegrinder/bot/dispatch/view/__init__.py +14 -2
  34. telegrinder/bot/dispatch/view/abc.py +128 -2
  35. telegrinder/bot/dispatch/view/box.py +38 -0
  36. telegrinder/bot/dispatch/view/callback_query.py +13 -39
  37. telegrinder/bot/dispatch/view/inline_query.py +11 -39
  38. telegrinder/bot/dispatch/view/message.py +11 -47
  39. telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
  40. telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
  41. telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
  42. telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
  43. telegrinder/bot/polling/__init__.py +2 -0
  44. telegrinder/bot/polling/abc.py +11 -4
  45. telegrinder/bot/polling/polling.py +89 -40
  46. telegrinder/bot/rules/__init__.py +91 -5
  47. telegrinder/bot/rules/abc.py +81 -63
  48. telegrinder/bot/rules/adapter/__init__.py +11 -0
  49. telegrinder/bot/rules/adapter/abc.py +21 -0
  50. telegrinder/bot/rules/adapter/errors.py +5 -0
  51. telegrinder/bot/rules/adapter/event.py +49 -0
  52. telegrinder/bot/rules/adapter/raw_update.py +24 -0
  53. telegrinder/bot/rules/callback_data.py +159 -38
  54. telegrinder/bot/rules/command.py +116 -0
  55. telegrinder/bot/rules/enum_text.py +28 -0
  56. telegrinder/bot/rules/func.py +17 -17
  57. telegrinder/bot/rules/fuzzy.py +13 -10
  58. telegrinder/bot/rules/inline.py +61 -0
  59. telegrinder/bot/rules/integer.py +12 -7
  60. telegrinder/bot/rules/is_from.py +148 -7
  61. telegrinder/bot/rules/markup.py +21 -18
  62. telegrinder/bot/rules/mention.py +17 -0
  63. telegrinder/bot/rules/message_entities.py +33 -0
  64. telegrinder/bot/rules/regex.py +27 -19
  65. telegrinder/bot/rules/rule_enum.py +74 -0
  66. telegrinder/bot/rules/start.py +25 -13
  67. telegrinder/bot/rules/text.py +23 -14
  68. telegrinder/bot/scenario/__init__.py +2 -0
  69. telegrinder/bot/scenario/abc.py +12 -5
  70. telegrinder/bot/scenario/checkbox.py +48 -30
  71. telegrinder/bot/scenario/choice.py +16 -10
  72. telegrinder/client/__init__.py +3 -1
  73. telegrinder/client/abc.py +26 -16
  74. telegrinder/client/aiohttp.py +54 -32
  75. telegrinder/model.py +119 -40
  76. telegrinder/modules.py +189 -21
  77. telegrinder/msgspec_json.py +14 -0
  78. telegrinder/msgspec_utils.py +227 -0
  79. telegrinder/node/__init__.py +31 -0
  80. telegrinder/node/attachment.py +71 -0
  81. telegrinder/node/base.py +93 -0
  82. telegrinder/node/composer.py +71 -0
  83. telegrinder/node/container.py +22 -0
  84. telegrinder/node/message.py +18 -0
  85. telegrinder/node/rule.py +56 -0
  86. telegrinder/node/source.py +31 -0
  87. telegrinder/node/text.py +13 -0
  88. telegrinder/node/tools/__init__.py +3 -0
  89. telegrinder/node/tools/generator.py +40 -0
  90. telegrinder/node/update.py +12 -0
  91. telegrinder/rules.py +1 -1
  92. telegrinder/tools/__init__.py +138 -4
  93. telegrinder/tools/buttons.py +89 -51
  94. telegrinder/tools/error_handler/__init__.py +8 -0
  95. telegrinder/tools/error_handler/abc.py +30 -0
  96. telegrinder/tools/error_handler/error_handler.py +156 -0
  97. telegrinder/tools/formatting/__init__.py +81 -3
  98. telegrinder/tools/formatting/html.py +283 -37
  99. telegrinder/tools/formatting/links.py +32 -0
  100. telegrinder/tools/formatting/spec_html_formats.py +121 -0
  101. telegrinder/tools/global_context/__init__.py +12 -0
  102. telegrinder/tools/global_context/abc.py +66 -0
  103. telegrinder/tools/global_context/global_context.py +451 -0
  104. telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
  105. telegrinder/tools/i18n/__init__.py +12 -0
  106. telegrinder/tools/i18n/base.py +31 -0
  107. telegrinder/tools/i18n/middleware/__init__.py +3 -0
  108. telegrinder/tools/i18n/middleware/base.py +26 -0
  109. telegrinder/tools/i18n/simple.py +48 -0
  110. telegrinder/tools/kb_set/__init__.py +2 -0
  111. telegrinder/tools/kb_set/base.py +3 -0
  112. telegrinder/tools/kb_set/yaml.py +28 -17
  113. telegrinder/tools/keyboard.py +84 -62
  114. telegrinder/tools/loop_wrapper/__init__.py +4 -0
  115. telegrinder/tools/loop_wrapper/abc.py +18 -0
  116. telegrinder/tools/loop_wrapper/loop_wrapper.py +132 -0
  117. telegrinder/tools/magic.py +48 -23
  118. telegrinder/tools/parse_mode.py +1 -2
  119. telegrinder/types/__init__.py +1 -0
  120. telegrinder/types/enums.py +653 -0
  121. telegrinder/types/methods.py +4107 -1279
  122. telegrinder/types/objects.py +4771 -1745
  123. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/LICENSE +2 -1
  124. telegrinder-0.1.dev159.dist-info/METADATA +109 -0
  125. telegrinder-0.1.dev159.dist-info/RECORD +126 -0
  126. {telegrinder-0.1.dev20.dist-info → telegrinder-0.1.dev159.dist-info}/WHEEL +1 -1
  127. telegrinder/bot/dispatch/waiter.py +0 -38
  128. telegrinder/result.py +0 -38
  129. telegrinder/tools/formatting/abc.py +0 -52
  130. telegrinder/tools/formatting/markdown.py +0 -57
  131. telegrinder-0.1.dev20.dist-info/METADATA +0 -22
  132. telegrinder-0.1.dev20.dist-info/RECORD +0 -71
@@ -1,3 +1,5 @@
1
1
  from .abc import ABCScenario
2
2
  from .checkbox import Checkbox
3
3
  from .choice import SingleChoice
4
+
5
+ __all__ = ("ABCScenario", "Checkbox", "SingleChoice")
@@ -1,12 +1,19 @@
1
- from abc import ABC, abstractmethod
2
1
  import typing
2
+ from abc import ABC, abstractmethod
3
+
4
+ from telegrinder.bot.cute_types.base import BaseCute
3
5
 
4
6
  if typing.TYPE_CHECKING:
5
- from telegrinder.bot.dispatch import Dispatch
6
- from telegrinder.api import API
7
+ from telegrinder.api import ABCAPI
8
+ from telegrinder.bot.dispatch.view.abc import ABCStateView
7
9
 
10
+ EventT = typing.TypeVar("EventT", bound=BaseCute)
8
11
 
9
- class ABCScenario(ABC):
12
+
13
+ class ABCScenario(ABC, typing.Generic[EventT]):
10
14
  @abstractmethod
11
- def wait(self, api: "API", dispatch: "Dispatch") -> typing.Any:
15
+ def wait(self, api: "ABCAPI", view: "ABCStateView[EventT]") -> typing.Any:
12
16
  pass
17
+
18
+
19
+ __all__ = ("ABCScenario",)
@@ -1,22 +1,25 @@
1
- from .abc import ABCScenario
2
- from dataclasses import dataclass
3
- from telegrinder.tools import InlineKeyboard, InlineButton
4
- from telegrinder.types.objects import InlineKeyboardMarkup
5
- from telegrinder.bot.cute_types import CallbackQueryCute
6
- import typing
1
+ import dataclasses
7
2
  import random
8
3
  import string
4
+ import typing
5
+
6
+ from telegrinder.bot.cute_types import CallbackQueryCute
7
+ from telegrinder.bot.dispatch.waiter_machine import WaiterMachine
8
+ from telegrinder.tools import InlineButton, InlineKeyboard
9
+ from telegrinder.tools.parse_mode import ParseMode
10
+ from telegrinder.types.objects import InlineKeyboardMarkup
11
+
12
+ from .abc import ABCScenario
9
13
 
10
14
  if typing.TYPE_CHECKING:
11
- from telegrinder.bot.dispatch import Dispatch
12
15
  from telegrinder.api import API
16
+ from telegrinder.bot.dispatch.view.abc import BaseStateView
13
17
 
14
18
 
15
- @dataclass
19
+ @dataclasses.dataclass
16
20
  class Choice:
17
21
  name: str
18
22
  is_picked: bool
19
-
20
23
  default_text: str
21
24
  picked_text: str
22
25
  code: str
@@ -26,13 +29,14 @@ def random_code(length: int) -> str:
26
29
  return "".join(random.choices(string.ascii_letters + string.digits, k=length))
27
30
 
28
31
 
29
- class Checkbox(ABCScenario):
30
- INVALID_CODE: str = "Invalid code"
31
- CALLBACK_ANSWER: str = "Done"
32
- PARSE_MODE: str = "MarkdownV2"
32
+ class Checkbox(ABCScenario[CallbackQueryCute]):
33
+ INVALID_CODE: typing.ClassVar[str] = "Invalid code"
34
+ CALLBACK_ANSWER: typing.ClassVar[str] = "Done"
35
+ PARSE_MODE: typing.ClassVar[str] = ParseMode.MARKDOWNV2
33
36
 
34
37
  def __init__(
35
38
  self,
39
+ waiter_machine: WaiterMachine,
36
40
  chat_id: int,
37
41
  msg: str,
38
42
  ready_text: str = "Ready",
@@ -40,13 +44,14 @@ class Checkbox(ABCScenario):
40
44
  ):
41
45
  self.chat_id = chat_id
42
46
  self.msg = msg
43
- self.choices: typing.List[Choice] = []
47
+ self.choices: list[Choice] = []
44
48
  self.ready = ready_text
45
49
  self.max_in_row = max_in_row
46
50
  self.random_code = random_code(16)
51
+ self.waiter_machine = waiter_machine
47
52
 
48
53
  def get_markup(self) -> InlineKeyboardMarkup:
49
- kb = InlineKeyboard(resize_keyboard=True)
54
+ kb = InlineKeyboard()
50
55
  choices = self.choices.copy()
51
56
  while choices:
52
57
  while len(kb.keyboard[-1]) < self.max_in_row and choices:
@@ -60,20 +65,24 @@ class Checkbox(ABCScenario):
60
65
  )
61
66
  )
62
67
  kb.row()
68
+
63
69
  kb.add(InlineButton(self.ready, callback_data=self.random_code + "/ready"))
64
70
  return kb.get_markup()
65
71
 
66
72
  def add_option(
67
- self, name: str, default_text: str, picked_text: str, is_picked: bool = False
68
- ) -> "Checkbox":
73
+ self,
74
+ name: str,
75
+ default_text: str,
76
+ picked_text: str,
77
+ is_picked: bool = False,
78
+ ) -> typing.Self:
69
79
  self.choices.append(
70
- Choice(name, is_picked, default_text, picked_text, random_code(16))
80
+ Choice(name, is_picked, default_text, picked_text, random_code(16)),
71
81
  )
72
82
  return self
73
83
 
74
84
  async def handle(self, cb: CallbackQueryCute) -> bool:
75
- code = cb.data.replace(self.random_code + "/", "", 1)
76
-
85
+ code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
77
86
  if code == "ready":
78
87
  return False
79
88
 
@@ -81,9 +90,7 @@ class Checkbox(ABCScenario):
81
90
  if choice.code == code:
82
91
  # Toggle choice
83
92
  self.choices[i].is_picked = not self.choices[i].is_picked
84
- await cb.ctx_api.edit_message_text(
85
- cb.message.chat.id,
86
- cb.message.message_id,
93
+ await cb.edit_text(
87
94
  text=self.msg,
88
95
  parse_mode=self.PARSE_MODE,
89
96
  reply_markup=self.get_markup(),
@@ -93,21 +100,32 @@ class Checkbox(ABCScenario):
93
100
  return True
94
101
 
95
102
  async def wait(
96
- self, api: "API", dispatch: "Dispatch"
97
- ) -> typing.Tuple[typing.Dict[str, bool], int]:
103
+ self,
104
+ api: "API",
105
+ view: "BaseStateView[CallbackQueryCute]",
106
+ ) -> tuple[dict[str, bool], int]:
98
107
  assert len(self.choices) > 1
99
108
  message = (
100
109
  await api.send_message(
101
- self.chat_id, text=self.msg, parse_mode=self.PARSE_MODE, reply_markup=self.get_markup()
110
+ self.chat_id,
111
+ text=self.msg,
112
+ parse_mode=self.PARSE_MODE,
113
+ reply_markup=self.get_markup(),
102
114
  )
103
115
  ).unwrap()
116
+
104
117
  while True:
105
118
  q: CallbackQueryCute
106
- q, _ = await dispatch.callback_query.wait_for_answer(message.message_id)
119
+ q, _ = await self.waiter_machine.wait(view, (api, message.message_id))
107
120
  should_continue = await self.handle(q)
108
121
  await q.answer(self.CALLBACK_ANSWER)
109
122
  if not should_continue:
110
123
  break
111
- return {
112
- choice.name: choice.is_picked for choice in self.choices
113
- }, message.message_id
124
+
125
+ return (
126
+ {choice.name: choice.is_picked for choice in self.choices},
127
+ message.message_id,
128
+ )
129
+
130
+
131
+ __all__ = ("Checkbox", "Choice", "random_code")
@@ -1,16 +1,17 @@
1
- from .checkbox import Checkbox
2
- from telegrinder.bot.cute_types import CallbackQueryCute
3
1
  import typing
4
2
 
3
+ from telegrinder.bot.cute_types import CallbackQueryCute
4
+
5
+ from .checkbox import Checkbox
6
+
5
7
  if typing.TYPE_CHECKING:
6
8
  from telegrinder.api import API
7
- from telegrinder.bot.dispatch import Dispatch
9
+ from telegrinder.bot.dispatch.view.abc import BaseStateView
8
10
 
9
11
 
10
12
  class SingleChoice(Checkbox):
11
13
  async def handle(self, cb: CallbackQueryCute) -> bool:
12
- code = cb.data.replace(self.random_code + "/", "", 1)
13
-
14
+ code = cb.data.unwrap().replace(self.random_code + "/", "", 1)
14
15
  if code == "ready":
15
16
  return False
16
17
 
@@ -21,8 +22,8 @@ class SingleChoice(Checkbox):
21
22
  if choice.code == code:
22
23
  self.choices[i].is_picked = True
23
24
  await cb.ctx_api.edit_message_text(
24
- cb.message.chat.id,
25
- cb.message.message_id,
25
+ chat_id=cb.message.unwrap().v.chat.id,
26
+ message_id=cb.message.unwrap().v.message_id,
26
27
  text=self.msg,
27
28
  parse_mode=self.PARSE_MODE,
28
29
  reply_markup=self.get_markup(),
@@ -31,9 +32,14 @@ class SingleChoice(Checkbox):
31
32
  return True
32
33
 
33
34
  async def wait(
34
- self, api: "API", dispatch: "Dispatch"
35
- ) -> typing.Tuple[str, int]:
35
+ self,
36
+ api: "API",
37
+ cb_view: "BaseStateView[CallbackQueryCute]",
38
+ ) -> tuple[str, int]:
36
39
  if len([choice for choice in self.choices if choice.is_picked]) != 1:
37
40
  raise ValueError("Exactly one choice must be picked")
38
- choices, m_id = await super().wait(api, dispatch)
41
+ choices, m_id = await super().wait(api, cb_view)
39
42
  return list(choices.keys())[list(choices.values()).index(True)], m_id
43
+
44
+
45
+ __all__ = ("SingleChoice",)
@@ -1,2 +1,4 @@
1
- from .abc import ABCClient, ClientData
1
+ from .abc import ABCClient
2
2
  from .aiohttp import AiohttpClient
3
+
4
+ __all__ = ("ABCClient", "AiohttpClient")
telegrinder/client/abc.py CHANGED
@@ -1,12 +1,10 @@
1
- from abc import ABC, abstractmethod
2
1
  import typing
3
-
4
- ClientData = typing.Any
2
+ from abc import ABC, abstractmethod
5
3
 
6
4
 
7
5
  class ABCClient(ABC):
8
6
  @abstractmethod
9
- def __init__(self, *args, **kwargs):
7
+ def __init__(self, *args: typing.Any, **kwargs: typing.Any):
10
8
  pass
11
9
 
12
10
  @abstractmethod
@@ -14,8 +12,8 @@ class ABCClient(ABC):
14
12
  self,
15
13
  url: str,
16
14
  method: str = "GET",
17
- data: typing.Optional[dict] = None,
18
- **kwargs
15
+ data: dict[str, typing.Any] | None = None,
16
+ **kwargs: typing.Any,
19
17
  ) -> str:
20
18
  pass
21
19
 
@@ -24,9 +22,9 @@ class ABCClient(ABC):
24
22
  self,
25
23
  url: str,
26
24
  method: str = "GET",
27
- data: typing.Optional[dict] = None,
28
- **kwargs
29
- ) -> dict:
25
+ data: dict[str, typing.Any] | None = None,
26
+ **kwargs: typing.Any,
27
+ ) -> dict[str, typing.Any]:
30
28
  pass
31
29
 
32
30
  @abstractmethod
@@ -34,8 +32,8 @@ class ABCClient(ABC):
34
32
  self,
35
33
  url: str,
36
34
  method: str = "GET",
37
- data: typing.Optional[dict] = None,
38
- **kwargs
35
+ data: dict[str, typing.Any] | None = None,
36
+ **kwargs: typing.Any,
39
37
  ) -> bytes:
40
38
  pass
41
39
 
@@ -44,8 +42,8 @@ class ABCClient(ABC):
44
42
  self,
45
43
  url: str,
46
44
  method: str = "GET",
47
- data: typing.Optional[dict] = None,
48
- **kwargs
45
+ data: dict[str, typing.Any] | None = None,
46
+ **kwargs: typing.Any,
49
47
  ) -> bytes:
50
48
  pass
51
49
 
@@ -55,11 +53,23 @@ class ABCClient(ABC):
55
53
 
56
54
  @classmethod
57
55
  @abstractmethod
58
- def get_form(cls, data: dict) -> typing.Any:
56
+ def get_form(
57
+ cls,
58
+ data: dict[str, typing.Any],
59
+ files: dict[str, tuple[str, bytes]] | None = None,
60
+ ) -> typing.Any:
59
61
  pass
60
62
 
61
- async def __aenter__(self) -> "ABCClient":
63
+ async def __aenter__(self) -> typing.Self:
62
64
  return self
63
65
 
64
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
66
+ async def __aexit__(
67
+ self,
68
+ exc_type: type[BaseException],
69
+ exc_val: typing.Any,
70
+ exc_tb: typing.Any,
71
+ ) -> None:
65
72
  await self.close()
73
+
74
+
75
+ __all__ = ("ABCClient",)
@@ -1,12 +1,13 @@
1
+ import secrets
1
2
  import ssl
2
3
  import typing
3
4
 
4
5
  import aiohttp
6
+ import certifi
7
+ from aiohttp import ClientSession, TCPConnector
5
8
 
6
9
  from telegrinder.client.abc import ABCClient
7
- from aiohttp import ClientSession, TCPConnector
8
- from telegrinder.modules import json, JSONModule
9
- import certifi
10
+ from telegrinder.modules import JSONModule, json
10
11
 
11
12
  if typing.TYPE_CHECKING:
12
13
  from aiohttp import ClientResponse
@@ -15,22 +16,30 @@ if typing.TYPE_CHECKING:
15
16
  class AiohttpClient(ABCClient):
16
17
  def __init__(
17
18
  self,
18
- session: typing.Optional[ClientSession] = None,
19
- json_processing_module: typing.Optional[JSONModule] = None,
20
- timeout: typing.Optional[aiohttp.ClientTimeout] = None,
21
- **session_params
22
- ):
19
+ session: ClientSession | None = None,
20
+ json_processing_module: JSONModule | None = None,
21
+ timeout: aiohttp.ClientTimeout | None = None,
22
+ **session_params: typing.Any,
23
+ ) -> None:
23
24
  self.session = session
24
25
  self.json_processing_module = json_processing_module or json
25
26
  self.session_params = session_params
26
27
  self.timeout = timeout or aiohttp.ClientTimeout(total=0)
27
-
28
+
29
+ def __repr__(self) -> str:
30
+ return "<{}: session={!r}, timeout={}, closed={}>".format(
31
+ self.__class__.__name__,
32
+ self.session,
33
+ self.timeout,
34
+ False if self.session is None else self.session.closed,
35
+ )
36
+
28
37
  async def request_raw(
29
38
  self,
30
39
  url: str,
31
40
  method: str = "GET",
32
- data: typing.Optional[dict] = None,
33
- **kwargs
41
+ data: dict[str, typing.Any] | None = None,
42
+ **kwargs: typing.Any,
34
43
  ) -> "ClientResponse":
35
44
  if not self.session:
36
45
  self.session = ClientSession(
@@ -41,7 +50,11 @@ class AiohttpClient(ABCClient):
41
50
  **self.session_params,
42
51
  )
43
52
  async with self.session.request(
44
- url=url, method=method, data=data, timeout=self.timeout, **kwargs
53
+ url=url,
54
+ method=method,
55
+ data=data,
56
+ timeout=self.timeout,
57
+ **kwargs,
45
58
  ) as response:
46
59
  await response.read()
47
60
  return response
@@ -50,32 +63,34 @@ class AiohttpClient(ABCClient):
50
63
  self,
51
64
  url: str,
52
65
  method: str = "GET",
53
- data: typing.Optional[dict] = None,
54
- **kwargs
55
- ) -> dict:
66
+ data: dict[str, typing.Any] | None = None,
67
+ **kwargs: typing.Any,
68
+ ) -> dict[str, typing.Any]:
56
69
  response = await self.request_raw(url, method, data, **kwargs)
57
70
  return await response.json(
58
- encoding="utf-8", loads=self.json_processing_module.loads, content_type=None
71
+ encoding="utf-8",
72
+ loads=self.json_processing_module.loads,
73
+ content_type=None,
59
74
  )
60
75
 
61
76
  async def request_text(
62
77
  self,
63
78
  url: str,
64
79
  method: str = "GET",
65
- data: typing.Union[dict, aiohttp.FormData, None] = None,
66
- **kwargs
80
+ data: dict[str, typing.Any] | aiohttp.FormData | None = None,
81
+ **kwargs: typing.Any,
67
82
  ) -> str:
68
- response = await self.request_raw(url, method, data, **kwargs)
83
+ response = await self.request_raw(url, method, data, **kwargs) # type: ignore
69
84
  return await response.text(encoding="utf-8")
70
85
 
71
86
  async def request_bytes(
72
87
  self,
73
88
  url: str,
74
89
  method: str = "GET",
75
- data: typing.Union[dict, aiohttp.FormData, None] = None,
76
- **kwargs
90
+ data: dict[str, typing.Any] | aiohttp.FormData | None = None,
91
+ **kwargs: typing.Any,
77
92
  ) -> bytes:
78
- response = await self.request_raw(url, method, data, **kwargs)
93
+ response = await self.request_raw(url, method, data, **kwargs) # type: ignore
79
94
  if response._body is None:
80
95
  await response.read()
81
96
  return response._body
@@ -84,8 +99,8 @@ class AiohttpClient(ABCClient):
84
99
  self,
85
100
  url: str,
86
101
  method: str = "GET",
87
- data: typing.Optional[dict] = None,
88
- **kwargs
102
+ data: dict[str, typing.Any] | None = None,
103
+ **kwargs: typing.Any,
89
104
  ) -> bytes:
90
105
  response = await self.request_raw(url, method, data, **kwargs)
91
106
  return response._body
@@ -95,19 +110,26 @@ class AiohttpClient(ABCClient):
95
110
  await self.session.close()
96
111
 
97
112
  @classmethod
98
- def get_form(cls, data: dict) -> aiohttp.formdata.FormData:
113
+ def get_form(
114
+ cls,
115
+ data: dict[str, typing.Any],
116
+ files: dict[str, tuple[str, bytes]] | None = None,
117
+ ) -> aiohttp.formdata.FormData:
118
+ files = files or {}
99
119
  form = aiohttp.formdata.FormData(quote_fields=False)
100
120
  for k, v in data.items():
101
- params = {}
102
- if isinstance(v, tuple):
103
- params["filename"], v = v[0], v[1]
104
- else:
105
- v = str(v)
106
- form.add_field(k, v, **params)
121
+ form.add_field(k, str(v))
122
+
123
+ for n, f in files.items():
124
+ form.add_field(n, f[1], filename=f[0])
125
+
107
126
  return form
108
127
 
109
- def __del__(self):
128
+ def __del__(self) -> None:
110
129
  if self.session and not self.session.closed:
111
130
  if self.session._connector is not None and self.session._connector_owner:
112
131
  self.session._connector.close()
113
132
  self.session._connector = None
133
+
134
+
135
+ __all__ = ("AiohttpClient",)
telegrinder/model.py CHANGED
@@ -1,65 +1,144 @@
1
- import msgspec
2
- from telegrinder.result import Result
3
- from telegrinder.modules import json
4
- from msgspec import Raw
1
+ import dataclasses
2
+ import enum
3
+ import secrets
5
4
  import typing
5
+ from datetime import datetime
6
+ from types import NoneType
7
+
8
+ import msgspec
9
+ from fntypes.co import Nothing, Result, Some
10
+
11
+ from .msgspec_utils import decoder, encoder, get_origin
12
+
13
+ T = typing.TypeVar("T")
6
14
 
7
15
  if typing.TYPE_CHECKING:
8
16
  from telegrinder.api.error import APIError
17
+
9
18
 
10
- T = typing.TypeVar("T")
11
- encoder = msgspec.json.Encoder()
19
+ MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
20
+ "omit_defaults": True,
21
+ "dict": True,
22
+ "rename": {"from_": "from"},
23
+ }
12
24
 
13
25
 
26
+ @typing.overload
14
27
  def full_result(
15
- result: Result[msgspec.Raw, "APIError"], full_t: typing.Type[T]
28
+ result: Result[msgspec.Raw, "APIError"], full_t: type[T]
16
29
  ) -> Result[T, "APIError"]:
17
- if not result.is_ok:
18
- return result
19
- return Result(True, value=msgspec.json.decode(result.value, type=full_t))
20
-
21
-
22
- def convert(d: typing.Any) -> typing.Any:
23
- if isinstance(d, Model):
24
- return msgspec.json.encode(d).decode()
25
- elif isinstance(d, dict):
26
- return {k: convert(v) for k, v in d.items() if v is not None}
27
- elif isinstance(d, list):
28
- return json.dumps(d)
29
- return d
30
+ ...
30
31
 
31
32
 
32
- model_config = {"rename": {"from_": "from"}, "omit_defaults": True}
33
-
33
+ @typing.overload
34
+ def full_result(
35
+ result: Result[msgspec.Raw, "APIError"],
36
+ full_t: tuple[type[T], ...],
37
+ ) -> Result[T, "APIError"]:
38
+ ...
34
39
 
35
- class Model(msgspec.Struct, **model_config):
36
- _dict_cached: typing.Optional[dict] = None
37
40
 
38
- def to_dict(self) -> dict:
39
- if self._dict_cached is not None:
40
- return self._dict_cached
41
- self._dict_cached = {k: getattr(self, k) for k in self.__struct_fields__}
42
- return self._dict_cached
41
+ def full_result(
42
+ result: Result[msgspec.Raw, "APIError"],
43
+ full_t: type[T] | tuple[type[T], ...],
44
+ ) -> Result[T, "APIError"]:
45
+ return result.map(lambda v: decoder.decode(v, type=full_t)) # type: ignore
43
46
 
44
47
 
45
- def get_params(params: dict) -> dict:
48
+ def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
46
49
  return {
47
- k: v for k, v in (
50
+ k: v.unwrap() if v and isinstance(v, Some) else v
51
+ for k, v in (
52
+ *params.pop("other", {}).items(),
48
53
  *params.items(),
49
- *params.pop("other").items()
50
54
  )
51
- if k != "self"
52
- and v is not None
55
+ if k != "self" and type(v) not in (NoneType, Nothing)
53
56
  }
54
57
 
55
58
 
59
+ class Model(msgspec.Struct, **MODEL_CONFIG):
60
+ def to_dict(
61
+ self,
62
+ *,
63
+ exclude_fields: set[str] | None = None,
64
+ ) -> dict[str, typing.Any]:
65
+ exclude_fields = exclude_fields or set()
66
+ if "model_as_dict" not in self.__dict__:
67
+ self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
68
+ return {
69
+ key: value
70
+ for key, value in self.__dict__["model_as_dict"].items()
71
+ if key not in exclude_fields
72
+ }
73
+
74
+
75
+ @dataclasses.dataclass(kw_only=True)
76
+ class DataConverter:
77
+ files: dict[str, tuple[str, bytes]] = dataclasses.field(default_factory=lambda: {})
78
+
79
+ @property
80
+ def converters(self) -> dict[
81
+ type[typing.Any], typing.Callable[[typing.Self, typing.Any, bool], typing.Any]
82
+ ]:
83
+ return {
84
+ get_origin(value.__annotations__["d"]): value
85
+ for key, value in vars(self.__class__).items()
86
+ if key.startswith("convert_")
87
+ and callable(value)
88
+ }
89
+
90
+ def get_converter(
91
+ self, t: type[typing.Any]
92
+ ) -> typing.Callable[[typing.Self, typing.Any, bool], typing.Any] | None:
93
+ for type, converter in self.converters.items():
94
+ if issubclass(t, type):
95
+ return converter
96
+ return None
97
+
98
+ def convert_model(self, d: Model, serialize: bool = True) -> str | dict[str, typing.Any]:
99
+ converted_dct = self.convert(d.to_dict(), serialize=False)
100
+ return encoder.encode(converted_dct) if serialize is True else converted_dct
101
+
102
+ def convert_dct(self, d: dict[str, typing.Any], serialize: bool = True) -> dict[str, typing.Any]:
103
+ return {
104
+ k: self.convert(v, serialize=serialize)
105
+ for k, v in d.items()
106
+ if type(v) not in (NoneType, Nothing)
107
+ }
108
+
109
+ def convert_lst(self, d: list[typing.Any], serialize: bool = True) -> str | list[typing.Any]:
110
+ converted_lst = [self.convert(x, serialize=False) for x in d]
111
+ return encoder.encode(converted_lst) if serialize is True else converted_lst
112
+
113
+ def convert_tpl(self, d: tuple[typing.Any, ...], serialize: bool = True) -> str | tuple[typing.Any, ...]:
114
+ if (
115
+ isinstance(d, tuple)
116
+ and len(d) == 2
117
+ and isinstance(d[0], str)
118
+ and isinstance(d[1], bytes)
119
+ ):
120
+ attach_name = secrets.token_urlsafe(16)
121
+ self.files[attach_name] = d
122
+ return "attach://{}".format(attach_name)
123
+ return d
124
+
125
+ def convert_enum(self, d: enum.Enum, serialize: bool = True) -> str:
126
+ return d.value
127
+
128
+ def convert_datetime(self, d: datetime, serialize: bool = True) -> int:
129
+ return int(d.timestamp())
130
+
131
+ def convert(self, data: typing.Any, *, serialize: bool = True) -> typing.Any:
132
+ converter = self.get_converter(get_origin(type(data)))
133
+ if converter is not None:
134
+ return converter(self, data, serialize)
135
+ return data
136
+
137
+
56
138
  __all__ = (
57
- "convert",
58
- "model_config",
59
- "encoder",
60
- "full_result",
61
- "msgspec",
139
+ "DataConverter",
62
140
  "Model",
63
- "Raw",
141
+ "full_result",
64
142
  "get_params",
143
+ "MODEL_CONFIG",
65
144
  )