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

@@ -31,6 +31,7 @@ class RabbitMQListenerConfig(BaseModel):
31
31
  durable: bool = Field(True, description="是否持久化")
32
32
  auto_delete: bool = Field(False, description="是否自动删除队列")
33
33
  auto_parse_json: bool = Field(True, description="是否自动解析JSON消息")
34
+ prefetch_count: int = Field(2, description="mq同时消费数量")
34
35
 
35
36
  class Config:
36
37
  """模型配置"""
@@ -488,7 +488,7 @@ class RabbitMQClient:
488
488
  message,
489
489
  routing_key=routing_key or self.routing_key or '#',
490
490
  mandatory=True,
491
- timeout=5.0
491
+ timeout=15.0
492
492
  )
493
493
  if not confirmed:
494
494
  raise Exception("消息未被服务器确认接收")
@@ -167,7 +167,7 @@ class RabbitMQService:
167
167
  'max_reconnection_attempts', 5),
168
168
  prefetch_count=kwargs.get('prefetch_count', 2),
169
169
  consumption_stall_threshold=kwargs.get(
170
- 'consumption_stall_threshold', 10)
170
+ 'consumption_stall_threshold', 10),
171
171
  )
172
172
 
173
173
  # 使用declare_queue控制是否声明队列(发送器不声明,监听器声明)
@@ -276,7 +276,7 @@ class RabbitMQService:
276
276
 
277
277
  # 以下方法逻辑与原有保持一致(无需修改)
278
278
  @classmethod
279
- async def setup_senders(cls, senders: List[RabbitMQSendConfig], has_listeners: bool = False) -> None:
279
+ async def setup_senders(cls, senders: List[RabbitMQSendConfig], has_listeners: bool = False, ** kwargs) -> None:
280
280
  """设置消息发送器"""
281
281
  cls._has_senders = True
282
282
  cls._has_listeners = has_listeners
@@ -287,6 +287,7 @@ class RabbitMQService:
287
287
  if not sender_config.queue_name:
288
288
  raise ValueError(f"发送器配置第{idx+1}项缺少queue_name")
289
289
 
290
+ prefetch_count = sender_config.prefetch_count
290
291
  queue_name = sender_config.queue_name
291
292
  app_name = cls._config.get(
292
293
  "APP_NAME", "") if cls._config else ""
@@ -315,7 +316,9 @@ class RabbitMQService:
315
316
  auto_delete=sender_config.auto_delete,
316
317
  auto_parse_json=sender_config.auto_parse_json,
317
318
  queue_name=queue_name,
318
- create_if_not_exists=False
319
+ create_if_not_exists=False,
320
+ prefetch_count=prefetch_count,
321
+ ** kwargs
319
322
  )
320
323
 
321
324
  # 记录客户端
@@ -334,7 +337,7 @@ class RabbitMQService:
334
337
  logger.info(f"消息发送器设置完成,共 {len(cls._sender_client_names)} 个发送器")
335
338
 
336
339
  @classmethod
337
- async def setup_listeners(cls, listeners: List[RabbitMQListenerConfig], has_senders: bool = False) -> None:
340
+ async def setup_listeners(cls, listeners: List[RabbitMQListenerConfig], has_senders: bool = False, ** kwargs) -> None:
338
341
  """设置消息监听器"""
339
342
  cls._has_listeners = True
340
343
  cls._has_senders = has_senders
sycommon/services.py CHANGED
@@ -145,6 +145,12 @@ class Services(metaclass=SingletonMeta):
145
145
 
146
146
  # 设置发送器,传递是否有监听器的标志
147
147
  if rabbitmq_senders:
148
+ # 判断是否有监听器,如果有遍历监听器列表,队列名一样将prefetch_count属性设置到发送器对象中
149
+ if rabbitmq_listeners:
150
+ for sender in rabbitmq_senders:
151
+ for listener in rabbitmq_listeners:
152
+ if sender.queue_name == listener.queue_name:
153
+ sender.prefetch_count = listener.prefetch_count
148
154
  await self._setup_senders_async(rabbitmq_senders, has_listeners)
149
155
 
150
156
  # 设置监听器,传递是否有发送器的标志
@@ -0,0 +1,153 @@
1
+ from typing import Dict, Any, Optional, List, Union
2
+
3
+ from sycommon.synacos.param import Body, Cookie, File, Form, Header, Path, Query
4
+ from sycommon.synacos.feign_client import feign_client, feign_request, feign_upload
5
+
6
+
7
+ @feign_client(
8
+ service_name="product-service",
9
+ path_prefix="/api/v1",
10
+ default_headers={
11
+ "User-Agent": "Feign-Client/1.0",
12
+ "Accept": "application/json"
13
+ }
14
+ )
15
+ class ProductServiceClient:
16
+ """商品服务Feign客户端(优化版)"""
17
+
18
+ # ------------------------------
19
+ # 场景1: 基础参数 + 动态Header + Cookie
20
+ # ------------------------------
21
+ @feign_request(
22
+ "GET",
23
+ "/products/{product_id}/reviews",
24
+ headers={"X-Request-Source": "mobile"}, # 固定头
25
+ timeout=10 # 接口级超时(10秒)
26
+ )
27
+ async def get_product_reviews(
28
+ self,
29
+ product_id: int = Path(..., description="商品ID"),
30
+ status: Optional[str] = Query(None, description="评价状态"),
31
+ page: int = Query(1, description="页码"),
32
+ size: int = Query(10, description="每页条数"),
33
+ x_auth_token: str = Header(..., description="动态令牌头"), # 动态头
34
+ session_id: str = Cookie(..., description="会话Cookie") # Cookie
35
+ ) -> Dict[str, Any]:
36
+ """获取商品评价列表"""
37
+ pass
38
+
39
+ # ------------------------------
40
+ # 场景2: 仅Query参数(默认不超时)
41
+ # ------------------------------
42
+ @feign_request("GET", "/products") # 未指定timeout,使用客户端默认(不超时)
43
+ async def search_products(
44
+ self,
45
+ category: str = Query(..., description="商品分类"),
46
+ min_price: Optional[float] = Query(None, description="最低价格"),
47
+ max_price: Optional[float] = Query(None, description="最高价格"),
48
+ sort: str = Query("created_desc", description="排序方式")
49
+ ) -> Dict[str, Any]:
50
+ """搜索商品"""
51
+ pass
52
+
53
+ # ------------------------------
54
+ # 场景3: JSON请求体 + 动态签名头
55
+ # (通过Header参数传递动态生成的签名)
56
+ # ------------------------------
57
+ @feign_request(
58
+ "POST",
59
+ "/products",
60
+ headers={"s-y-version": "2.1"},
61
+ timeout=15 # 15秒超时
62
+ )
63
+ async def create_product(
64
+ self,
65
+ product_data: Dict[str, Any] = Body(..., description="商品信息"),
66
+ x_signature: str = Header(..., description="动态生成的签名头") # 签名头
67
+ ) -> Dict[str, Any]:
68
+ """创建商品(动态签名通过Header参数传递)"""
69
+ pass
70
+
71
+ # ------------------------------
72
+ # 场景4: 文件上传 + 分片上传头
73
+ # ------------------------------
74
+ @feign_upload(field_name="image_file")
75
+ @feign_request(
76
+ "POST",
77
+ "/products/{product_id}/images",
78
+ headers={"X-Upload-Type": "product-image"},
79
+ timeout=60 # 上传超时60秒
80
+ )
81
+ async def upload_product_image(
82
+ self,
83
+ product_id: int = Path(..., description="商品ID"),
84
+ file_paths: Union[str, List[str]] = File(..., description="文件路径"),
85
+ image_type: str = Form(..., description="图片类型"),
86
+ is_primary: bool = Form(False, description="是否主图"),
87
+ x_chunked: bool = Header(False, description="是否分片上传") # 分片上传头
88
+ ) -> Dict[str, Any]:
89
+ """上传商品图片(支持分片上传标记)"""
90
+ pass
91
+
92
+ # ------------------------------
93
+ # 场景5: 表单提交 + 操作时间头
94
+ # ------------------------------
95
+ @feign_request(
96
+ "POST",
97
+ "/products/batch-status",
98
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
99
+ timeout=5
100
+ )
101
+ async def batch_update_status(
102
+ self,
103
+ product_ids: str = Form(..., description="商品ID列表"),
104
+ status: str = Form(..., description="目标状态"),
105
+ operator: str = Form(..., description="操作人"),
106
+ x_operate_time: str = Header(..., description="操作时间戳头") # 动态时间头
107
+ ) -> Dict[str, Any]:
108
+ """批量更新商品状态"""
109
+ pass
110
+
111
+
112
+ # ------------------------------
113
+ # 2. 完整调用示例(含Session和Header用法)
114
+ # ------------------------------
115
+ async def feign_advanced_demo():
116
+ client = ProductServiceClient()
117
+
118
+ # 场景1: 动态Header和Cookie
119
+ reviews = await client.get_product_reviews(
120
+ product_id=10086,
121
+ status="APPROVED",
122
+ x_auth_token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", # 动态令牌
123
+ session_id="sid_123456" # Cookie
124
+ )
125
+ print(f"场景1 - 评价数: {reviews.get('total', 0)}")
126
+
127
+ # 场景3: 动态生成签名头(直接通过Header参数传递)
128
+ import hashlib
129
+ product_data = {
130
+ "name": "无线蓝牙耳机",
131
+ "price": 299.99,
132
+ "stock": 500
133
+ }
134
+ # 生成签名(业务逻辑)
135
+ sign_str = f"{product_data['name']}_{product_data['price']}_secret"
136
+ signature = hashlib.md5(sign_str.encode()).hexdigest()
137
+ # 传递签名头
138
+ new_product = await client.create_product(
139
+ product_data=product_data,
140
+ x_signature=signature # 动态签名通过Header参数传入
141
+ )
142
+ print(f"场景3 - 商品ID: {new_product.get('id')}")
143
+
144
+ # 场景4: 分片上传(通过x_chunked头控制)
145
+ product_id = new_product.get('id')
146
+ if product_id:
147
+ upload_result = await client.upload_product_image(
148
+ product_id=product_id,
149
+ file_paths=["/tmp/main.jpg", "/tmp/detail.jpg"],
150
+ image_type="detail",
151
+ x_chunked=True # 启用分片上传
152
+ )
153
+ print(f"场景4 - 上传图片数: {len(upload_result.get('image_urls', []))}")
@@ -0,0 +1,129 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Any, Optional, List, Dict
3
+
4
+ from sycommon.synacos.feign_client import feign_client, feign_request
5
+ from sycommon.synacos.param import Body, Form, Query
6
+
7
+
8
+ # ------------------------------
9
+ # 请求模型(req)
10
+ # ------------------------------
11
+ class ProductCreateReq(BaseModel):
12
+ """创建商品的请求模型"""
13
+ name: str = Field(..., description="商品名称")
14
+ category: str = Field(..., description="商品分类")
15
+ price: float = Field(..., gt=0, description="商品价格(必须>0)")
16
+ stock: int = Field(..., ge=0, description="库存(必须>=0)")
17
+ attributes: Optional[Dict[str, str]] = Field(None, description="商品属性")
18
+
19
+
20
+ class BatchUpdateStatusReq(BaseModel):
21
+ """批量更新状态的请求模型(表单提交)"""
22
+ product_ids: str = Field(..., description="商品ID列表(逗号分隔)")
23
+ status: str = Field(..., description="目标状态(如ON_SALE/OFF_SALE)")
24
+ operator: str = Field(..., description="操作人")
25
+
26
+
27
+ # ------------------------------
28
+ # 响应模型(resp)
29
+ # ------------------------------
30
+ class ProductResp(BaseModel):
31
+ """商品详情响应模型"""
32
+ id: int = Field(..., description="商品ID")
33
+ name: str = Field(..., description="商品名称")
34
+ category: str = Field(..., description="分类")
35
+ price: float = Field(..., description="价格")
36
+ stock: int = Field(..., description="库存")
37
+ created_at: str = Field(..., description="创建时间")
38
+
39
+
40
+ class PageResp(BaseModel):
41
+ """分页响应模型"""
42
+ total: int = Field(..., description="总条数")
43
+ items: List[ProductResp] = Field(..., description="商品列表")
44
+ page: int = Field(..., description="当前页码")
45
+ size: int = Field(..., description="每页条数")
46
+
47
+
48
+ @feign_client(
49
+ service_name="product-service",
50
+ path_prefix="/api/v2",
51
+ default_headers={"Accept": "application/json"}
52
+ )
53
+ class ProductServiceClient:
54
+ # ------------------------------
55
+ # 场景1: Pydantic 作为请求体(Body)
56
+ # ------------------------------
57
+ @feign_request("POST", "/products", timeout=15)
58
+ async def create_product(
59
+ self,
60
+ # 使用 Pydantic 模型作为请求体
61
+ product: ProductCreateReq = Body(..., description="商品信息")
62
+ ) -> ProductResp: # 响应自动解析为 ProductResp 模型
63
+ """创建商品(Pydantic请求体 + Pydantic响应)"""
64
+ pass
65
+
66
+ # ------------------------------
67
+ # 场景2: Pydantic 作为表单参数(Form)
68
+ # ------------------------------
69
+ @feign_request(
70
+ "POST",
71
+ "/products/batch-status",
72
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
73
+ timeout=5
74
+ )
75
+ async def batch_update_status(
76
+ self,
77
+ # 使用 Pydantic 模型作为表单参数
78
+ update_data: BatchUpdateStatusReq = Form(..., description="批量更新信息")
79
+ ) -> Dict[str, Any]: # 普通字典响应
80
+ """批量更新商品状态(Pydantic表单)"""
81
+ pass
82
+
83
+ # ------------------------------
84
+ # 场景3: 响应为分页模型(嵌套Pydantic)
85
+ # ------------------------------
86
+ @feign_request("GET", "/products", timeout=10)
87
+ async def search_products(
88
+ self,
89
+ category: str = Query(..., description="商品分类"),
90
+ page: int = Query(1, description="页码"),
91
+ size: int = Query(10, description="每页条数")
92
+ ) -> PageResp: # 响应自动解析为 PageResp(嵌套 ProductResp)
93
+ """搜索商品(Pydantic分页响应)"""
94
+ pass
95
+
96
+
97
+ async def pydantic_feign_demo():
98
+ client = ProductServiceClient()
99
+
100
+ # 场景1: 创建商品(Pydantic请求体 + 响应)
101
+ create_req = ProductCreateReq(
102
+ name="无线蓝牙耳机",
103
+ category="electronics",
104
+ price=299.99,
105
+ stock=500,
106
+ attributes={"brand": "Feign", "battery_life": "24h"}
107
+ )
108
+ product = await client.create_product(product=create_req)
109
+ # 直接使用 Pydantic 模型的属性
110
+ print(f"创建商品: ID={product.id}, 名称={product.name}, 价格={product.price}")
111
+
112
+ # 场景2: 批量更新(Pydantic表单)
113
+ batch_req = BatchUpdateStatusReq(
114
+ product_ids=f"{product.id},1002,1003",
115
+ status="ON_SALE",
116
+ operator="system"
117
+ )
118
+ batch_result = await client.batch_update_status(update_data=batch_req)
119
+ print(f"批量更新成功: {batch_result.get('success_count')}个")
120
+
121
+ # 场景3: 搜索商品(Pydantic分页响应)
122
+ page_resp = await client.search_products(
123
+ category="electronics",
124
+ page=1,
125
+ size=10
126
+ )
127
+ # 分页模型的属性和嵌套列表
128
+ print(
129
+ f"搜索结果: 共{page_resp.total}条,第1页商品: {[p.name for p in page_resp.items]}")
sycommon/synacos/feign.py CHANGED
@@ -1,8 +1,6 @@
1
1
  import io
2
2
  import os
3
3
  import time
4
- import inspect
5
- from urllib.parse import urljoin
6
4
 
7
5
  import aiohttp
8
6
  from sycommon.logging.kafka_log import SYLogger
@@ -14,430 +12,6 @@ from sycommon.synacos.nacos_service import NacosService
14
12
  方式二: 使用 feign 函数
15
13
  """
16
14
 
17
- # 示例Feign客户端接口
18
- # 1. 定义完整的Feign客户端接口
19
- # @feign_client(service_name="product-service", path_prefix="/api/v2")
20
- # class ProductServiceClient:
21
- # """商品服务Feign客户端示例,涵盖所有参数类型"""
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
- # """获取商品评价列表"""
36
- # pass
37
- #
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
- # """搜索商品(仅查询参数)"""
51
- # pass
52
- #
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请求体)"""
63
- # pass
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
- # # 支持同时上传多个文件,共用字段名 "image_file"
100
- # # ------------------------------
101
- # @feign_upload(field_name="image_file") # 指定所有文件的表单字段名
102
- # @feign_request("POST", "/products/{product_id}/images")
103
- # async def upload_product_image(
104
- # self,
105
- # product_id: int, # Path参数(URL路径中的占位符)
106
- # file_paths: str | list[str], # 本地文件路径(单个路径字符串或多个路径列表)
107
- # image_type: str, # 表单字段(图片类型,如"main"、"detail")
108
- # is_primary: bool = False, # 表单字段(是否主图)
109
- # remark: Optional[str] = None # 可选表单字段(备注信息)
110
- # ) -> Dict[str, Any]:
111
- # """上传商品图片(支持多文件 + 表单字段混合)"""
112
- # pass
113
- #
114
- # # ------------------------------
115
- # # 场景7: 多Path参数 + DELETE请求
116
- # # 请求示例: DELETE /products/{product_id}/skus/{sku_id}
117
- # # ------------------------------
118
- # @feign_request("DELETE", "/products/{product_id}/skus/{sku_id}")
119
- # async def delete_product_sku(
120
- # self,
121
- # product_id: int, # Path参数1
122
- # sku_id: int # Path参数2
123
- # ) -> Dict[str, Any]:
124
- # """删除商品SKU(多路径参数)"""
125
- # pass
126
- #
127
- # # ------------------------------
128
- # # 场景8: 复杂JSON Body + Query参数
129
- # # 请求示例: POST /products/filter?include_out_of_stock=false
130
- # # ------------------------------
131
- # @feign_request("POST", "/products/filter")
132
- # async def advanced_filter(
133
- # self,
134
- # filter_condition: Dict[str, Any], # JSON请求体(复杂筛选条件)
135
- # include_out_of_stock: bool = False, # Query参数
136
- # page: int = 1, # Query参数
137
- # size: int = 20 # Query参数
138
- # ) -> Dict[str, Any]:
139
- # """高级筛选商品(JSON体+查询参数)"""
140
- # pass
141
- #
142
- #
143
- # # 2. 完整调用示例
144
- # async def feign_complete_demo():
145
- # # ------------------------------
146
- # # 调用场景1: Path参数 + Query参数
147
- # # ------------------------------
148
- # reviews = await ProductServiceClient().get_product_reviews(
149
- # product_id=10086, # Path参数
150
- # status="APPROVED", # Query参数
151
- # page=1,
152
- # size=20
153
- # )
154
- # print(f"场景1 - 商品评价: {reviews.get('total', 0)}条评价")
155
- #
156
- # # ------------------------------
157
- # # 调用场景2: 仅Query参数
158
- # # ------------------------------
159
- # electronics = await ProductServiceClient().search_products(
160
- # category="electronics", # 必选Query
161
- # min_price=1000,
162
- # max_price=5000,
163
- # sort="price_asc"
164
- # )
165
- # print(f"场景2 - 搜索结果: {len(electronics.get('items', []))}个商品")
166
- #
167
- # # ------------------------------
168
- # # 调用场景3: JSON Body参数 (POST)
169
- # # ------------------------------
170
- # new_product = await ProductServiceClient().create_product({
171
- # "name": "无线蓝牙耳机",
172
- # "category": "electronics",
173
- # "price": 299.99,
174
- # "stock": 500,
175
- # "attributes": {
176
- # "brand": "Feign",
177
- # "battery_life": "24h"
178
- # }
179
- # })
180
- # print(f"场景3 - 新建商品: ID={new_product.get('id')}")
181
- # product_id = new_product.get('id') # 用于后续示例
182
- #
183
- # # ------------------------------
184
- # # 调用场景4: Path参数 + JSON Body (PUT)
185
- # # ------------------------------
186
- # if product_id:
187
- # updated = await ProductServiceClient().update_product(
188
- # product_id=product_id,
189
- # update_data={
190
- # "price": 279.99, # 降价
191
- # "stock": 600
192
- # }
193
- # )
194
- # print(f"场景4 - 商品更新: 状态={updated.get('success')}")
195
- #
196
- # # ------------------------------
197
- # # 调用场景5: FormData表单提交
198
- # # ------------------------------
199
- # batch_result = await ProductServiceClient().batch_update_status(
200
- # product_ids="1001,1002,1003", # 多个ID用逗号分隔
201
- # status="ON_SALE",
202
- # operator="system"
203
- # )
204
- # print(f"场景5 - 批量更新: 成功{batch_result.get('success_count')}个")
205
- #
206
- # # ------------------------------
207
- # # 调用场景6: 多文件上传 + 表单字段
208
- # # 支持两种调用方式:单文件上传 / 多文件上传
209
- # # ------------------------------
210
- # if product_id:
211
- # single_upload_result = await ProductServiceClient().upload_product_image(
212
- # product_id=product_id,
213
- # file_paths="/tmp/product_main.jpg", # 单个文件路径或多个路径[""]
214
- # image_type="main",
215
- # is_primary=True,
216
- # remark="商品主图(单文件)"
217
- # )
218
- # print(f"场景6 - 单文件上传: 主图URL={single_upload_result.get('image_urls')[0]}")
219
- #
220
- # # ------------------------------
221
- # # 调用场景7: 多Path参数 + DELETE
222
- # # ------------------------------
223
- # delete_result = await ProductServiceClient().delete_product_sku(
224
- # product_id=10086,
225
- # sku_id=5001
226
- # )
227
- # print(f"场景7 - 删除SKU: {delete_result.get('message')}")
228
- #
229
- # # ------------------------------
230
- # # 调用场景8: 复杂JSON Body + Query参数
231
- # # ------------------------------
232
- # filtered = await ProductServiceClient().advanced_filter(
233
- # filter_condition={ # 复杂JSON条件
234
- # "categories": ["electronics", "home"],
235
- # "price_range": {"min": 500, "max": 3000},
236
- # "tags": ["new", "promotion"]
237
- # },
238
- # include_out_of_stock=False, # Query参数
239
- # page=1,
240
- # size=10
241
- # )
242
- # print(f"场景8 - 高级筛选: {filtered.get('total')}个匹配商品")
243
-
244
-
245
- def feign_client(service_name: str, path_prefix: str = "", default_timeout: float | None = None):
246
- """Feign客户端装饰器,每次请求后自动关闭会话"""
247
- def decorator(cls):
248
- class FeignWrapper:
249
- def __init__(self):
250
- self.service_name = service_name
251
- self.path_prefix = path_prefix
252
- self.default_timeout = default_timeout
253
- self.nacos_manager = None # 延迟初始化Nacos
254
- self.session = None # 延迟初始化aiohttp会话
255
-
256
- def __getattr__(self, name):
257
- """动态获取方法并包装为Feign调用,请求后自动关闭会话"""
258
- func = getattr(cls, name)
259
-
260
- async def wrapper(*args, **kwargs):
261
- # 确保会话初始化
262
- if self.session is None:
263
- self.session = aiohttp.ClientSession()
264
- if self.nacos_manager is None:
265
- self.nacos_manager = NacosService(None)
266
-
267
- try:
268
- # 1. 解析参数
269
- sig = inspect.signature(func)
270
- param_names = list(sig.parameters.keys())
271
- try:
272
- if param_names and param_names[0] == 'self':
273
- bound_args = sig.bind(self, *args, **kwargs)
274
- else:
275
- bound_args = sig.bind(*args, **kwargs)
276
- bound_args.apply_defaults()
277
- params = dict(bound_args.arguments)
278
- params.pop('self', None)
279
- SYLogger.debug(f"解析参数: {params}")
280
- except TypeError as e:
281
- SYLogger.error(f"参数绑定失败: {str(e)}")
282
- raise
283
-
284
- # 2. 构建请求
285
- request_meta = getattr(func, "_feign_meta", {})
286
- method = request_meta.get("method", "GET")
287
- path = request_meta.get("path", "")
288
- headers = request_meta.get("headers", {}).copy()
289
- timeout = kwargs.pop('timeout', self.default_timeout)
290
-
291
- full_path = f"{self.path_prefix}{path}"
292
- for param_name, param_value in params.items():
293
- if param_value is not None:
294
- full_path = full_path.replace(
295
- f"{{{param_name}}}", str(param_value))
296
-
297
- is_json_request = method.upper() in ["POST", "PUT", "PATCH"] and \
298
- not request_meta.get("is_upload", False)
299
- if is_json_request and "Content-Type" not in headers:
300
- headers["Content-Type"] = "application/json"
301
-
302
- # 3. 服务发现
303
- version = headers.get('s-y-version')
304
- instances = self.nacos_manager.get_service_instances(
305
- self.service_name, target_version=version)
306
- if not instances:
307
- raise RuntimeError(
308
- f"未找到服务 {self.service_name} 的健康实例")
309
-
310
- instance = instances[int(time.time()) % len(instances)]
311
- base_url = f"http://{instance['ip']}:{instance['port']}"
312
- url = urljoin(base_url, full_path)
313
- SYLogger.info(f"请求: {method} {url}")
314
-
315
- # 4. 准备请求参数
316
- query_params = {k: v for k, v in params.items()
317
- if f"{{{k}}}" not in path and v is not None}
318
- request_data = None
319
- files = None
320
-
321
- if request_meta.get("is_upload", False):
322
- files = aiohttp.FormData()
323
- file_path = params.get('file_path')
324
- if file_path and os.path.exists(file_path):
325
- file_field = request_meta.get(
326
- "upload_field", "file")
327
- with open(file_path, 'rb') as f:
328
- files.add_field(
329
- file_field,
330
- f.read(),
331
- filename=os.path.basename(file_path)
332
- )
333
- for key, value in params.items():
334
- if key != 'file_path' and value is not None:
335
- files.add_field(key, str(value))
336
- headers.pop('Content-Type', None)
337
- elif is_json_request:
338
- body_params = [k for k in params if k not in query_params
339
- and f"{{{k}}}" not in path]
340
- if body_params:
341
- request_data = params[body_params[0]] if len(body_params) == 1 else \
342
- {k: params[k] for k in body_params}
343
-
344
- # 5. 发送请求并获取响应
345
- async with self.session.request(
346
- method=method,
347
- url=url,
348
- headers=headers,
349
- params=query_params,
350
- json=request_data if not files else None,
351
- data=files,
352
- timeout=timeout
353
- ) as response:
354
- return await self._handle_response(response)
355
-
356
- finally:
357
- # 请求完成后自动关闭会话
358
- if self.session:
359
- await self.session.close()
360
- self.session = None # 重置会话,下次调用重新创建
361
- SYLogger.info(
362
- f"自动关闭aiohttp会话: {self.service_name}")
363
-
364
- return wrapper
365
-
366
- async def _handle_response(self, response):
367
- """处理响应结果(保持不变)"""
368
- status = response.status
369
- if 200 <= status < 300:
370
- content_type = response.headers.get('Content-Type', '')
371
- if 'application/json' in content_type:
372
- return await response.json()
373
- else:
374
- return await response.read()
375
- else:
376
- error_msg = await response.text()
377
- SYLogger.error(f"响应错误: {status} - {error_msg}")
378
- raise RuntimeError(f"HTTP {status}: {error_msg}")
379
-
380
- return FeignWrapper
381
- return decorator
382
-
383
-
384
- def feign_request(method: str, path: str, headers: dict = None):
385
- """定义请求元数据的装饰器"""
386
- def decorator(func):
387
- func._feign_meta = {
388
- "method": method.upper(),
389
- "path": path,
390
- "headers": headers.copy() if headers else {}
391
- }
392
- return func
393
- return decorator
394
-
395
-
396
- def feign_upload(field_name: str = "file"):
397
- """处理多文件上传的装饰器(支持同字段名多文件 + 表单字段混合)"""
398
- def decorator(func):
399
- async def wrapper(*args, **kwargs):
400
- # 获取文件路径列表(支持单个文件路径字符串或多个文件路径列表)
401
- file_paths = kwargs.get('file_paths')
402
- if not file_paths:
403
- raise ValueError("缺少文件路径参数: file_paths(可为单个路径字符串或列表)")
404
-
405
- # 统一转为列表格式(兼容单个文件的情况)
406
- if isinstance(file_paths, str):
407
- file_paths = [file_paths]
408
- if not isinstance(file_paths, list):
409
- raise ValueError("file_paths 必须是字符串或列表")
410
-
411
- # 验证所有文件是否存在
412
- for path in file_paths:
413
- if not os.path.exists(path):
414
- raise FileNotFoundError(f"文件不存在: {path}")
415
-
416
- # 构建 multipart/form-data 表单数据
417
- form_data = aiohttp.FormData()
418
-
419
- # 添加所有文件(共用同一个 field_name)
420
- for file_path in file_paths:
421
- with open(file_path, 'rb') as f:
422
- form_data.add_field(
423
- field_name, # 所有文件使用相同的表单字段名
424
- f.read(),
425
- filename=os.path.basename(file_path) # 保留原文件名
426
- )
427
-
428
- # 添加其他表单字段(从 kwargs 中提取非文件参数)
429
- form_fields = {k: v for k, v in kwargs.items() if k !=
430
- 'file_paths'}
431
- for key, value in form_fields.items():
432
- if value is not None:
433
- form_data.add_field(key, str(value)) # 表单字段转为字符串
434
-
435
- # 将构建好的表单数据传入原函数
436
- kwargs['form_data'] = form_data
437
- return await func(*args, **kwargs)
438
- return wrapper
439
- return decorator
440
-
441
15
 
442
16
  async def feign(service_name, api_path, method='GET', params=None, headers=None, file_path=None,
443
17
  path_params=None, body=None, files=None, form_data=None, timeout=None):
@@ -0,0 +1,317 @@
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
+ import aiohttp
9
+ from pydantic import BaseModel
10
+ from sycommon.synacos.param import Body, Cookie, File, Form, Header, Param, Path, Query
11
+ from sycommon.logging.kafka_log import SYLogger
12
+ from sycommon.synacos.nacos_service import NacosService
13
+
14
+ # 定义 Pydantic 模型泛型(用于响应解析)
15
+ T = TypeVar('T', bound=BaseModel)
16
+
17
+ # ------------------------------
18
+ # Feign客户端装饰器(支持Pydantic)
19
+ # ------------------------------
20
+
21
+
22
+ def feign_client(
23
+ service_name: str,
24
+ path_prefix: str = "",
25
+ default_timeout: Optional[float] = None,
26
+ default_headers: Optional[Dict[str, str]] = None
27
+ ):
28
+ default_headers = default_headers or {}
29
+
30
+ def decorator(cls):
31
+ class FeignClient:
32
+ def __init__(self):
33
+ self.service_name = service_name
34
+ self.path_prefix = path_prefix
35
+ self.default_timeout = default_timeout
36
+ self.default_headers = default_headers.copy()
37
+ self.nacos_manager: Optional[NacosService] = None
38
+ self.session: Optional[aiohttp.ClientSession] = None
39
+
40
+ def __getattr__(self, name: str):
41
+ if not hasattr(cls, name):
42
+ raise AttributeError(f"类 {cls.__name__} 不存在方法 {name}")
43
+
44
+ func = getattr(cls, name)
45
+ sig = inspect.signature(func)
46
+ param_meta = self._parse_param_meta(sig)
47
+ # 获取响应模型(从返回类型注解中提取 Pydantic 模型)
48
+ resp_model = self._get_response_model(sig)
49
+
50
+ async def wrapper(*args, **kwargs) -> Any:
51
+ if not self.session:
52
+ self.session = aiohttp.ClientSession()
53
+ if not self.nacos_manager:
54
+ self.nacos_manager = NacosService(None)
55
+
56
+ try:
57
+ bound_args = self._bind_arguments(
58
+ func, sig, args, kwargs)
59
+ self._validate_required_params(param_meta, bound_args)
60
+
61
+ request_meta = getattr(func, "_feign_meta", {})
62
+ method = request_meta.get("method", "GET").upper()
63
+ path = request_meta.get("path", "")
64
+ is_upload = request_meta.get("is_upload", False)
65
+ method_headers = request_meta.get("headers", {})
66
+ timeout = request_meta.get(
67
+ "timeout", self.default_timeout)
68
+
69
+ headers = self._build_headers(
70
+ param_meta, bound_args, method_headers)
71
+ full_path = f"{self.path_prefix}{path}"
72
+ full_path = self._replace_path_params(
73
+ full_path, param_meta, bound_args)
74
+
75
+ base_url = await self._get_service_base_url(headers)
76
+ url = urljoin(base_url, full_path)
77
+ SYLogger.info(f"请求: {method} {url}")
78
+
79
+ query_params = self._get_query_params(
80
+ param_meta, bound_args)
81
+ cookies = self._get_cookies(param_meta, bound_args)
82
+ # 处理请求数据(支持 Pydantic 模型转字典)
83
+ request_data = await self._get_request_data(
84
+ method, param_meta, bound_args, is_upload, method_headers
85
+ )
86
+
87
+ async with self.session.request(
88
+ method=method,
89
+ url=url,
90
+ headers=headers,
91
+ params=query_params,
92
+ cookies=cookies,
93
+ json=request_data if not (is_upload or isinstance(
94
+ request_data, aiohttp.FormData)) else None,
95
+ data=request_data if is_upload or isinstance(
96
+ request_data, aiohttp.FormData) else None,
97
+ timeout=timeout
98
+ ) as response:
99
+ # 处理响应(支持 Pydantic 模型解析)
100
+ return await self._handle_response(response, resp_model)
101
+
102
+ finally:
103
+ if self.session:
104
+ await self.session.close()
105
+ self.session = None
106
+
107
+ return wrapper
108
+
109
+ def _parse_param_meta(self, sig: inspect.Signature) -> Dict[str, Param]:
110
+ param_meta = {}
111
+ for param in sig.parameters.values():
112
+ if param.name == "self":
113
+ continue
114
+ if isinstance(param.default, Param):
115
+ param_meta[param.name] = param.default
116
+ else:
117
+ if param.default == inspect.Parameter.empty:
118
+ param_meta[param.name] = Query(..., description="")
119
+ else:
120
+ param_meta[param.name] = Query(
121
+ param.default, description="")
122
+ return param_meta
123
+
124
+ def _get_response_model(self, sig: inspect.Signature) -> Optional[Type[BaseModel]]:
125
+ """从函数返回类型注解中提取 Pydantic 模型"""
126
+ return_annotation = sig.return_annotation
127
+ # 支持直接注解(如 -> ProductResp)或 Optional(如 -> Optional[ProductResp])
128
+ if hasattr(return_annotation, '__origin__') and return_annotation.__origin__ is Optional:
129
+ return_annotation = return_annotation.__args__[0]
130
+ # 检查是否为 Pydantic 模型
131
+ if inspect.isclass(return_annotation) and issubclass(return_annotation, BaseModel):
132
+ return return_annotation
133
+ return None
134
+
135
+ def _bind_arguments(self, func, sig: inspect.Signature, args, kwargs) -> Dict[str, Any]:
136
+ try:
137
+ bound_args = sig.bind(*args, **kwargs)
138
+ bound_args.apply_defaults()
139
+ return {k: v for k, v in bound_args.arguments.items() if k != "self"}
140
+ except TypeError as e:
141
+ SYLogger.error(f"参数绑定失败 [{func.__name__}]: {str(e)}")
142
+ raise
143
+
144
+ def _validate_required_params(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any]):
145
+ missing = [
146
+ meta.get_key(name) for name, meta in param_meta.items()
147
+ if meta.is_required() and name not in bound_args
148
+ ]
149
+ if missing:
150
+ raise ValueError(f"缺少必填参数: {', '.join(missing)}")
151
+
152
+ def _build_headers(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any], method_headers: Dict[str, str]) -> Dict[str, str]:
153
+ headers = self.default_headers.copy()
154
+ headers.update(method_headers)
155
+ for name, meta in param_meta.items():
156
+ if isinstance(meta, Header) and name in bound_args:
157
+ value = bound_args[name]
158
+ if value is not None:
159
+ headers[meta.get_key(name)] = str(value)
160
+ return headers
161
+
162
+ def _replace_path_params(self, path: str, param_meta: Dict[str, Param], bound_args: Dict[str, Any]) -> str:
163
+ for name, meta in param_meta.items():
164
+ if isinstance(meta, Path) and name in bound_args:
165
+ path = path.replace(
166
+ f"{{{meta.get_key(name)}}}", str(bound_args[name]))
167
+ return path
168
+
169
+ def _get_query_params(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any]) -> Dict[str, str]:
170
+ return {
171
+ param_meta[name].get_key(name): str(value)
172
+ for name, value in bound_args.items()
173
+ if isinstance(param_meta.get(name), Query) and value is not None
174
+ }
175
+
176
+ def _get_cookies(self, param_meta: Dict[str, Param], bound_args: Dict[str, Any]) -> Dict[str, str]:
177
+ return {
178
+ param_meta[name].get_key(name): str(value)
179
+ for name, value in bound_args.items()
180
+ if isinstance(param_meta.get(name), Cookie) and value is not None
181
+ }
182
+
183
+ async def _get_request_data(
184
+ self,
185
+ method: str,
186
+ param_meta: Dict[str, Param],
187
+ bound_args: Dict[str, Any],
188
+ is_upload: bool,
189
+ method_headers: Dict[str, str]
190
+ ) -> Any:
191
+ """处理请求数据(支持 Pydantic 模型转字典)"""
192
+ if is_upload:
193
+ form_data = aiohttp.FormData()
194
+ # 处理文件
195
+ file_params = {
196
+ n: m for n, m in param_meta.items() if isinstance(m, File)}
197
+ for name, meta in file_params.items():
198
+ if name not in bound_args:
199
+ continue
200
+ file_paths = bound_args[name]
201
+ file_paths = [file_paths] if isinstance(
202
+ file_paths, str) else file_paths
203
+ for path in file_paths:
204
+ if not os.path.exists(path):
205
+ raise FileNotFoundError(f"文件不存在: {path}")
206
+ with open(path, "rb") as f:
207
+ form_data.add_field(
208
+ meta.field_name, f.read(), filename=os.path.basename(path)
209
+ )
210
+ # 处理表单字段(支持 Pydantic 模型)
211
+ form_params = {
212
+ n: m for n, m in param_meta.items() if isinstance(m, Form)}
213
+ for name, meta in form_params.items():
214
+ if name not in bound_args or bound_args[name] is None:
215
+ continue
216
+ value = bound_args[name]
217
+ # 若为 Pydantic 模型,转为字典
218
+ if isinstance(value, BaseModel):
219
+ value = value.dict()
220
+ form_data.add_field(meta.get_key(name), str(
221
+ value) if not isinstance(value, dict) else value)
222
+ return form_data
223
+
224
+ # 处理表单提交(x-www-form-urlencoded)
225
+ content_type = self.default_headers.get(
226
+ "Content-Type") or method_headers.get("Content-Type", "")
227
+ if "application/x-www-form-urlencoded" in content_type:
228
+ form_data = {}
229
+ for name, value in bound_args.items():
230
+ meta = param_meta.get(name)
231
+ if isinstance(meta, Form) and value is not None:
232
+ # Pydantic 模型转字典
233
+ if isinstance(value, BaseModel):
234
+ value = value.dict()
235
+ form_data[meta.get_key(name)] = str(
236
+ value) if not isinstance(value, dict) else value
237
+ return form_data
238
+
239
+ # 处理 JSON 请求体(支持 Pydantic 模型)
240
+ if method in ["POST", "PUT", "PATCH", "DELETE"]:
241
+ body_params = [
242
+ name for name, meta in param_meta.items() if isinstance(meta, Body)]
243
+ if body_params:
244
+ body_data = {}
245
+ for name in body_params:
246
+ meta = param_meta[name]
247
+ value = bound_args.get(name)
248
+ if value is None:
249
+ continue
250
+ # 若为 Pydantic 模型,转为字典
251
+ if isinstance(value, BaseModel):
252
+ value = value.dict()
253
+ if meta.embed:
254
+ body_data[meta.get_key(name)] = value
255
+ else:
256
+ body_data = value if not isinstance(value, dict) else {
257
+ ** body_data, **value}
258
+ return body_data
259
+ return None
260
+
261
+ async def _get_service_base_url(self, headers: Dict[str, str]) -> str:
262
+ version = headers.get("s-y-version")
263
+ instances = self.nacos_manager.get_service_instances(
264
+ self.service_name, target_version=version)
265
+ if not instances:
266
+ raise RuntimeError(f"服务 [{self.service_name}] 无可用实例")
267
+ return f"http://{instances[int(time.time()) % len(instances)]['ip']}:{instances[0]['port']}"
268
+
269
+ async def _handle_response(self, response: aiohttp.ClientResponse, resp_model: Optional[Type[BaseModel]]) -> Any:
270
+ """处理响应(支持 Pydantic 模型解析)"""
271
+ status = response.status
272
+ if 200 <= status < 300:
273
+ content_type = response.headers.get("Content-Type", "")
274
+ if "application/json" in content_type:
275
+ json_data = await response.json()
276
+ # 若指定了 Pydantic 响应模型,自动解析
277
+ if resp_model is not None:
278
+ return resp_model(** json_data) # 用响应数据初始化模型
279
+ return json_data
280
+ else:
281
+ return io.BytesIO(await response.read())
282
+ else:
283
+ error_msg = await response.text()
284
+ SYLogger.error(f"请求失败 [{status}]: {error_msg}")
285
+ raise RuntimeError(f"HTTP {status}: {error_msg}")
286
+
287
+ return FeignClient
288
+
289
+ return decorator
290
+
291
+
292
+ def feign_request(
293
+ method: Literal["GET", "POST", "PUT", "DELETE", "PATCH"],
294
+ path: str,
295
+ headers: Optional[Dict[str, str]] = None,
296
+ timeout: Optional[float] = None
297
+ ):
298
+ def decorator(func):
299
+ func._feign_meta = {
300
+ "method": method.upper(),
301
+ "path": path,
302
+ "headers": headers.copy() if headers else {},
303
+ "is_upload": False,
304
+ "timeout": timeout
305
+ }
306
+ return func
307
+ return decorator
308
+
309
+
310
+ def feign_upload(field_name: str = "file"):
311
+ def decorator(func):
312
+ if not hasattr(func, "_feign_meta"):
313
+ raise ValueError("feign_upload必须与feign_request一起使用")
314
+ func._feign_meta["is_upload"] = True
315
+ func._feign_meta["upload_field"] = field_name
316
+ return func
317
+ return decorator
@@ -0,0 +1,75 @@
1
+ from typing import Any, Optional
2
+
3
+ # ------------------------------
4
+ # 参数类型标记类(兼容 Pydantic)
5
+ # ------------------------------
6
+
7
+
8
+ class Param:
9
+ """基础参数元信息类"""
10
+
11
+ def __init__(
12
+ self,
13
+ default: Any = ...,
14
+ description: str = "",
15
+ alias: Optional[str] = None,
16
+ deprecated: bool = False
17
+ ):
18
+ self.default = default # ... 表示必填
19
+ self.description = description
20
+ self.alias = alias
21
+ self.deprecated = deprecated
22
+
23
+ def is_required(self) -> bool:
24
+ return self.default is ...
25
+
26
+ def get_key(self, param_name: str) -> str:
27
+ return self.alias if self.alias is not None else param_name
28
+
29
+
30
+ class Path(Param):
31
+ """路径参数"""
32
+ pass
33
+
34
+
35
+ class Query(Param):
36
+ """查询参数"""
37
+ pass
38
+
39
+
40
+ class Header(Param):
41
+ """请求头参数"""
42
+
43
+ def __init__(self, *args, convert_underscores: bool = True, **kwargs):
44
+ super().__init__(*args, **kwargs)
45
+ self.convert_underscores = convert_underscores
46
+
47
+ def get_key(self, param_name: str) -> str:
48
+ key = super().get_key(param_name)
49
+ return key.replace("_", "-") if self.convert_underscores else key
50
+
51
+
52
+ class Cookie(Param):
53
+ """Cookie参数"""
54
+ pass
55
+
56
+
57
+ class Body(Param):
58
+ """JSON请求体参数(支持Pydantic模型)"""
59
+
60
+ def __init__(self, *args, embed: bool = False, ** kwargs):
61
+ super().__init__(*args, **kwargs)
62
+ self.embed = embed # 是否包装在键中
63
+
64
+
65
+ class Form(Param):
66
+ """表单参数(支持Pydantic模型)"""
67
+ pass
68
+
69
+
70
+ class File(Param):
71
+ """文件上传参数"""
72
+
73
+ def __init__(self, *args, field_name: str = "file", ** kwargs):
74
+ super().__init__(*args, **kwargs)
75
+ self.field_name = field_name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sycommon-python-lib
3
- Version: 0.1.29
3
+ Version: 0.1.31
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  command/cli.py,sha256=bP2LCLkRvfETIwWkVD70q5xFxMI4D3BpH09Ws1f-ENc,5849
2
2
  sycommon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- sycommon/services.py,sha256=s9xbXiwehZeO4ZnS_8UKFY-LBkdGwbmsK-BB3pmFZuo,8766
3
+ sycommon/services.py,sha256=r631K2RWfQJL7ZP4eiipVEQ2DvRbOcSvf5rqc_ACV9o,9190
4
4
  sycommon/config/Config.py,sha256=9yO5b8WfvEDvkyrGrlwrLFasgh_-MjcEvGF20Gz5Xo4,3041
5
5
  sycommon/config/DatabaseConfig.py,sha256=ILiUuYT9_xJZE2W-RYuC3JCt_YLKc1sbH13-MHIOPhg,804
6
6
  sycommon/config/EmbeddingConfig.py,sha256=gPKwiDYbeu1GpdIZXMmgqM7JqBIzCXi0yYuGRLZooMI,362
@@ -31,25 +31,29 @@ sycommon/middleware/traceid.py,sha256=oGTJ2jtdea_3NgaAwXLpUug5dGUYRQeM4r1n2icuvC
31
31
  sycommon/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  sycommon/models/base_http.py,sha256=EICAAibx3xhjBsLqm35Mi3DCqxp0FME4rD_3iQVjT_E,3051
33
33
  sycommon/models/log.py,sha256=rZpj6VkDRxK3B6H7XSeWdYZshU8F0Sks8bq1p6pPlDw,500
34
- sycommon/models/mqlistener_config.py,sha256=PPwhAVJ2AWvVAvNox_1t0fuBKTyRH3Ui9cuuU-q7Byo,1590
34
+ sycommon/models/mqlistener_config.py,sha256=vXp2uMmd0XQ5B9noSRXWHewTy-juQ2y7IsWtISJD5aI,1661
35
35
  sycommon/models/mqmsg_model.py,sha256=cxn0M5b0utQK6crMYmL-1waeGYHvK3AlGaRy23clqTE,277
36
36
  sycommon/models/mqsend_config.py,sha256=NQX9dc8PpuquMG36GCVhJe8omAW1KVXXqr6lSRU6D7I,268
37
37
  sycommon/models/sso_user.py,sha256=i1WAN6k5sPcPApQEdtjpWDy7VrzWLpOrOQewGLGoGIw,2702
38
- sycommon/rabbitmq/rabbitmq_client.py,sha256=kiQBMwLJW1sx9llxHUMXHHKXY5SJdafIHyVrEOu6OO4,27259
38
+ sycommon/rabbitmq/rabbitmq_client.py,sha256=a91aWJ1ttVmMrfXGTBkoSuNgpsTHbQvuEjozswBp_Yk,27260
39
39
  sycommon/rabbitmq/rabbitmq_pool.py,sha256=_NMOO4CZy-I_anMqpzfYinz-8373_rg5FM9eqzdjGyU,3598
40
- sycommon/rabbitmq/rabbitmq_service.py,sha256=NWoMtRhvLjEsIX3sMgaSkrguE3gT5wLYj0u7laWZh8c,29997
40
+ sycommon/rabbitmq/rabbitmq_service.py,sha256=5a6F4RlzRYLbDm0qJSx9BMfvOIzwSQsiA7T-oAROZ5k,30172
41
41
  sycommon/sse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
42
  sycommon/sse/event.py,sha256=k_rBJy23R7crtzQeetT0Q73D8o5-5p-eESGSs_BPOj0,2797
43
43
  sycommon/sse/sse.py,sha256=__CfWEcYxOxQ-HpLor4LTZ5hLWqw9-2X7CngqbVHsfw,10128
44
44
  sycommon/synacos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- sycommon/synacos/feign.py,sha256=g2Mt3Iyxrk8doQsuIRNYMoHi-TP-k6FO5SDMWAbyqTo,24208
45
+ sycommon/synacos/example.py,sha256=61XL03tU8WTNOo3FUduf93F2fAwah1S0lbH1ufhRhRk,5739
46
+ sycommon/synacos/example2.py,sha256=adUaru3Hy482KrOA17DfaC4nwvLj8etIDS_KrWLWmCU,4811
47
+ sycommon/synacos/feign.py,sha256=i-e2TgzAho3HZosQnzNy8Xl-8Q8a4BWsGey1Y-Kr4_8,5518
48
+ sycommon/synacos/feign_client.py,sha256=JxzxohrsscQNlAjRVo_3ZQrMQSfVHFOtRYyEnP6sDGk,15205
46
49
  sycommon/synacos/nacos_service.py,sha256=SO1s83Y8A5jyQNFhk7ZZ_BrGQyGZ8TXBKtzRYxI-uDQ,34661
50
+ sycommon/synacos/param.py,sha256=KcfSkxnXOa0TGmCjY8hdzU9pzUsA8-4PeyBKWI2-568,1765
47
51
  sycommon/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
52
  sycommon/tools/docs.py,sha256=OPj2ETheuWjXLyaXtaZPbwmJKfJaYXV5s4XMVAUNrms,1607
49
53
  sycommon/tools/snowflake.py,sha256=DdEj3T5r5OEvikp3puxqmmmz6BrggxomoSlnsRFb5dM,1174
50
54
  sycommon/tools/timing.py,sha256=OiiE7P07lRoMzX9kzb8sZU9cDb0zNnqIlY5pWqHcnkY,2064
51
- sycommon_python_lib-0.1.29.dist-info/METADATA,sha256=iun0vsoi2Xdi4hRDNxwAldSoOhVsM8mEdqNIoQ0Vwg8,7037
52
- sycommon_python_lib-0.1.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- sycommon_python_lib-0.1.29.dist-info/entry_points.txt,sha256=q_h2nbvhhmdnsOUZEIwpuoDjaNfBF9XqppDEmQn9d_A,46
54
- sycommon_python_lib-0.1.29.dist-info/top_level.txt,sha256=98CJ-cyM2WIKxLz-Pf0AitWLhJyrfXvyY8slwjTXNuc,17
55
- sycommon_python_lib-0.1.29.dist-info/RECORD,,
55
+ sycommon_python_lib-0.1.31.dist-info/METADATA,sha256=egvprkvuPRH0lVt9bM2SwWS-5dvTA_TrHXYvvSah4QY,7037
56
+ sycommon_python_lib-0.1.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
+ sycommon_python_lib-0.1.31.dist-info/entry_points.txt,sha256=q_h2nbvhhmdnsOUZEIwpuoDjaNfBF9XqppDEmQn9d_A,46
58
+ sycommon_python_lib-0.1.31.dist-info/top_level.txt,sha256=98CJ-cyM2WIKxLz-Pf0AitWLhJyrfXvyY8slwjTXNuc,17
59
+ sycommon_python_lib-0.1.31.dist-info/RECORD,,