telegrinder 0.1.dev19__py3-none-any.whl → 0.1.dev158__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 (136) 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 +29 -24
  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 +47 -0
  11. telegrinder/bot/cute_types/callback_query.py +64 -14
  12. telegrinder/bot/cute_types/inline_query.py +22 -16
  13. telegrinder/bot/cute_types/message.py +163 -43
  14. telegrinder/bot/cute_types/update.py +23 -0
  15. telegrinder/bot/dispatch/__init__.py +56 -3
  16. telegrinder/bot/dispatch/abc.py +9 -7
  17. telegrinder/bot/dispatch/composition.py +74 -0
  18. telegrinder/bot/dispatch/context.py +71 -0
  19. telegrinder/bot/dispatch/dispatch.py +86 -49
  20. telegrinder/bot/dispatch/handler/__init__.py +3 -0
  21. telegrinder/bot/dispatch/handler/abc.py +11 -5
  22. telegrinder/bot/dispatch/handler/func.py +41 -32
  23. telegrinder/bot/dispatch/handler/message_reply.py +46 -0
  24. telegrinder/bot/dispatch/middleware/__init__.py +2 -0
  25. telegrinder/bot/dispatch/middleware/abc.py +10 -4
  26. telegrinder/bot/dispatch/process.py +53 -49
  27. telegrinder/bot/dispatch/return_manager/__init__.py +19 -0
  28. telegrinder/bot/dispatch/return_manager/abc.py +95 -0
  29. telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
  30. telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
  31. telegrinder/bot/dispatch/return_manager/message.py +25 -0
  32. telegrinder/bot/dispatch/view/__init__.py +14 -2
  33. telegrinder/bot/dispatch/view/abc.py +121 -2
  34. telegrinder/bot/dispatch/view/box.py +38 -0
  35. telegrinder/bot/dispatch/view/callback_query.py +13 -38
  36. telegrinder/bot/dispatch/view/inline_query.py +11 -38
  37. telegrinder/bot/dispatch/view/message.py +11 -46
  38. telegrinder/bot/dispatch/waiter_machine/__init__.py +9 -0
  39. telegrinder/bot/dispatch/waiter_machine/machine.py +116 -0
  40. telegrinder/bot/dispatch/waiter_machine/middleware.py +76 -0
  41. telegrinder/bot/dispatch/waiter_machine/short_state.py +37 -0
  42. telegrinder/bot/polling/__init__.py +2 -0
  43. telegrinder/bot/polling/abc.py +11 -4
  44. telegrinder/bot/polling/polling.py +89 -40
  45. telegrinder/bot/rules/__init__.py +92 -5
  46. telegrinder/bot/rules/abc.py +81 -63
  47. telegrinder/bot/rules/adapter/__init__.py +11 -0
  48. telegrinder/bot/rules/adapter/abc.py +21 -0
  49. telegrinder/bot/rules/adapter/errors.py +5 -0
  50. telegrinder/bot/rules/adapter/event.py +43 -0
  51. telegrinder/bot/rules/adapter/raw_update.py +24 -0
  52. telegrinder/bot/rules/callback_data.py +159 -38
  53. telegrinder/bot/rules/command.py +116 -0
  54. telegrinder/bot/rules/enum_text.py +28 -0
  55. telegrinder/bot/rules/func.py +17 -17
  56. telegrinder/bot/rules/fuzzy.py +13 -10
  57. telegrinder/bot/rules/inline.py +61 -0
  58. telegrinder/bot/rules/integer.py +12 -7
  59. telegrinder/bot/rules/is_from.py +148 -7
  60. telegrinder/bot/rules/markup.py +21 -18
  61. telegrinder/bot/rules/mention.py +17 -0
  62. telegrinder/bot/rules/message_entities.py +33 -0
  63. telegrinder/bot/rules/regex.py +27 -19
  64. telegrinder/bot/rules/rule_enum.py +74 -0
  65. telegrinder/bot/rules/start.py +42 -0
  66. telegrinder/bot/rules/text.py +23 -14
  67. telegrinder/bot/scenario/__init__.py +2 -0
  68. telegrinder/bot/scenario/abc.py +12 -5
  69. telegrinder/bot/scenario/checkbox.py +48 -30
  70. telegrinder/bot/scenario/choice.py +16 -10
  71. telegrinder/client/__init__.py +2 -0
  72. telegrinder/client/abc.py +8 -21
  73. telegrinder/client/aiohttp.py +30 -21
  74. telegrinder/model.py +68 -37
  75. telegrinder/modules.py +189 -21
  76. telegrinder/msgspec_json.py +14 -0
  77. telegrinder/msgspec_utils.py +207 -0
  78. telegrinder/node/__init__.py +31 -0
  79. telegrinder/node/attachment.py +71 -0
  80. telegrinder/node/base.py +93 -0
  81. telegrinder/node/composer.py +71 -0
  82. telegrinder/node/container.py +22 -0
  83. telegrinder/node/message.py +18 -0
  84. telegrinder/node/rule.py +56 -0
  85. telegrinder/node/source.py +31 -0
  86. telegrinder/node/text.py +13 -0
  87. telegrinder/node/tools/__init__.py +3 -0
  88. telegrinder/node/tools/generator.py +40 -0
  89. telegrinder/node/update.py +12 -0
  90. telegrinder/rules.py +1 -1
  91. telegrinder/tools/__init__.py +165 -4
  92. telegrinder/tools/buttons.py +75 -51
  93. telegrinder/tools/error_handler/__init__.py +8 -0
  94. telegrinder/tools/error_handler/abc.py +30 -0
  95. telegrinder/tools/error_handler/error_handler.py +156 -0
  96. telegrinder/tools/formatting/__init__.py +81 -3
  97. telegrinder/tools/formatting/html.py +283 -37
  98. telegrinder/tools/formatting/links.py +32 -0
  99. telegrinder/tools/formatting/spec_html_formats.py +121 -0
  100. telegrinder/tools/global_context/__init__.py +12 -0
  101. telegrinder/tools/global_context/abc.py +66 -0
  102. telegrinder/tools/global_context/global_context.py +451 -0
  103. telegrinder/tools/global_context/telegrinder_ctx.py +25 -0
  104. telegrinder/tools/i18n/__init__.py +12 -0
  105. telegrinder/tools/i18n/base.py +31 -0
  106. telegrinder/tools/i18n/middleware/__init__.py +3 -0
  107. telegrinder/tools/i18n/middleware/base.py +26 -0
  108. telegrinder/tools/i18n/simple.py +48 -0
  109. telegrinder/tools/inline_query.py +684 -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 +651 -0
  121. telegrinder/types/methods.py +3933 -1128
  122. telegrinder/types/objects.py +4755 -1633
  123. {telegrinder-0.1.dev19.dist-info → telegrinder-0.1.dev158.dist-info}/LICENSE +2 -1
  124. telegrinder-0.1.dev158.dist-info/METADATA +108 -0
  125. telegrinder-0.1.dev158.dist-info/RECORD +126 -0
  126. {telegrinder-0.1.dev19.dist-info → telegrinder-0.1.dev158.dist-info}/WHEEL +1 -1
  127. telegrinder/bot/dispatch/waiter.py +0 -37
  128. telegrinder/result.py +0 -38
  129. telegrinder/tools/formatting/abc.py +0 -52
  130. telegrinder/tools/formatting/markdown.py +0 -57
  131. telegrinder/typegen/__init__.py +0 -1
  132. telegrinder/typegen/__main__.py +0 -3
  133. telegrinder/typegen/nicification.py +0 -20
  134. telegrinder/typegen/schema_generator.py +0 -259
  135. telegrinder-0.1.dev19.dist-info/METADATA +0 -22
  136. telegrinder-0.1.dev19.dist-info/RECORD +0 -74
@@ -2,11 +2,11 @@ import ssl
2
2
  import typing
3
3
 
4
4
  import aiohttp
5
+ import certifi
6
+ from aiohttp import ClientSession, TCPConnector
5
7
 
6
8
  from telegrinder.client.abc import ABCClient
7
- from aiohttp import ClientSession, TCPConnector
8
- from telegrinder.modules import json, JSONModule
9
- import certifi
9
+ from telegrinder.modules import JSONModule, json
10
10
 
11
11
  if typing.TYPE_CHECKING:
12
12
  from aiohttp import ClientResponse
@@ -15,10 +15,10 @@ if typing.TYPE_CHECKING:
15
15
  class AiohttpClient(ABCClient):
16
16
  def __init__(
17
17
  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
18
+ session: ClientSession | None = None,
19
+ json_processing_module: JSONModule | None = None,
20
+ timeout: aiohttp.ClientTimeout | None = None,
21
+ **session_params,
22
22
  ):
23
23
  self.session = session
24
24
  self.json_processing_module = json_processing_module or json
@@ -29,8 +29,8 @@ class AiohttpClient(ABCClient):
29
29
  self,
30
30
  url: str,
31
31
  method: str = "GET",
32
- data: typing.Optional[dict] = None,
33
- **kwargs
32
+ data: dict | None = None,
33
+ **kwargs,
34
34
  ) -> "ClientResponse":
35
35
  if not self.session:
36
36
  self.session = ClientSession(
@@ -41,7 +41,11 @@ class AiohttpClient(ABCClient):
41
41
  **self.session_params,
42
42
  )
43
43
  async with self.session.request(
44
- url=url, method=method, data=data, timeout=self.timeout, **kwargs
44
+ url=url,
45
+ method=method,
46
+ data=data,
47
+ timeout=self.timeout,
48
+ **kwargs,
45
49
  ) as response:
46
50
  await response.read()
47
51
  return response
@@ -50,32 +54,34 @@ class AiohttpClient(ABCClient):
50
54
  self,
51
55
  url: str,
52
56
  method: str = "GET",
53
- data: typing.Optional[dict] = None,
54
- **kwargs
57
+ data: dict | None = None,
58
+ **kwargs,
55
59
  ) -> dict:
56
60
  response = await self.request_raw(url, method, data, **kwargs)
57
61
  return await response.json(
58
- encoding="utf-8", loads=self.json_processing_module.loads, content_type=None
62
+ encoding="utf-8",
63
+ loads=self.json_processing_module.loads,
64
+ content_type=None,
59
65
  )
60
66
 
61
67
  async def request_text(
62
68
  self,
63
69
  url: str,
64
70
  method: str = "GET",
65
- data: typing.Union[dict, aiohttp.FormData, None] = None,
66
- **kwargs
71
+ data: dict | aiohttp.FormData | None = None,
72
+ **kwargs,
67
73
  ) -> str:
68
- response = await self.request_raw(url, method, data, **kwargs)
74
+ response = await self.request_raw(url, method, data, **kwargs) # type: ignore
69
75
  return await response.text(encoding="utf-8")
70
76
 
71
77
  async def request_bytes(
72
78
  self,
73
79
  url: str,
74
80
  method: str = "GET",
75
- data: typing.Union[dict, aiohttp.FormData, None] = None,
76
- **kwargs
81
+ data: dict | aiohttp.FormData | None = None,
82
+ **kwargs,
77
83
  ) -> bytes:
78
- response = await self.request_raw(url, method, data, **kwargs)
84
+ response = await self.request_raw(url, method, data, **kwargs) # type: ignore
79
85
  if response._body is None:
80
86
  await response.read()
81
87
  return response._body
@@ -84,8 +90,8 @@ class AiohttpClient(ABCClient):
84
90
  self,
85
91
  url: str,
86
92
  method: str = "GET",
87
- data: typing.Optional[dict] = None,
88
- **kwargs
93
+ data: dict | None = None,
94
+ **kwargs,
89
95
  ) -> bytes:
90
96
  response = await self.request_raw(url, method, data, **kwargs)
91
97
  return response._body
@@ -111,3 +117,6 @@ class AiohttpClient(ABCClient):
111
117
  if self.session._connector is not None and self.session._connector_owner:
112
118
  self.session._connector.close()
113
119
  self.session._connector = None
120
+
121
+
122
+ __all__ = ("AiohttpClient",)
telegrinder/model.py CHANGED
@@ -1,65 +1,96 @@
1
- import msgspec
2
- from telegrinder.result import Result
3
- from telegrinder.modules import json
4
- from msgspec import Raw
5
1
  import typing
2
+ from types import NoneType
3
+
4
+ import msgspec
5
+ from fntypes.co import Nothing, Result, Some
6
+
7
+ from .msgspec_utils import decoder, encoder
8
+
9
+ T = typing.TypeVar("T")
6
10
 
7
11
  if typing.TYPE_CHECKING:
8
12
  from telegrinder.api.error import APIError
13
+
9
14
 
10
- T = typing.TypeVar("T")
11
- encoder = msgspec.json.Encoder()
15
+ MODEL_CONFIG: typing.Final[dict[str, typing.Any]] = {
16
+ "omit_defaults": True,
17
+ "dict": True,
18
+ "rename": {"from_": "from"},
19
+ }
12
20
 
13
21
 
22
+ @typing.overload
14
23
  def full_result(
15
- result: Result[msgspec.Raw, "APIError"], full_t: typing.Type[T]
24
+ result: Result[msgspec.Raw, "APIError"], full_t: type[T]
16
25
  ) -> 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
-
26
+ ...
21
27
 
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
28
 
29
+ @typing.overload
30
+ def full_result(
31
+ result: Result[msgspec.Raw, "APIError"],
32
+ full_t: tuple[type[T], ...],
33
+ ) -> Result[T, "APIError"]:
34
+ ...
31
35
 
32
- model_config = {"rename": {"from_": "from"}, "omit_defaults": True}
33
36
 
37
+ def full_result(
38
+ result: Result[msgspec.Raw, "APIError"],
39
+ full_t: type[T] | tuple[type[T], ...],
40
+ ) -> Result[T, "APIError"]:
41
+ return result.map(lambda v: decoder.decode(v, type=full_t)) # type: ignore
34
42
 
35
- class Model(msgspec.Struct, **model_config):
36
- _dict_cached: typing.Optional[dict] = None
37
43
 
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
44
+ def convert(d: typing.Any, serialize: bool = True) -> typing.Any:
45
+ if isinstance(d, Model):
46
+ converted_dct = convert(d.to_dict(), serialize=False)
47
+ return encoder.encode(converted_dct) if serialize is True else converted_dct
48
+
49
+ if isinstance(d, dict):
50
+ return {
51
+ k: convert(v, serialize=serialize)
52
+ for k, v in d.items()
53
+ if type(v) not in (NoneType, Nothing)
54
+ }
55
+
56
+ if isinstance(d, list):
57
+ converted_lst = [convert(x, serialize=False) for x in d]
58
+ return encoder.encode(converted_lst) if serialize is True else converted_lst
59
+
60
+ return d
43
61
 
44
62
 
45
- def get_params(params: dict) -> dict:
63
+ def get_params(params: dict[str, typing.Any]) -> dict[str, typing.Any]:
46
64
  return {
47
- k: v for k, v in (
65
+ k: v.unwrap() if v and isinstance(v, Some) else v
66
+ for k, v in (
48
67
  *params.items(),
49
- *params.pop("other").items()
68
+ *params.pop("other", {}).items(),
50
69
  )
51
- if k != "self"
52
- and v is not None
70
+ if k != "self" and type(v) not in (NoneType, Nothing)
53
71
  }
54
72
 
55
73
 
74
+ class Model(msgspec.Struct, **MODEL_CONFIG):
75
+ def to_dict(
76
+ self,
77
+ *,
78
+ exclude_fields: set[str] | None = None,
79
+ ) -> dict[str, typing.Any]:
80
+ exclude_fields = exclude_fields or set()
81
+ if "model_as_dict" not in self.__dict__:
82
+ self.__dict__["model_as_dict"] = msgspec.structs.asdict(self)
83
+ return {
84
+ key: value
85
+ for key, value in self.__dict__["model_as_dict"].items()
86
+ if key not in exclude_fields
87
+ }
88
+
89
+
56
90
  __all__ = (
91
+ "Model",
57
92
  "convert",
58
- "model_config",
59
- "encoder",
60
93
  "full_result",
61
- "msgspec",
62
- "Model",
63
- "Raw",
64
94
  "get_params",
95
+ "MODEL_CONFIG",
65
96
  )
telegrinder/modules.py CHANGED
@@ -1,39 +1,78 @@
1
- import logging
1
+ import os
2
2
  import typing
3
3
 
4
4
  from choicelib import choice_in_order
5
- from typing_extensions import Protocol
6
5
 
7
6
 
8
- class JSONModule(Protocol):
9
- def loads(self, s: str) -> typing.Union[dict, list]:
7
+ class JSONModule(typing.Protocol):
8
+ def loads(self, s: str) -> dict | list:
10
9
  ...
11
10
 
12
- def dumps(self, o: typing.Union[dict, list]) -> str:
11
+ def dumps(self, o: dict | list) -> str:
13
12
  ...
14
13
 
15
14
 
15
+ class LoggerModule(typing.Protocol):
16
+ def debug(self, __msg: object, *args: object, **kwargs: object):
17
+ ...
18
+
19
+ def info(self, __msg: object, *args: object, **kwargs: object):
20
+ ...
21
+
22
+ def warning(self, __msg: object, *args: object, **kwargs: object):
23
+ ...
24
+
25
+ def error(self, __msg: object, *args: object, **kwargs: object):
26
+ ...
27
+
28
+ def critical(self, __msg: object, *args: object, **kwargs: object):
29
+ ...
30
+
31
+ def exception(self, __msg: object, *args: object, **kwargs: object):
32
+ ...
33
+
34
+ def set_level(
35
+ self,
36
+ level: typing.Literal[
37
+ "DEBUG",
38
+ "INFO",
39
+ "WARNING",
40
+ "ERROR",
41
+ "CRITICAL",
42
+ "EXCEPTION",
43
+ ],
44
+ ) -> None:
45
+ ...
46
+
47
+
48
+ logger: LoggerModule
16
49
  json: JSONModule = choice_in_order(
17
- ["ujson", "hyperjson", "orjson"], do_import=True, default="json"
50
+ ["ujson", "hyperjson", "orjson"], do_import=True, default="telegrinder.msgspec_json"
18
51
  )
19
-
20
52
  logging_module = choice_in_order(["loguru"], default="logging")
53
+ logging_level = os.getenv("LOGGER_LEVEL", default="DEBUG").upper()
21
54
 
22
55
  if logging_module == "loguru":
23
56
  import os
24
57
  import sys
25
58
 
26
- if not os.environ.get("LOGURU_AUTOINIT"):
27
- os.environ["LOGURU_AUTOINIT"] = "0"
28
59
  from loguru import logger # type: ignore
29
60
 
30
- if not logger._core.handlers: # type: ignore
31
- log_format = (
32
- "<level>{level: <8}</level> | "
33
- "{time:YYYY-MM-DD HH:mm:ss} | "
34
- "{name}:{function}:{line} > <level>{message}</level>"
35
- )
36
- logger.add(sys.stderr, format=log_format, enqueue=True, colorize=True)
61
+ os.environ.setdefault("LOGURU_AUTOINIT", "0")
62
+ log_format = (
63
+ "<level>{level: <8}</level> | "
64
+ "<lg>{time:YYYY-MM-DD HH:mm:ss}</lg> | "
65
+ "<le>{name}</le>:<le>{function}</le>:"
66
+ "<le>{line}</le> > <lw>{message}</lw>"
67
+ )
68
+ logger.remove() # type: ignore
69
+ handler_id = logger.add( # type: ignore
70
+ sink=sys.stderr,
71
+ format=log_format,
72
+ enqueue=True,
73
+ colorize=True,
74
+ level=logging_level,
75
+ )
37
76
 
38
77
  elif logging_module == "logging":
39
78
  """
@@ -41,8 +80,111 @@ elif logging_module == "logging":
41
80
  About:
42
81
  https://docs.python.org/3/howto/logging-cookbook.html#use-of-alternative-formatting-styles
43
82
  """
83
+
44
84
  import inspect
45
85
  import logging
86
+ import sys
87
+
88
+ import colorama
89
+
90
+ colorama.just_fix_windows_console() # init & fix console
91
+
92
+ FORMAT = (
93
+ "<white>{name: <4} |</white> <level>{levelname: <8}</level>"
94
+ " <white>|</white> <green>{asctime}</green> <white>|</white> <level_module>"
95
+ "{module}</level_module><white>:</white><level_func>"
96
+ "{funcName}</level_func><white>:</white><level_lineno>"
97
+ "{lineno}</level_lineno><white> > </white><level_message>"
98
+ "{message}</level_message>"
99
+ )
100
+ COLORS = {
101
+ "red": colorama.Fore.LIGHTRED_EX,
102
+ "green": colorama.Fore.LIGHTGREEN_EX,
103
+ "blue": colorama.Fore.LIGHTBLUE_EX,
104
+ "white": colorama.Fore.LIGHTWHITE_EX,
105
+ "yellow": colorama.Fore.LIGHTYELLOW_EX,
106
+ "magenta": colorama.Fore.LIGHTMAGENTA_EX,
107
+ "cyan": colorama.Fore.LIGHTCYAN_EX,
108
+ "reset": colorama.Style.RESET_ALL,
109
+ }
110
+ LEVEL_SETTINGS = {
111
+ "INFO": {
112
+ "level": "green",
113
+ "level_module": "blue",
114
+ "level_func": "cyan",
115
+ "level_lineno": "green",
116
+ "level_message": "white",
117
+ },
118
+ "DEBUG": {
119
+ "level": "blue",
120
+ "level_module": "yellow",
121
+ "level_func": "green",
122
+ "level_lineno": "cyan",
123
+ "level_message": "blue",
124
+ },
125
+ "WARNING": {
126
+ "level": "yellow",
127
+ "level_module": "red",
128
+ "level_func": "green",
129
+ "level_lineno": "red",
130
+ "level_message": "yellow",
131
+ },
132
+ "ERROR": {
133
+ "level": "red",
134
+ "level_module": "magenta",
135
+ "level_func": "yellow",
136
+ "level_lineno": "green",
137
+ "level_message": "red",
138
+ },
139
+ "CRITICAL": {
140
+ "level": "cyan",
141
+ "level_module": "yellow",
142
+ "level_func": "yellow",
143
+ "level_lineno": "yellow",
144
+ "level_message": "cyan",
145
+ },
146
+ }
147
+ FORMAT = (
148
+ FORMAT
149
+ .replace("<white>", COLORS["white"])
150
+ .replace("</white>", COLORS["reset"])
151
+ .replace("<green>", COLORS["green"])
152
+ .replace("</green>", COLORS["reset"])
153
+ )
154
+ LEVEL_FORMATS: dict[str, str] = {}
155
+ for level, settings in LEVEL_SETTINGS.items():
156
+ fmt = FORMAT
157
+ for name, color in settings.items():
158
+ fmt = (
159
+ fmt
160
+ .replace(f"<{name}>", COLORS[color])
161
+ .replace(f"</{name}>", COLORS["reset"])
162
+ )
163
+ LEVEL_FORMATS[level] = fmt
164
+
165
+
166
+ class TelegrinderLoggingFormatter(logging.Formatter):
167
+ def format(self, record: logging.LogRecord) -> str:
168
+ if not record.funcName or record.funcName == "<module>":
169
+ record.funcName = "\b"
170
+ frame = next(
171
+ (
172
+ frame
173
+ for frame in inspect.stack()
174
+ if frame.filename == record.pathname
175
+ and frame.lineno == record.lineno
176
+ ),
177
+ None,
178
+ )
179
+ if frame:
180
+ module = inspect.getmodule(frame.frame)
181
+ record.module = module.__name__ if module else "<module>"
182
+ return logging.Formatter(
183
+ LEVEL_FORMATS.get(record.levelname),
184
+ datefmt="%Y-%m-%d %H:%M:%S",
185
+ style="{",
186
+ ).format(record)
187
+
46
188
 
47
189
  class LogMessage:
48
190
  def __init__(self, fmt, args, kwargs):
@@ -50,15 +192,16 @@ elif logging_module == "logging":
50
192
  self.args = args
51
193
  self.kwargs = kwargs
52
194
 
53
- def __str__(self):
54
- return self.fmt.format(*self.args)
195
+ def __str__(self) -> str:
196
+ return self.fmt.format(*self.args, **self.kwargs)
55
197
 
56
- class StyleAdapter(logging.LoggerAdapter):
198
+ class TelegrinderLoggingStyleAdapter(logging.LoggerAdapter):
57
199
  def __init__(self, logger, extra=None):
58
200
  super().__init__(logger, extra or {})
59
201
 
60
202
  def log(self, level, msg, *args, **kwargs):
61
203
  if self.isEnabledFor(level):
204
+ kwargs.setdefault("stacklevel", 2)
62
205
  msg, args, kwargs = self.proc(msg, args, kwargs)
63
206
  self.logger._log(level, msg, args, **kwargs)
64
207
 
@@ -68,9 +211,34 @@ elif logging_module == "logging":
68
211
  for key in inspect.getfullargspec(self.logger._log).args[1:]
69
212
  if key in kwargs
70
213
  }
214
+
71
215
  if isinstance(msg, str):
72
216
  msg = LogMessage(msg, args, kwargs)
73
- args = ()
217
+ args = tuple()
74
218
  return msg, args, log_kwargs
75
219
 
76
- logger = StyleAdapter(logging.getLogger("telegrinder")) # type: ignore
220
+ handler = logging.StreamHandler(sys.stderr)
221
+ handler.setFormatter(TelegrinderLoggingFormatter())
222
+ logger = logging.getLogger("telegrinder") # type: ignore
223
+ logger.setLevel(logging.getLevelName(logging_level)) # type: ignore
224
+ logger.addHandler(handler) # type: ignore
225
+ logger = TelegrinderLoggingStyleAdapter(logger) # type: ignore
226
+
227
+
228
+ def _set_logger_level(level):
229
+ level = level.upper()
230
+ if logging_module == "logging":
231
+ import logging
232
+
233
+ logging.getLogger("telegrinder").setLevel(logging.getLevelName(level))
234
+ elif logging_module == "loguru":
235
+ import loguru # type: ignore
236
+
237
+ if handler_id in loguru.logger._core.handlers: # type: ignore
238
+ loguru.logger._core.handlers[handler_id]._levelno = loguru.logger.level(level).no # type: ignore
239
+
240
+
241
+ setattr(logger, "set_level", staticmethod(_set_logger_level)) # type: ignore
242
+
243
+
244
+ __all__ = ("json", "logger")
@@ -0,0 +1,14 @@
1
+ import typing
2
+
3
+ from .msgspec_utils import decoder, encoder
4
+
5
+
6
+ def loads(s: str | bytes) -> dict[str, typing.Any] | list[typing.Any]:
7
+ return decoder.decode(s, type=dict[str, typing.Any] | list[typing.Any]) # type: ignore
8
+
9
+
10
+ def dumps(o: dict[str, typing.Any] | list[typing.Any]) -> str:
11
+ return encoder.encode(o)
12
+
13
+
14
+ __all__ = ("dumps", "loads")