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.
- telegrinder/__init__.py +258 -0
- telegrinder/__meta__.py +1 -0
- telegrinder/api/__init__.py +15 -0
- telegrinder/api/api.py +175 -0
- telegrinder/api/error.py +50 -0
- telegrinder/api/response.py +23 -0
- telegrinder/api/token.py +30 -0
- telegrinder/api/validators.py +30 -0
- telegrinder/bot/__init__.py +144 -0
- telegrinder/bot/bot.py +70 -0
- telegrinder/bot/cute_types/__init__.py +41 -0
- telegrinder/bot/cute_types/base.py +228 -0
- telegrinder/bot/cute_types/base.pyi +49 -0
- telegrinder/bot/cute_types/business_connection.py +9 -0
- telegrinder/bot/cute_types/business_messages_deleted.py +9 -0
- telegrinder/bot/cute_types/callback_query.py +248 -0
- telegrinder/bot/cute_types/chat_boost_removed.py +9 -0
- telegrinder/bot/cute_types/chat_boost_updated.py +9 -0
- telegrinder/bot/cute_types/chat_join_request.py +59 -0
- telegrinder/bot/cute_types/chat_member_updated.py +158 -0
- telegrinder/bot/cute_types/chosen_inline_result.py +11 -0
- telegrinder/bot/cute_types/inline_query.py +41 -0
- telegrinder/bot/cute_types/message.py +2809 -0
- telegrinder/bot/cute_types/message_reaction_count_updated.py +9 -0
- telegrinder/bot/cute_types/message_reaction_updated.py +9 -0
- telegrinder/bot/cute_types/paid_media_purchased.py +11 -0
- telegrinder/bot/cute_types/poll.py +9 -0
- telegrinder/bot/cute_types/poll_answer.py +9 -0
- telegrinder/bot/cute_types/pre_checkout_query.py +36 -0
- telegrinder/bot/cute_types/shipping_query.py +11 -0
- telegrinder/bot/cute_types/update.py +209 -0
- telegrinder/bot/cute_types/utils.py +141 -0
- telegrinder/bot/dispatch/__init__.py +99 -0
- telegrinder/bot/dispatch/abc.py +74 -0
- telegrinder/bot/dispatch/action.py +99 -0
- telegrinder/bot/dispatch/context.py +162 -0
- telegrinder/bot/dispatch/dispatch.py +362 -0
- telegrinder/bot/dispatch/handler/__init__.py +23 -0
- telegrinder/bot/dispatch/handler/abc.py +25 -0
- telegrinder/bot/dispatch/handler/audio_reply.py +43 -0
- telegrinder/bot/dispatch/handler/base.py +34 -0
- telegrinder/bot/dispatch/handler/document_reply.py +43 -0
- telegrinder/bot/dispatch/handler/func.py +73 -0
- telegrinder/bot/dispatch/handler/media_group_reply.py +43 -0
- telegrinder/bot/dispatch/handler/message_reply.py +35 -0
- telegrinder/bot/dispatch/handler/photo_reply.py +43 -0
- telegrinder/bot/dispatch/handler/sticker_reply.py +36 -0
- telegrinder/bot/dispatch/handler/video_reply.py +43 -0
- telegrinder/bot/dispatch/middleware/__init__.py +13 -0
- telegrinder/bot/dispatch/middleware/abc.py +112 -0
- telegrinder/bot/dispatch/middleware/box.py +32 -0
- telegrinder/bot/dispatch/middleware/filter.py +88 -0
- telegrinder/bot/dispatch/middleware/media_group.py +69 -0
- telegrinder/bot/dispatch/process.py +93 -0
- telegrinder/bot/dispatch/return_manager/__init__.py +21 -0
- telegrinder/bot/dispatch/return_manager/abc.py +107 -0
- telegrinder/bot/dispatch/return_manager/callback_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/inline_query.py +14 -0
- telegrinder/bot/dispatch/return_manager/message.py +34 -0
- telegrinder/bot/dispatch/return_manager/pre_checkout_query.py +19 -0
- telegrinder/bot/dispatch/return_manager/utils.py +20 -0
- telegrinder/bot/dispatch/router/__init__.py +4 -0
- telegrinder/bot/dispatch/router/abc.py +15 -0
- telegrinder/bot/dispatch/router/base.py +154 -0
- telegrinder/bot/dispatch/view/__init__.py +15 -0
- telegrinder/bot/dispatch/view/abc.py +15 -0
- telegrinder/bot/dispatch/view/base.py +226 -0
- telegrinder/bot/dispatch/view/box.py +207 -0
- telegrinder/bot/dispatch/view/media_group.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/__init__.py +25 -0
- telegrinder/bot/dispatch/waiter_machine/actions.py +16 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/__init__.py +13 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/callback.py +53 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/hasher.py +61 -0
- telegrinder/bot/dispatch/waiter_machine/hasher/message.py +49 -0
- telegrinder/bot/dispatch/waiter_machine/machine.py +264 -0
- telegrinder/bot/dispatch/waiter_machine/middleware.py +77 -0
- telegrinder/bot/dispatch/waiter_machine/short_state.py +105 -0
- telegrinder/bot/polling/__init__.py +4 -0
- telegrinder/bot/polling/abc.py +25 -0
- telegrinder/bot/polling/error_handler.py +93 -0
- telegrinder/bot/polling/polling.py +167 -0
- telegrinder/bot/polling/utils.py +12 -0
- telegrinder/bot/rules/__init__.py +166 -0
- telegrinder/bot/rules/abc.py +150 -0
- telegrinder/bot/rules/button.py +20 -0
- telegrinder/bot/rules/callback_data.py +109 -0
- telegrinder/bot/rules/chat_join.py +28 -0
- telegrinder/bot/rules/chat_member_updated.py +145 -0
- telegrinder/bot/rules/command.py +137 -0
- telegrinder/bot/rules/enum_text.py +29 -0
- telegrinder/bot/rules/func.py +21 -0
- telegrinder/bot/rules/fuzzy.py +21 -0
- telegrinder/bot/rules/inline.py +45 -0
- telegrinder/bot/rules/integer.py +19 -0
- telegrinder/bot/rules/is_from.py +213 -0
- telegrinder/bot/rules/logic.py +22 -0
- telegrinder/bot/rules/magic.py +60 -0
- telegrinder/bot/rules/markup.py +51 -0
- telegrinder/bot/rules/media.py +13 -0
- telegrinder/bot/rules/mention.py +15 -0
- telegrinder/bot/rules/message_entities.py +37 -0
- telegrinder/bot/rules/node.py +43 -0
- telegrinder/bot/rules/payload.py +89 -0
- telegrinder/bot/rules/payment_invoice.py +14 -0
- telegrinder/bot/rules/regex.py +34 -0
- telegrinder/bot/rules/rule_enum.py +71 -0
- telegrinder/bot/rules/start.py +73 -0
- telegrinder/bot/rules/state.py +35 -0
- telegrinder/bot/rules/text.py +27 -0
- telegrinder/bot/rules/update.py +14 -0
- telegrinder/bot/scenario/__init__.py +5 -0
- telegrinder/bot/scenario/abc.py +16 -0
- telegrinder/bot/scenario/checkbox.py +183 -0
- telegrinder/bot/scenario/choice.py +44 -0
- telegrinder/client/__init__.py +11 -0
- telegrinder/client/abc.py +136 -0
- telegrinder/client/form_data.py +34 -0
- telegrinder/client/rnet.py +198 -0
- telegrinder/model.py +133 -0
- telegrinder/model.pyi +57 -0
- telegrinder/modules.py +1081 -0
- telegrinder/msgspec_utils/__init__.py +42 -0
- telegrinder/msgspec_utils/abc.py +16 -0
- telegrinder/msgspec_utils/custom_types/__init__.py +6 -0
- telegrinder/msgspec_utils/custom_types/datetime.py +24 -0
- telegrinder/msgspec_utils/custom_types/enum_meta.py +61 -0
- telegrinder/msgspec_utils/custom_types/literal.py +25 -0
- telegrinder/msgspec_utils/custom_types/option.py +17 -0
- telegrinder/msgspec_utils/decoder.py +388 -0
- telegrinder/msgspec_utils/encoder.py +204 -0
- telegrinder/msgspec_utils/json.py +15 -0
- telegrinder/msgspec_utils/tools.py +80 -0
- telegrinder/node/__init__.py +80 -0
- telegrinder/node/compose.py +193 -0
- telegrinder/node/nodes/__init__.py +96 -0
- telegrinder/node/nodes/attachment.py +169 -0
- telegrinder/node/nodes/callback_query.py +25 -0
- telegrinder/node/nodes/channel.py +97 -0
- telegrinder/node/nodes/command.py +33 -0
- telegrinder/node/nodes/error.py +43 -0
- telegrinder/node/nodes/event.py +70 -0
- telegrinder/node/nodes/file.py +39 -0
- telegrinder/node/nodes/global_node.py +66 -0
- telegrinder/node/nodes/i18n.py +110 -0
- telegrinder/node/nodes/me.py +26 -0
- telegrinder/node/nodes/message_entities.py +15 -0
- telegrinder/node/nodes/payload.py +84 -0
- telegrinder/node/nodes/reply_message.py +14 -0
- telegrinder/node/nodes/source.py +172 -0
- telegrinder/node/nodes/state_mutator.py +71 -0
- telegrinder/node/nodes/text.py +62 -0
- telegrinder/node/scope.py +88 -0
- telegrinder/node/utils.py +38 -0
- telegrinder/py.typed +0 -0
- telegrinder/rules.py +1 -0
- telegrinder/tools/__init__.py +183 -0
- telegrinder/tools/aio.py +147 -0
- telegrinder/tools/final.py +21 -0
- telegrinder/tools/formatting/__init__.py +85 -0
- telegrinder/tools/formatting/deep_links/__init__.py +39 -0
- telegrinder/tools/formatting/deep_links/links.py +468 -0
- telegrinder/tools/formatting/deep_links/parsing.py +88 -0
- telegrinder/tools/formatting/deep_links/validators.py +8 -0
- telegrinder/tools/formatting/html.py +241 -0
- telegrinder/tools/fullname.py +82 -0
- telegrinder/tools/global_context/__init__.py +13 -0
- telegrinder/tools/global_context/abc.py +63 -0
- telegrinder/tools/global_context/builtin_context.py +45 -0
- telegrinder/tools/global_context/global_context.py +614 -0
- telegrinder/tools/input_file_directory.py +30 -0
- telegrinder/tools/keyboard/__init__.py +6 -0
- telegrinder/tools/keyboard/abc.py +84 -0
- telegrinder/tools/keyboard/base.py +108 -0
- telegrinder/tools/keyboard/button.py +181 -0
- telegrinder/tools/keyboard/data.py +31 -0
- telegrinder/tools/keyboard/keyboard.py +160 -0
- telegrinder/tools/keyboard/utils.py +95 -0
- telegrinder/tools/lifespan.py +188 -0
- telegrinder/tools/limited_dict.py +35 -0
- telegrinder/tools/loop_wrapper.py +271 -0
- telegrinder/tools/magic/__init__.py +29 -0
- telegrinder/tools/magic/annotations.py +172 -0
- telegrinder/tools/magic/descriptors.py +57 -0
- telegrinder/tools/magic/function.py +254 -0
- telegrinder/tools/magic/inspect.py +16 -0
- telegrinder/tools/magic/shortcut.py +107 -0
- telegrinder/tools/member_descriptor_proxy.py +95 -0
- telegrinder/tools/parse_mode.py +12 -0
- telegrinder/tools/serialization/__init__.py +5 -0
- telegrinder/tools/serialization/abc.py +34 -0
- telegrinder/tools/serialization/json_ser.py +60 -0
- telegrinder/tools/serialization/msgpack_ser.py +197 -0
- telegrinder/tools/serialization/utils.py +18 -0
- telegrinder/tools/singleton/__init__.py +4 -0
- telegrinder/tools/singleton/abc.py +14 -0
- telegrinder/tools/singleton/singleton.py +18 -0
- telegrinder/tools/state_mutator/__init__.py +4 -0
- telegrinder/tools/state_mutator/mutation.py +85 -0
- telegrinder/tools/state_storage/__init__.py +4 -0
- telegrinder/tools/state_storage/abc.py +38 -0
- telegrinder/tools/state_storage/memory.py +27 -0
- telegrinder/tools/strings.py +22 -0
- telegrinder/types/__init__.py +323 -0
- telegrinder/types/enums.py +754 -0
- telegrinder/types/input_file.py +51 -0
- telegrinder/types/methods.py +6143 -0
- telegrinder/types/methods_utils.py +66 -0
- telegrinder/types/objects.py +8184 -0
- telegrinder/types/webapp.py +129 -0
- telegrinder/verification_utils.py +35 -0
- telegrinder-1.0.0rc1.dist-info/METADATA +166 -0
- telegrinder-1.0.0rc1.dist-info/RECORD +215 -0
- telegrinder-1.0.0rc1.dist-info/WHEEL +4 -0
- 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
|
+
)
|