sycommon-python-lib 0.1.16__py3-none-any.whl → 0.1.56b1__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 (36) hide show
  1. sycommon/config/Config.py +6 -2
  2. sycommon/config/RerankerConfig.py +1 -0
  3. sycommon/database/async_base_db_service.py +36 -0
  4. sycommon/database/async_database_service.py +96 -0
  5. sycommon/database/database_service.py +6 -1
  6. sycommon/health/metrics.py +13 -0
  7. sycommon/llm/__init__.py +0 -0
  8. sycommon/llm/embedding.py +149 -0
  9. sycommon/llm/get_llm.py +177 -0
  10. sycommon/llm/llm_logger.py +126 -0
  11. sycommon/logging/async_sql_logger.py +65 -0
  12. sycommon/logging/kafka_log.py +36 -14
  13. sycommon/logging/logger_levels.py +23 -0
  14. sycommon/logging/sql_logger.py +53 -0
  15. sycommon/middleware/context.py +2 -0
  16. sycommon/middleware/middleware.py +4 -0
  17. sycommon/middleware/traceid.py +155 -32
  18. sycommon/models/mqlistener_config.py +1 -0
  19. sycommon/rabbitmq/rabbitmq_client.py +377 -821
  20. sycommon/rabbitmq/rabbitmq_pool.py +338 -0
  21. sycommon/rabbitmq/rabbitmq_service.py +411 -229
  22. sycommon/services.py +116 -61
  23. sycommon/synacos/example.py +153 -0
  24. sycommon/synacos/example2.py +129 -0
  25. sycommon/synacos/feign.py +90 -413
  26. sycommon/synacos/feign_client.py +335 -0
  27. sycommon/synacos/nacos_service.py +159 -106
  28. sycommon/synacos/param.py +75 -0
  29. sycommon/tools/merge_headers.py +97 -0
  30. sycommon/tools/snowflake.py +296 -7
  31. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/METADATA +19 -13
  32. sycommon_python_lib-0.1.56b1.dist-info/RECORD +68 -0
  33. sycommon_python_lib-0.1.16.dist-info/RECORD +0 -52
  34. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/WHEEL +0 -0
  35. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/entry_points.txt +0 -0
  36. {sycommon_python_lib-0.1.16.dist-info → sycommon_python_lib-0.1.56b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,335 @@
1
+ import io
2
+ import os
3
+ import time
4
+ import inspect
5
+ from typing import Any, Dict, Optional, Literal, Type, TypeVar
6
+ from urllib.parse import urljoin
7
+
8
+ from sycommon.tools.merge_headers import merge_headers
9
+ from sycommon.tools.snowflake import Snowflake
10
+
11
+ import aiohttp
12
+ from pydantic import BaseModel
13
+ from sycommon.synacos.param import Body, Cookie, File, Form, Header, Param, Path, Query
14
+ from sycommon.logging.kafka_log import SYLogger
15
+ from sycommon.synacos.nacos_service import NacosService
16
+
17
+ # 定义 Pydantic 模型泛型(用于响应解析)
18
+ T = TypeVar('T', bound=BaseModel)
19
+
20
+ # ------------------------------
21
+ # Feign客户端装饰器(支持Pydantic)
22
+ # ------------------------------
23
+
24
+
25
+ def feign_client(
26
+ service_name: str,
27
+ path_prefix: str = "",
28
+ default_timeout: Optional[float] = None,
29
+ default_headers: Optional[Dict[str, str]] = None
30
+ ):
31
+ default_headers = default_headers or {}
32
+ default_headers = {k.lower(): v for k, v in default_headers.items()}
33
+ default_headers = merge_headers(SYLogger.get_headers(), default_headers)
34
+ default_headers["x-traceId-header"] = SYLogger.get_trace_id() or Snowflake.id
35
+
36
+ def decorator(cls):
37
+ class FeignClient:
38
+ def __init__(self):
39
+ self.service_name = service_name
40
+ self.path_prefix = path_prefix
41
+ self.default_timeout = default_timeout
42
+ self.default_headers = {
43
+ k.lower(): v for k, v in default_headers.copy().items()}
44
+ self.nacos_manager: Optional[NacosService] = None
45
+ self.session: Optional[aiohttp.ClientSession] = None
46
+
47
+ def __getattr__(self, name: str):
48
+ if not hasattr(cls, name):
49
+ raise AttributeError(f"类 {cls.__name__} 不存在方法 {name}")
50
+
51
+ func = getattr(cls, name)
52
+ sig = inspect.signature(func)
53
+ param_meta = self._parse_param_meta(sig)
54
+ # 获取响应模型(从返回类型注解中提取 Pydantic 模型)
55
+ resp_model = self._get_response_model(sig)
56
+
57
+ async def wrapper(*args, **kwargs) -> Any:
58
+ if not self.session:
59
+ self.session = aiohttp.ClientSession()
60
+ if not self.nacos_manager:
61
+ self.nacos_manager = NacosService(None)
62
+
63
+ try:
64
+ bound_args = self._bind_arguments(
65
+ func, sig, args, kwargs)
66
+ self._validate_required_params(param_meta, bound_args)
67
+
68
+ request_meta = getattr(func, "_feign_meta", {})
69
+ method = request_meta.get("method", "GET").upper()
70
+ path = request_meta.get("path", "")
71
+ is_upload = request_meta.get("is_upload", False)
72
+ method_headers = {
73
+ k.lower(): v for k, v in request_meta.get("headers", {}).items()}
74
+ timeout = request_meta.get(
75
+ "timeout", self.default_timeout)
76
+
77
+ headers = self._build_headers(
78
+ param_meta, bound_args, method_headers)
79
+ full_path = f"{self.path_prefix}{path}"
80
+ full_path = self._replace_path_params(
81
+ full_path, param_meta, bound_args)
82
+
83
+ base_url = await self._get_service_base_url(headers)
84
+ url = urljoin(base_url, full_path)
85
+ SYLogger.info(f"请求: {method} {url}")
86
+
87
+ query_params = self._get_query_params(
88
+ param_meta, bound_args)
89
+ cookies = self._get_cookies(param_meta, bound_args)
90
+ # 处理请求数据(支持 Pydantic 模型转字典)
91
+ request_data = await self._get_request_data(
92
+ method, param_meta, bound_args, is_upload, method_headers
93
+ )
94
+
95
+ async with self.session.request(
96
+ method=method,
97
+ url=url,
98
+ headers=headers,
99
+ params=query_params,
100
+ cookies=cookies,
101
+ json=request_data if not (is_upload or isinstance(
102
+ request_data, aiohttp.FormData)) else None,
103
+ data=request_data if is_upload or isinstance(
104
+ request_data, aiohttp.FormData) else None,
105
+ timeout=timeout
106
+ ) as response:
107
+ # 处理响应(支持 Pydantic 模型解析)
108
+ return await self._handle_response(response, resp_model)
109
+
110
+ finally:
111
+ if self.session:
112
+ await self.session.close()
113
+ self.session = None
114
+
115
+ return wrapper
116
+
117
+ def _parse_param_meta(self, sig: inspect.Signature) -> Dict[str, Param]:
118
+ param_meta = {}
119
+ for param in sig.parameters.values():
120
+ if param.name == "self":
121
+ continue
122
+ if isinstance(param.default, Param):
123
+ param_meta[param.name] = param.default
124
+ else:
125
+ if param.default == inspect.Parameter.empty:
126
+ param_meta[param.name] = Query(..., description="")
127
+ else:
128
+ param_meta[param.name] = Query(
129
+ param.default, description="")
130
+ return param_meta
131
+
132
+ def _get_response_model(self, sig: inspect.Signature) -> Optional[Type[BaseModel]]:
133
+ """从函数返回类型注解中提取 Pydantic 模型"""
134
+ return_annotation = sig.return_annotation
135
+ # 支持直接注解(如 -> ProductResp)或 Optional(如 -> Optional[ProductResp])
136
+ if hasattr(return_annotation, '__origin__') and return_annotation.__origin__ is Optional:
137
+ return_annotation = return_annotation.__args__[0]
138
+ # 检查是否为 Pydantic 模型
139
+ if inspect.isclass(return_annotation) and issubclass(return_annotation, BaseModel):
140
+ return return_annotation
141
+ return None
142
+
143
+ def _bind_arguments(self, func, sig: inspect.Signature, args, kwargs) -> Dict[str, Any]:
144
+ try:
145
+ bound_args = sig.bind(*args, **kwargs)
146
+ bound_args.apply_defaults()
147
+ return {k: v for k, v in bound_args.arguments.items() if k != "self"}
148
+ except TypeError as e:
149
+ SYLogger.error(f"参数绑定失败 [{func.__name__}]: {str(e)}")
150
+ raise
151
+
152
+ def _validate_required_params(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any]):
153
+ missing = [
154
+ meta.get_key(name) for name, meta in param_meta.items()
155
+ if meta.is_required() and name not in bound_args
156
+ ]
157
+ if missing:
158
+ raise ValueError(f"缺少必填参数: {', '.join(missing)}")
159
+
160
+ def _build_headers(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any], method_headers: Dict[str, str]) -> Dict[str, str]:
161
+ headers = self.default_headers.copy()
162
+ headers.update(method_headers)
163
+ headers = merge_headers(SYLogger.get_headers(), headers)
164
+ headers["x-traceId-header"] = SYLogger.get_trace_id() or Snowflake.id
165
+
166
+ # 处理参数中的Header类型
167
+ for name, meta in param_meta.items():
168
+ if isinstance(meta, Header) and name in bound_args:
169
+ value = bound_args[name]
170
+ if value is not None:
171
+ header_key = meta.get_key(name).lower()
172
+ headers[header_key] = str(value)
173
+ return headers
174
+
175
+ def _replace_path_params(self, path: str, param_meta: Dict[str, Param], bound_args: Dict[str, Any]) -> str:
176
+ for name, meta in param_meta.items():
177
+ if isinstance(meta, Path) and name in bound_args:
178
+ path = path.replace(
179
+ f"{{{meta.get_key(name)}}}", str(bound_args[name]))
180
+ return path
181
+
182
+ def _get_query_params(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any]) -> Dict[str, str]:
183
+ return {
184
+ param_meta[name].get_key(name): str(value)
185
+ for name, value in bound_args.items()
186
+ if isinstance(param_meta.get(name), Query) and value is not None
187
+ }
188
+
189
+ def _get_cookies(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any]) -> Dict[str, str]:
190
+ return {
191
+ param_meta[name].get_key(name): str(value)
192
+ for name, value in bound_args.items()
193
+ if isinstance(param_meta.get(name), Cookie) and value is not None
194
+ }
195
+
196
+ async def _get_request_data(
197
+ self,
198
+ method: str,
199
+ param_meta: Dict[str, Param],
200
+ bound_args: Dict[str, Any],
201
+ is_upload: bool,
202
+ method_headers: Dict[str, str]
203
+ ) -> Any:
204
+ """处理请求数据(支持 Pydantic 模型转字典)"""
205
+ if is_upload:
206
+ form_data = aiohttp.FormData()
207
+ # 处理文件
208
+ file_params = {
209
+ n: m for n, m in param_meta.items() if isinstance(m, File)}
210
+ for name, meta in file_params.items():
211
+ if name not in bound_args:
212
+ continue
213
+ file_paths = bound_args[name]
214
+ file_paths = [file_paths] if isinstance(
215
+ file_paths, str) else file_paths
216
+ for path in file_paths:
217
+ if not os.path.exists(path):
218
+ raise FileNotFoundError(f"文件不存在: {path}")
219
+ with open(path, "rb") as f:
220
+ form_data.add_field(
221
+ meta.field_name, f.read(), filename=os.path.basename(path)
222
+ )
223
+ # 处理表单字段(支持 Pydantic 模型)
224
+ form_params = {
225
+ n: m for n, m in param_meta.items() if isinstance(m, Form)}
226
+ for name, meta in form_params.items():
227
+ if name not in bound_args or bound_args[name] is None:
228
+ continue
229
+ value = bound_args[name]
230
+ # 若为 Pydantic 模型,转为字典
231
+ if isinstance(value, BaseModel):
232
+ value = value.dict()
233
+ form_data.add_field(meta.get_key(name), str(
234
+ value) if not isinstance(value, dict) else value)
235
+ return form_data
236
+
237
+ # 从headers中获取Content-Type(已小写key)
238
+ content_type = self.default_headers.get(
239
+ "content-type") or method_headers.get("content-type", "")
240
+ # 转为小写进行判断
241
+ content_type_lower = content_type.lower()
242
+
243
+ # 处理表单提交(x-www-form-urlencoded)
244
+ if "application/x-www-form-urlencoded" in content_type_lower:
245
+ form_data = {}
246
+ for name, value in bound_args.items():
247
+ meta = param_meta.get(name)
248
+ if isinstance(meta, Form) and value is not None:
249
+ # Pydantic 模型转字典
250
+ if isinstance(value, BaseModel):
251
+ value = value.dict()
252
+ form_data[meta.get_key(name)] = str(
253
+ value) if not isinstance(value, dict) else value
254
+ return form_data
255
+
256
+ # 处理 JSON 请求体(支持 Pydantic 模型)
257
+ if method in ["POST", "PUT", "PATCH", "DELETE"]:
258
+ body_params = [
259
+ name for name, meta in param_meta.items() if isinstance(meta, Body)]
260
+ if body_params:
261
+ body_data = {}
262
+ for name in body_params:
263
+ meta = param_meta[name]
264
+ value = bound_args.get(name)
265
+ if value is None:
266
+ continue
267
+ # 若为 Pydantic 模型,转为字典
268
+ if isinstance(value, BaseModel):
269
+ value = value.dict()
270
+ if meta.embed:
271
+ body_data[meta.get_key(name)] = value
272
+ else:
273
+ body_data = value if not isinstance(value, dict) else {
274
+ ** body_data, **value}
275
+ return body_data
276
+ return None
277
+
278
+ async def _get_service_base_url(self, headers: Dict[str, str]) -> str:
279
+ version = headers.get("s-y-version")
280
+ instances = self.nacos_manager.get_service_instances(
281
+ self.service_name, target_version=version)
282
+ if not instances:
283
+ raise RuntimeError(f"服务 [{self.service_name}] 无可用实例")
284
+ return f"http://{instances[int(time.time()) % len(instances)]['ip']}:{instances[0]['port']}"
285
+
286
+ async def _handle_response(self, response: aiohttp.ClientResponse, resp_model: Optional[Type[BaseModel]]) -> Any:
287
+ """处理响应(支持 Pydantic 模型解析)"""
288
+ status = response.status
289
+ if 200 <= status < 300:
290
+ content_type = response.headers.get(
291
+ "content-type", "").lower()
292
+ if "application/json" in content_type:
293
+ json_data = await response.json()
294
+ # 若指定了 Pydantic 响应模型,自动解析
295
+ if resp_model is not None:
296
+ return resp_model(** json_data) # 用响应数据初始化模型
297
+ return json_data
298
+ else:
299
+ return io.BytesIO(await response.read())
300
+ else:
301
+ error_msg = await response.text()
302
+ SYLogger.error(f"请求失败 [{status}]: {error_msg}")
303
+ raise RuntimeError(f"HTTP {status}: {error_msg}")
304
+
305
+ return FeignClient
306
+
307
+ return decorator
308
+
309
+
310
+ def feign_request(
311
+ method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"],
312
+ path: str,
313
+ headers: Optional[Dict[str, str]] = None,
314
+ timeout: Optional[float] = None
315
+ ):
316
+ def decorator(func):
317
+ func._feign_meta = {
318
+ "method": method.upper(),
319
+ "path": path,
320
+ "headers": {k.lower(): v for k, v in headers.items()} if headers else {},
321
+ "is_upload": False,
322
+ "timeout": timeout
323
+ }
324
+ return func
325
+ return decorator
326
+
327
+
328
+ def feign_upload(field_name: str = "file"):
329
+ def decorator(func):
330
+ if not hasattr(func, "_feign_meta"):
331
+ raise ValueError("feign_upload必须与feign_request一起使用")
332
+ func._feign_meta["is_upload"] = True
333
+ func._feign_meta["upload_field"] = field_name
334
+ return func
335
+ return decorator