sycommon-python-lib 0.1.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 sycommon-python-lib might be problematic. Click here for more details.

@@ -0,0 +1,32 @@
1
+ from sycommon.health.ping import setup_ping_handler
2
+ from sycommon.middleware.cors import setup_cors_handler
3
+ from sycommon.middleware.exception import setup_exception_handler
4
+ from sycommon.middleware.monitor_memory import setup_monitor_memory_middleware
5
+ from sycommon.middleware.timeout import setup_request_timeout_middleware
6
+ from sycommon.middleware.traceid import setup_trace_id_handler
7
+ from sycommon.health.health_check import setup_health_handler
8
+
9
+
10
+ def setup_middleware(app, config: dict):
11
+ # 设置请求超时中间件
12
+ app = setup_request_timeout_middleware(app, config)
13
+
14
+ # 设置异常处理
15
+ app = setup_exception_handler(app, config)
16
+
17
+ # 设置 trace_id 处理中间件
18
+ app = setup_trace_id_handler(app)
19
+
20
+ # 设置内存监控中间件
21
+ # app = setup_monitor_memory_middleware(app)
22
+
23
+ # 设置cors
24
+ # app = setup_cors_handler(app)
25
+
26
+ # 健康检查
27
+ app = setup_health_handler(app)
28
+
29
+ # ping
30
+ app = setup_ping_handler(app)
31
+
32
+ return app
@@ -0,0 +1,22 @@
1
+
2
+ import tracemalloc
3
+ from fastapi import Request
4
+
5
+ from sycommon.logging.kafka_log import SYLogger
6
+
7
+
8
+ def setup_monitor_memory_middleware(app):
9
+ @app.middleware("http")
10
+ async def before_request(request: Request, call_next):
11
+ # if not tracemalloc.is_tracing():
12
+ # tracemalloc.start()
13
+
14
+ # snapshot1 = tracemalloc.take_snapshot()
15
+ # await call_next(request)
16
+ # snapshot2 = tracemalloc.take_snapshot()
17
+
18
+ # top_stats = snapshot2.compare_to(snapshot1, 'lineno')
19
+ # if top_stats:
20
+ # SYLogger.info(f"内存增长最大项: {top_stats[0]}")
21
+ pass
22
+ return app
@@ -0,0 +1,19 @@
1
+
2
+ import time
3
+ from fastapi import Request
4
+ from fastapi.responses import JSONResponse
5
+
6
+
7
+ def setup_request_timeout_middleware(app, config: dict):
8
+ # 设置全局请求超时时间
9
+ REQUEST_TIMEOUT = int(config.get('Timeout', 30000))/1000
10
+
11
+ @app.middleware("http")
12
+ async def before_request(request: Request, call_next):
13
+ request.state.start_time = time.time()
14
+ response = await call_next(request)
15
+ duration = time.time() - request.state.start_time
16
+ if duration > REQUEST_TIMEOUT:
17
+ return JSONResponse(content={'code': 1, 'error': 'Request timed out'}, status_code=504)
18
+ return response
19
+ return app
@@ -0,0 +1,138 @@
1
+ import json
2
+ import re
3
+ from typing import Dict, Any
4
+ from fastapi import Request, Response
5
+ from sycommon.logging.kafka_log import SYLogger
6
+ from sycommon.tools.snowflake import Snowflake
7
+
8
+
9
+ def setup_trace_id_handler(app):
10
+ @app.middleware("http")
11
+ async def trace_id_and_log_middleware(request: Request, call_next):
12
+ # 生成或获取 traceId
13
+ trace_id = request.headers.get("x-traceId-header")
14
+ if not trace_id:
15
+ trace_id = Snowflake.next_id()
16
+
17
+ # 设置 trace_id 上下文
18
+ token = SYLogger.set_trace_id(trace_id)
19
+
20
+ # 获取请求参数
21
+ query_params = dict(request.query_params)
22
+ request_body: Dict[str, Any] = {}
23
+ files_info: Dict[str, str] = {}
24
+
25
+ # 检测请求内容类型
26
+ content_type = request.headers.get("content-type", "").lower()
27
+
28
+ if "application/json" in content_type and request.method in ["POST", "PUT", "PATCH"]:
29
+ try:
30
+ request_body = await request.json()
31
+ except Exception as e:
32
+ request_body = {"error": f"Failed to parse JSON: {str(e)}"}
33
+
34
+ elif "multipart/form-data" in content_type and request.method in ["POST", "PUT"]:
35
+ try:
36
+ # 从请求头中提取boundary
37
+ boundary = None
38
+ if "boundary=" in content_type:
39
+ boundary = content_type.split("boundary=")[1].strip()
40
+ boundary = boundary.encode('ascii')
41
+
42
+ if boundary:
43
+ # 读取原始请求体
44
+ body = await request.body()
45
+
46
+ # 尝试从原始请求体中提取文件名
47
+ parts = body.split(boundary)
48
+ for part in parts:
49
+ part_str = part.decode('utf-8', errors='ignore')
50
+
51
+ # 使用正则表达式查找文件名
52
+ filename_match = re.search(
53
+ r'filename="([^"]+)"', part_str)
54
+ if filename_match:
55
+ field_name_match = re.search(
56
+ r'name="([^"]+)"', part_str)
57
+ field_name = field_name_match.group(
58
+ 1) if field_name_match else "unknown"
59
+ filename = filename_match.group(1)
60
+ files_info[field_name] = filename
61
+ except Exception as e:
62
+ request_body = {
63
+ "error": f"Failed to process form data: {str(e)}"}
64
+
65
+ # 构建请求日志信息
66
+ request_message = {
67
+ "method": request.method,
68
+ "url": str(request.url),
69
+ "query_params": query_params,
70
+ "request_body": request_body,
71
+ "uploaded_files": files_info if files_info else None
72
+ }
73
+ request_message_str = json.dumps(request_message, ensure_ascii=False)
74
+ SYLogger.info(request_message_str)
75
+
76
+ try:
77
+ # 处理请求
78
+ response = await call_next(request)
79
+
80
+ content_type = response.headers.get("Content-Type", "")
81
+ if "text/event-stream" in content_type:
82
+ # 处理 SSE 响应
83
+ response.headers["x-traceId-header"] = trace_id
84
+ return response
85
+
86
+ response_body = b""
87
+ try:
88
+ async for chunk in response.body_iterator:
89
+ response_body += chunk
90
+
91
+ content_disposition = response.headers.get(
92
+ "Content-Disposition", "")
93
+
94
+ # 判断是否能添加 trace_id
95
+ if "application/json" in content_type and not content_disposition.startswith("attachment"):
96
+ try:
97
+ data = json.loads(response_body)
98
+ data["trace_id"] = trace_id
99
+ new_body = json.dumps(
100
+ data, ensure_ascii=False).encode()
101
+ response = Response(
102
+ content=new_body,
103
+ status_code=response.status_code,
104
+ headers=dict(response.headers)
105
+ )
106
+ response.headers["Content-Length"] = str(len(new_body))
107
+ except json.JSONDecodeError:
108
+ pass
109
+ except StopAsyncIteration:
110
+ pass
111
+
112
+ # 构建响应日志信息
113
+ response_message = {
114
+ "status_code": response.status_code,
115
+ "response_body": response_body.decode('utf-8', errors='ignore'),
116
+ }
117
+ response_message_str = json.dumps(
118
+ response_message, ensure_ascii=False)
119
+ SYLogger.info(response_message_str)
120
+
121
+ response.headers["x-traceId-header"] = trace_id
122
+
123
+ return response
124
+ except Exception as e:
125
+ error_message = {
126
+ "error": str(e),
127
+ "query_params": query_params,
128
+ "request_body": request_body,
129
+ "uploaded_files": files_info if files_info else None
130
+ }
131
+ error_message_str = json.dumps(error_message, ensure_ascii=False)
132
+ SYLogger.error(error_message_str) # 无需显式传递 trace_id
133
+ raise
134
+ finally:
135
+ # 清理上下文变量,防止泄漏
136
+ SYLogger.reset_trace_id(token)
137
+
138
+ return app
File without changes
sycommon/models/log.py ADDED
@@ -0,0 +1,30 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class LogModel(BaseModel):
5
+ traceId: str
6
+ sySpanId: str
7
+ syBizId: str
8
+ ptxId: str
9
+ time: str
10
+ day: str
11
+ msg: str
12
+ detail: str
13
+ IP: str
14
+ hostName: str
15
+ tenantId: str
16
+ userId: str
17
+ customerId: str
18
+ env: str
19
+ priReqSource: str
20
+ reqSource: str
21
+ serviceId: str
22
+ logLevel: str
23
+ classShortName: str
24
+ method: str
25
+ line: str
26
+ theadName: str
27
+ className: str
28
+ sqlCost: float
29
+ size: float
30
+ uid: float
sycommon/services.py ADDED
@@ -0,0 +1,29 @@
1
+ from typing import Callable, List, Tuple
2
+
3
+ from sycommon.config.Config import SingletonMeta
4
+
5
+
6
+ class Services(metaclass=SingletonMeta):
7
+ def __init__(self, config):
8
+ self.config = config
9
+
10
+ def plugins(self, nacos_service=None, logging_service=None, database_service: Tuple[Callable, str] | List[Tuple[Callable, str]] = None):
11
+ # 注册nacos服务
12
+ if nacos_service:
13
+ nacos_service(self.config)
14
+ # 注册日志服务
15
+ if logging_service:
16
+ logging_service(self.config)
17
+ # 注册数据库服务
18
+ if database_service:
19
+ if isinstance(database_service, tuple):
20
+ # 单个数据库服务
21
+ db_setup, db_name = database_service
22
+ db_setup(self.config, db_name)
23
+ elif isinstance(database_service, list):
24
+ # 多个数据库服务
25
+ for db_setup, db_name in database_service:
26
+ db_setup(self.config, db_name)
27
+ else:
28
+ raise TypeError(
29
+ "database_service must be a tuple or a list of tuples")
File without changes
@@ -0,0 +1,307 @@
1
+ import io
2
+ import os
3
+ import time
4
+ from urllib.parse import urljoin
5
+
6
+ import aiohttp
7
+ from sycommon.logging.kafka_log import SYLogger
8
+ from sycommon.synacos.nacos_service import NacosService
9
+
10
+ """
11
+ 支持异步Feign客户端
12
+ 方式一: 使用 @feign_client 和 @feign_request 装饰器
13
+ 方式二: 使用 feign 函数
14
+ """
15
+
16
+ # 示例Feign客户端接口
17
+ # @feign_client(service_name="user-service", path_prefix="/api/v1")
18
+ # class UserServiceClient:
19
+ #
20
+ # @feign_request("GET", "/users/{user_id}")
21
+ # async def get_user(self, user_id):
22
+ # """获取用户信息"""
23
+ # pass
24
+ #
25
+ # @feign_request("POST", "/users", headers={"Content-Type": "application/json"})
26
+ # async def create_user(self, user_data):
27
+ # """创建用户"""
28
+ # pass
29
+ #
30
+ # @feign_upload("avatar")
31
+ # @feign_request("POST", "/users/{user_id}/avatar")
32
+ # async def upload_avatar(self, user_id, file_path):
33
+ # """上传用户头像"""
34
+ # pass
35
+
36
+ # # 使用示例
37
+ # async def get_user_info(user_id: int, request=None):
38
+ # """获取用户信息"""
39
+ # try:
40
+ # user_service = UserServiceClient()
41
+ # # 设置请求头中的版本信息
42
+ # user_service.get_user._feign_meta['headers']['s-y-version'] = "1.0.0"
43
+ # return await user_service.get_user(user_id=user_id, request=request)
44
+ # except Exception as e:
45
+ # SYLogger.error(f"获取用户信息失败: {str(e)}", TraceId(request))
46
+ # return None
47
+
48
+
49
+ def feign_client(service_name: str, path_prefix: str = ""):
50
+ # Feign风格客户端装饰器
51
+ """声明式HTTP客户端装饰器"""
52
+ def decorator(cls):
53
+ class FeignWrapper:
54
+ def __init__(self):
55
+ self.service_name = service_name
56
+ self.nacos_manager = NacosService(None)
57
+ self.path_prefix = path_prefix
58
+ self.session = aiohttp.ClientSession()
59
+
60
+ def __getattr__(self, name):
61
+ func = getattr(cls, name)
62
+
63
+ async def wrapper(*args, **kwargs):
64
+ # 获取请求元数据
65
+ request_meta = getattr(func, "_feign_meta", {})
66
+ method = request_meta.get("method", "GET")
67
+ path = request_meta.get("path", "")
68
+ headers = request_meta.get("headers", {})
69
+
70
+ # 获取版本信息
71
+ version = headers.get('s-y-version')
72
+
73
+ # 构建完整URL
74
+ full_path = f"{self.path_prefix}{path}"
75
+ for k, v in kwargs.items():
76
+ full_path = full_path.replace(f"{{{k}}}", str(v))
77
+
78
+ # 服务发现与负载均衡
79
+ instances = self.nacos_manager.get_service_instances(
80
+ self.service_name, version=version)
81
+ if not instances:
82
+ SYLogger.error(
83
+ f"nacos:未找到 {self.service_name} 的健康实例")
84
+ raise RuntimeError(
85
+ f"No instances available for {self.service_name}")
86
+
87
+ # 简单轮询负载均衡
88
+ instance = instances[int(time.time()) % len(instances)]
89
+ base_url = f"http://{instance['ip']}:{instance['port']}"
90
+ url = urljoin(base_url, full_path)
91
+
92
+ SYLogger.info(
93
+ f"nacos:调用服务: {self.service_name} -> {url}")
94
+
95
+ # 构建请求
96
+ params = request_meta.get("params", {})
97
+ body = request_meta.get("body", {})
98
+ files = request_meta.get("files", None)
99
+ form_data = request_meta.get("form_data", None)
100
+
101
+ # 发送请求
102
+ try:
103
+ # 处理文件上传
104
+ if files or form_data:
105
+ # 创建表单数据
106
+ data = aiohttp.FormData()
107
+ if form_data:
108
+ for key, value in form_data.items():
109
+ data.add_field(key, value)
110
+ if files:
111
+ for field_name, (filename, content) in files.items():
112
+ data.add_field(
113
+ field_name, content, filename=filename)
114
+ # 移除 Content-Type 头,让 aiohttp 自动设置 boundary
115
+ headers.pop('Content-Type', None)
116
+ # 发送表单数据
117
+ async with self.session.request(
118
+ method=method,
119
+ url=url,
120
+ headers=headers,
121
+ params=params,
122
+ data=data,
123
+ timeout=10
124
+ ) as response:
125
+ return await self._handle_response(response)
126
+ else:
127
+ # 普通请求
128
+ async with self.session.request(
129
+ method=method,
130
+ url=url,
131
+ headers=headers,
132
+ params=params,
133
+ json=body,
134
+ timeout=10
135
+ ) as response:
136
+ return await self._handle_response(response)
137
+ except Exception as e:
138
+ SYLogger.error(f"nacos:服务调用失败: {str(e)}")
139
+ raise RuntimeError(f"Feign call failed: {str(e)}")
140
+
141
+ return wrapper
142
+
143
+ async def _handle_response(self, response):
144
+ # 处理响应
145
+ if 200 <= response.status < 300:
146
+ content_type = response.headers.get('Content-Type', '')
147
+ if 'application/json' in content_type:
148
+ return await response.json()
149
+ else:
150
+ return await response.read()
151
+ raise RuntimeError(
152
+ f"请求失败: {response.status} - {await response.text()}")
153
+
154
+ async def close(self):
155
+ """关闭 aiohttp 会话"""
156
+ await self.session.close()
157
+
158
+ return FeignWrapper()
159
+ return decorator
160
+
161
+
162
+ def feign_request(method: str, path: str, headers: dict = None):
163
+ # Feign请求方法装饰器
164
+ def decorator(func):
165
+ func._feign_meta = {
166
+ "method": method.upper(),
167
+ "path": path,
168
+ "headers": headers or {}
169
+ }
170
+ return func
171
+ return decorator
172
+
173
+
174
+ def feign_upload(field_name: str = "file"):
175
+ # 文件上传装饰器
176
+ def decorator(func):
177
+ async def wrapper(*args, **kwargs):
178
+ file_path = kwargs.get('file_path')
179
+ if not file_path:
180
+ raise ValueError("file_path is required for upload")
181
+
182
+ with open(file_path, 'rb') as f:
183
+ files = {field_name: (os.path.basename(file_path), f.read())}
184
+ kwargs['files'] = files
185
+ return await func(*args, **kwargs)
186
+ return wrapper
187
+ return decorator
188
+
189
+
190
+ async def feign(service_name, api_path, method='GET', params=None, headers=None, file_path=None,
191
+ path_params=None, request=None, body=None, files=None, form_data=None):
192
+ """
193
+ feign 函数,支持 form-data 表单上传文件和其他字段
194
+
195
+ 参数:
196
+ files: 字典,用于上传文件,格式: {'field_name': (filename, file_content)}
197
+ form_data: 字典,用于上传表单字段
198
+ """
199
+ file_stream = None
200
+ session = aiohttp.ClientSession()
201
+ try:
202
+ # 获取健康的服务实例
203
+ if not headers:
204
+ headers = {}
205
+
206
+ nacos_service = NacosService(None)
207
+ version = headers.get('s-y-version')
208
+
209
+ # 使用 discover_services 方法获取服务实例列表
210
+ instances = nacos_service.get_service_instances(
211
+ service_name, version=version)
212
+ if not instances:
213
+ SYLogger.error(f"nacos:未找到 {service_name} 的健康实例")
214
+ return None
215
+
216
+ # 简单轮询负载均衡
217
+ instance = instances[int(time.time()) % len(instances)]
218
+
219
+ SYLogger.info(f"nacos:开始调用服务: {service_name}")
220
+ SYLogger.info(
221
+ f"nacos:服务实例信息: IP={instance['ip']}, Port={instance['port']}")
222
+
223
+ if instance:
224
+ ip = instance.get('ip')
225
+ port = instance.get('port')
226
+
227
+ SYLogger.info(f"nacos:服务实例信息: IP={ip}, Port={port}")
228
+
229
+ # 处理 path 参数
230
+ if path_params:
231
+ for key, value in path_params.items():
232
+ api_path = api_path.replace(f"{{{key}}}", str(value))
233
+
234
+ url = f"http://{ip}:{port}{api_path}"
235
+ SYLogger.info(f"nacos:请求地址: {url}")
236
+
237
+ try:
238
+ # 处理文件上传
239
+ if files or form_data or file_path:
240
+ # 创建表单数据
241
+ data = aiohttp.FormData()
242
+ if form_data:
243
+ for key, value in form_data.items():
244
+ data.add_field(key, value)
245
+ if files:
246
+ for field_name, (filename, content) in files.items():
247
+ data.add_field(field_name, content,
248
+ filename=filename)
249
+ if file_path:
250
+ filename = os.path.basename(file_path)
251
+ with open(file_path, 'rb') as f:
252
+ data.add_field('file', f, filename=filename)
253
+ # 移除 Content-Type 头,让 aiohttp 自动设置 boundary
254
+ headers.pop('Content-Type', None)
255
+ # 发送表单数据
256
+ async with session.request(
257
+ method=method.upper(),
258
+ url=url,
259
+ headers=headers,
260
+ params=params,
261
+ data=data,
262
+ timeout=10
263
+ ) as response:
264
+ return await _handle_feign_response(response)
265
+ else:
266
+ # 普通请求
267
+ if body and 'Content-Type' not in headers:
268
+ headers['Content-Type'] = 'application/json'
269
+ async with session.request(
270
+ method=method.upper(),
271
+ url=url,
272
+ headers=headers,
273
+ params=params,
274
+ json=body,
275
+ timeout=10
276
+ ) as response:
277
+ return await _handle_feign_response(response)
278
+ except aiohttp.ClientError as e:
279
+ SYLogger.error(
280
+ f"nacos:请求服务接口时出错ClientError: {e}")
281
+ print(f"请求服务接口时出错: {e}")
282
+ return None
283
+ except Exception as e:
284
+ SYLogger.error(f"nacos:请求服务接口时出错: {e}")
285
+ print(f"nacos:请求服务接口时出错: {e}")
286
+ return None
287
+ finally:
288
+ SYLogger.info(f"nacos:结束调用服务: {service_name}, {api_path}")
289
+ await session.close()
290
+
291
+
292
+ async def _handle_feign_response(response):
293
+ """处理Feign请求的响应"""
294
+ if response.status == 200:
295
+ content_type = response.headers.get('Content-Type')
296
+ if 'application/json' in content_type:
297
+ return await response.json()
298
+ else:
299
+ # 如果是文件流,将其封装成 io.BytesIO 对象返回
300
+ content = await response.read()
301
+ return io.BytesIO(content)
302
+ else:
303
+ print(
304
+ f"nacos:请求失败,状态码: {response.status},响应内容: {await response.text()}")
305
+ SYLogger.error(
306
+ f"nacos:请求服务接口时出错: {response.status}, 响应内容: {await response.text()}")
307
+ return None