telegrinder 1.0.0rc1__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.
Files changed (215) hide show
  1. telegrinder/__init__.py +258 -0
  2. telegrinder/__meta__.py +1 -0
  3. telegrinder/api/__init__.py +15 -0
  4. telegrinder/api/api.py +175 -0
  5. telegrinder/api/error.py +50 -0
  6. telegrinder/api/response.py +23 -0
  7. telegrinder/api/token.py +30 -0
  8. telegrinder/api/validators.py +30 -0
  9. telegrinder/bot/__init__.py +144 -0
  10. telegrinder/bot/bot.py +70 -0
  11. telegrinder/bot/cute_types/__init__.py +41 -0
  12. telegrinder/bot/cute_types/base.py +228 -0
  13. telegrinder/bot/cute_types/base.pyi +49 -0
  14. telegrinder/bot/cute_types/business_connection.py +9 -0
  15. telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
  16. telegrinder/bot/cute_types/callback_query.py +248 -0
  17. telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
  18. telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
  19. telegrinder/bot/cute_types/chat_join_request.py +59 -0
  20. telegrinder/bot/cute_types/chat_member_updated.py +158 -0
  21. telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
  22. telegrinder/bot/cute_types/inline_query.py +41 -0
  23. telegrinder/bot/cute_types/message.py +2809 -0
  24. telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
  25. telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
  26. telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
  27. telegrinder/bot/cute_types/poll.py +9 -0
  28. telegrinder/bot/cute_types/poll_answer.py +9 -0
  29. telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
  30. telegrinder/bot/cute_types/shipping_query.py +11 -0
  31. telegrinder/bot/cute_types/update.py +209 -0
  32. telegrinder/bot/cute_types/utils.py +141 -0
  33. telegrinder/bot/dispatch/__init__.py +99 -0
  34. telegrinder/bot/dispatch/abc.py +74 -0
  35. telegrinder/bot/dispatch/action.py +99 -0
  36. telegrinder/bot/dispatch/context.py +162 -0
  37. telegrinder/bot/dispatch/dispatch.py +362 -0
  38. telegrinder/bot/dispatch/handler/__init__.py +23 -0
  39. telegrinder/bot/dispatch/handler/abc.py +25 -0
  40. telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
  41. telegrinder/bot/dispatch/handler/base.py +34 -0
  42. telegrinder/bot/dispatch/handler/document_reply.py +43 -0
  43. telegrinder/bot/dispatch/handler/func.py +73 -0
  44. telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
  45. telegrinder/bot/dispatch/handler/message_reply.py +35 -0
  46. telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
  47. telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
  48. telegrinder/bot/dispatch/handler/video_reply.py +43 -0
  49. telegrinder/bot/dispatch/middleware/__init__.py +13 -0
  50. telegrinder/bot/dispatch/middleware/abc.py +112 -0
  51. telegrinder/bot/dispatch/middleware/box.py +32 -0
  52. telegrinder/bot/dispatch/middleware/filter.py +88 -0
  53. telegrinder/bot/dispatch/middleware/media_group.py +69 -0
  54. telegrinder/bot/dispatch/process.py +93 -0
  55. telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
  56. telegrinder/bot/dispatch/return_manager/abc.py +107 -0
  57. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  58. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  59. telegrinder/bot/dispatch/return_manager/message.py +34 -0
  60. telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
  61. telegrinder/bot/dispatch/return_manager/utils.py +20 -0
  62. telegrinder/bot/dispatch/router/__init__.py +4 -0
  63. telegrinder/bot/dispatch/router/abc.py +15 -0
  64. telegrinder/bot/dispatch/router/base.py +154 -0
  65. telegrinder/bot/dispatch/view/__init__.py +15 -0
  66. telegrinder/bot/dispatch/view/abc.py +15 -0
  67. telegrinder/bot/dispatch/view/base.py +226 -0
  68. telegrinder/bot/dispatch/view/box.py +207 -0
  69. telegrinder/bot/dispatch/view/media_group.py +25 -0
  70. telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
  71. telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
  72. telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
  73. telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
  74. telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
  75. telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
  76. telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
  77. telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
  78. telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
  79. telegrinder/bot/polling/__init__.py +4 -0
  80. telegrinder/bot/polling/abc.py +25 -0
  81. telegrinder/bot/polling/error_handler.py +93 -0
  82. telegrinder/bot/polling/polling.py +167 -0
  83. telegrinder/bot/polling/utils.py +12 -0
  84. telegrinder/bot/rules/__init__.py +166 -0
  85. telegrinder/bot/rules/abc.py +150 -0
  86. telegrinder/bot/rules/button.py +20 -0
  87. telegrinder/bot/rules/callback_data.py +109 -0
  88. telegrinder/bot/rules/chat_join.py +28 -0
  89. telegrinder/bot/rules/chat_member_updated.py +145 -0
  90. telegrinder/bot/rules/command.py +137 -0
  91. telegrinder/bot/rules/enum_text.py +29 -0
  92. telegrinder/bot/rules/func.py +21 -0
  93. telegrinder/bot/rules/fuzzy.py +21 -0
  94. telegrinder/bot/rules/inline.py +45 -0
  95. telegrinder/bot/rules/integer.py +19 -0
  96. telegrinder/bot/rules/is_from.py +213 -0
  97. telegrinder/bot/rules/logic.py +22 -0
  98. telegrinder/bot/rules/magic.py +60 -0
  99. telegrinder/bot/rules/markup.py +51 -0
  100. telegrinder/bot/rules/media.py +13 -0
  101. telegrinder/bot/rules/mention.py +15 -0
  102. telegrinder/bot/rules/message_entities.py +37 -0
  103. telegrinder/bot/rules/node.py +43 -0
  104. telegrinder/bot/rules/payload.py +89 -0
  105. telegrinder/bot/rules/payment_invoice.py +14 -0
  106. telegrinder/bot/rules/regex.py +34 -0
  107. telegrinder/bot/rules/rule_enum.py +71 -0
  108. telegrinder/bot/rules/start.py +73 -0
  109. telegrinder/bot/rules/state.py +35 -0
  110. telegrinder/bot/rules/text.py +27 -0
  111. telegrinder/bot/rules/update.py +14 -0
  112. telegrinder/bot/scenario/__init__.py +5 -0
  113. telegrinder/bot/scenario/abc.py +16 -0
  114. telegrinder/bot/scenario/checkbox.py +183 -0
  115. telegrinder/bot/scenario/choice.py +44 -0
  116. telegrinder/client/__init__.py +11 -0
  117. telegrinder/client/abc.py +136 -0
  118. telegrinder/client/form_data.py +34 -0
  119. telegrinder/client/rnet.py +198 -0
  120. telegrinder/model.py +133 -0
  121. telegrinder/model.pyi +57 -0
  122. telegrinder/modules.py +1081 -0
  123. telegrinder/msgspec_utils/__init__.py +42 -0
  124. telegrinder/msgspec_utils/abc.py +16 -0
  125. telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
  126. telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
  127. telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
  128. telegrinder/msgspec_utils/custom_types/literal.py +25 -0
  129. telegrinder/msgspec_utils/custom_types/option.py +17 -0
  130. telegrinder/msgspec_utils/decoder.py +388 -0
  131. telegrinder/msgspec_utils/encoder.py +204 -0
  132. telegrinder/msgspec_utils/json.py +15 -0
  133. telegrinder/msgspec_utils/tools.py +80 -0
  134. telegrinder/node/__init__.py +80 -0
  135. telegrinder/node/compose.py +193 -0
  136. telegrinder/node/nodes/__init__.py +96 -0
  137. telegrinder/node/nodes/attachment.py +169 -0
  138. telegrinder/node/nodes/callback_query.py +25 -0
  139. telegrinder/node/nodes/channel.py +97 -0
  140. telegrinder/node/nodes/command.py +33 -0
  141. telegrinder/node/nodes/error.py +43 -0
  142. telegrinder/node/nodes/event.py +70 -0
  143. telegrinder/node/nodes/file.py +39 -0
  144. telegrinder/node/nodes/global_node.py +66 -0
  145. telegrinder/node/nodes/i18n.py +110 -0
  146. telegrinder/node/nodes/me.py +26 -0
  147. telegrinder/node/nodes/message_entities.py +15 -0
  148. telegrinder/node/nodes/payload.py +84 -0
  149. telegrinder/node/nodes/reply_message.py +14 -0
  150. telegrinder/node/nodes/source.py +172 -0
  151. telegrinder/node/nodes/state_mutator.py +71 -0
  152. telegrinder/node/nodes/text.py +62 -0
  153. telegrinder/node/scope.py +88 -0
  154. telegrinder/node/utils.py +38 -0
  155. telegrinder/py.typed +0 -0
  156. telegrinder/rules.py +1 -0
  157. telegrinder/tools/__init__.py +183 -0
  158. telegrinder/tools/aio.py +147 -0
  159. telegrinder/tools/final.py +21 -0
  160. telegrinder/tools/formatting/__init__.py +85 -0
  161. telegrinder/tools/formatting/deep_links/__init__.py +39 -0
  162. telegrinder/tools/formatting/deep_links/links.py +468 -0
  163. telegrinder/tools/formatting/deep_links/parsing.py +88 -0
  164. telegrinder/tools/formatting/deep_links/validators.py +8 -0
  165. telegrinder/tools/formatting/html.py +241 -0
  166. telegrinder/tools/fullname.py +82 -0
  167. telegrinder/tools/global_context/__init__.py +13 -0
  168. telegrinder/tools/global_context/abc.py +63 -0
  169. telegrinder/tools/global_context/builtin_context.py +45 -0
  170. telegrinder/tools/global_context/global_context.py +614 -0
  171. telegrinder/tools/input_file_directory.py +30 -0
  172. telegrinder/tools/keyboard/__init__.py +6 -0
  173. telegrinder/tools/keyboard/abc.py +84 -0
  174. telegrinder/tools/keyboard/base.py +108 -0
  175. telegrinder/tools/keyboard/button.py +181 -0
  176. telegrinder/tools/keyboard/data.py +31 -0
  177. telegrinder/tools/keyboard/keyboard.py +160 -0
  178. telegrinder/tools/keyboard/utils.py +95 -0
  179. telegrinder/tools/lifespan.py +188 -0
  180. telegrinder/tools/limited_dict.py +35 -0
  181. telegrinder/tools/loop_wrapper.py +271 -0
  182. telegrinder/tools/magic/__init__.py +29 -0
  183. telegrinder/tools/magic/annotations.py +172 -0
  184. telegrinder/tools/magic/descriptors.py +57 -0
  185. telegrinder/tools/magic/function.py +254 -0
  186. telegrinder/tools/magic/inspect.py +16 -0
  187. telegrinder/tools/magic/shortcut.py +107 -0
  188. telegrinder/tools/member_descriptor_proxy.py +95 -0
  189. telegrinder/tools/parse_mode.py +12 -0
  190. telegrinder/tools/serialization/__init__.py +5 -0
  191. telegrinder/tools/serialization/abc.py +34 -0
  192. telegrinder/tools/serialization/json_ser.py +60 -0
  193. telegrinder/tools/serialization/msgpack_ser.py +197 -0
  194. telegrinder/tools/serialization/utils.py +18 -0
  195. telegrinder/tools/singleton/__init__.py +4 -0
  196. telegrinder/tools/singleton/abc.py +14 -0
  197. telegrinder/tools/singleton/singleton.py +18 -0
  198. telegrinder/tools/state_mutator/__init__.py +4 -0
  199. telegrinder/tools/state_mutator/mutation.py +85 -0
  200. telegrinder/tools/state_storage/__init__.py +4 -0
  201. telegrinder/tools/state_storage/abc.py +38 -0
  202. telegrinder/tools/state_storage/memory.py +27 -0
  203. telegrinder/tools/strings.py +22 -0
  204. telegrinder/types/__init__.py +323 -0
  205. telegrinder/types/enums.py +754 -0
  206. telegrinder/types/input_file.py +51 -0
  207. telegrinder/types/methods.py +6143 -0
  208. telegrinder/types/methods_utils.py +66 -0
  209. telegrinder/types/objects.py +8184 -0
  210. telegrinder/types/webapp.py +129 -0
  211. telegrinder/verification_utils.py +35 -0
  212. telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
  213. telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
  214. telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
  215. telegrinder-1.0.0rc1.dist-info/licenses/LICENSE +22 -0
@@ -0,0 +1,42 @@
1
+ from telegrinder.msgspec_utils.abc import SupportsCast, is_supports_cast
2
+ from telegrinder.msgspec_utils.custom_types.datetime import datetime, timedelta
3
+ from telegrinder.msgspec_utils.custom_types.enum_meta import BaseEnumMeta
4
+ from telegrinder.msgspec_utils.custom_types.literal import Literal
5
+ from telegrinder.msgspec_utils.custom_types.option import Option
6
+ from telegrinder.msgspec_utils.decoder import Decoder, convert, decoder
7
+ from telegrinder.msgspec_utils.encoder import Encoder, encoder, to_builtins
8
+ from telegrinder.msgspec_utils.json import dumps, loads
9
+ from telegrinder.msgspec_utils.tools import (
10
+ get_class_annotations,
11
+ get_origin,
12
+ get_type_hints,
13
+ is_common_type,
14
+ is_none,
15
+ struct_asdict,
16
+ type_check,
17
+ )
18
+
19
+ __all__ = (
20
+ "BaseEnumMeta",
21
+ "Decoder",
22
+ "Encoder",
23
+ "Literal",
24
+ "Option",
25
+ "SupportsCast",
26
+ "convert",
27
+ "datetime",
28
+ "decoder",
29
+ "dumps",
30
+ "encoder",
31
+ "get_class_annotations",
32
+ "get_origin",
33
+ "get_type_hints",
34
+ "is_common_type",
35
+ "is_none",
36
+ "is_supports_cast",
37
+ "loads",
38
+ "struct_asdict",
39
+ "timedelta",
40
+ "to_builtins",
41
+ "type_check",
42
+ )
@@ -0,0 +1,16 @@
1
+ import abc
2
+ import typing
3
+
4
+
5
+ def is_supports_cast(obj: typing.Any, /) -> typing.TypeGuard[SupportsCast]:
6
+ return isinstance(obj, SupportsCast)
7
+
8
+
9
+ @typing.runtime_checkable
10
+ class SupportsCast(typing.Protocol):
11
+ @classmethod
12
+ @abc.abstractmethod
13
+ def cast(cls, obj: typing.Any) -> typing.Self: ...
14
+
15
+
16
+ __all__ = ("SupportsCast", "is_supports_cast")
@@ -0,0 +1,6 @@
1
+ from telegrinder.msgspec_utils.custom_types.datetime import datetime, timedelta
2
+ from telegrinder.msgspec_utils.custom_types.enum_meta import BaseEnumMeta
3
+ from telegrinder.msgspec_utils.custom_types.literal import Literal
4
+ from telegrinder.msgspec_utils.custom_types.option import Option
5
+
6
+ __all__ = ("BaseEnumMeta", "Literal", "Option", "datetime", "timedelta")
@@ -0,0 +1,24 @@
1
+ import datetime as dt
2
+ import typing
3
+
4
+ from telegrinder.msgspec_utils.abc import SupportsCast
5
+
6
+
7
+ class datetime(dt.datetime, SupportsCast): # noqa: N801 # type: ignore
8
+ @classmethod
9
+ def cast(cls, obj: dt.datetime) -> typing.Self:
10
+ return cls.fromtimestamp(timestamp=obj.timestamp(), tz=obj.tzinfo)
11
+
12
+
13
+ class timedelta(dt.timedelta, SupportsCast): # noqa: N801 # type: ignore
14
+ @classmethod
15
+ def cast(cls, obj: dt.timedelta) -> typing.Self:
16
+ return cls(seconds=obj.total_seconds())
17
+
18
+
19
+ if typing.TYPE_CHECKING:
20
+ datetime: typing.TypeAlias = dt.datetime
21
+ timedelta: typing.TypeAlias = dt.timedelta
22
+
23
+
24
+ __all__ = ("datetime", "timedelta")
@@ -0,0 +1,61 @@
1
+ import enum
2
+ import math
3
+ import sys
4
+ import typing
5
+
6
+ from telegrinder.modules import logger
7
+
8
+ NOT_SUPPORTED: typing.Final = "NOT_SUPPORTED"
9
+ ENUM_FRIENDS: typing.Final = (str, int, float)
10
+ NOT_SUPPORTED_VALUES: typing.Final = {
11
+ str: NOT_SUPPORTED,
12
+ int: math.inf,
13
+ float: sys.maxsize,
14
+ }
15
+
16
+
17
+ def _is_friend(bases: tuple[type[typing.Any], ...], /) -> bool:
18
+ return any(friend in bases for friend in ENUM_FRIENDS)
19
+
20
+
21
+ class BaseEnumMeta(enum.EnumMeta, type):
22
+ if typing.TYPE_CHECKING:
23
+
24
+ class _BaseEnumMeta(enum.Enum): # noqa
25
+ NOT_SUPPORTED = enum.auto()
26
+
27
+ NOT_SUPPORTED: typing.Literal[_BaseEnumMeta.NOT_SUPPORTED]
28
+
29
+ else:
30
+
31
+ @staticmethod
32
+ def _member_missing(cls, value):
33
+ logger.warning(
34
+ "Unsupported value {!r} for enum of type {}. Probably telegrinder needs "
35
+ "to be updated to support the latest version of Telegram Bot API.",
36
+ value,
37
+ cls,
38
+ )
39
+ return cls._member_map_["NOT_SUPPORTED"]
40
+
41
+ def __new__(
42
+ metacls,
43
+ cls,
44
+ bases,
45
+ classdict,
46
+ *,
47
+ boundary=None,
48
+ _simple=False,
49
+ **kwds,
50
+ ):
51
+ if _is_friend(bases):
52
+ classdict["NOT_SUPPORTED"] = next(
53
+ (value for base, value in NOT_SUPPORTED_VALUES.items() if base in bases),
54
+ NOT_SUPPORTED,
55
+ )
56
+
57
+ classdict["_missing_"] = classmethod(BaseEnumMeta._member_missing)
58
+ return super().__new__(metacls, cls, bases, classdict, boundary=boundary, _simple=_simple, **kwds)
59
+
60
+
61
+ __all__ = ("BaseEnumMeta",)
@@ -0,0 +1,25 @@
1
+ import typing
2
+
3
+
4
+ class _LiteralMeta(type):
5
+ __args__: tuple[typing.Any, ...]
6
+
7
+ def __getitem__[T](cls: type[T], values: typing.Any, /) -> type[T]:
8
+ values = (values,) if not isinstance(values, tuple) else values
9
+ return _LiteralMeta(cls.__name__, (cls,), {"__args__": values}) # type: ignore
10
+
11
+ def __instancecheck__(cls, instance: typing.Any, /) -> bool:
12
+ return instance in cls.__args__
13
+
14
+
15
+ class _Literal(metaclass=_LiteralMeta):
16
+ pass
17
+
18
+
19
+ if typing.TYPE_CHECKING:
20
+ from typing import Literal
21
+ else:
22
+ Literal = _Literal
23
+
24
+
25
+ __all__ = ("Literal",)
@@ -0,0 +1,17 @@
1
+ import typing
2
+
3
+ if typing.TYPE_CHECKING:
4
+ from kungfu.library.monad.option import Option
5
+ else:
6
+ import kungfu.library
7
+ import msgspec
8
+
9
+ class OptionMeta(type):
10
+ def __instancecheck__(cls, __instance):
11
+ return isinstance(__instance, (kungfu.library.Some | kungfu.library.Nothing, msgspec.UnsetType))
12
+
13
+ class Option[Value](metaclass=OptionMeta):
14
+ pass
15
+
16
+
17
+ __all__ = ("Option",)
@@ -0,0 +1,388 @@
1
+ import datetime as dt
2
+ import typing
3
+ from contextlib import contextmanager
4
+
5
+ import kungfu.library
6
+ import msgspec
7
+ from kungfu.library.monad.option import NOTHING
8
+
9
+ from telegrinder.msgspec_utils.abc import SupportsCast
10
+ from telegrinder.msgspec_utils.custom_types.datetime import datetime, timedelta
11
+ from telegrinder.msgspec_utils.custom_types.enum_meta import BaseEnumMeta
12
+ from telegrinder.msgspec_utils.custom_types.literal import _Literal
13
+ from telegrinder.msgspec_utils.custom_types.option import Option
14
+ from telegrinder.msgspec_utils.tools import (
15
+ bundle,
16
+ fullname,
17
+ get_origin,
18
+ is_common_type,
19
+ type_check,
20
+ )
21
+
22
+ type Context = dict[str, typing.Any]
23
+ type DecHook[T] = typing.Callable[typing.Concatenate[type[T], typing.Any, ...], typing.Any]
24
+
25
+
26
+ def option_dec_hook(
27
+ tp: type[Option[typing.Any]],
28
+ obj: typing.Any,
29
+ /,
30
+ ) -> kungfu.library.Option[typing.Any] | msgspec.UnsetType:
31
+ if obj is msgspec.UNSET:
32
+ return obj
33
+
34
+ if obj is None or obj is NOTHING:
35
+ return NOTHING
36
+
37
+ (value_type,) = typing.get_args(tp) or (typing.Any,)
38
+ orig_value_type = typing.get_origin(value_type) or value_type
39
+ orig_obj = obj
40
+
41
+ if not isinstance(orig_obj, dict | list) and is_common_type(orig_value_type):
42
+ if orig_value_type is kungfu.library.Sum:
43
+ obj = value_type(orig_obj) # type: ignore
44
+ orig_value_type = typing.get_args(value_type)
45
+
46
+ if not type_check(orig_obj, orig_value_type):
47
+ raise msgspec.ValidationError(
48
+ f"Expected `{fullname(orig_value_type)}` or `builtins.None`, got `{fullname(orig_obj)}`.",
49
+ )
50
+
51
+ return kungfu.library.Some(obj)
52
+
53
+ return kungfu.library.Some(decoder.convert(orig_obj, type=value_type))
54
+
55
+
56
+ def sum_dec_hook(tp: type[kungfu.library.Sum], obj: typing.Any, /) -> kungfu.library.Sum:
57
+ union_types = typing.get_args(tp)
58
+
59
+ if isinstance(obj, dict):
60
+ reverse = False
61
+ models_fields_count: dict[type[msgspec.Struct], int] = {
62
+ m: sum(1 for k in obj if k in m.__struct_fields__)
63
+ for m in union_types
64
+ if issubclass(get_origin(m), msgspec.Struct)
65
+ }
66
+ union_types = tuple(t for t in union_types if t not in models_fields_count)
67
+
68
+ if len(set(models_fields_count.values())) != len(models_fields_count.values()):
69
+ models_fields_count = {m: len(m.__struct_fields__) for m in models_fields_count}
70
+ reverse = True
71
+
72
+ union_types = (
73
+ *sorted(
74
+ models_fields_count,
75
+ key=lambda k: models_fields_count[k],
76
+ reverse=reverse,
77
+ ),
78
+ *union_types,
79
+ )
80
+
81
+ if not isinstance(obj, dict | list) and any(is_common_type(t) and type_check(obj, t) for t in union_types):
82
+ return tp(obj)
83
+
84
+ for t in union_types:
85
+ match convert(obj, t):
86
+ case kungfu.library.Ok(value):
87
+ return tp(value)
88
+ case kungfu.library.Error(_):
89
+ continue
90
+ case _ as arg:
91
+ typing.assert_never(arg)
92
+
93
+ raise msgspec.ValidationError(
94
+ "Object of type `{}` doesn't belong to `{}[{}]`.".format(
95
+ fullname(obj),
96
+ fullname(kungfu.library.Sum),
97
+ ", ".join(fullname(get_origin(x)) for x in union_types),
98
+ )
99
+ )
100
+
101
+
102
+ def datetime_dec_hook(tp: type[datetime], obj: typing.Any, /) -> datetime:
103
+ if isinstance(obj, datetime):
104
+ return obj
105
+
106
+ if isinstance(obj, dt.datetime):
107
+ assert issubclass(tp, SupportsCast)
108
+ return tp.cast(obj)
109
+
110
+ if isinstance(obj, int | float):
111
+ return tp.fromtimestamp(timestamp=obj)
112
+
113
+ raise TypeError(
114
+ "Cannot validate object of type `{}` into `datetime.datetime`".format(
115
+ fullname(obj),
116
+ ),
117
+ )
118
+
119
+
120
+ def timedelta_dec_hook(tp: type[timedelta], obj: typing.Any, /) -> timedelta:
121
+ if isinstance(obj, timedelta):
122
+ return obj
123
+
124
+ if isinstance(obj, dt.timedelta):
125
+ assert issubclass(tp, SupportsCast)
126
+ return tp.cast(obj)
127
+
128
+ if isinstance(obj, int | float):
129
+ return tp(seconds=obj)
130
+
131
+ raise TypeError(
132
+ "Cannot validate object of type `{}` into `datetime.timedelta`".format(
133
+ fullname(obj),
134
+ ),
135
+ )
136
+
137
+
138
+ def convert[T](
139
+ obj: typing.Any,
140
+ t: type[T],
141
+ /,
142
+ *,
143
+ context: Context | None = None,
144
+ ) -> kungfu.library.Result[T, str]:
145
+ try:
146
+ return kungfu.library.Ok(decoder.convert(obj, type=t, strict=True, context=context))
147
+ except msgspec.ValidationError:
148
+ return kungfu.library.Error(
149
+ "Expected object of type `{}`, got `{}`.".format(
150
+ fullname(t),
151
+ fullname(obj),
152
+ )
153
+ )
154
+
155
+
156
+ def literal_dec_hook(literal: type[_Literal], obj: typing.Any, /) -> typing.Any:
157
+ if obj in literal.__args__:
158
+ return obj
159
+
160
+ raise msgspec.ValidationError(
161
+ "Invalid literal value {!r} of either {}.".format(
162
+ obj,
163
+ literal.__args__[0]
164
+ if len(literal.__args__) == 1
165
+ else (", ".join(f"`{x}`" for x in literal.__args__[:-1])) + f" or `{literal.__args__[-1]}`",
166
+ )
167
+ )
168
+
169
+
170
+ class Decoder:
171
+ """Class `Decoder` for `msgspec` module with decode hook
172
+ for objects with the specified type.
173
+
174
+ ```
175
+ import enum
176
+
177
+ from datetime import datetime as dt
178
+
179
+ class Digit(enum.IntEnum):
180
+ ONE = 1
181
+ TWO = 2
182
+ THREE = 3
183
+
184
+ decoder = Decoder()
185
+ decoder.dec_hooks[dt] = lambda t, timestamp: t.fromtimestamp(timestamp)
186
+
187
+ decoder.dec_hook(dt, 1713354732) #> datetime.datetime(2024, 4, 17, 14, 52, 12)
188
+
189
+ decoder.convert("123", type=int, strict=False) #> 123
190
+ decoder.convert(1, type=Digit) #> <Digit.ONE: 1>
191
+
192
+ decoder.decode(b'{"digit":3}', type=dict[str, Digit]) #> {'digit': <Digit.THREE: 3>}
193
+ ```
194
+ """
195
+
196
+ dec_hooks: dict[typing.Any, DecHook[typing.Any]]
197
+ abstract_dec_hooks: dict[typing.Any, DecHook[typing.Any]]
198
+
199
+ def __init__(self) -> None:
200
+ self.dec_hooks = {
201
+ Option: option_dec_hook,
202
+ kungfu.library.Sum: sum_dec_hook,
203
+ datetime: datetime_dec_hook,
204
+ timedelta: timedelta_dec_hook,
205
+ kungfu.library.Some: option_dec_hook,
206
+ kungfu.library.Nothing: option_dec_hook,
207
+ }
208
+ self.abstract_dec_hooks = {
209
+ BaseEnumMeta: lambda enum_type, member: enum_type(member),
210
+ _Literal: literal_dec_hook,
211
+ }
212
+
213
+ def __repr__(self) -> str:
214
+ return "<{}: dec_hooks={!r}, abstract_dec_hooks={!r}>".format(
215
+ type(self).__name__,
216
+ self.dec_hooks,
217
+ self.abstract_dec_hooks,
218
+ )
219
+
220
+ @typing.overload
221
+ def __call__[T](
222
+ self,
223
+ type: type[T],
224
+ context: Context | None = None,
225
+ ) -> typing.ContextManager[msgspec.json.Decoder[T]]: ...
226
+
227
+ @typing.overload
228
+ def __call__(
229
+ self,
230
+ type: typing.Any,
231
+ context: Context | None = None,
232
+ ) -> typing.ContextManager[msgspec.json.Decoder[typing.Any]]: ...
233
+
234
+ @typing.overload
235
+ def __call__[T](
236
+ self,
237
+ type: type[T],
238
+ *,
239
+ strict: bool = True,
240
+ context: Context | None = None,
241
+ ) -> typing.ContextManager[msgspec.json.Decoder[T]]: ...
242
+
243
+ @typing.overload
244
+ def __call__(
245
+ self,
246
+ type: typing.Any,
247
+ *,
248
+ strict: bool = True,
249
+ context: Context | None = None,
250
+ ) -> typing.ContextManager[msgspec.json.Decoder[typing.Any]]: ...
251
+
252
+ @contextmanager
253
+ def __call__(
254
+ self,
255
+ type: typing.Any = object,
256
+ *,
257
+ strict: bool = True,
258
+ context: Context | None = None,
259
+ ) -> typing.Generator[msgspec.json.Decoder, typing.Any, None]:
260
+ """Context manager returns an `msgspec.json.Decoder` object with passed the `dec_hook`."""
261
+ yield msgspec.json.Decoder(
262
+ type=typing.Any if type is object else type,
263
+ strict=strict,
264
+ dec_hook=self.dec_hook(context),
265
+ )
266
+
267
+ def add_dec_hook[T](self, t: type[T], /) -> typing.Callable[[DecHook[T]], DecHook[T]]:
268
+ def decorator(func: DecHook[T], /) -> DecHook[T]:
269
+ return self.dec_hooks.setdefault(get_origin(t), func)
270
+
271
+ return decorator
272
+
273
+ def add_abstract_dec_hook[T](self, abstract_type: type[T], /) -> typing.Callable[[DecHook[T]], DecHook[T]]:
274
+ def decorator(func: DecHook[T], /) -> DecHook[T]:
275
+ return self.abstract_dec_hooks.setdefault(get_origin(abstract_type), func)
276
+
277
+ return decorator
278
+
279
+ def get_abstract_dec_hook(self, subtype: type[typing.Any], /) -> DecHook[typing.Any] | None:
280
+ for abstract, dec_hook in self.abstract_dec_hooks.items():
281
+ if issubclass(subtype, abstract) or issubclass(type(subtype), abstract):
282
+ return dec_hook
283
+
284
+ return None
285
+
286
+ def dec_hook(self, context: Context | None = None, /) -> DecHook[typing.Any]:
287
+ def inner(tp: typing.Any, obj: typing.Any, /) -> typing.Any:
288
+ origin_type = get_origin(tp)
289
+
290
+ if (dec_hook_func := self.dec_hooks.get(origin_type)) is None and (
291
+ dec_hook_func := self.get_abstract_dec_hook(origin_type)
292
+ ) is None:
293
+ raise NotImplementedError(
294
+ f"Not implemented decode hook for type `{fullname(origin_type)}`. "
295
+ "You can implement decode hook for this type.",
296
+ )
297
+
298
+ return bundle(dec_hook_func, context or {}, start_idx=2)(tp, obj)
299
+
300
+ return inner
301
+
302
+ def convert[T](
303
+ self,
304
+ obj: object,
305
+ *,
306
+ type: type[T] = dict,
307
+ strict: bool = True,
308
+ from_attributes: bool = False,
309
+ builtin_types: typing.Iterable[type[typing.Any]] | None = None,
310
+ str_keys: bool = False,
311
+ context: Context | None = None,
312
+ ) -> T:
313
+ return msgspec.convert(
314
+ obj,
315
+ type,
316
+ strict=strict,
317
+ from_attributes=from_attributes,
318
+ dec_hook=self.dec_hook(context),
319
+ builtin_types=builtin_types,
320
+ str_keys=str_keys,
321
+ )
322
+
323
+ @typing.overload
324
+ def decode(
325
+ self,
326
+ buf: str | bytes,
327
+ *,
328
+ context: Context | None = None,
329
+ ) -> typing.Any: ...
330
+
331
+ @typing.overload
332
+ def decode[T](
333
+ self,
334
+ buf: str | bytes,
335
+ *,
336
+ type: type[T],
337
+ context: Context | None = None,
338
+ ) -> T: ...
339
+
340
+ @typing.overload
341
+ def decode(
342
+ self,
343
+ buf: str | bytes,
344
+ *,
345
+ type: typing.Any,
346
+ context: Context | None = None,
347
+ ) -> typing.Any: ...
348
+
349
+ @typing.overload
350
+ def decode[T](
351
+ self,
352
+ buf: str | bytes,
353
+ *,
354
+ type: type[T],
355
+ strict: bool = True,
356
+ context: Context | None = None,
357
+ ) -> T: ...
358
+
359
+ @typing.overload
360
+ def decode(
361
+ self,
362
+ buf: str | bytes,
363
+ *,
364
+ type: typing.Any,
365
+ strict: bool = True,
366
+ context: Context | None = None,
367
+ ) -> typing.Any: ...
368
+
369
+ def decode(
370
+ self,
371
+ buf: str | bytes,
372
+ *,
373
+ type: typing.Any = object,
374
+ strict: bool = True,
375
+ context: Context | None = None,
376
+ ) -> typing.Any:
377
+ return msgspec.json.decode(
378
+ buf,
379
+ type=typing.Any if type is object else type,
380
+ strict=strict,
381
+ dec_hook=self.dec_hook(context),
382
+ )
383
+
384
+
385
+ decoder: typing.Final = Decoder()
386
+
387
+
388
+ __all__ = ("Decoder", "convert", "decoder")