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
telegrinder/modules.py ADDED
@@ -0,0 +1,1081 @@
1
+ # pyright: reportMissingImports=none, reportAttributeAccessIssue=none, reportMissingModuleSource=none
2
+
3
+ import asyncio
4
+ import contextvars
5
+ import inspect
6
+ import logging
7
+ import os
8
+ import pathlib
9
+ import re
10
+ import shlex
11
+ import sys
12
+ import traceback
13
+ import types
14
+ import typing
15
+ from annotationlib import type_repr
16
+ from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler, WatchedFileHandler
17
+
18
+ import betterconf
19
+ from choicelib import choice_in_order
20
+ from kungfu.library.monad.option import NOTHING, Option, Some
21
+
22
+ if typing.TYPE_CHECKING:
23
+ from _typeshed import OptExcInfo
24
+ from loguru import FileHandlerConfig as _LoguruFileHandler
25
+ else:
26
+
27
+ class _LoguruFileHandler(dict):
28
+ def __new__(cls, *args, **kwargs):
29
+ if logging_module != "loguru":
30
+ raise ModuleNotFoundError("FileHandlerConfig uses for loguru.") from None
31
+
32
+ return super().__new__(cls)
33
+
34
+
35
+ type LoggerModule = typing.Literal["logging", "loguru", "structlog"]
36
+ type Sink = typing.TextIO | typing.Any
37
+ type LoggerLevel = typing.Literal["DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL", "EXCEPTION"]
38
+
39
+ _LoggingFileHandler = RotatingFileHandler | TimedRotatingFileHandler | WatchedFileHandler | logging.FileHandler
40
+
41
+
42
+ class Colors:
43
+ RESET = "\033[0m"
44
+ RED = "\033[31m"
45
+ GREEN = "\033[32m"
46
+ YELLOW = "\033[33m"
47
+ BLUE = "\033[34m"
48
+ MAGENTA = "\033[35m"
49
+ CYAN = "\033[36m"
50
+ WHITE = "\033[37m"
51
+ BLACK = "\033[30m"
52
+ LIGHT_RED = "\033[91m"
53
+ LIGHT_GREEN = "\033[92m"
54
+ LIGHT_YELLOW = "\033[93m"
55
+ LIGHT_BLUE = "\033[94m"
56
+ LIGHT_MAGENTA = "\033[95m"
57
+ LIGHT_CYAN = "\033[96m"
58
+ LIGHT_WHITE = "\033[97m"
59
+ LIGHT_BLACK = "\033[90m"
60
+
61
+
62
+ LOGGER_MODULE: typing.Final = typing.cast(
63
+ "LoggerModule",
64
+ choice_in_order(["structlog", "loguru"], default="logging", do_import=False),
65
+ )
66
+ DEFAULT_LOGGING_FORMAT: typing.Final = (
67
+ "<light_white>{name: <4} |</light_white> <level>{levelname: <8}</level>"
68
+ " <light_white>|</light_white> <light_green>{asctime}</light_green> <light_white>"
69
+ "|</light_white> <level_module>{module}</level_module><light_white>:</light_white>"
70
+ "<func_name>{funcName}</func_name><light_white>:</light_white><lineno>{lineno}</lineno>"
71
+ " <light_white>></light_white> <message>{message}</message>"
72
+ )
73
+ DEFAULT_STRUCTLOG_FORMAT: typing.Final = (
74
+ "[<light_blue>{name}</light_blue>] {location} "
75
+ "[<light_black>{asctime}</light_black>] "
76
+ "<light_white>~</light_white> {message}"
77
+ )
78
+ DEFAULT_LOGURU_FORMAT: typing.Final = (
79
+ "telegrinder | <level>{level: <8}</level> | "
80
+ "<lg>{time:YYYY-MM-DD HH:mm:ss}</lg> | "
81
+ "<le>{name}</le>:<le>{function}</le>:"
82
+ "<le>{line}</le> > <lw>{message}</lw>"
83
+ )
84
+
85
+ CALL_STACK_CONTEXT: typing.Final = contextvars.ContextVar[tuple[types.FrameType, "OptExcInfo"]]("_call_stack")
86
+
87
+ COLORS: typing.Final = dict(
88
+ reset=Colors.RESET,
89
+ red=Colors.RED,
90
+ green=Colors.GREEN,
91
+ blue=Colors.BLUE,
92
+ white=Colors.WHITE,
93
+ yellow=Colors.YELLOW,
94
+ magenta=Colors.MAGENTA,
95
+ cyan=Colors.CYAN,
96
+ black=Colors.BLACK,
97
+ light_red=Colors.LIGHT_RED,
98
+ light_green=Colors.LIGHT_GREEN,
99
+ light_blue=Colors.LIGHT_BLUE,
100
+ light_white=Colors.LIGHT_WHITE,
101
+ light_yellow=Colors.LIGHT_YELLOW,
102
+ light_magenta=Colors.LIGHT_MAGENTA,
103
+ light_cyan=Colors.LIGHT_CYAN,
104
+ light_black=Colors.LIGHT_BLACK,
105
+ )
106
+ LEVEL_FORMAT_SETTINGS: typing.Final = dict(
107
+ DEBUG=dict(
108
+ level="light_blue",
109
+ level_module="blue",
110
+ func_name="blue",
111
+ lineno="light_yellow",
112
+ message="light_blue",
113
+ ),
114
+ INFO=dict(
115
+ level="cyan",
116
+ level_module="light_cyan",
117
+ func_name="light_cyan",
118
+ lineno="light_yellow",
119
+ message="light_green",
120
+ ),
121
+ WARNING=dict(
122
+ level="light_yellow",
123
+ level_module="light_magenta",
124
+ func_name="light_magenta",
125
+ lineno="light_blue",
126
+ message="light_yellow",
127
+ ),
128
+ ERROR=dict(
129
+ level="red",
130
+ level_module="light_yellow",
131
+ func_name="light_yellow",
132
+ lineno="light_blue",
133
+ message="light_red",
134
+ ),
135
+ CRITICAL=dict(
136
+ level="magenta",
137
+ level_module="light_red",
138
+ func_name="light_red",
139
+ lineno="light_yellow",
140
+ message="light_magenta",
141
+ ),
142
+ )
143
+ VARIABLE_NAME_PATTERN: typing.Final = re.compile(r"[A-Za-z_][A-Za-z_0-9]*")
144
+ ASSIGNMENT_OPERATOR: typing.Final = "="
145
+ LOGGER_LEVELS: typing.Final = ("DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL", "EXCEPTION")
146
+ LOGGER_MODULES: typing.Final = ("logging", "loguru", "structlog")
147
+ _NODEFAULT: typing.Final = typing.cast("typing.Any", object())
148
+ _LOAD_ENV_FILE = True
149
+ _ENV_FILE_NAME = ".env"
150
+ _ENV_FILE_PATH: pathlib.Path | None = None
151
+ _ANSI_ESCAPE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
152
+
153
+
154
+ def _find_env_file() -> Option[pathlib.Path]:
155
+ caller_frame = sys._getframe()
156
+
157
+ while caller_frame:
158
+ if caller_frame.f_back is None:
159
+ break
160
+
161
+ caller_frame = caller_frame.f_back
162
+
163
+ caller_dir = os.path.dirname(caller_frame.f_code.co_filename)
164
+ start_dir = sys.path[0]
165
+
166
+ # Handle empty paths (can happen on Windows in certain launch conditions)
167
+ if not caller_dir or not start_dir:
168
+ search_path = caller_dir or start_dir or "."
169
+ else:
170
+ try:
171
+ search_path = os.path.relpath(caller_dir, start_dir)
172
+ except ValueError:
173
+ search_path = caller_dir
174
+
175
+ for root, _, files in os.walk(search_path):
176
+ if _ENV_FILE_NAME in files:
177
+ return Some(pathlib.Path(root) / _ENV_FILE_NAME)
178
+
179
+ return NOTHING
180
+
181
+
182
+ @typing.overload
183
+ def take_env(var_name: str, /) -> str: ...
184
+
185
+
186
+ @typing.overload
187
+ def take_env[T](var_name: str, var_type: type[T], /) -> T: ...
188
+
189
+
190
+ @typing.overload
191
+ def take_env[T](var_name: str, var_type: type[T], /, *, default: T) -> T: ...
192
+
193
+
194
+ def take_env[T](
195
+ name: str,
196
+ var_type: type[T] = str,
197
+ /,
198
+ *,
199
+ default: T = _NODEFAULT,
200
+ ) -> T:
201
+ try:
202
+ value = DOTENV.get(name)
203
+ except betterconf.VariableNotFoundError:
204
+ if default is _NODEFAULT:
205
+ raise
206
+ return default
207
+
208
+ if var_type is str:
209
+ return value # type: ignore
210
+
211
+ if var_type not in CASTERS:
212
+ raise NotImplementedError(f"Caster for type `{type_repr(var_type)}` is not implemented.")
213
+
214
+ return CASTERS[var_type].cast(value)
215
+
216
+
217
+ class _LoggerLevelCaster(betterconf.AbstractCaster):
218
+ def cast(self, value: str) -> LoggerLevel:
219
+ if value not in LOGGER_LEVELS:
220
+ raise betterconf.ImpossibleToCastError(value, self)
221
+ return value
222
+
223
+
224
+ class _LoggerModuleCaster(betterconf.AbstractCaster):
225
+ def cast(self, value: str) -> LoggerModule:
226
+ if value not in LOGGER_MODULES:
227
+ raise betterconf.ImpossibleToCastError(value, self)
228
+ return value
229
+
230
+
231
+ class _DotenvProvider(betterconf.AbstractProvider):
232
+ __slots__ = ("env_file", "loaded")
233
+
234
+ env_file: pathlib.Path | None
235
+ loaded: bool
236
+
237
+ def __init__(self) -> None:
238
+ self.env_file = None
239
+ self.loaded = False
240
+
241
+ def load(self) -> None:
242
+ if self.loaded:
243
+ return
244
+
245
+ self.loaded = True
246
+ self.env_file = _ENV_FILE_PATH if _ENV_FILE_PATH is not None else _find_env_file().unwrap_or_none()
247
+
248
+ if self.env_file is None:
249
+ return
250
+
251
+ variables: dict[str, str] = {}
252
+
253
+ for line in self.env_file.read_text().splitlines():
254
+ match tuple(shlex.shlex(instream=line, posix=True)):
255
+ case (var_name, operator, *tokens) if (
256
+ tokens and operator == ASSIGNMENT_OPERATOR and VARIABLE_NAME_PATTERN.match(var_name)
257
+ ):
258
+ variables[var_name] = "".join(tokens).replace(r"\n", "\n").replace(r"\t", "\t")
259
+ case _:
260
+ continue
261
+
262
+ os.environ.update(variables)
263
+
264
+ def get(self, name: str) -> str:
265
+ if not self.loaded and _LOAD_ENV_FILE is True:
266
+ self.load()
267
+ return ENV.get(name)
268
+
269
+
270
+ DOTENV: typing.Final = _DotenvProvider()
271
+ ENV: typing.Final = betterconf.EnvironmentProvider()
272
+ CASTERS: typing.Final[dict[type[typing.Any], betterconf.AbstractCaster]] = {
273
+ LoggerLevel: (to_logger_level := _LoggerLevelCaster()), # type: ignore
274
+ **betterconf.caster.BUILTIN_CASTERS,
275
+ }
276
+
277
+
278
+ @betterconf.betterconf(prefix="TELEGRINDER_LOGGER", provider=DOTENV)
279
+ class LoggerConfig:
280
+ MODULE: typing.Literal["logging", "loguru", "structlog"] = betterconf.field(
281
+ default=LOGGER_MODULE,
282
+ caster=_LoggerModuleCaster(),
283
+ )
284
+ LEVEL: LoggerLevel = betterconf.field(
285
+ default="DEBUG",
286
+ caster=to_logger_level,
287
+ )
288
+ FORMAT: str | None = betterconf.constant_field(None)
289
+ COLORIZE: bool = betterconf.field(
290
+ default=True,
291
+ caster=betterconf.caster.to_bool,
292
+ )
293
+ FILE_HANDLER_FORMAT: str | None = betterconf.constant_field(None)
294
+ FILE_HANDLER_COLORIZE: bool = betterconf.field(
295
+ default=False,
296
+ caster=betterconf.caster.to_bool,
297
+ )
298
+
299
+
300
+ def _is_async_logger(logger: typing.Any, /) -> bool:
301
+ return all(hasattr(logger, attr) for attr in AnyAsyncLogger.__protocol_attrs__)
302
+
303
+
304
+ class AnyLogger(typing.Protocol):
305
+ def debug(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
306
+
307
+ def info(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
308
+
309
+ def warning(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
310
+
311
+ def error(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
312
+
313
+ def critical(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
314
+
315
+ def exception(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
316
+
317
+
318
+ class AnyAsyncLogger(typing.Protocol):
319
+ async def adebug(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
320
+
321
+ async def ainfo(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
322
+
323
+ async def awarning(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
324
+
325
+ async def aerror(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
326
+
327
+ async def acritical(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
328
+
329
+ async def aexception(self, __msg: str, *args: typing.Any, **kwargs: typing.Any) -> None: ...
330
+
331
+
332
+ class Logger(AnyLogger, AnyAsyncLogger, typing.Protocol):
333
+ def set_logger(self, __logger: AnyLogger) -> None: ...
334
+
335
+
336
+ class WrapperAsyncLogger:
337
+ def __init__(self, logger: Logger, /) -> None:
338
+ self._logger = logger
339
+
340
+ def __getattr__(self, __name: str) -> typing.Any:
341
+ if __name in AnyAsyncLogger.__dict__:
342
+ return lambda *args, **kwargs: self._async_log(
343
+ getattr(self._logger, __name.removeprefix("a")),
344
+ *args,
345
+ **kwargs,
346
+ )
347
+
348
+ return super().__getattribute__(__name)
349
+
350
+ async def _async_log(
351
+ self,
352
+ method: typing.Callable[..., typing.Any],
353
+ /,
354
+ *args: typing.Any,
355
+ **kwargs: typing.Any,
356
+ ) -> None:
357
+ tok = CALL_STACK_CONTEXT.set((sys._getframe(0).f_back, sys.exc_info())) # type: ignore
358
+ ctx = contextvars.copy_context()
359
+
360
+ try:
361
+ await asyncio.get_running_loop().run_in_executor(
362
+ executor=None,
363
+ func=lambda: ctx.run(lambda: method(*args, **kwargs)),
364
+ )
365
+ finally:
366
+ CALL_STACK_CONTEXT.reset(tok)
367
+
368
+
369
+ class LoggingFormatter(logging.Formatter):
370
+ def __init__(self, format: str, colorize: bool, logger_module: str, /) -> None:
371
+ self.level_formats = _get_level_format(format, colorize)
372
+ self.colorize = colorize
373
+ self.logger_module = logger_module
374
+ super().__init__()
375
+
376
+ def format(self, record: logging.LogRecord) -> str:
377
+ if self.logger_module == "logging":
378
+ record = _rich_log_record(record, self.logger_module)
379
+
380
+ message = logging.Formatter(
381
+ fmt=self.level_formats.get(record.levelname),
382
+ datefmt="%Y-%m-%d %H:%M:%S",
383
+ style="{",
384
+ ).format(record)
385
+
386
+ if not self.colorize:
387
+ message = _remove_ansi_colors(message)
388
+
389
+ return message
390
+
391
+
392
+ class LogMessage:
393
+ def __init__(self, fmt: str, args: typing.Any, kwargs: typing.Any) -> None:
394
+ self.fmt = fmt
395
+ self.args = args
396
+ self.kwargs = kwargs
397
+
398
+ def __str__(self) -> str:
399
+ return self.fmt.format(*self.args, **self.kwargs)
400
+
401
+
402
+ class LoggingStyleAdapter(logging.LoggerAdapter):
403
+ logger: logging.Logger
404
+
405
+ def __init__(
406
+ self,
407
+ logger: logging.Logger,
408
+ **extra: typing.Any,
409
+ ) -> None:
410
+ super().__init__(logger, extra=extra or None)
411
+ self.log_arg_names = frozenset(inspect.getfullargspec(self.logger._log).args[1:])
412
+
413
+ def log(self, level: int, msg: typing.Any, *args: typing.Any, **kwargs: typing.Any) -> None:
414
+ if self.isEnabledFor(level):
415
+ msg, args, kwargs = self.proc(msg, args, kwargs)
416
+ self.logger._log(level, msg, args, **kwargs)
417
+
418
+ def proc(
419
+ self,
420
+ msg: typing.Any,
421
+ args: tuple[typing.Any, ...],
422
+ kwargs: dict[str, typing.Any],
423
+ ) -> tuple[typing.Any, tuple[typing.Any, ...], dict[str, typing.Any]]:
424
+ kwargs.setdefault("stacklevel", 2)
425
+
426
+ if isinstance(msg, str):
427
+ msg = LogMessage(msg, args, kwargs)
428
+ args = tuple()
429
+
430
+ return msg, args, {name: kwargs[name] for name in self.log_arg_names if name in kwargs}
431
+
432
+
433
+ def _remove_handlers(logger: typing.Any, /) -> None:
434
+ for hdlr in logger.handlers[:]:
435
+ logger.removeHandler(hdlr)
436
+
437
+
438
+ def _get_level_format(format: str, colorize: bool, /) -> dict[str, str]:
439
+ level_formats = {}
440
+
441
+ for level, settings in LEVEL_FORMAT_SETTINGS.items():
442
+ fmt = format
443
+
444
+ for name, color in COLORS.items():
445
+ fmt = fmt.replace(f"<{name}>", color if colorize else "").replace(
446
+ f"</{name}>", COLORS["reset"] if colorize else ""
447
+ )
448
+
449
+ for name, color in settings.items():
450
+ fmt = fmt.replace(f"<{name}>", COLORS[color] if colorize else "").replace(
451
+ f"</{name}>", COLORS["reset"] if colorize else ""
452
+ )
453
+
454
+ level_formats[level] = fmt
455
+
456
+ return level_formats
457
+
458
+
459
+ def _rich_log_record(record: logging.LogRecord, logger_module: str, /) -> logging.LogRecord:
460
+ call_stack, exc_info = CALL_STACK_CONTEXT.get((None, None))
461
+ frame = call_stack or sys._getframe(1)
462
+
463
+ if call_stack is None:
464
+ while frame:
465
+ if frame.f_code.co_filename == record.pathname and frame.f_lineno == record.lineno:
466
+ if logger_module == "structlog":
467
+ frame = frame.f_back.f_back.f_back # pyright: ignore[reportOptionalMemberAccess]
468
+
469
+ break
470
+
471
+ frame = frame.f_back
472
+
473
+ if record.levelno >= logging.ERROR and record.exc_info is not None:
474
+ if exc_info is not None and any(exc_info):
475
+ record.exc_info = exc_info
476
+ elif logger_module == "structlog":
477
+ record.exc_info = None
478
+
479
+ if frame is not None:
480
+ record.funcName = frame.f_code.co_name
481
+ record.module = frame.f_globals.get("__name__", "<module>")
482
+ record.lineno = frame.f_lineno
483
+
484
+ if not record.funcName or record.funcName == "<module>":
485
+ record.funcName = "\b"
486
+
487
+ return record
488
+
489
+
490
+ def _loguru_filter(record: dict[str, typing.Any]) -> bool:
491
+ if record["extra"].get("telegrinder", False) is not True:
492
+ return False
493
+
494
+ frame, exc_info = CALL_STACK_CONTEXT.get((None, None))
495
+
496
+ if frame is not None:
497
+ if "file" in record:
498
+ file_name = frame.f_code.co_filename
499
+ record["file"].name = os.path.basename(file_name)
500
+ record["file"].path = file_name
501
+
502
+ record["name"] = frame.f_globals.get("__name__", "<module>")
503
+ record["module"] = record["name"].split(".")[-1]
504
+ record["line"] = frame.f_lineno
505
+ record["function"] = frame.f_code.co_name
506
+
507
+ if (
508
+ exc_info is not None
509
+ and any(exc_info)
510
+ and record["exception"] is not None
511
+ and not any(record["exception"])
512
+ and record["level"].no >= logger.logger.level("ERROR").no
513
+ ):
514
+ from loguru._recattrs import RecordException # type: ignore
515
+
516
+ record["exception"] = RecordException(exc_info[0], exc_info[1], exc_info[2]) # type: ignore
517
+
518
+ return True
519
+
520
+
521
+ def _remove_ansi_colors(text: str) -> str:
522
+ return _ANSI_ESCAPE.sub("", text)
523
+
524
+
525
+ class _json: # noqa: N801
526
+ def __getattr__(self, __name: str) -> typing.Any:
527
+ from telegrinder.msgspec_utils import json
528
+
529
+ return getattr(json, __name)
530
+
531
+ def __repr__(self) -> str:
532
+ return "<module 'telegrinder.msgspec_utils.json'>"
533
+
534
+
535
+ class _LoggerProxy:
536
+ def __init__(self) -> None:
537
+ self.logger = None
538
+ self.logger_module = None
539
+
540
+ def __repr__(self) -> str:
541
+ return "<LoggerProxy {}: {}>".format(
542
+ self.logger_module or "unknown",
543
+ "(NOT SETUP)" if self.logger is None else repr(self.logger),
544
+ )
545
+
546
+ def __await__(self) -> typing.Generator[typing.Any, typing.Any, typing.Any]:
547
+ return iter(()) # type: ignore
548
+
549
+ def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Self:
550
+ return self
551
+
552
+ def __getattr__(self, __name: str) -> typing.Any:
553
+ if self.logger is None:
554
+ if __name in ("exception", "aexception") and (exc := sys.exception()) is not None:
555
+ traceback.print_exception(exc, chain=True, file=sys.stderr)
556
+
557
+ return self
558
+
559
+ if __name in AnyLogger.__dict__ or __name in AnyAsyncLogger.__dict__:
560
+ is_async = __name.startswith("a")
561
+
562
+ if self.logging_module is not None:
563
+ level_name = __name.removeprefix("a").upper()
564
+ level_name = (
565
+ "INFO"
566
+ if level_name == "SUCCESS" and self.logger_module != "loguru"
567
+ else "ERROR"
568
+ if level_name == "EXCEPTION"
569
+ else level_name
570
+ )
571
+ level = (
572
+ logging._nameToLevel.get(level_name, logging.NOTSET)
573
+ if self.logger_module != "loguru"
574
+ else level_name
575
+ )
576
+ if not hasattr(self.logger, "isEnabledFor") and self.logger.isEnabledFor(level): # type: ignore
577
+ return self
578
+
579
+ return getattr(self.logger if not is_async else self.async_logger, __name)
580
+
581
+ return self
582
+
583
+ def set_logger(self, logger: Logger, logger_module: LoggerModule | None = None) -> None:
584
+ self.logger = logger if not isinstance(logger, logging.Logger) else LoggingStyleAdapter(logger)
585
+ self.async_logger = WrapperAsyncLogger(logger) if not _is_async_logger(logger) else logger
586
+ self.logger_module = logger_module
587
+
588
+
589
+ class _SetupLoggerKwargs(typing.TypedDict):
590
+ module: typing.NotRequired[LoggerModule]
591
+ level: typing.NotRequired[LoggerLevel]
592
+ format: typing.NotRequired[str]
593
+ colorize: typing.NotRequired[bool]
594
+ console_sink: typing.NotRequired[Sink | None]
595
+ file: typing.NotRequired[FileHandlerConfig]
596
+
597
+
598
+ class _LoguruFileHandlerConfig(_LoguruFileHandler):
599
+ pass
600
+
601
+
602
+ class FileHandlerConfig:
603
+ handler: _LoggingFileHandler | _LoguruFileHandlerConfig
604
+ format: str
605
+ colorize: bool
606
+
607
+ def __init__(
608
+ self,
609
+ handler: _LoggingFileHandler | _LoguruFileHandlerConfig,
610
+ format: str = _NODEFAULT,
611
+ colorize: bool = _NODEFAULT,
612
+ logger_module: str = LOGGER_MODULE,
613
+ ) -> None:
614
+ config = LoggerConfig()
615
+ format = (config.FILE_HANDLER_FORMAT if format is _NODEFAULT else format) or (
616
+ DEFAULT_LOGURU_FORMAT
617
+ if logger_module == "loguru"
618
+ else DEFAULT_STRUCTLOG_FORMAT
619
+ if logger_module == "structlog"
620
+ else DEFAULT_LOGGING_FORMAT
621
+ )
622
+ colorize = config.FILE_HANDLER_COLORIZE if colorize is _NODEFAULT else colorize
623
+
624
+ self.handler = handler
625
+
626
+ if isinstance(self.handler, dict):
627
+ self.format = self.handler.setdefault("format", format)
628
+ self.colorize = self.handler.setdefault("colorize", colorize)
629
+ else:
630
+ self.format = format
631
+ self.colorize = colorize
632
+
633
+ if handler.formatter is None:
634
+ handler.setFormatter(LoggingFormatter(format, colorize, logger_module))
635
+
636
+ @classmethod
637
+ def from_logging(
638
+ cls,
639
+ handler: _LoggingFileHandler,
640
+ format: str = _NODEFAULT,
641
+ colorize: bool = _NODEFAULT,
642
+ ) -> typing.Self:
643
+ return cls(handler, format, colorize)
644
+
645
+ @classmethod
646
+ def from_loguru(
647
+ cls,
648
+ **kwargs: typing.Unpack[_LoguruFileHandlerConfig], # type: ignore
649
+ ) -> typing.Self:
650
+ return cls(kwargs) # type: ignore
651
+
652
+
653
+ logger: Logger = typing.cast("Logger", _LoggerProxy())
654
+
655
+
656
+ def _configure_structlog(
657
+ level: LoggerLevel,
658
+ format: str | None,
659
+ colorize: bool,
660
+ console_sink: Sink | None = None,
661
+ file: FileHandlerConfig | None = None,
662
+ /,
663
+ ) -> None:
664
+ import re
665
+ from contextlib import suppress
666
+
667
+ import structlog
668
+
669
+ levels_colors = dict(
670
+ debug=COLORS["light_blue"],
671
+ info=COLORS["light_green"],
672
+ warning=COLORS["light_yellow"],
673
+ error=COLORS["light_red"],
674
+ critical=COLORS["light_red"],
675
+ )
676
+
677
+ class SLF4JStyleFormatter:
678
+ BRACE_PATTERN = re.compile(r"\{(?:([^}]*?)(?:!([sra]))?)?\}")
679
+ PERCENT_PATTERN = re.compile(r"%(?:\(([^)]+)\))?([sdfrx])")
680
+
681
+ def __init__(self, *, remove_positional_args: bool = True, colors: bool = True) -> None:
682
+ self.remove_positional_args = remove_positional_args
683
+ self.colors = colors
684
+
685
+ def __call__(
686
+ self,
687
+ logger: typing.Any,
688
+ method_name: str,
689
+ event_dict: typing.MutableMapping[str, typing.Any],
690
+ ) -> typing.MutableMapping[str, typing.Any]:
691
+ args = event_dict.get("positional_args", ())
692
+ event = event_dict.pop("event", "")
693
+ if not isinstance(event, str):
694
+ return event_dict
695
+
696
+ log_level = event_dict.get("level", "debug")
697
+ system_fields = {"level", "logger", "timestamp", "positional_args"}
698
+ kwargs = {k: v for k, v in event_dict.items() if k not in system_fields}
699
+ used_kwargs = set()
700
+
701
+ with suppress(TypeError, ValueError, IndexError, KeyError):
702
+ if self.BRACE_PATTERN.search(event):
703
+ event_dict["event"], used_kwargs = self._format_braces(event, args, kwargs, log_level)
704
+ elif self.PERCENT_PATTERN.search(event):
705
+ event_dict["event"], used_kwargs = self._format_percent(event, args, kwargs, log_level)
706
+ elif args:
707
+ event_dict["event"] = self._highlight_values(event, args, log_level)
708
+ else:
709
+ event_dict["event"] = event
710
+
711
+ if self.remove_positional_args:
712
+ if "positional_args" in event_dict:
713
+ del event_dict["positional_args"]
714
+ for key in used_kwargs:
715
+ if key in event_dict:
716
+ del event_dict[key]
717
+
718
+ return event_dict
719
+
720
+ def _colorize(self, value: typing.Any, log_level: str) -> str:
721
+ return f"{levels_colors[log_level]}{value}{Colors.RESET}" if self.colors else value
722
+
723
+ def _format_braces(
724
+ self,
725
+ message: str,
726
+ args: tuple[typing.Any, ...],
727
+ kwargs: dict[str, typing.Any],
728
+ log_level: str,
729
+ ) -> tuple[str, set[str]]:
730
+ result = []
731
+ last_end = 0
732
+ arg_index = 0
733
+ used_kwargs = set()
734
+
735
+ for match in self.BRACE_PATTERN.finditer(message):
736
+ result.append(message[last_end : match.start()])
737
+
738
+ field_name = match.group(1)
739
+ conversion = match.group(2)
740
+
741
+ if field_name:
742
+ if field_name in kwargs:
743
+ value = kwargs[field_name]
744
+ used_kwargs.add(field_name)
745
+ else:
746
+ result.append(match.group(0))
747
+ last_end = match.end()
748
+ continue
749
+ elif arg_index < len(args):
750
+ value = args[arg_index]
751
+ arg_index += 1
752
+ else:
753
+ result.append(match.group(0))
754
+ last_end = match.end()
755
+ continue
756
+
757
+ if conversion == "r":
758
+ formatted_value = repr(value)
759
+ elif conversion == "s":
760
+ formatted_value = str(value)
761
+ elif conversion == "a":
762
+ formatted_value = ascii(value)
763
+ else:
764
+ formatted_value = str(value)
765
+
766
+ result.append(self._colorize(formatted_value, log_level))
767
+ last_end = match.end()
768
+
769
+ result.append(message[last_end:])
770
+ return "".join(result), used_kwargs
771
+
772
+ def _format_percent(
773
+ self,
774
+ message: str,
775
+ args: tuple[typing.Any, ...],
776
+ kwargs: dict[str, typing.Any],
777
+ log_level: str,
778
+ ) -> tuple[str, set[str]]:
779
+ used_kwargs = set[str]()
780
+
781
+ try:
782
+ has_named = bool(re.search(r"%\([^)]+\)", message))
783
+ has_positional = bool(re.search(r"%[sdfrx]", message))
784
+
785
+ if has_named and not has_positional:
786
+ formatted = message % kwargs
787
+ used_kwargs = set(kwargs.keys())
788
+ for value in kwargs.values():
789
+ formatted = self._highlight_single_value(formatted, value, log_level)
790
+ elif has_positional and not has_named:
791
+ formatted = message % args
792
+ for value in args:
793
+ formatted = self._highlight_single_value(formatted, value, log_level)
794
+ elif has_named and has_positional:
795
+ temp_formatted = message
796
+
797
+ for key, value in kwargs.items():
798
+ placeholder = f"%({key})s"
799
+ if placeholder in temp_formatted:
800
+ used_kwargs.add(key)
801
+ replacement = self._colorize(str(value), log_level)
802
+ temp_formatted = temp_formatted.replace(placeholder, replacement)
803
+
804
+ if args and "%s" in temp_formatted:
805
+ temp_formatted = temp_formatted % args
806
+ for value in args:
807
+ temp_formatted = self._highlight_single_value(temp_formatted, value, log_level)
808
+
809
+ formatted = temp_formatted
810
+ else:
811
+ formatted = message
812
+
813
+ return formatted, used_kwargs
814
+ except (TypeError, KeyError, ValueError):
815
+ if kwargs:
816
+ try:
817
+ formatted = message % kwargs
818
+ used_kwargs = set(kwargs.keys())
819
+ for value in kwargs.values():
820
+ formatted = self._highlight_single_value(formatted, value, log_level)
821
+ return formatted, used_kwargs
822
+ except (TypeError, KeyError):
823
+ pass
824
+
825
+ if args:
826
+ try:
827
+ formatted = message % args
828
+ for value in args:
829
+ formatted = self._highlight_single_value(formatted, value, log_level)
830
+ return formatted, used_kwargs
831
+ except (TypeError, ValueError):
832
+ pass
833
+
834
+ return message, used_kwargs
835
+
836
+ def _highlight_single_value(
837
+ self,
838
+ message: str,
839
+ value: typing.Any,
840
+ log_level: str,
841
+ ) -> str:
842
+ with suppress(Exception):
843
+ for raw in (str(value), repr(value)):
844
+ if raw in message:
845
+ pattern = re.compile(rf"(?<!\w){re.escape(raw)}(?!\w)")
846
+ message = pattern.sub(lambda m: self._colorize(m.group(0), log_level), message, count=1)
847
+ break
848
+
849
+ return message
850
+
851
+ def _highlight_values(
852
+ self,
853
+ full_message: str,
854
+ values: typing.Iterable[typing.Any],
855
+ log_level: str,
856
+ ) -> str:
857
+ for value in values:
858
+ full_message = self._highlight_single_value(full_message, value, log_level)
859
+ return full_message
860
+
861
+ class LogLevelColumnFormatter:
862
+ def __init__(self, colorize: bool) -> None:
863
+ self.colorize = colorize
864
+
865
+ def __call__(self, key: str, value: typing.Any) -> str:
866
+ if self.colorize:
867
+ color = levels_colors[value]
868
+ return f"[{color}{value:^12}{Colors.RESET}]"
869
+
870
+ return f"[{value:^12}]"
871
+
872
+ class Filter(logging.Filter):
873
+ def __init__(self, colorize: bool) -> None:
874
+ self.colorize = colorize
875
+ super().__init__()
876
+
877
+ def filter(self, record: logging.LogRecord) -> bool:
878
+ record = _rich_log_record(record, "structlog")
879
+
880
+ if self.colorize:
881
+ level_color = levels_colors[record.levelname.lower()]
882
+ location = (
883
+ f"{Colors.LIGHT_CYAN}{record.module}{Colors.RESET}:"
884
+ f"{level_color}{record.funcName}{Colors.RESET}:"
885
+ f"{Colors.LIGHT_MAGENTA}{record.lineno}{Colors.RESET} "
886
+ )
887
+ else:
888
+ location = f"{record.module}:{record.funcName}:{record.lineno} "
889
+
890
+ record.location = location
891
+ return True
892
+
893
+ console_renderer = structlog.dev.ConsoleRenderer(colors=True)
894
+
895
+ for column in console_renderer._columns:
896
+ if column.key == "level":
897
+ column.formatter = LogLevelColumnFormatter(colorize)
898
+ break
899
+
900
+ telegrinder_logger = logging.getLogger("telegrinder")
901
+ telegrinder_logger.setLevel(level)
902
+ _remove_handlers(telegrinder_logger)
903
+
904
+ if console_sink is not None:
905
+ console_handler = logging.StreamHandler(console_sink)
906
+ console_handler.setFormatter(LoggingFormatter(format or DEFAULT_STRUCTLOG_FORMAT, colorize, "structlog"))
907
+ console_handler.addFilter(Filter(colorize))
908
+ telegrinder_logger.addHandler(console_handler)
909
+
910
+ if file is not None and isinstance(file.handler, _LoggingFileHandler):
911
+ file.handler.addFilter(Filter(file.colorize))
912
+ telegrinder_logger.addHandler(file.handler)
913
+
914
+ struct_logger = structlog.wrap_logger(
915
+ logger=telegrinder_logger,
916
+ processors=[
917
+ structlog.stdlib.filter_by_level,
918
+ structlog.stdlib.add_log_level,
919
+ SLF4JStyleFormatter(),
920
+ structlog.processors.StackInfoRenderer(),
921
+ structlog.processors.format_exc_info,
922
+ structlog.processors.UnicodeDecoder(),
923
+ console_renderer,
924
+ ],
925
+ wrapper_class=structlog.stdlib.BoundLogger,
926
+ context_class=dict,
927
+ cache_logger_on_first_use=True,
928
+ )
929
+ logger.set_logger(struct_logger, "structlog") # type: ignore
930
+
931
+
932
+ def _configure_loguru(
933
+ level: LoggerLevel,
934
+ format: str | None,
935
+ colorize: bool,
936
+ console_sink: Sink | None = None,
937
+ file: FileHandlerConfig | None = None,
938
+ /,
939
+ ) -> None:
940
+ import atexit
941
+
942
+ from loguru._logger import Core, Logger
943
+
944
+ loguru_logger = Logger(
945
+ core=Core(),
946
+ exception=None,
947
+ depth=0,
948
+ record=False,
949
+ lazy=False,
950
+ colors=False,
951
+ raw=False,
952
+ capture=True,
953
+ patchers=[],
954
+ extra=dict(telegrinder=True),
955
+ )
956
+
957
+ def is_enabled_for(lvl: str) -> bool:
958
+ try:
959
+ lno = loguru_logger.level(lvl).no
960
+ except ValueError:
961
+ return False
962
+
963
+ return any(lno >= x.levelno for x in loguru_logger._core.handlers.values())
964
+
965
+ loguru_logger.isEnabledFor = is_enabled_for
966
+ handlers = []
967
+
968
+ if console_sink is not None:
969
+ handlers.append(
970
+ dict(
971
+ sink=console_sink,
972
+ level=level,
973
+ enqueue=True,
974
+ colorize=colorize,
975
+ format=format or DEFAULT_LOGURU_FORMAT,
976
+ filter=_loguru_filter,
977
+ ),
978
+ )
979
+
980
+ if file is not None and isinstance(file.handler, dict): # type: ignore
981
+ handlers.append(file.handler)
982
+
983
+ if handlers:
984
+ handlers_ids = loguru_logger.configure(handlers=handlers)
985
+
986
+ @atexit.register
987
+ def _() -> None:
988
+ for handler_id in handlers_ids:
989
+ loguru_logger.remove(handler_id)
990
+
991
+ logger.set_logger(loguru_logger, "loguru") # type: ignore
992
+
993
+
994
+ def _configure_logging(
995
+ level: LoggerLevel,
996
+ format: str | None,
997
+ colorize: bool,
998
+ console_sink: Sink | None = None,
999
+ file: FileHandlerConfig | None = None,
1000
+ /,
1001
+ ) -> None:
1002
+ _logger = logging.getLogger("telegrinder")
1003
+ _logger.setLevel(level)
1004
+ _remove_handlers(_logger)
1005
+
1006
+ if console_sink is not None:
1007
+ console_handler = logging.StreamHandler(console_sink)
1008
+ console_handler.setFormatter(LoggingFormatter(format or DEFAULT_LOGGING_FORMAT, colorize, "logging"))
1009
+ _logger.addHandler(console_handler)
1010
+
1011
+ if file is not None and isinstance(file.handler, _LoggingFileHandler):
1012
+ _logger.addHandler(file.handler)
1013
+
1014
+ logger.set_logger(LoggingStyleAdapter(logger=_logger), "logging") # type: ignore
1015
+
1016
+
1017
+ def setup_logger(**setup_kwargs: typing.Unpack[_SetupLoggerKwargs]) -> Logger:
1018
+ if logger.logger is not None:
1019
+ return logger
1020
+
1021
+ config = LoggerConfig()
1022
+ args = (
1023
+ setup_kwargs.get("level", config.LEVEL),
1024
+ setup_kwargs.get("format", config.FORMAT),
1025
+ setup_kwargs.get("colorize", config.COLORIZE),
1026
+ setup_kwargs.get("console_sink", sys.stderr),
1027
+ setup_kwargs.get("file"),
1028
+ )
1029
+
1030
+ match setup_kwargs.get("module", config.MODULE):
1031
+ case "logging":
1032
+ _configure_logging(*args)
1033
+ case "loguru":
1034
+ _configure_loguru(*args)
1035
+ case "structlog":
1036
+ _configure_structlog(*args)
1037
+ case _ as value:
1038
+ typing.assert_never(value)
1039
+
1040
+ return logger
1041
+
1042
+
1043
+ def configure_dotenv(
1044
+ *,
1045
+ load_file: bool = True,
1046
+ file_name: str | None = None,
1047
+ file_path: str | pathlib.Path | None = None,
1048
+ ) -> None:
1049
+ global _LOAD_ENV_FILE, _ENV_FILE_NAME, _ENV_FILE_PATH
1050
+
1051
+ _LOAD_ENV_FILE = load_file
1052
+
1053
+ if file_name and file_path:
1054
+ _ENV_FILE_PATH = pathlib.Path(file_path) / file_name
1055
+ _ENV_FILE_NAME = file_name
1056
+
1057
+ elif file_name is not None:
1058
+ _ENV_FILE_NAME = file_name
1059
+
1060
+ elif file_path is not None:
1061
+ _ENV_FILE_PATH = pathlib.Path(file_path)
1062
+
1063
+ if _ENV_FILE_PATH is not None and not _ENV_FILE_PATH.exists():
1064
+ raise FileNotFoundError(f"Env file '{_ENV_FILE_PATH!s}' not found")
1065
+
1066
+
1067
+ if typing.TYPE_CHECKING:
1068
+ from telegrinder.msgspec_utils import json
1069
+ else:
1070
+ json = _json()
1071
+
1072
+
1073
+ __all__ = (
1074
+ "FileHandlerConfig",
1075
+ "LoggingStyleAdapter",
1076
+ "configure_dotenv",
1077
+ "json",
1078
+ "logger",
1079
+ "setup_logger",
1080
+ "take_env",
1081
+ )