rbm_tools 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rbm_tools might be problematic. Click here for more details.

rbm_tools/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ 定义 sdk 为 python 的一个包,注意导包顺序,必须保证 logger 最先导包,否则会导致 opentelemetry 注册失效
3
+ """
4
+
5
+ from rbm_tools import logger
@@ -0,0 +1,51 @@
1
+ """
2
+ 命令行工具类
3
+ """
4
+ import sys
5
+ import os
6
+
7
+ from jsonargparse import auto_cli
8
+ from rbm_tools.client.sort_class_methods import reorder_class_methods
9
+
10
+ class RBMToolsCLI:
11
+ """
12
+ 命令行工具类
13
+ """
14
+ def __init__(self):
15
+ pass
16
+
17
+ def check_code_style(self):
18
+ """
19
+ 检查代码风格
20
+ """
21
+ success_flag = True
22
+ directory = os.getcwd()
23
+ entries = os.listdir(directory)
24
+ # 只看 py 文件
25
+ filename_list = [
26
+ os.path.join(directory, i) for i in entries
27
+ if os.path.isfile(os.path.join(directory, i)) and i.endswith(".py")
28
+ ]
29
+ for filename in filename_list:
30
+ # 检查对象的成员方法排序
31
+ old_code = ""
32
+ with open(filename, "r", encoding="utf-8") as f:
33
+ old_code = f.read()
34
+ new_code = reorder_class_methods(old_code)
35
+ with open(filename, "w", encoding="utf-8") as f:
36
+ f.write(new_code)
37
+ if old_code != new_code:
38
+ success_flag = False
39
+ if success_flag:
40
+ sys.exit(0)
41
+ else:
42
+ sys.exit(1)
43
+
44
+ def main():
45
+ """
46
+ 入口函数
47
+ """
48
+ auto_cli(RBMToolsCLI)
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,78 @@
1
+ """
2
+ 自动整理 Python 类中的方法顺序
3
+
4
+ 依赖:
5
+ 需要安装 libcst 库(pip install libcst)
6
+
7
+ 适用场景:
8
+ 适用于需要规范化类方法顺序、提升代码可读性和维护性的场景
9
+ """
10
+
11
+ import libcst as cst
12
+ import libcst.matchers as m
13
+
14
+
15
+ class ClassMethodSorter(cst.CSTTransformer):
16
+ """
17
+ 一个 CSTTransformer,用于对每个类中的方法按如下规则排序:
18
+ 1. 魔法方法(即名称前后都有双下划线,如 __init__、__str__ 等),按名称排序,排在最前面;
19
+ 2. 公共方法(不以下划线开头),按名称排序,排在中间;
20
+ 3. 私有方法(以下划线开头,但不是魔法方法),按名称排序,排在最后;
21
+ 其他类成员(如属性、docstring、嵌套类等)保持原有顺序,并始终排在方法之前。
22
+ """
23
+
24
+ def leave_ClassDef(self, original_node, updated_node):
25
+ """
26
+ 在离开类定义节点时,对类体中的方法按指定规则排序。
27
+ 非方法成员保持原有顺序,并排在方法之前。
28
+
29
+ :param original_node: 原始的 ClassDef 节点
30
+ :param updated_node: 可能已被修改的 ClassDef 节点
31
+ :return: 排序后的新的 ClassDef 节点
32
+ """
33
+ # 分离方法和其他成员
34
+ magic_methods = []
35
+ public_methods = []
36
+ private_methods = []
37
+ others = []
38
+ for stmt in updated_node.body.body:
39
+ if m.matches(stmt, m.FunctionDef()):
40
+ name = stmt.name.value # type: ignore
41
+ # 魔法方法:前后都有双下划线
42
+ if name.startswith("__") and name.endswith("__"):
43
+ magic_methods.append(stmt)
44
+ # 私有方法:以单下划线或双下划线开头,但不是魔法方法
45
+ elif name.startswith("_"):
46
+ private_methods.append(stmt)
47
+ # 公共方法:不以下划线开头
48
+ else:
49
+ public_methods.append(stmt)
50
+ else:
51
+ others.append(stmt)
52
+ # 分别排序
53
+ magic_methods_sorted = sorted(magic_methods, key=lambda f: f.name.value)
54
+ public_methods_sorted = sorted(public_methods, key=lambda f: f.name.value)
55
+ private_methods_sorted = sorted(private_methods, key=lambda f: f.name.value)
56
+ # 合并:其他成员 + 魔法方法 + 公共方法 + 私有方法
57
+ new_body = (
58
+ list(others)
59
+ + magic_methods_sorted
60
+ + public_methods_sorted
61
+ + private_methods_sorted
62
+ )
63
+ # 返回新的类定义节点
64
+ return updated_node.with_changes(
65
+ body=updated_node.body.with_changes(body=new_body)
66
+ )
67
+
68
+
69
+ def reorder_class_methods(source_code):
70
+ """
71
+ 对给定 Python 源代码中的每个类的方法和成员按照 ClassMethodSorter 定义的规则排序
72
+
73
+ :param source_code: 需要处理的 Python 源代码字符串
74
+ :return: 方法已按类型排序后的新源代码字符串
75
+ """
76
+ tree = cst.parse_module(source_code)
77
+ new_tree = tree.visit(ClassMethodSorter())
78
+ return new_tree.code
rbm_tools/env.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ 环境变量注册
3
+ """
4
+
5
+ import os
6
+
7
+ CONF_ENV = os.getenv("CONF_ENV", "test")
8
+ HOSTNAME = os.getenv("HOSTNAME", "localhost")
9
+ MODULE = os.getenv("MODULE", "defaultmodule")
10
+ POD_IP = os.getenv("POD_IP", "127.0.0.1")
11
+ POD_NAME = os.getenv("POD_NAME", "defaultpodname")
rbm_tools/logger.py ADDED
@@ -0,0 +1,278 @@
1
+ """
2
+ 日志类
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ import os
8
+ from copy import copy
9
+ from logging.handlers import QueueHandler, QueueListener, RotatingFileHandler
10
+ from multiprocessing import Queue
11
+
12
+ import click
13
+ from opentelemetry import trace
14
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
15
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
16
+ from opentelemetry.instrumentation.logging import LoggingInstrumentor
17
+ from opentelemetry.instrumentation.openai import OpenAIInstrumentor
18
+ from opentelemetry.instrumentation.openai.shared import chat_wrappers
19
+ from opentelemetry.sdk.resources import Resource
20
+ from opentelemetry.sdk.trace import TracerProvider
21
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
22
+ from pydantic import BaseModel, Field
23
+
24
+ from rbm_tools.env import CONF_ENV, HOSTNAME, MODULE, POD_IP
25
+
26
+
27
+ class ColourizedFormatter(logging.Formatter):
28
+ """
29
+ 日志内容上颜色模块
30
+ """
31
+
32
+ level_name_colors = {
33
+ logging.DEBUG: lambda field: click.style(str(field), fg="bright_cyan"),
34
+ logging.WARNING: lambda field: click.style(str(field), fg="bright_yellow"),
35
+ logging.ERROR: lambda field: click.style(str(field), fg="bright_red"),
36
+ logging.CRITICAL: lambda field: click.style(str(field), fg="bright_red"),
37
+ }
38
+
39
+ def formatMessage(self, record: logging.LogRecord) -> str:
40
+ recordcopy = copy(record)
41
+ # message
42
+ if "color_message" in recordcopy.__dict__:
43
+ recordcopy.msg = recordcopy.__dict__["color_message"]
44
+ recordcopy.__dict__["message"] = recordcopy.getMessage()
45
+ recordcopy.__dict__["message"] = self.__color_field_by_level_no(
46
+ recordcopy.message, recordcopy.levelno
47
+ )
48
+ # asctime
49
+ recordcopy.__dict__["asctime"] = self.__color_field_by_fg(
50
+ recordcopy.asctime, "bright_green"
51
+ )
52
+ # levelname
53
+ seperator = " " * (8 - len(recordcopy.levelname))
54
+ levelname = self.__color_field_by_level_no(
55
+ recordcopy.levelname, recordcopy.levelno
56
+ )
57
+ recordcopy.__dict__["levelname"] = levelname + seperator
58
+ # otelTraceID
59
+ otel_trace_id = recordcopy.otelTraceID if hasattr(recordcopy, "otelTraceID") else "null" # type: ignore # pylint: disable=C0301
60
+ seperator = " " * (32 - len(otel_trace_id))
61
+ recordcopy.__dict__["otelTraceID"] = otel_trace_id + seperator
62
+ # otelSpanID
63
+ otel_span_id = recordcopy.otelSpanID if hasattr(recordcopy, "otelSpanID") else "null" # type: ignore # pylint: disable=C0301
64
+ seperator = " " * (16 - len(otel_span_id))
65
+ recordcopy.__dict__["otelSpanID"] = otel_span_id + seperator
66
+ # otelTraceSampled
67
+ otel_trace_sampled = recordcopy.otelTraceSampled if hasattr(recordcopy, "otelTraceSampled") else "null" # type: ignore # pylint: disable=C0301
68
+ seperator = " " * (5 - len(str(otel_trace_sampled)))
69
+ recordcopy.__dict__["otelTraceSampled"] = str(otel_trace_sampled) + seperator
70
+ # name
71
+ recordcopy.__dict__["name"] = self.__color_field_by_fg(
72
+ recordcopy.name, "bright_magenta"
73
+ )
74
+ # module
75
+ recordcopy.__dict__["module"] = self.__color_field_by_fg(
76
+ recordcopy.module, "bright_blue"
77
+ )
78
+ # funcName
79
+ recordcopy.__dict__["funcName"] = self.__color_field_by_fg(
80
+ recordcopy.funcName, "bright_blue"
81
+ )
82
+ # lineno
83
+ recordcopy.__dict__["lineno"] = self.__color_field_by_fg(
84
+ str(recordcopy.lineno), "bright_blue"
85
+ )
86
+ # customized_data_info
87
+ customized_data_info = "{}"
88
+ if hasattr(recordcopy, "customized_data_info"):
89
+ try:
90
+ customized_data_info = json.dumps(
91
+ recordcopy.customized_data_info, cls=CustomJSONEncoder, ensure_ascii=False # type: ignore # pylint: disable=C0301
92
+ )
93
+ except Exception: # pylint: disable=W0718
94
+ customized_data_info = recordcopy.customized_data_info # type: ignore
95
+ recordcopy.__dict__["customized_data_info"] = self.__color_field_by_level_no(
96
+ field=customized_data_info,
97
+ level_no=recordcopy.levelno,
98
+ default_fg="bright_green",
99
+ )
100
+ return super().formatMessage(recordcopy)
101
+
102
+ def __color_field_by_fg(self, field: str, fg: str) -> str:
103
+ return click.style(str(field), fg=fg)
104
+
105
+ def __color_field_by_level_no(
106
+ self, field: str, level_no: int, default_fg: str = "reset"
107
+ ) -> str:
108
+ def default(field: str) -> str:
109
+ return click.style(str(field), fg=default_fg)
110
+
111
+ func = self.level_name_colors.get(level_no, default)
112
+ return func(field)
113
+
114
+
115
+ class CustomJSONEncoder(json.JSONEncoder):
116
+ """
117
+ 自定义的 JSONEncoder
118
+ """
119
+
120
+ def default(self, o):
121
+ if isinstance(o, BaseModel):
122
+ return o.model_dump()
123
+ return super().default(o)
124
+
125
+
126
+ class NoneRootWarningFilter(logging.Filter):
127
+ """
128
+ 非 root 的 logger 只允许 WARNING 级别的日志(除 uvicorn 相关的日志外)
129
+ """
130
+
131
+ def filter(self, record):
132
+ return (
133
+ "uvicorn" in record.name
134
+ or record.name == "root"
135
+ or record.levelno >= logging.WARNING
136
+ )
137
+
138
+
139
+ def process_image_item(item: dict) -> dict:
140
+ """
141
+ 自定义大模型调用中图片 base64 字符串的处理
142
+ """
143
+ customized_url_for_trace = item.get("image_url", {}).get(
144
+ "customized_url_for_trace", ""
145
+ )
146
+ if customized_url_for_trace:
147
+ return {"type": "image_url", "image_url": {"url": customized_url_for_trace}}
148
+ else:
149
+ return item
150
+
151
+
152
+ async def _process_image_item(
153
+ item, trace_id, span_id, message_index, content_index # pylint: disable=W0613
154
+ ) -> dict:
155
+ return process_image_item(item)
156
+
157
+
158
+ # 不优雅的解决方案,如果要优雅的话需要开源社区支持
159
+ chat_wrappers._process_image_item = _process_image_item # pylint: disable=W0212
160
+
161
+ # 获取环境变量
162
+ app_name = MODULE
163
+ app_env = CONF_ENV
164
+ hostname = HOSTNAME
165
+ ip = POD_IP
166
+ # 日志配置
167
+ format_str = "%(asctime)s | %(levelname)-8s | trace_id=%(otelTraceID)-32s span_id=%(otelSpanID)-16s sampled=%(otelTraceSampled)-5s | %(name)s | %(module)s:%(funcName)s:%(lineno)s - %(message)s | %(customized_data_info)s" # pylint: disable=C0103,C0301
168
+ log_config = {
169
+ "version": 1,
170
+ "loggers": {
171
+ "uvicorn": {"level": "INFO", "propagate": True},
172
+ "uvicorn.error": {"level": "INFO", "propagate": True},
173
+ "uvicorn.access": {"level": "INFO", "propagate": True},
174
+ },
175
+ }
176
+ resource = Resource.create(
177
+ attributes={
178
+ "service_name": app_name,
179
+ "service_env": app_env,
180
+ "service.name": app_name,
181
+ "service.env": app_env,
182
+ }
183
+ )
184
+ # trace Tempo 配置
185
+ # os.environ["OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"] = "http/protobuf"
186
+ # os.environ["OTEL_EXPORTER_OTLP_ENDPOINT"] = tempo_url
187
+ # os.environ["OTEL_EXPORTER_OTLP_HEADERS"] = f"Authorization=Basic%20{tempo_credentials}"
188
+ # os.environ["OTEL_PYTHON_LOG_CORRELATION"] = "true"
189
+ # os.environ["OTEL_TRACES_SAMPLER"] = "traceidratio"
190
+ # os.environ["OTEL_TRACES_SAMPLER_ARG"] = "1.00"
191
+ tracer_provider = TracerProvider(resource=resource)
192
+ tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
193
+ # 设置 trace
194
+ trace.set_tracer_provider(tracer_provider)
195
+ # 初始化 root logger
196
+ logger = logging.getLogger()
197
+ logger.setLevel(logging.DEBUG)
198
+ logger.addFilter(NoneRootWarningFilter())
199
+ logger.handlers.clear()
200
+ formatter = ColourizedFormatter(fmt=format_str)
201
+ # 设置 logger 的控制台输出
202
+ console_handler = logging.StreamHandler()
203
+ console_handler.setLevel(logging.INFO)
204
+ console_handler.addFilter(NoneRootWarningFilter())
205
+ console_handler.setFormatter(formatter)
206
+ logger.addHandler(console_handler)
207
+ # 设置 logger 的文件输出(最多保留 3 + 1 个文件)
208
+ file_handler = RotatingFileHandler(
209
+ filename=os.getcwd() + "/logger.log",
210
+ maxBytes=500 * 1024 * 1024, # 每个文件最大 500 MB
211
+ backupCount=3,
212
+ encoding="utf-8"
213
+ )
214
+ file_handler.setLevel(logging.DEBUG)
215
+ file_handler.addFilter(NoneRootWarningFilter())
216
+ file_handler.setFormatter(formatter)
217
+ # 使用 Python 官方推荐的异步日志方案,注意程序结束前需要调用 queue_listener.stop()
218
+ report_queue = Queue(-1)
219
+ queue_handler = QueueHandler(report_queue)
220
+ queue_handler.setLevel(logging.DEBUG)
221
+ queue_handler.addFilter(NoneRootWarningFilter())
222
+ queue_listener = QueueListener(report_queue, file_handler, respect_handler_level=True)
223
+ queue_listener.start()
224
+ logger.addHandler(queue_handler)
225
+ # 设置 Instrumentor
226
+ FastAPIInstrumentor().instrument(
227
+ tracer_provider=tracer_provider, excluded_urls="metrics,health"
228
+ )
229
+ LoggingInstrumentor().instrument(set_logging_format=True, log_format=format_str)
230
+ OpenAIInstrumentor().instrument(tracer_provider=tracer_provider)
231
+
232
+
233
+ def close_logger():
234
+ """
235
+ 确保日志和 trace 都写入完成
236
+ """
237
+ logger.info("close_logger start")
238
+ queue_listener.stop()
239
+ tracer_provider.shutdown()
240
+ logger.info("close_logger success") # 这条日志永远写不了
241
+
242
+
243
+ if __name__ == "__main__":
244
+ from typing import Generic, TypeVar
245
+
246
+ T = TypeVar("T")
247
+
248
+ class TestClass(BaseModel, Generic[T]):
249
+ """
250
+ 用来测试日志输出的类
251
+ """
252
+
253
+ code: int = Field(..., title="业务相关的 code")
254
+ message: str = Field(..., title="业务相关的信息")
255
+ data: T = Field(..., title="数据")
256
+
257
+ a = TestClass[str](code=0, message="testa", data="testa")
258
+ b = TestClass[str](code=0, message="testb", data="testb")
259
+ logger.info(
260
+ "logger init success",
261
+ extra={
262
+ "customized_data_info": {
263
+ "test_list": [1, 2, 3],
264
+ # "test_object_list": [a, b], # bad
265
+ "test_object_str_list": [a.model_dump_json(), b.model_dump_json()],
266
+ "test_dict": {"a": 1, "b": 2, "c": 3},
267
+ # "test_object_dict": {"a": a, "b": b}, # bad
268
+ "test_object_str_dict": {
269
+ "a": a.model_dump_json(),
270
+ "b": b.model_dump_json(),
271
+ },
272
+ "test_none": None,
273
+ # "test_bytes": "test_bytes".encode(), # bad
274
+ "test_bytes_str": str("test_bytes".encode()),
275
+ }
276
+ },
277
+ )
278
+ close_logger()