sycommon-python-lib 0.1.29__py3-none-any.whl → 0.1.30__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/models/mqlistener_config.py +1 -0
- sycommon/rabbitmq/rabbitmq_service.py +7 -4
- sycommon/services.py +6 -0
- sycommon/synacos/example.py +153 -0
- sycommon/synacos/example2.py +129 -0
- sycommon/synacos/feign.py +0 -426
- sycommon/synacos/feign_client.py +317 -0
- sycommon/synacos/param.py +75 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.30.dist-info}/METADATA +1 -1
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.30.dist-info}/RECORD +13 -9
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.30.dist-info}/WHEEL +0 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.30.dist-info}/entry_points.txt +0 -0
- {sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.30.dist-info}/top_level.txt +0 -0
|
@@ -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
|
"""模型配置"""
|
|
@@ -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
|
command/cli.py,sha256=bP2LCLkRvfETIwWkVD70q5xFxMI4D3BpH09Ws1f-ENc,5849
|
|
2
2
|
sycommon/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
sycommon/services.py,sha256=
|
|
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=
|
|
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
38
|
sycommon/rabbitmq/rabbitmq_client.py,sha256=kiQBMwLJW1sx9llxHUMXHHKXY5SJdafIHyVrEOu6OO4,27259
|
|
39
39
|
sycommon/rabbitmq/rabbitmq_pool.py,sha256=_NMOO4CZy-I_anMqpzfYinz-8373_rg5FM9eqzdjGyU,3598
|
|
40
|
-
sycommon/rabbitmq/rabbitmq_service.py,sha256=
|
|
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/
|
|
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.
|
|
52
|
-
sycommon_python_lib-0.1.
|
|
53
|
-
sycommon_python_lib-0.1.
|
|
54
|
-
sycommon_python_lib-0.1.
|
|
55
|
-
sycommon_python_lib-0.1.
|
|
55
|
+
sycommon_python_lib-0.1.30.dist-info/METADATA,sha256=tXTKj35fJRbr5aJ58cTsbxfe4P_TnVqpfXa_k8TqXeY,7037
|
|
56
|
+
sycommon_python_lib-0.1.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
57
|
+
sycommon_python_lib-0.1.30.dist-info/entry_points.txt,sha256=q_h2nbvhhmdnsOUZEIwpuoDjaNfBF9XqppDEmQn9d_A,46
|
|
58
|
+
sycommon_python_lib-0.1.30.dist-info/top_level.txt,sha256=98CJ-cyM2WIKxLz-Pf0AitWLhJyrfXvyY8slwjTXNuc,17
|
|
59
|
+
sycommon_python_lib-0.1.30.dist-info/RECORD,,
|
|
File without changes
|
{sycommon_python_lib-0.1.29.dist-info → sycommon_python_lib-0.1.30.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|