sycommon-python-lib 0.1.9__py3-none-any.whl → 0.1.11__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.

sycommon/services.py CHANGED
@@ -2,12 +2,13 @@ from typing import Any, Callable, Dict, List, Tuple, Union, Optional, AsyncGener
2
2
  import asyncio
3
3
  import logging
4
4
  from contextlib import asynccontextmanager
5
- from fastapi import FastAPI
5
+ from fastapi import FastAPI, applications
6
6
  from pydantic import BaseModel
7
7
  from sycommon.config.Config import SingletonMeta
8
8
  from sycommon.models.mqlistener_config import RabbitMQListenerConfig
9
9
  from sycommon.models.mqsend_config import RabbitMQSendConfig
10
10
  from sycommon.rabbitmq.rabbitmq_service import RabbitMQService
11
+ from sycommon.tools.docs import custom_redoc_html, custom_swagger_ui_html
11
12
 
12
13
 
13
14
  class Services(metaclass=SingletonMeta):
@@ -53,6 +54,9 @@ class Services(metaclass=SingletonMeta):
53
54
  # 保存应用实例和配置
54
55
  cls._app = app
55
56
  cls._config = config
57
+ # 设置文档
58
+ applications.get_swagger_ui_html = custom_swagger_ui_html
59
+ applications.get_redoc_html = custom_redoc_html
56
60
 
57
61
  # 立即配置非异步服务(在应用启动前)
58
62
  if middleware:
@@ -125,11 +129,7 @@ class Services(metaclass=SingletonMeta):
125
129
  has_listeners: bool = False,
126
130
  has_senders: bool = False,
127
131
  ):
128
- """异步设置MQ相关服务,保存发送器和监听器存在状态"""
129
- # 保存状态到类变量
130
- Services._has_listeners = has_listeners
131
- Services._has_senders = has_senders
132
-
132
+ """异步设置MQ相关服务"""
133
133
  # 初始化RabbitMQ服务,传递状态
134
134
  RabbitMQService.init(self._config, has_listeners, has_senders)
135
135
 
@@ -141,6 +141,13 @@ class Services(metaclass=SingletonMeta):
141
141
  if rabbitmq_listeners:
142
142
  await self._setup_listeners_async(rabbitmq_listeners, has_senders)
143
143
 
144
+ # 验证初始化结果
145
+ if has_listeners:
146
+ listener_count = len(RabbitMQService._clients)
147
+ logging.info(f"监听器初始化完成,共启动 {listener_count} 个消费者")
148
+ if listener_count == 0:
149
+ logging.warning("未成功初始化任何监听器,请检查配置")
150
+
144
151
  async def _setup_senders_async(self, rabbitmq_senders, has_listeners: bool):
145
152
  Services._registered_senders = [
146
153
  sender.queue_name for sender in rabbitmq_senders]
@@ -158,7 +165,7 @@ class Services(metaclass=SingletonMeta):
158
165
  queue_name: str,
159
166
  data: Union[str, Dict[str, Any], BaseModel, None],
160
167
  max_retries: int = 3,
161
- retry_delay: float = 1.0, ** kwargs
168
+ retry_delay: float = 1.0, **kwargs
162
169
  ) -> None:
163
170
  """发送消息,添加重试机制"""
164
171
  if not cls._initialized or not cls._loop:
@@ -168,7 +175,7 @@ class Services(metaclass=SingletonMeta):
168
175
  for attempt in range(max_retries):
169
176
  try:
170
177
  if queue_name not in cls._registered_senders:
171
- cls._registered_senders = RabbitMQService.sender_client_names
178
+ cls._registered_senders = RabbitMQService._sender_client_names
172
179
  if queue_name not in cls._registered_senders:
173
180
  raise ValueError(f"发送器 {queue_name} 未注册")
174
181
 
@@ -176,7 +183,7 @@ class Services(metaclass=SingletonMeta):
176
183
  if not sender:
177
184
  raise ValueError(f"发送器 '{queue_name}' 不存在")
178
185
 
179
- await RabbitMQService.send_message(data, queue_name, **kwargs)
186
+ await RabbitMQService.send_message(data, queue_name, ** kwargs)
180
187
  logging.info(f"消息发送成功(尝试 {attempt+1}/{max_retries})")
181
188
  return
182
189
 
sycommon/synacos/feign.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import io
2
2
  import os
3
3
  import time
4
+ import inspect
4
5
  from urllib.parse import urljoin
5
6
 
6
7
  import aiohttp
@@ -14,163 +15,373 @@ from sycommon.synacos.nacos_service import NacosService
14
15
  """
15
16
 
16
17
  # 示例Feign客户端接口
17
- # @feign_client(service_name="user-service", path_prefix="/api/v1")
18
- # class UserServiceClient:
18
+ # 1. 定义完整的Feign客户端接口
19
+ # @feign_client(service_name="product-service", path_prefix="/api/v2")
20
+ # class ProductServiceClient:
21
+ # """商品服务Feign客户端示例,涵盖所有参数类型"""
19
22
  #
20
- # @feign_request("GET", "/users/{user_id}")
21
- # async def get_user(self, user_id):
22
- # """获取用户信息"""
23
+ # # ------------------------------
24
+ # # 场景1: Path参数 + Query参数
25
+ # # 请求示例: GET /products/{product_id}/reviews?status=APPROVED&page=1&size=10
26
+ # # ------------------------------
27
+ # @feign_request("GET", "/products/{product_id}/reviews")
28
+ # async def get_product_reviews(
29
+ # self,
30
+ # product_id: int, # Path参数 (URL路径中的占位符)
31
+ # status: Optional[str] = None, # Query参数 (URL查询字符串)
32
+ # page: int = 1, # Query参数 (默认值)
33
+ # size: int = 10 # Query参数 (默认值)
34
+ # ) -> Dict[str, Any]:
35
+ # """获取商品评价列表"""
23
36
  # pass
24
37
  #
25
- # @feign_request("POST", "/users", headers={"Content-Type": "application/json"})
26
- # async def create_user(self, user_data):
27
- # """创建用户"""
38
+ # # ------------------------------
39
+ # # 场景2: 仅Query参数
40
+ # # 请求示例: GET /products?category=electronics&min_price=100&max_price=500&sort=price_asc
41
+ # # ------------------------------
42
+ # @feign_request("GET", "/products")
43
+ # async def search_products(
44
+ # self,
45
+ # category: str, # 必选Query参数
46
+ # min_price: Optional[float] = None, # 可选Query参数
47
+ # max_price: Optional[float] = None, # 可选Query参数
48
+ # sort: str = "created_desc" # 带默认值的Query参数
49
+ # ) -> Dict[str, Any]:
50
+ # """搜索商品(仅查询参数)"""
28
51
  # pass
29
52
  #
30
- # @feign_upload("avatar")
31
- # @feign_request("POST", "/users/{user_id}/avatar")
32
- # async def upload_avatar(self, user_id, file_path):
33
- # """上传用户头像"""
53
+ # # ------------------------------
54
+ # # 场景3: JSON Body参数 (POST)
55
+ # # 请求示例: POST /products (请求体为JSON)
56
+ # # ------------------------------
57
+ # @feign_request("POST", "/products", headers={"s-y-version": "2.1"})
58
+ # async def create_product(
59
+ # self,
60
+ # product_data: Dict[str, Any] # JSON请求体
61
+ # ) -> Dict[str, Any]:
62
+ # """创建商品(JSON请求体)"""
34
63
  # 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
64
+ #
65
+ # # ------------------------------
66
+ # # 场景4: Path参数 + JSON Body (PUT)
67
+ # # 请求示例: PUT /products/{product_id} (请求体为JSON)
68
+ # # ------------------------------
69
+ # @feign_request("PUT", "/products/{product_id}")
70
+ # async def update_product(
71
+ # self,
72
+ # product_id: int, # Path参数
73
+ # update_data: Dict[str, Any] # JSON请求体
74
+ # ) -> Dict[str, Any]:
75
+ # """更新商品信息"""
76
+ # pass
77
+ #
78
+ # # ------------------------------
79
+ # # 场景5: FormData表单提交 (x-www-form-urlencoded)
80
+ # # 请求示例: POST /products/batch-status (表单字段)
81
+ # # ------------------------------
82
+ # @feign_request(
83
+ # "POST",
84
+ # "/products/batch-status",
85
+ # headers={"Content-Type": "application/x-www-form-urlencoded"}
86
+ # )
87
+ # async def batch_update_status(
88
+ # self,
89
+ # product_ids: str, # 表单字段 (多个ID用逗号分隔)
90
+ # status: str, # 表单字段 (目标状态)
91
+ # operator: str # 表单字段 (操作人)
92
+ # ) -> Dict[str, Any]:
93
+ # """批量更新商品状态(表单提交)"""
94
+ # pass
95
+ #
96
+ # # ------------------------------
97
+ # # 场景6: 文件上传 + 表单字段混合
98
+ # # 请求示例: POST /products/{product_id}/images (multipart/form-data)
99
+ # # ------------------------------
100
+ # @feign_upload(field_name="image_file") # 指定文件表单字段名
101
+ # @feign_request("POST", "/products/{product_id}/images")
102
+ # async def upload_product_image(
103
+ # self,
104
+ # product_id: int, # Path参数
105
+ # file_path: str, # 本地文件路径(会被转为文件上传)
106
+ # image_type: str, # 表单字段(图片类型)
107
+ # is_primary: bool = False, # 表单字段(是否主图)
108
+ # remark: Optional[str] = None # 可选表单字段
109
+ # ) -> Dict[str, Any]:
110
+ # """上传商品图片(文件+表单字段)"""
111
+ # pass
112
+ #
113
+ # # ------------------------------
114
+ # # 场景7: 多Path参数 + DELETE请求
115
+ # # 请求示例: DELETE /products/{product_id}/skus/{sku_id}
116
+ # # ------------------------------
117
+ # @feign_request("DELETE", "/products/{product_id}/skus/{sku_id}")
118
+ # async def delete_product_sku(
119
+ # self,
120
+ # product_id: int, # Path参数1
121
+ # sku_id: int # Path参数2
122
+ # ) -> Dict[str, Any]:
123
+ # """删除商品SKU(多路径参数)"""
124
+ # pass
125
+ #
126
+ # # ------------------------------
127
+ # # 场景8: 复杂JSON Body + Query参数
128
+ # # 请求示例: POST /products/filter?include_out_of_stock=false
129
+ # # ------------------------------
130
+ # @feign_request("POST", "/products/filter")
131
+ # async def advanced_filter(
132
+ # self,
133
+ # filter_condition: Dict[str, Any], # JSON请求体(复杂筛选条件)
134
+ # include_out_of_stock: bool = False, # Query参数
135
+ # page: int = 1, # Query参数
136
+ # size: int = 20 # Query参数
137
+ # ) -> Dict[str, Any]:
138
+ # """高级筛选商品(JSON体+查询参数)"""
139
+ # pass
140
+ #
141
+ #
142
+ # # 2. 完整调用示例
143
+ # async def feign_complete_demo():
144
+ # # ------------------------------
145
+ # # 调用场景1: Path参数 + Query参数
146
+ # # ------------------------------
147
+ # reviews = await ProductServiceClient().get_product_reviews(
148
+ # product_id=10086, # Path参数
149
+ # status="APPROVED", # Query参数
150
+ # page=1,
151
+ # size=20
152
+ # )
153
+ # print(f"场景1 - 商品评价: {reviews.get('total', 0)}条评价")
154
+ #
155
+ # # ------------------------------
156
+ # # 调用场景2: 仅Query参数
157
+ # # ------------------------------
158
+ # electronics = await ProductServiceClient().search_products(
159
+ # category="electronics", # 必选Query
160
+ # min_price=1000,
161
+ # max_price=5000,
162
+ # sort="price_asc"
163
+ # )
164
+ # print(f"场景2 - 搜索结果: {len(electronics.get('items', []))}个商品")
165
+ #
166
+ # # ------------------------------
167
+ # # 调用场景3: JSON Body参数 (POST)
168
+ # # ------------------------------
169
+ # new_product = await ProductServiceClient().create_product({
170
+ # "name": "无线蓝牙耳机",
171
+ # "category": "electronics",
172
+ # "price": 299.99,
173
+ # "stock": 500,
174
+ # "attributes": {
175
+ # "brand": "Feign",
176
+ # "battery_life": "24h"
177
+ # }
178
+ # })
179
+ # print(f"场景3 - 新建商品: ID={new_product.get('id')}")
180
+ # product_id = new_product.get('id') # 用于后续示例
181
+ #
182
+ # # ------------------------------
183
+ # # 调用场景4: Path参数 + JSON Body (PUT)
184
+ # # ------------------------------
185
+ # if product_id:
186
+ # updated = await ProductServiceClient().update_product(
187
+ # product_id=product_id,
188
+ # update_data={
189
+ # "price": 279.99, # 降价
190
+ # "stock": 600
191
+ # }
192
+ # )
193
+ # print(f"场景4 - 商品更新: 状态={updated.get('success')}")
194
+ #
195
+ # # ------------------------------
196
+ # # 调用场景5: FormData表单提交
197
+ # # ------------------------------
198
+ # batch_result = await ProductServiceClient().batch_update_status(
199
+ # product_ids="1001,1002,1003", # 多个ID用逗号分隔
200
+ # status="ON_SALE",
201
+ # operator="system"
202
+ # )
203
+ # print(f"场景5 - 批量更新: 成功{batch_result.get('success_count')}个")
204
+ #
205
+ # # ------------------------------
206
+ # # 调用场景6: 文件上传 + 表单字段
207
+ # # ------------------------------
208
+ # if product_id:
209
+ # upload_result = await ProductServiceClient().upload_product_image(
210
+ # product_id=product_id,
211
+ # file_path="/tmp/product_main.jpg", # 本地图片路径
212
+ # image_type="main",
213
+ # is_primary=True,
214
+ # remark="商品主图"
215
+ # )
216
+ # print(f"场景6 - 图片上传: URL={upload_result.get('image_url')}")
217
+ #
218
+ # # ------------------------------
219
+ # # 调用场景7: 多Path参数 + DELETE
220
+ # # ------------------------------
221
+ # delete_result = await ProductServiceClient().delete_product_sku(
222
+ # product_id=10086,
223
+ # sku_id=5001
224
+ # )
225
+ # print(f"场景7 - 删除SKU: {delete_result.get('message')}")
226
+ #
227
+ # # ------------------------------
228
+ # # 调用场景8: 复杂JSON Body + Query参数
229
+ # # ------------------------------
230
+ # filtered = await ProductServiceClient().advanced_filter(
231
+ # filter_condition={ # 复杂JSON条件
232
+ # "categories": ["electronics", "home"],
233
+ # "price_range": {"min": 500, "max": 3000},
234
+ # "tags": ["new", "promotion"]
235
+ # },
236
+ # include_out_of_stock=False, # Query参数
237
+ # page=1,
238
+ # size=10
239
+ # )
240
+ # print(f"场景8 - 高级筛选: {filtered.get('total')}个匹配商品")
47
241
 
48
242
 
49
243
  def feign_client(service_name: str, path_prefix: str = "", default_timeout: float | None = None):
244
+ """Feign客户端装饰器,每次请求后自动关闭会话"""
50
245
  def decorator(cls):
51
246
  class FeignWrapper:
52
247
  def __init__(self):
53
248
  self.service_name = service_name
54
- self.nacos_manager = NacosService(None)
55
249
  self.path_prefix = path_prefix
56
- self.session = aiohttp.ClientSession()
57
250
  self.default_timeout = default_timeout
251
+ self.nacos_manager = None # 延迟初始化Nacos
252
+ self.session = None # 延迟初始化aiohttp会话
58
253
 
59
254
  def __getattr__(self, name):
255
+ """动态获取方法并包装为Feign调用,请求后自动关闭会话"""
60
256
  func = getattr(cls, name)
61
257
 
62
258
  async def wrapper(*args, **kwargs):
63
- # 获取请求元数据
64
- request_meta = getattr(func, "_feign_meta", {})
65
- method = request_meta.get("method", "GET")
66
- path = request_meta.get("path", "")
67
- headers = request_meta.get(
68
- "headers", {}).copy() # 复制 headers 避免修改原对象
69
-
70
- timeout = kwargs.pop('timeout', self.default_timeout)
71
-
72
- # 处理JSON请求的Content-Type
73
- is_json_request = method.upper() in [
74
- "POST", "PUT", "PATCH"] and not request_meta.get("files")
75
- if is_json_request and "Content-Type" not in headers:
76
- headers["Content-Type"] = "application/json"
77
-
78
- # 获取版本信息
79
- version = headers.get('s-y-version')
80
-
81
- # 构建完整URL
82
- full_path = f"{self.path_prefix}{path}"
83
- for k, v in kwargs.items():
84
- full_path = full_path.replace(f"{{{k}}}", str(v))
85
-
86
- # 服务发现与负载均衡
87
- instances = self.nacos_manager.get_service_instances(
88
- self.service_name, version=version)
89
- if not instances:
90
- SYLogger.error(
91
- f"nacos:未找到 {self.service_name} 的健康实例")
92
- raise RuntimeError(
93
- f"No instances available for {self.service_name}")
94
-
95
- # 简单轮询负载均衡
96
- instance = instances[int(time.time()) % len(instances)]
97
- base_url = f"http://{instance['ip']}:{instance['port']}"
98
- url = urljoin(base_url, full_path)
99
-
100
- SYLogger.info(
101
- f"nacos:调用服务: {self.service_name} -> {url}")
102
- SYLogger.info(f"nacos:请求头: {headers}")
103
-
104
- # 构建请求
105
- params = request_meta.get("params", {})
106
- body = request_meta.get("body", {})
107
- files = request_meta.get("files", None)
108
- form_data = request_meta.get("form_data", None)
109
-
110
- # 发送请求
259
+ # 确保会话初始化
260
+ if self.session is None:
261
+ self.session = aiohttp.ClientSession()
262
+ if self.nacos_manager is None:
263
+ self.nacos_manager = NacosService(None)
264
+
111
265
  try:
112
- # 处理文件上传
113
- if files or form_data:
114
- # 创建表单数据
115
- data = aiohttp.FormData()
116
- if form_data:
117
- for key, value in form_data.items():
118
- data.add_field(key, value)
119
- if files:
120
- for field_name, (filename, content) in files.items():
121
- data.add_field(
122
- field_name, content, filename=filename)
123
- # 移除 Content-Type 头,让 aiohttp 自动设置 boundary
266
+ # 1. 解析参数
267
+ sig = inspect.signature(func)
268
+ param_names = list(sig.parameters.keys())
269
+ try:
270
+ if param_names and param_names[0] == 'self':
271
+ bound_args = sig.bind(self, *args, **kwargs)
272
+ else:
273
+ bound_args = sig.bind(*args, **kwargs)
274
+ bound_args.apply_defaults()
275
+ params = dict(bound_args.arguments)
276
+ params.pop('self', None)
277
+ SYLogger.debug(f"解析参数: {params}")
278
+ except TypeError as e:
279
+ SYLogger.error(f"参数绑定失败: {str(e)}")
280
+ raise
281
+
282
+ # 2. 构建请求
283
+ request_meta = getattr(func, "_feign_meta", {})
284
+ method = request_meta.get("method", "GET")
285
+ path = request_meta.get("path", "")
286
+ headers = request_meta.get("headers", {}).copy()
287
+ timeout = kwargs.pop('timeout', self.default_timeout)
288
+
289
+ full_path = f"{self.path_prefix}{path}"
290
+ for param_name, param_value in params.items():
291
+ if param_value is not None:
292
+ full_path = full_path.replace(
293
+ f"{{{param_name}}}", str(param_value))
294
+
295
+ is_json_request = method.upper() in ["POST", "PUT", "PATCH"] and \
296
+ not request_meta.get("is_upload", False)
297
+ if is_json_request and "Content-Type" not in headers:
298
+ headers["Content-Type"] = "application/json"
299
+
300
+ # 3. 服务发现
301
+ version = headers.get('s-y-version')
302
+ instances = self.nacos_manager.get_service_instances(
303
+ self.service_name, version=version)
304
+ if not instances:
305
+ raise RuntimeError(
306
+ f"未找到服务 {self.service_name} 的健康实例")
307
+
308
+ instance = instances[int(time.time()) % len(instances)]
309
+ base_url = f"http://{instance['ip']}:{instance['port']}"
310
+ url = urljoin(base_url, full_path)
311
+ SYLogger.info(f"请求: {method} {url}")
312
+
313
+ # 4. 准备请求参数
314
+ query_params = {k: v for k, v in params.items()
315
+ if f"{{{k}}}" not in path and v is not None}
316
+ request_data = None
317
+ files = None
318
+
319
+ if request_meta.get("is_upload", False):
320
+ files = aiohttp.FormData()
321
+ file_path = params.get('file_path')
322
+ if file_path and os.path.exists(file_path):
323
+ file_field = request_meta.get(
324
+ "upload_field", "file")
325
+ with open(file_path, 'rb') as f:
326
+ files.add_field(
327
+ file_field,
328
+ f.read(),
329
+ filename=os.path.basename(file_path)
330
+ )
331
+ for key, value in params.items():
332
+ if key != 'file_path' and value is not None:
333
+ files.add_field(key, str(value))
124
334
  headers.pop('Content-Type', None)
125
- # 发送表单数据
126
- async with self.session.request(
127
- method=method,
128
- url=url,
129
- headers=headers,
130
- params=params,
131
- data=data,
132
- timeout=timeout
133
- ) as response:
134
- return await self._handle_response(response)
135
- else:
136
- # 普通请求(JSON)
137
- async with self.session.request(
138
- method=method,
139
- url=url,
140
- headers=headers,
141
- params=params,
142
- json=body,
143
- timeout=timeout
144
- ) as response:
145
- return await self._handle_response(response)
146
- except Exception as e:
147
- SYLogger.error(f"nacos:服务调用失败: {str(e)}")
148
- raise RuntimeError(f"Feign call failed: {str(e)}")
335
+ elif is_json_request:
336
+ body_params = [k for k in params if k not in query_params
337
+ and f"{{{k}}}" not in path]
338
+ if body_params:
339
+ request_data = params[body_params[0]] if len(body_params) == 1 else \
340
+ {k: params[k] for k in body_params}
341
+
342
+ # 5. 发送请求并获取响应
343
+ async with self.session.request(
344
+ method=method,
345
+ url=url,
346
+ headers=headers,
347
+ params=query_params,
348
+ json=request_data if not files else None,
349
+ data=files,
350
+ timeout=timeout
351
+ ) as response:
352
+ return await self._handle_response(response)
353
+
354
+ finally:
355
+ # 请求完成后自动关闭会话
356
+ if self.session:
357
+ await self.session.close()
358
+ self.session = None # 重置会话,下次调用重新创建
359
+ SYLogger.info(
360
+ f"自动关闭aiohttp会话: {self.service_name}")
149
361
 
150
362
  return wrapper
151
363
 
152
364
  async def _handle_response(self, response):
153
- # 处理响应
154
- if 200 <= response.status < 300:
365
+ """处理响应结果(保持不变)"""
366
+ status = response.status
367
+ if 200 <= status < 300:
155
368
  content_type = response.headers.get('Content-Type', '')
156
369
  if 'application/json' in content_type:
157
370
  return await response.json()
158
371
  else:
159
372
  return await response.read()
160
- raise RuntimeError(
161
- f"请求失败: {response.status} - {await response.text()}")
373
+ else:
374
+ error_msg = await response.text()
375
+ SYLogger.error(f"响应错误: {status} - {error_msg}")
376
+ raise RuntimeError(f"HTTP {status}: {error_msg}")
162
377
 
163
- async def close(self):
164
- """关闭 aiohttp 会话"""
165
- await self.session.close()
166
-
167
- return FeignWrapper()
378
+ return FeignWrapper
168
379
  return decorator
169
380
 
170
381
 
171
382
  def feign_request(method: str, path: str, headers: dict = None):
383
+ """定义请求元数据的装饰器"""
172
384
  def decorator(func):
173
- # 初始化请求元数据,确保headers是可修改的字典
174
385
  func._feign_meta = {
175
386
  "method": method.upper(),
176
387
  "path": path,
@@ -181,13 +392,12 @@ def feign_request(method: str, path: str, headers: dict = None):
181
392
 
182
393
 
183
394
  def feign_upload(field_name: str = "file"):
184
- # 文件上传装饰器
395
+ """处理文件上传的装饰器"""
185
396
  def decorator(func):
186
397
  async def wrapper(*args, **kwargs):
187
398
  file_path = kwargs.get('file_path')
188
- if not file_path:
189
- raise ValueError("file_path is required for upload")
190
-
399
+ if not file_path or not os.path.exists(file_path):
400
+ raise ValueError(f"文件路径不存在: {file_path}")
191
401
  with open(file_path, 'rb') as f:
192
402
  files = {field_name: (os.path.basename(file_path), f.read())}
193
403
  kwargs['files'] = files
@@ -276,11 +486,13 @@ async def feign(service_name, api_path, method='GET', params=None, headers=None,
276
486
  ) as response:
277
487
  return await _handle_feign_response(response)
278
488
  except aiohttp.ClientError as e:
279
- SYLogger.error(f"nacos:请求服务接口时出错ClientError: {e}")
489
+ SYLogger.error(
490
+ f"nacos:请求服务接口时出错ClientError path: {api_path} error:{e}")
280
491
  return None
281
492
  except Exception as e:
282
493
  import traceback
283
- SYLogger.error(f"nacos:请求服务接口时出错: {traceback.format_exc()}")
494
+ SYLogger.error(
495
+ f"nacos:请求服务接口时出错 path: {api_path} error:{traceback.format_exc()}")
284
496
  return None
285
497
  finally:
286
498
  await session.close()
sycommon/tools/docs.py ADDED
@@ -0,0 +1,42 @@
1
+ from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html, swagger_ui_default_parameters
2
+
3
+
4
+ def custom_swagger_ui_html(*args, **kwargs):
5
+ custom_params = {
6
+ "dom_id": "#swagger-ui",
7
+ "layout": "BaseLayout",
8
+ "deepLinking": True,
9
+ "showExtensions": True,
10
+ "showCommonExtensionsExtensions": True,
11
+ "defaultModelsExpandDepth": -1,
12
+ "persistAuthorization": True,
13
+ "displayRequestDuration": True
14
+ }
15
+
16
+ # 初始化合并参数为默认参数的副本
17
+ merged_params = swagger_ui_default_parameters.copy()
18
+
19
+ # 安全地合并kwargs中的参数(处理可能为None的情况)
20
+ if "swagger_ui_parameters" in kwargs and kwargs["swagger_ui_parameters"] is not None:
21
+ merged_params.update(kwargs["swagger_ui_parameters"])
22
+
23
+ # 最后应用自定义参数
24
+ merged_params.update(custom_params)
25
+ kwargs["swagger_ui_parameters"] = merged_params
26
+
27
+ return get_swagger_ui_html(
28
+ *args, ** kwargs,
29
+ swagger_favicon_url="https://static.sytechnology.com/img/sylogopng.png",
30
+ swagger_js_url="https://cdn.bootcdn.net/ajax/libs/swagger-ui/5.27.1/swagger-ui-bundle.js",
31
+ swagger_css_url="https://cdn.bootcdn.net/ajax/libs/swagger-ui/5.27.1/swagger-ui.css",
32
+ )
33
+
34
+
35
+ def custom_redoc_html(*args, **kwargs):
36
+ return get_redoc_html(
37
+ *args,
38
+ **kwargs,
39
+ redoc_favicon_url='https://static.sytechnology.com/img/sylogopng.png',
40
+ # redoc_js_url="https://cdn.jsdelivr.net/npm/@stardustai/redoc@2.0.0-rc.66/bundles/redoc.browser.lib.min.js",
41
+ with_google_fonts=False,
42
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown