hello-datap-component-base 0.2.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.
@@ -0,0 +1,187 @@
1
+ import importlib
2
+ import inspect
3
+ import pkgutil
4
+ from pathlib import Path
5
+ from typing import List, Type, Optional, Tuple
6
+ from .base import BaseService
7
+
8
+
9
+ def find_service_classes(
10
+ search_path: str = ".",
11
+ exclude_dirs: List[str] = None
12
+ ) -> List[Tuple[str, Type[BaseService]]]:
13
+ """
14
+ 查找所有继承自 BaseService 的类
15
+
16
+ Args:
17
+ search_path: 搜索路径
18
+ exclude_dirs: 排除的目录
19
+
20
+ Returns:
21
+ 列表,每个元素是 (模块名, 类) 的元组
22
+ """
23
+ if exclude_dirs is None:
24
+ exclude_dirs = ["__pycache__", ".git", ".pytest_cache", "venv", "env", ".venv",
25
+ "hello_datap_component_base.egg-info", "build", "dist"]
26
+
27
+ import sys
28
+ import os
29
+
30
+ # 确保搜索路径在 Python 路径中,以便能够导入模块
31
+ search_path_obj = Path(search_path).resolve()
32
+ search_path_str = str(search_path_obj)
33
+
34
+ # 确保当前工作目录和搜索路径都在 sys.path 中
35
+ current_dir = os.getcwd()
36
+ if current_dir not in sys.path:
37
+ sys.path.insert(0, current_dir)
38
+ if search_path_str not in sys.path:
39
+ sys.path.insert(0, search_path_str)
40
+ # 确保 '.' 也在路径中(相对导入)
41
+ if '.' not in sys.path:
42
+ sys.path.insert(0, '.')
43
+
44
+ service_classes = []
45
+
46
+ # 遍历目录查找 Python 文件
47
+ for py_file in search_path_obj.rglob("*.py"):
48
+ # 跳过排除的目录
49
+ py_file_str = str(py_file)
50
+ py_file_parts = py_file.parts
51
+
52
+ # 检查是否在排除的目录中
53
+ if any(exclude in py_file_parts for exclude in exclude_dirs):
54
+ continue
55
+
56
+ # 跳过包目录(hello_datap_component_base)下的文件
57
+ # 但允许根目录下的其他文件(如 example_service.py)
58
+ if "hello_datap_component_base" in py_file_parts:
59
+ # 如果文件在包目录内(不是包目录本身作为文件名),跳过
60
+ idx = py_file_parts.index("hello_datap_component_base")
61
+ if idx < len(py_file_parts) - 1: # 包目录下还有子路径
62
+ continue
63
+
64
+ # 计算模块路径
65
+ relative_path = py_file.relative_to(search_path_obj)
66
+ module_path = str(relative_path.with_suffix('')).replace('/', '.')
67
+
68
+ try:
69
+ # 动态导入模块
70
+ module = importlib.import_module(module_path)
71
+
72
+ # 查找模块中继承自 BaseService 的类
73
+ for name, obj in inspect.getmembers(module, inspect.isclass):
74
+ try:
75
+ # 检查是否是 BaseService 的子类
76
+ if (inspect.isclass(obj) and
77
+ issubclass(obj, BaseService) and
78
+ obj is not BaseService and
79
+ not inspect.isabstract(obj)):
80
+
81
+ # 检查是否实现了 process 方法
82
+ if hasattr(obj, 'process'):
83
+ service_classes.append((module_path, obj))
84
+ except (TypeError, AttributeError) as e:
85
+ # 跳过不是类的对象或无法检查的对象
86
+ continue
87
+
88
+ except (ImportError, ValueError, AttributeError) as e:
89
+ # 对于根目录下的用户文件,输出错误信息以便调试
90
+ # 检查是否是搜索路径下的直接文件(不是包内的文件)
91
+ relative_to_search = py_file.relative_to(search_path_obj)
92
+ is_root_file = len(relative_to_search.parts) == 1 and py_file.name not in ["__init__.py"]
93
+
94
+ if is_root_file:
95
+ # 输出到stderr以便用户看到
96
+ import sys
97
+ error_msg = str(e)
98
+ # 检查是否是缺少模块的错误
99
+ if "No module named" in error_msg or "ModuleNotFoundError" in error_msg:
100
+ print(f"⚠️ 警告: 导入 {module_path} 失败,缺少依赖: {error_msg}", file=sys.stderr)
101
+ print(f" 提示: 请检查配置文件的 runtime_env.pip 是否包含所需的包", file=sys.stderr)
102
+ else:
103
+ print(f"⚠️ 警告: 导入 {module_path} 失败: {error_msg}", file=sys.stderr)
104
+ continue
105
+ except Exception as e:
106
+ # 对于根目录下的用户文件,输出错误信息以便调试
107
+ relative_to_search = py_file.relative_to(search_path_obj)
108
+ is_root_file = len(relative_to_search.parts) == 1 and py_file.name not in ["__init__.py"]
109
+
110
+ if is_root_file:
111
+ import sys
112
+ error_msg = str(e)
113
+ if "No module named" in error_msg or "ModuleNotFoundError" in error_msg:
114
+ print(f"⚠️ 警告: 导入 {module_path} 失败,缺少依赖: {error_msg}", file=sys.stderr)
115
+ print(f" 提示: 请检查配置文件的 runtime_env.pip 是否包含所需的包", file=sys.stderr)
116
+ else:
117
+ print(f"⚠️ 警告: 导入 {module_path} 时出错: {error_msg}", file=sys.stderr)
118
+ import traceback
119
+ traceback.print_exc(file=sys.stderr)
120
+ continue
121
+
122
+ return service_classes
123
+
124
+
125
+ def get_single_service_class(
126
+ search_path: str = ".",
127
+ class_name: Optional[str] = None
128
+ ) -> Type[BaseService]:
129
+ """
130
+ 获取单个服务类
131
+
132
+ Args:
133
+ search_path: 搜索路径
134
+ class_name: 指定的类名(可选)
135
+
136
+ Returns:
137
+ 服务类
138
+
139
+ Raises:
140
+ ValueError: 如果找到0个或多个服务类
141
+ """
142
+ service_classes = find_service_classes(search_path)
143
+
144
+ if not service_classes:
145
+ import os
146
+ current_dir = os.getcwd()
147
+ search_abs = os.path.abspath(search_path)
148
+
149
+ # 尝试列出一些可能的服务文件
150
+ possible_files = []
151
+ for py_file in Path(search_abs).rglob("*.py"):
152
+ if "example" in str(py_file).lower() and "service" in str(py_file).lower():
153
+ possible_files.append(str(py_file.relative_to(search_abs)))
154
+ if len(possible_files) >= 3:
155
+ break
156
+
157
+ error_msg = (
158
+ f"No service class found in '{search_abs}' (current directory: {current_dir}).\n"
159
+ f"Please ensure:\n"
160
+ f" 1. You are in the project root directory\n"
161
+ f" 2. There is a Python file containing a class that inherits from BaseService\n"
162
+ f" 3. The service class implements the 'process' method\n"
163
+ )
164
+ if possible_files:
165
+ error_msg += f"\nPossible service files found:\n"
166
+ for f in possible_files:
167
+ error_msg += f" - {f}\n"
168
+ error_msg += f"\nTry using --class-name to specify the service class name."
169
+
170
+ raise ValueError(error_msg)
171
+
172
+ if class_name:
173
+ # 查找指定类名的服务
174
+ for module_path, cls in service_classes:
175
+ if cls.__name__ == class_name:
176
+ return cls
177
+ raise ValueError(f"Service class '{class_name}' not found")
178
+
179
+ # 检查是否只有一个服务类
180
+ if len(service_classes) > 1:
181
+ class_list = [f"{module}.{cls.__name__}" for module, cls in service_classes]
182
+ raise ValueError(
183
+ f"Multiple service classes found: {class_list}. "
184
+ f"Please specify which one to use with --class-name."
185
+ )
186
+
187
+ return service_classes[0][1]
@@ -0,0 +1,290 @@
1
+ import logging
2
+ import sys
3
+ from typing import Optional, Dict, Any
4
+ import json
5
+ from datetime import datetime
6
+ from pythonjsonlogger import jsonlogger
7
+
8
+
9
+ class ServiceLoggerAdapter(logging.LoggerAdapter):
10
+ """服务日志适配器,自动在每条日志中添加服务名称和版本信息"""
11
+
12
+ def __init__(self, logger: logging.Logger, service_name: str, version: Optional[str] = None):
13
+ super().__init__(logger, {})
14
+ self.service_name = service_name
15
+ self.version = version
16
+
17
+ def process(self, msg, kwargs):
18
+ """处理日志消息,添加服务信息"""
19
+ # 确保 extra 字典存在
20
+ if 'extra' not in kwargs:
21
+ kwargs['extra'] = {}
22
+
23
+ # 添加服务名称和版本信息
24
+ kwargs['extra']['service'] = self.service_name
25
+ if self.version:
26
+ kwargs['extra']['version'] = self.version
27
+
28
+ return msg, kwargs
29
+
30
+
31
+ class CustomJsonFormatter(jsonlogger.JsonFormatter):
32
+ """自定义 JSON 格式化器"""
33
+
34
+ def add_fields(self, log_record: Dict[str, Any], record: logging.LogRecord, message_dict: Dict[str, Any]):
35
+ super().add_fields(log_record, record, message_dict)
36
+
37
+ # 添加时间戳
38
+ if not log_record.get('timestamp'):
39
+ log_record['timestamp'] = datetime.utcnow().isoformat()
40
+
41
+ # 添加日志级别
42
+ if not log_record.get('level'):
43
+ log_record['level'] = record.levelname
44
+
45
+ # 添加进程信息
46
+ log_record['pid'] = record.process
47
+ log_record['process_name'] = record.processName
48
+
49
+ # 添加文件位置
50
+ log_record['file'] = record.filename
51
+ log_record['line'] = record.lineno
52
+ log_record['function'] = record.funcName
53
+
54
+ # 确保服务信息在 JSON 日志中(如果 extra 中有的话)
55
+ if 'service' in log_record:
56
+ log_record['service'] = log_record['service']
57
+ if 'version' in log_record:
58
+ log_record['version'] = log_record['version']
59
+
60
+
61
+ def setup_logging(
62
+ level: str = "INFO",
63
+ json_format: bool = False,
64
+ service_name: str = "unknown",
65
+ version: Optional[str] = None
66
+ ):
67
+ """
68
+ 设置日志配置
69
+
70
+ 兼容 Ray 分布式计算框架:日志输出到 stdout/stderr,Ray 会自动收集这些日志。
71
+
72
+ Args:
73
+ level: 日志级别
74
+ json_format: 是否使用 JSON 格式
75
+ service_name: 服务名称
76
+ version: 服务版本(可选)
77
+ """
78
+ # 检测是否在 Ray 环境中运行
79
+ is_ray_env = False
80
+ try:
81
+ import ray
82
+ is_ray_env = ray.is_initialized()
83
+ except ImportError:
84
+ pass
85
+
86
+ # 移除所有现有的处理器
87
+ root_logger = logging.getLogger()
88
+ for handler in root_logger.handlers[:]:
89
+ root_logger.removeHandler(handler)
90
+
91
+ # 设置日志级别
92
+ numeric_level = getattr(logging, level.upper(), None)
93
+ if not isinstance(numeric_level, int):
94
+ numeric_level = logging.INFO
95
+
96
+ root_logger.setLevel(numeric_level)
97
+
98
+ # 创建处理器
99
+ if json_format:
100
+ formatter = CustomJsonFormatter(
101
+ '%(timestamp)s %(level)s %(name)s %(message)s',
102
+ rename_fields={
103
+ 'level': 'levelname',
104
+ 'timestamp': 'asctime'
105
+ }
106
+ )
107
+ else:
108
+ # 改进日志格式,包含服务名称和版本
109
+ version_str = f" v{version}" if version else ""
110
+ formatter = logging.Formatter(
111
+ f'%(asctime)s - [{service_name}{version_str}] - %(levelname)s - '
112
+ f'%(filename)s:%(lineno)d - %(message)s',
113
+ datefmt='%Y-%m-%d %H:%M:%S'
114
+ )
115
+
116
+ # 控制台处理器(输出到 stdout,Ray 会自动捕获)
117
+ console_handler = logging.StreamHandler(sys.stdout)
118
+ console_handler.setFormatter(formatter)
119
+ root_logger.addHandler(console_handler)
120
+
121
+ # 错误日志输出到 stderr(Ray 也会捕获)
122
+ error_handler = logging.StreamHandler(sys.stderr)
123
+ error_handler.setFormatter(formatter)
124
+ error_handler.setLevel(logging.ERROR)
125
+ root_logger.addHandler(error_handler)
126
+
127
+ # 文件处理器(在 Ray 环境中可能不可用,优雅处理)
128
+ if not is_ray_env:
129
+ # 非 Ray 环境才尝试创建文件日志
130
+ try:
131
+ file_handler = logging.FileHandler(f"{service_name}.log", encoding='utf-8')
132
+ file_handler.setFormatter(formatter)
133
+ root_logger.addHandler(file_handler)
134
+ except (IOError, PermissionError):
135
+ pass # 无法创建日志文件,只输出到控制台
136
+
137
+
138
+ def get_service_logger(name: str, version: Optional[str] = None) -> ServiceLoggerAdapter:
139
+ """
140
+ 获取服务专用的日志器(带服务名称和版本信息)
141
+
142
+ 兼容 Ray 分布式计算框架:日志输出到 stdout/stderr,Ray 会自动收集。
143
+
144
+ Args:
145
+ name: 服务名称
146
+ version: 服务版本(可选)
147
+
148
+ Returns:
149
+ 配置好的日志适配器
150
+ """
151
+ logger = logging.getLogger(f"service.{name}")
152
+
153
+ # 如果还没有处理器,添加一个默认的
154
+ if not logger.handlers:
155
+ # 检测是否在 Ray 环境中运行
156
+ is_ray_env = False
157
+ try:
158
+ import ray
159
+ is_ray_env = ray.is_initialized()
160
+ except ImportError:
161
+ pass
162
+
163
+ # stdout 处理器(Ray 会自动捕获)
164
+ console_handler = logging.StreamHandler(sys.stdout)
165
+ # 改进日志格式,包含服务名称和版本
166
+ version_str = f" v{version}" if version else ""
167
+ formatter = logging.Formatter(
168
+ f'%(asctime)s - [{name}{version_str}] - %(levelname)s - %(message)s',
169
+ datefmt='%Y-%m-%d %H:%M:%S'
170
+ )
171
+ console_handler.setFormatter(formatter)
172
+ logger.addHandler(console_handler)
173
+
174
+ # stderr 处理器用于错误日志(Ray 也会捕获)
175
+ error_handler = logging.StreamHandler(sys.stderr)
176
+ error_handler.setFormatter(formatter)
177
+ error_handler.setLevel(logging.ERROR)
178
+ logger.addHandler(error_handler)
179
+
180
+ logger.setLevel(logging.INFO)
181
+
182
+ # 在非 Ray 环境中,尝试添加文件处理器
183
+ if not is_ray_env:
184
+ try:
185
+ file_handler = logging.FileHandler(f"{name}.log", encoding='utf-8')
186
+ file_handler.setFormatter(formatter)
187
+ logger.addHandler(file_handler)
188
+ except (IOError, PermissionError):
189
+ pass
190
+
191
+ # 返回适配器,自动添加服务信息
192
+ return ServiceLoggerAdapter(logger, name, version)
193
+
194
+
195
+ # 全局 logger 实例,方便用户直接导入使用
196
+ _global_logger: Optional[ServiceLoggerAdapter] = None
197
+ _global_service_name: Optional[str] = None
198
+ _global_version: Optional[str] = None
199
+
200
+
201
+ def set_service_context(service_name: str, version: Optional[str] = None):
202
+ """
203
+ 设置全局服务上下文(服务名称和版本)
204
+
205
+ 当服务初始化时,会自动调用此函数设置上下文。
206
+ 设置后,全局 logger 会自动包含服务信息。
207
+
208
+ Args:
209
+ service_name: 服务名称
210
+ version: 服务版本(可选)
211
+ """
212
+ global _global_logger, _global_service_name, _global_version
213
+ _global_service_name = service_name
214
+ _global_version = version
215
+ # 重新创建 logger 以应用新的上下文
216
+ _global_logger = get_service_logger(service_name, version)
217
+
218
+
219
+ def get_logger() -> ServiceLoggerAdapter:
220
+ """
221
+ 获取全局 logger 实例
222
+
223
+ 如果已经设置了服务上下文(通过 set_service_context),返回带服务信息的 logger。
224
+ 否则返回一个默认的 logger。
225
+
226
+ Returns:
227
+ 日志适配器实例
228
+ """
229
+ global _global_logger, _global_service_name, _global_version
230
+
231
+ if _global_logger is None:
232
+ # 如果还没有设置服务上下文,创建一个默认的 logger
233
+ if _global_service_name:
234
+ _global_logger = get_service_logger(_global_service_name, _global_version)
235
+ else:
236
+ # 使用默认名称创建 logger
237
+ _global_logger = get_service_logger("unknown", None)
238
+
239
+ return _global_logger
240
+
241
+
242
+ # 创建一个延迟加载的 logger 包装类,确保在 Ray 环境中也能正常工作
243
+ class _LazyLogger:
244
+ """
245
+ 延迟加载的 logger 包装类,确保在 Ray 环境中也能正常工作
246
+
247
+ 这个类通过 __getattr__ 代理所有方法调用到底层的 ServiceLoggerAdapter 实例,
248
+ 避免了模块级别全局变量在 Ray 序列化时可能出现的问题。
249
+ """
250
+
251
+ def __getattr__(self, name):
252
+ """
253
+ 延迟获取 logger 实例的属性
254
+
255
+ 每次访问属性时都会获取最新的 logger 实例,确保在 Ray 环境中
256
+ 即使模块被重新导入,也能获取到正确的 logger。
257
+ """
258
+ logger_instance = get_logger()
259
+ attr = getattr(logger_instance, name)
260
+ # 如果属性是可调用的,返回一个包装函数以确保每次调用都使用最新的 logger
261
+ if callable(attr):
262
+ def wrapper(*args, **kwargs):
263
+ current_logger = get_logger()
264
+ method = getattr(current_logger, name)
265
+ return method(*args, **kwargs)
266
+ return wrapper
267
+ return attr
268
+
269
+ def __call__(self, *args, **kwargs):
270
+ """如果 logger 被当作函数调用"""
271
+ logger_instance = get_logger()
272
+ return logger_instance(*args, **kwargs)
273
+
274
+ def __repr__(self):
275
+ """返回 logger 的字符串表示"""
276
+ logger_instance = get_logger()
277
+ return repr(logger_instance)
278
+
279
+ def __str__(self):
280
+ """返回 logger 的字符串表示"""
281
+ logger_instance = get_logger()
282
+ return str(logger_instance)
283
+
284
+ def __dir__(self):
285
+ """返回 logger 的所有属性,用于 IDE 自动补全"""
286
+ logger_instance = get_logger()
287
+ return dir(logger_instance)
288
+
289
+ # 创建延迟加载的 logger 实例
290
+ logger = _LazyLogger()