sycommon-python-lib 0.1.32__py3-none-any.whl → 0.1.56b5__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.
- sycommon/config/Config.py +6 -2
- sycommon/config/RerankerConfig.py +1 -0
- sycommon/database/async_base_db_service.py +36 -0
- sycommon/database/async_database_service.py +96 -0
- sycommon/llm/__init__.py +0 -0
- sycommon/llm/embedding.py +149 -0
- sycommon/llm/get_llm.py +246 -0
- sycommon/llm/llm_logger.py +126 -0
- sycommon/llm/llm_tokens.py +119 -0
- sycommon/logging/async_sql_logger.py +65 -0
- sycommon/logging/kafka_log.py +21 -9
- sycommon/logging/logger_levels.py +23 -0
- sycommon/middleware/context.py +2 -0
- sycommon/middleware/traceid.py +155 -32
- sycommon/notice/__init__.py +0 -0
- sycommon/notice/uvicorn_monitor.py +195 -0
- sycommon/rabbitmq/rabbitmq_client.py +385 -626
- sycommon/rabbitmq/rabbitmq_pool.py +287 -71
- sycommon/rabbitmq/rabbitmq_service.py +345 -191
- sycommon/services.py +104 -64
- sycommon/synacos/feign.py +71 -18
- sycommon/synacos/feign_client.py +26 -8
- sycommon/synacos/nacos_service.py +91 -54
- sycommon/tools/merge_headers.py +97 -0
- sycommon/tools/snowflake.py +290 -23
- {sycommon_python_lib-0.1.32.dist-info → sycommon_python_lib-0.1.56b5.dist-info}/METADATA +17 -12
- {sycommon_python_lib-0.1.32.dist-info → sycommon_python_lib-0.1.56b5.dist-info}/RECORD +30 -18
- {sycommon_python_lib-0.1.32.dist-info → sycommon_python_lib-0.1.56b5.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.32.dist-info → sycommon_python_lib-0.1.56b5.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.32.dist-info → sycommon_python_lib-0.1.56b5.dist-info}/top_level.txt +0 -0
sycommon/services.py
CHANGED
|
@@ -7,6 +7,7 @@ from fastapi import FastAPI, applications
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
from typing import Any, Callable, Dict, List, Tuple, Union, Optional, AsyncGenerator
|
|
9
9
|
from sycommon.config.Config import SingletonMeta
|
|
10
|
+
from sycommon.logging.logger_levels import setup_logger_levels
|
|
10
11
|
from sycommon.models.mqlistener_config import RabbitMQListenerConfig
|
|
11
12
|
from sycommon.models.mqsend_config import RabbitMQSendConfig
|
|
12
13
|
from sycommon.rabbitmq.rabbitmq_service import RabbitMQService
|
|
@@ -18,10 +19,13 @@ class Services(metaclass=SingletonMeta):
|
|
|
18
19
|
_config: Optional[dict] = None
|
|
19
20
|
_initialized: bool = False
|
|
20
21
|
_registered_senders: List[str] = []
|
|
21
|
-
_mq_tasks: List[asyncio.Task] = []
|
|
22
22
|
_instance: Optional['Services'] = None
|
|
23
23
|
_app: Optional[FastAPI] = None
|
|
24
24
|
_user_lifespan: Optional[Callable] = None
|
|
25
|
+
_shutdown_lock: asyncio.Lock = asyncio.Lock()
|
|
26
|
+
|
|
27
|
+
# 用于存储待执行的异步数据库初始化任务
|
|
28
|
+
_pending_async_db_setup: List[Tuple[Callable, str]] = []
|
|
25
29
|
|
|
26
30
|
def __init__(self, config: dict, app: FastAPI):
|
|
27
31
|
if not Services._config:
|
|
@@ -48,28 +52,32 @@ class Services(metaclass=SingletonMeta):
|
|
|
48
52
|
nacos_service: Optional[Callable[[dict], None]] = None,
|
|
49
53
|
logging_service: Optional[Callable[[dict], None]] = None,
|
|
50
54
|
database_service: Optional[Union[
|
|
51
|
-
Tuple[Callable
|
|
52
|
-
List[Tuple[Callable
|
|
55
|
+
Tuple[Callable, str],
|
|
56
|
+
List[Tuple[Callable, str]]
|
|
53
57
|
]] = None,
|
|
54
58
|
rabbitmq_listeners: Optional[List[RabbitMQListenerConfig]] = None,
|
|
55
59
|
rabbitmq_senders: Optional[List[RabbitMQSendConfig]] = None
|
|
56
60
|
) -> FastAPI:
|
|
57
61
|
load_dotenv()
|
|
58
|
-
|
|
62
|
+
setup_logger_levels()
|
|
59
63
|
cls._app = app
|
|
60
64
|
cls._config = config
|
|
61
65
|
cls._user_lifespan = app.router.lifespan_context
|
|
62
|
-
|
|
66
|
+
|
|
63
67
|
applications.get_swagger_ui_html = custom_swagger_ui_html
|
|
64
68
|
applications.get_redoc_html = custom_redoc_html
|
|
65
|
-
|
|
69
|
+
|
|
66
70
|
if not cls._config:
|
|
67
71
|
config = yaml.safe_load(open('app.yaml', 'r', encoding='utf-8'))
|
|
68
72
|
cls._config = config
|
|
69
|
-
app.host = cls._config.get('Host')
|
|
70
|
-
app.post = cls._config.get('Port')
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
app.state.config = {
|
|
75
|
+
"host": cls._config.get('Host', '0.0.0.0'),
|
|
76
|
+
"port": cls._config.get('Port', 8080),
|
|
77
|
+
"workers": cls._config.get('Workers', 1),
|
|
78
|
+
"h11_max_incomplete_event_size": cls._config.get('H11MaxIncompleteEventSize', 1024 * 1024 * 10)
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
if middleware:
|
|
74
82
|
middleware(app, config)
|
|
75
83
|
|
|
@@ -79,25 +87,62 @@ class Services(metaclass=SingletonMeta):
|
|
|
79
87
|
if logging_service:
|
|
80
88
|
logging_service(config)
|
|
81
89
|
|
|
90
|
+
# ========== 处理数据库服务 ==========
|
|
91
|
+
# 清空之前的待执行列表(防止热重载时重复)
|
|
92
|
+
cls._pending_async_db_setup = []
|
|
93
|
+
|
|
82
94
|
if database_service:
|
|
83
|
-
|
|
95
|
+
# 解析配置并区分同步/异步
|
|
96
|
+
items = [database_service] if isinstance(
|
|
97
|
+
database_service, tuple) else database_service
|
|
98
|
+
for item in items:
|
|
99
|
+
db_setup_func, db_name = item
|
|
100
|
+
if asyncio.iscoroutinefunction(db_setup_func):
|
|
101
|
+
# 如果是异步函数,加入待执行列表
|
|
102
|
+
logging.info(f"检测到异步数据库服务: {db_name},将在应用启动时初始化")
|
|
103
|
+
cls._pending_async_db_setup.append(item)
|
|
104
|
+
else:
|
|
105
|
+
# 如果是同步函数,立即执行
|
|
106
|
+
logging.info(f"执行同步数据库服务: {db_name}")
|
|
107
|
+
try:
|
|
108
|
+
db_setup_func(config, db_name)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logging.error(
|
|
111
|
+
f"同步数据库服务 {db_name} 初始化失败: {e}", exc_info=True)
|
|
112
|
+
raise
|
|
84
113
|
|
|
85
114
|
# 创建组合生命周期管理器
|
|
86
115
|
@asynccontextmanager
|
|
87
116
|
async def combined_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
88
117
|
# 1. 执行Services自身的初始化
|
|
89
118
|
instance = cls(config, app)
|
|
90
|
-
|
|
119
|
+
|
|
120
|
+
# ========== 执行挂起的异步数据库初始化 ==========
|
|
121
|
+
if cls._pending_async_db_setup:
|
|
122
|
+
logging.info("开始执行异步数据库初始化...")
|
|
123
|
+
for db_setup_func, db_name in cls._pending_async_db_setup:
|
|
124
|
+
try:
|
|
125
|
+
await db_setup_func(config, db_name)
|
|
126
|
+
logging.info(f"异步数据库服务 {db_name} 初始化成功")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logging.error(
|
|
129
|
+
f"异步数据库服务 {db_name} 初始化失败: {e}", exc_info=True)
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
# ========== 初始化 MQ ==========
|
|
133
|
+
has_valid_listeners = bool(
|
|
91
134
|
rabbitmq_listeners and len(rabbitmq_listeners) > 0)
|
|
92
|
-
|
|
135
|
+
has_valid_senders = bool(
|
|
136
|
+
rabbitmq_senders and len(rabbitmq_senders) > 0)
|
|
93
137
|
|
|
94
138
|
try:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
139
|
+
if has_valid_listeners or has_valid_senders:
|
|
140
|
+
await instance._setup_mq_async(
|
|
141
|
+
rabbitmq_listeners=rabbitmq_listeners if has_valid_listeners else None,
|
|
142
|
+
rabbitmq_senders=rabbitmq_senders if has_valid_senders else None,
|
|
143
|
+
has_listeners=has_valid_listeners,
|
|
144
|
+
has_senders=has_valid_senders
|
|
145
|
+
)
|
|
101
146
|
cls._initialized = True
|
|
102
147
|
logging.info("Services初始化完成")
|
|
103
148
|
except Exception as e:
|
|
@@ -109,28 +154,18 @@ class Services(metaclass=SingletonMeta):
|
|
|
109
154
|
# 2. 执行用户定义的生命周期
|
|
110
155
|
if cls._user_lifespan:
|
|
111
156
|
async with cls._user_lifespan(app):
|
|
112
|
-
yield
|
|
157
|
+
yield
|
|
113
158
|
else:
|
|
114
|
-
yield
|
|
159
|
+
yield
|
|
115
160
|
|
|
116
161
|
# 3. 执行Services的关闭逻辑
|
|
117
162
|
await cls.shutdown()
|
|
118
163
|
logging.info("Services已关闭")
|
|
119
164
|
|
|
120
|
-
# 设置组合生命周期
|
|
121
165
|
app.router.lifespan_context = combined_lifespan
|
|
122
|
-
|
|
123
166
|
return app
|
|
124
167
|
|
|
125
|
-
|
|
126
|
-
def _setup_database_static(database_service, config):
|
|
127
|
-
"""静态方法:设置数据库服务"""
|
|
128
|
-
if isinstance(database_service, tuple):
|
|
129
|
-
db_setup, db_name = database_service
|
|
130
|
-
db_setup(config, db_name)
|
|
131
|
-
elif isinstance(database_service, list):
|
|
132
|
-
for db_setup, db_name in database_service:
|
|
133
|
-
db_setup(config, db_name)
|
|
168
|
+
# 移除了 _setup_database_static,因为逻辑已内联到 plugins 中
|
|
134
169
|
|
|
135
170
|
async def _setup_mq_async(
|
|
136
171
|
self,
|
|
@@ -140,39 +175,46 @@ class Services(metaclass=SingletonMeta):
|
|
|
140
175
|
has_senders: bool = False,
|
|
141
176
|
):
|
|
142
177
|
"""异步设置MQ相关服务"""
|
|
143
|
-
|
|
178
|
+
if not (has_listeners or has_senders):
|
|
179
|
+
logging.info("无RabbitMQ监听器/发送器配置,跳过RabbitMQService初始化")
|
|
180
|
+
return
|
|
181
|
+
|
|
144
182
|
RabbitMQService.init(self._config, has_listeners, has_senders)
|
|
145
183
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
184
|
+
start_time = asyncio.get_event_loop().time()
|
|
185
|
+
while not (RabbitMQService._connection_pool and RabbitMQService._connection_pool._initialized) and not RabbitMQService._is_shutdown:
|
|
186
|
+
if asyncio.get_event_loop().time() - start_time > 30:
|
|
187
|
+
raise TimeoutError("RabbitMQ连接池初始化超时(30秒)")
|
|
188
|
+
logging.info("等待RabbitMQ连接池初始化...")
|
|
189
|
+
await asyncio.sleep(0.5)
|
|
190
|
+
|
|
191
|
+
if has_senders and rabbitmq_senders:
|
|
192
|
+
if has_listeners and rabbitmq_listeners:
|
|
150
193
|
for sender in rabbitmq_senders:
|
|
151
194
|
for listener in rabbitmq_listeners:
|
|
152
195
|
if sender.queue_name == listener.queue_name:
|
|
153
196
|
sender.prefetch_count = listener.prefetch_count
|
|
154
197
|
await self._setup_senders_async(rabbitmq_senders, has_listeners)
|
|
155
198
|
|
|
156
|
-
|
|
157
|
-
if rabbitmq_listeners:
|
|
199
|
+
if has_listeners and rabbitmq_listeners:
|
|
158
200
|
await self._setup_listeners_async(rabbitmq_listeners, has_senders)
|
|
159
201
|
|
|
160
|
-
# 验证初始化结果
|
|
161
202
|
if has_listeners:
|
|
162
|
-
listener_count = len(RabbitMQService.
|
|
203
|
+
listener_count = len(RabbitMQService._consumer_tasks)
|
|
163
204
|
logging.info(f"监听器初始化完成,共启动 {listener_count} 个消费者")
|
|
164
205
|
if listener_count == 0:
|
|
165
|
-
logging.warning("
|
|
206
|
+
logging.warning("未成功初始化任何监听器,请检查配置或MQ服务状态")
|
|
166
207
|
|
|
167
208
|
async def _setup_senders_async(self, rabbitmq_senders, has_listeners: bool):
|
|
209
|
+
"""设置发送器"""
|
|
168
210
|
Services._registered_senders = [
|
|
169
211
|
sender.queue_name for sender in rabbitmq_senders]
|
|
170
|
-
|
|
171
|
-
# 将是否有监听器的信息传递给RabbitMQService
|
|
172
212
|
await RabbitMQService.setup_senders(rabbitmq_senders, has_listeners)
|
|
213
|
+
Services._registered_senders = RabbitMQService._sender_client_names
|
|
173
214
|
logging.info(f"已注册的RabbitMQ发送器: {Services._registered_senders}")
|
|
174
215
|
|
|
175
216
|
async def _setup_listeners_async(self, rabbitmq_listeners, has_senders: bool):
|
|
217
|
+
"""设置监听器"""
|
|
176
218
|
await RabbitMQService.setup_listeners(rabbitmq_listeners, has_senders)
|
|
177
219
|
|
|
178
220
|
@classmethod
|
|
@@ -183,11 +225,15 @@ class Services(metaclass=SingletonMeta):
|
|
|
183
225
|
max_retries: int = 3,
|
|
184
226
|
retry_delay: float = 1.0, **kwargs
|
|
185
227
|
) -> None:
|
|
186
|
-
"""
|
|
228
|
+
"""发送消息"""
|
|
187
229
|
if not cls._initialized or not cls._loop:
|
|
188
230
|
logging.error("Services not properly initialized!")
|
|
189
231
|
raise ValueError("服务未正确初始化")
|
|
190
232
|
|
|
233
|
+
if RabbitMQService._is_shutdown:
|
|
234
|
+
logging.error("RabbitMQService已关闭,无法发送消息")
|
|
235
|
+
raise RuntimeError("RabbitMQ服务已关闭")
|
|
236
|
+
|
|
191
237
|
for attempt in range(max_retries):
|
|
192
238
|
try:
|
|
193
239
|
if queue_name not in cls._registered_senders:
|
|
@@ -195,11 +241,11 @@ class Services(metaclass=SingletonMeta):
|
|
|
195
241
|
if queue_name not in cls._registered_senders:
|
|
196
242
|
raise ValueError(f"发送器 {queue_name} 未注册")
|
|
197
243
|
|
|
198
|
-
sender = RabbitMQService.get_sender(queue_name)
|
|
244
|
+
sender = await RabbitMQService.get_sender(queue_name)
|
|
199
245
|
if not sender:
|
|
200
|
-
raise ValueError(f"发送器 '{queue_name}'
|
|
246
|
+
raise ValueError(f"发送器 '{queue_name}' 不存在或连接无效")
|
|
201
247
|
|
|
202
|
-
await RabbitMQService.send_message(data, queue_name, **
|
|
248
|
+
await RabbitMQService.send_message(data, queue_name, **kwargs)
|
|
203
249
|
logging.info(f"消息发送成功(尝试 {attempt+1}/{max_retries})")
|
|
204
250
|
return
|
|
205
251
|
|
|
@@ -208,24 +254,18 @@ class Services(metaclass=SingletonMeta):
|
|
|
208
254
|
logging.error(
|
|
209
255
|
f"消息发送失败(已尝试 {max_retries} 次): {str(e)}", exc_info=True)
|
|
210
256
|
raise
|
|
211
|
-
|
|
212
257
|
logging.warning(
|
|
213
|
-
f"消息发送失败(尝试 {attempt+1}/{max_retries}): {str(e)},"
|
|
214
|
-
f"{retry_delay}秒后重试..."
|
|
215
|
-
)
|
|
258
|
+
f"消息发送失败(尝试 {attempt+1}/{max_retries}): {str(e)},{retry_delay}秒后重试...")
|
|
216
259
|
await asyncio.sleep(retry_delay)
|
|
217
260
|
|
|
218
|
-
@
|
|
219
|
-
async def shutdown():
|
|
261
|
+
@classmethod
|
|
262
|
+
async def shutdown(cls):
|
|
220
263
|
"""关闭所有服务"""
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# 关闭RabbitMQ服务
|
|
231
|
-
await RabbitMQService.shutdown()
|
|
264
|
+
async with cls._shutdown_lock:
|
|
265
|
+
if RabbitMQService._is_shutdown:
|
|
266
|
+
logging.info("RabbitMQService已关闭,无需重复操作")
|
|
267
|
+
return
|
|
268
|
+
await RabbitMQService.shutdown()
|
|
269
|
+
cls._initialized = False
|
|
270
|
+
cls._registered_senders.clear()
|
|
271
|
+
logging.info("所有服务已关闭")
|
sycommon/synacos/feign.py
CHANGED
|
@@ -2,6 +2,9 @@ import io
|
|
|
2
2
|
import os
|
|
3
3
|
import time
|
|
4
4
|
|
|
5
|
+
from sycommon.tools.merge_headers import merge_headers
|
|
6
|
+
from sycommon.tools.snowflake import Snowflake
|
|
7
|
+
|
|
5
8
|
import aiohttp
|
|
6
9
|
from sycommon.logging.kafka_log import SYLogger
|
|
7
10
|
from sycommon.synacos.nacos_service import NacosService
|
|
@@ -22,12 +25,18 @@ async def feign(service_name, api_path, method='GET', params=None, headers=None,
|
|
|
22
25
|
try:
|
|
23
26
|
# 初始化headers,确保是可修改的字典
|
|
24
27
|
headers = headers.copy() if headers else {}
|
|
28
|
+
headers = merge_headers(SYLogger.get_headers(), headers)
|
|
29
|
+
if "x-traceId-header" not in headers:
|
|
30
|
+
headers["x-traceId-header"] = SYLogger.get_trace_id() or Snowflake.id
|
|
25
31
|
|
|
26
32
|
# 处理JSON请求的Content-Type
|
|
27
33
|
is_json_request = method.upper() in ["POST", "PUT", "PATCH"] and not (
|
|
28
34
|
files or form_data or file_path)
|
|
29
|
-
if is_json_request
|
|
30
|
-
headers
|
|
35
|
+
if is_json_request:
|
|
36
|
+
# 将headers的key全部转为小写,统一判断
|
|
37
|
+
headers_lower = {k.lower(): v for k, v in headers.items()}
|
|
38
|
+
if "content-type" not in headers_lower:
|
|
39
|
+
headers["Content-Type"] = "application/json"
|
|
31
40
|
|
|
32
41
|
nacos_service = NacosService(None)
|
|
33
42
|
version = headers.get('s-y-version')
|
|
@@ -43,7 +52,7 @@ async def feign(service_name, api_path, method='GET', params=None, headers=None,
|
|
|
43
52
|
instance = instances[int(time.time()) % len(instances)]
|
|
44
53
|
|
|
45
54
|
SYLogger.info(f"nacos:开始调用服务: {service_name}")
|
|
46
|
-
SYLogger.info(f"nacos:请求头: {headers}")
|
|
55
|
+
# SYLogger.info(f"nacos:请求头: {headers}")
|
|
47
56
|
|
|
48
57
|
ip = instance.get('ip')
|
|
49
58
|
port = instance.get('port')
|
|
@@ -97,7 +106,7 @@ async def feign(service_name, api_path, method='GET', params=None, headers=None,
|
|
|
97
106
|
data=data,
|
|
98
107
|
timeout=timeout
|
|
99
108
|
) as response:
|
|
100
|
-
return await _handle_feign_response(response)
|
|
109
|
+
return await _handle_feign_response(response, service_name, api_path)
|
|
101
110
|
else:
|
|
102
111
|
# 普通JSON请求
|
|
103
112
|
async with session.request(
|
|
@@ -108,30 +117,74 @@ async def feign(service_name, api_path, method='GET', params=None, headers=None,
|
|
|
108
117
|
json=body,
|
|
109
118
|
timeout=timeout
|
|
110
119
|
) as response:
|
|
111
|
-
return await _handle_feign_response(response)
|
|
120
|
+
return await _handle_feign_response(response, service_name, api_path)
|
|
112
121
|
except aiohttp.ClientError as e:
|
|
113
122
|
SYLogger.error(
|
|
114
|
-
f"nacos:请求服务接口时出错ClientError path: {api_path} error:{e}")
|
|
123
|
+
f"nacos:请求服务接口时出错ClientError server: {service_name} path: {api_path} error:{e}")
|
|
115
124
|
return None
|
|
116
125
|
except Exception as e:
|
|
117
126
|
import traceback
|
|
118
127
|
SYLogger.error(
|
|
119
|
-
f"nacos:请求服务接口时出错 path: {api_path} error:{traceback.format_exc()}")
|
|
128
|
+
f"nacos:请求服务接口时出错 server: {service_name} path: {api_path} error:{traceback.format_exc()}")
|
|
120
129
|
return None
|
|
121
130
|
finally:
|
|
122
131
|
await session.close()
|
|
123
132
|
|
|
124
133
|
|
|
125
|
-
async def _handle_feign_response(response):
|
|
126
|
-
"""
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
async def _handle_feign_response(response, service_name: str, api_path: str):
|
|
135
|
+
"""
|
|
136
|
+
处理Feign请求的响应,统一返回格式
|
|
137
|
+
调整逻辑:先判断状态码,再处理内容
|
|
138
|
+
- 200状态:优先识别JSON/文本,其他均按文件流(二进制)处理
|
|
139
|
+
- 非200状态:统一返回错误字典
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
status_code = response.status
|
|
143
|
+
content_type = response.headers.get('Content-Type', '')
|
|
144
|
+
content_type = content_type.lower() if content_type else ''
|
|
145
|
+
|
|
146
|
+
response_body = None
|
|
147
|
+
|
|
148
|
+
if status_code == 200:
|
|
149
|
+
if content_type and 'application/json' in content_type:
|
|
150
|
+
response_body = await response.json()
|
|
151
|
+
elif content_type and 'text/' in content_type:
|
|
152
|
+
# 文本类型(text/plain、text/html等):按文本读取
|
|
153
|
+
try:
|
|
154
|
+
response_body = await response.text(encoding='utf-8')
|
|
155
|
+
except UnicodeDecodeError:
|
|
156
|
+
# 兼容中文编码(gbk)
|
|
157
|
+
response_body = await response.text(encoding='gbk')
|
|
158
|
+
else:
|
|
159
|
+
# 其他类型(PDF、图片、octet-stream等):按文件流(二进制)读取
|
|
160
|
+
binary_data = await response.read()
|
|
161
|
+
SYLogger.info(
|
|
162
|
+
f"按文件流处理响应,类型:{content_type},大小:{len(binary_data)/1024:.2f}KB")
|
|
163
|
+
return io.BytesIO(binary_data) # 返回BytesIO,支持read()
|
|
164
|
+
return response_body
|
|
131
165
|
else:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
166
|
+
# 非200状态:统一读取响应体(兼容文本/二进制错误信息)
|
|
167
|
+
try:
|
|
168
|
+
if content_type and 'application/json' in content_type:
|
|
169
|
+
response_body = await response.json()
|
|
170
|
+
else:
|
|
171
|
+
response_body = await response.text(encoding='utf-8', errors='ignore')
|
|
172
|
+
except Exception:
|
|
173
|
+
binary_data = await response.read()
|
|
174
|
+
response_body = f"非200状态,响应无法解码:{binary_data[:100].hex()} server: {service_name} path: {api_path}"
|
|
175
|
+
|
|
176
|
+
error_msg = f"请求失败,状态码: {status_code},响应内容: {str(response_body)[:500]} server: {service_name} path: {api_path}"
|
|
177
|
+
SYLogger.error(error_msg)
|
|
178
|
+
return {
|
|
179
|
+
"success": False,
|
|
180
|
+
"code": status_code,
|
|
181
|
+
"message": error_msg,
|
|
182
|
+
"data": response_body
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
import traceback
|
|
187
|
+
error_detail = f"处理响应异常: {str(e)}\n{traceback.format_exc()}"
|
|
188
|
+
SYLogger.error(
|
|
189
|
+
f"nacos:处理响应时出错: {error_detail} server: {service_name} path: {api_path}")
|
|
137
190
|
return None
|
sycommon/synacos/feign_client.py
CHANGED
|
@@ -5,6 +5,9 @@ import inspect
|
|
|
5
5
|
from typing import Any, Dict, Optional, Literal, Type, TypeVar
|
|
6
6
|
from urllib.parse import urljoin
|
|
7
7
|
|
|
8
|
+
from sycommon.tools.merge_headers import merge_headers
|
|
9
|
+
from sycommon.tools.snowflake import Snowflake
|
|
10
|
+
|
|
8
11
|
import aiohttp
|
|
9
12
|
from pydantic import BaseModel
|
|
10
13
|
from sycommon.synacos.param import Body, Cookie, File, Form, Header, Param, Path, Query
|
|
@@ -26,6 +29,9 @@ def feign_client(
|
|
|
26
29
|
default_headers: Optional[Dict[str, str]] = None
|
|
27
30
|
):
|
|
28
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
|
|
29
35
|
|
|
30
36
|
def decorator(cls):
|
|
31
37
|
class FeignClient:
|
|
@@ -33,7 +39,8 @@ def feign_client(
|
|
|
33
39
|
self.service_name = service_name
|
|
34
40
|
self.path_prefix = path_prefix
|
|
35
41
|
self.default_timeout = default_timeout
|
|
36
|
-
self.default_headers =
|
|
42
|
+
self.default_headers = {
|
|
43
|
+
k.lower(): v for k, v in default_headers.copy().items()}
|
|
37
44
|
self.nacos_manager: Optional[NacosService] = None
|
|
38
45
|
self.session: Optional[aiohttp.ClientSession] = None
|
|
39
46
|
|
|
@@ -62,7 +69,8 @@ def feign_client(
|
|
|
62
69
|
method = request_meta.get("method", "GET").upper()
|
|
63
70
|
path = request_meta.get("path", "")
|
|
64
71
|
is_upload = request_meta.get("is_upload", False)
|
|
65
|
-
method_headers =
|
|
72
|
+
method_headers = {
|
|
73
|
+
k.lower(): v for k, v in request_meta.get("headers", {}).items()}
|
|
66
74
|
timeout = request_meta.get(
|
|
67
75
|
"timeout", self.default_timeout)
|
|
68
76
|
|
|
@@ -152,11 +160,16 @@ def feign_client(
|
|
|
152
160
|
def _build_headers(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any], method_headers: Dict[str, str]) -> Dict[str, str]:
|
|
153
161
|
headers = self.default_headers.copy()
|
|
154
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类型
|
|
155
167
|
for name, meta in param_meta.items():
|
|
156
168
|
if isinstance(meta, Header) and name in bound_args:
|
|
157
169
|
value = bound_args[name]
|
|
158
170
|
if value is not None:
|
|
159
|
-
|
|
171
|
+
header_key = meta.get_key(name).lower()
|
|
172
|
+
headers[header_key] = str(value)
|
|
160
173
|
return headers
|
|
161
174
|
|
|
162
175
|
def _replace_path_params(self, path: str, param_meta: Dict[str, Param], bound_args: Dict[str, Any]) -> str:
|
|
@@ -221,10 +234,14 @@ def feign_client(
|
|
|
221
234
|
value) if not isinstance(value, dict) else value)
|
|
222
235
|
return form_data
|
|
223
236
|
|
|
224
|
-
#
|
|
237
|
+
# 从headers中获取Content-Type(已小写key)
|
|
225
238
|
content_type = self.default_headers.get(
|
|
226
|
-
"
|
|
227
|
-
|
|
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:
|
|
228
245
|
form_data = {}
|
|
229
246
|
for name, value in bound_args.items():
|
|
230
247
|
meta = param_meta.get(name)
|
|
@@ -270,7 +287,8 @@ def feign_client(
|
|
|
270
287
|
"""处理响应(支持 Pydantic 模型解析)"""
|
|
271
288
|
status = response.status
|
|
272
289
|
if 200 <= status < 300:
|
|
273
|
-
content_type = response.headers.get(
|
|
290
|
+
content_type = response.headers.get(
|
|
291
|
+
"content-type", "").lower()
|
|
274
292
|
if "application/json" in content_type:
|
|
275
293
|
json_data = await response.json()
|
|
276
294
|
# 若指定了 Pydantic 响应模型,自动解析
|
|
@@ -299,7 +317,7 @@ def feign_request(
|
|
|
299
317
|
func._feign_meta = {
|
|
300
318
|
"method": method.upper(),
|
|
301
319
|
"path": path,
|
|
302
|
-
"headers": headers.
|
|
320
|
+
"headers": {k.lower(): v for k, v in headers.items()} if headers else {},
|
|
303
321
|
"is_upload": False,
|
|
304
322
|
"timeout": timeout
|
|
305
323
|
}
|