Skip to content

Commit 6475159

Browse files
authored
Merge pull request #137 from dashscope/fix/aiohttp-call
Fix/aiohttp call
2 parents c155982 + 93a0d28 commit 6475159

23 files changed

Lines changed: 647 additions & 377 deletions

README.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
<h4 align="center">
2-
<p>
3-
<b>English</b>
4-
<p>
5-
</h4>
6-
7-
8-
</div>
9-
101
# DashScope Python SDK
112

123
The DashScope Python SDK provides a comprehensive interface to [Alibaba Cloud Model Studio (Bailian)](https://www.alibabacloud.com/help/en/model-studio/) APIs, covering text generation, multi-modal understanding, embeddings, reranking, image/video generation, speech synthesis & recognition, and more.

dashscope/aigc/image_synthesis.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,16 +261,16 @@ def _get_input( # pylint: disable=too-many-branches
261261
kwargs["headers"] = headers
262262

263263
def __get_i2i_task(task, model) -> str:
264-
# 处理task参数:优先使用有效的task值
264+
# Handle task parameter: prefer valid task value
265265
if task is not None and task != "":
266266
return task
267267

268-
# 根据model确定任务类型
268+
# Determine task type based on model
269269
if model is not None and model != "":
270270
if "imageedit" in model or "wan2.5-i2i" in model:
271271
return "image2image"
272272

273-
# 默认返回文本到图像任务
273+
# Default to text-to-image task
274274
return ImageSynthesis.task
275275

276276
task = __get_i2i_task(task, model)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) Alibaba, Inc. and its affiliates.
3+
"""Shared aiohttp session pool with cached SSL context.
4+
5+
Provides connection reuse across async API calls. Each event loop gets
6+
its own ClientSession (aiohttp sessions are loop-bound). The SSL context
7+
is created once and shared across all sessions.
8+
"""
9+
import asyncio
10+
import ssl
11+
import weakref
12+
from typing import Optional
13+
14+
import aiohttp
15+
import certifi
16+
17+
_shared_ssl_context: Optional[ssl.SSLContext] = None
18+
_aio_sessions: "weakref.WeakKeyDictionary" = weakref.WeakKeyDictionary()
19+
20+
21+
def get_ssl_context() -> ssl.SSLContext:
22+
global _shared_ssl_context
23+
if _shared_ssl_context is None:
24+
_shared_ssl_context = ssl.create_default_context(
25+
cafile=certifi.where(),
26+
)
27+
return _shared_ssl_context
28+
29+
30+
async def get_shared_aio_session() -> aiohttp.ClientSession:
31+
"""Return a shared aiohttp.ClientSession bound to the running event loop.
32+
33+
The session is lazily created on first use and reused for all
34+
subsequent calls on the same event loop. Connection pooling (keep-alive)
35+
is handled by the underlying TCPConnector.
36+
"""
37+
loop = asyncio.get_running_loop()
38+
39+
session = _aio_sessions.get(loop)
40+
if session is not None and not session.closed:
41+
return session
42+
43+
connector = aiohttp.TCPConnector(ssl=get_ssl_context())
44+
session = aiohttp.ClientSession(connector=connector)
45+
_aio_sessions[loop] = session
46+
return session
47+
48+
49+
async def close_shared_aio_session() -> None:
50+
"""Close the shared session for the current event loop."""
51+
loop = asyncio.get_running_loop()
52+
session = _aio_sessions.pop(loop, None)
53+
if session is not None and not session.closed:
54+
await session.close()

dashscope/api_entities/aiohttp_request.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import aiohttp
88

9+
from dashscope.api_entities.aio_session import get_shared_aio_session
910
from dashscope.api_entities.base_request import AioBaseRequest
1011
from dashscope.api_entities.dashscope_response import DashScopeAPIResponse
1112
from dashscope.common.constants import (
@@ -38,7 +39,9 @@ def __init__(
3839
api_key (str): The api key.
3940
method (str): The http method(GET|POST).
4041
stream (bool, optional): Is stream request. Defaults to True.
41-
timeout (int, optional): Total request timeout.
42+
timeout (int, optional): Request timeout in seconds. For streaming
43+
requests, this is the idle timeout between chunks (sock_read);
44+
for non-streaming requests, this is the total request timeout.
4245
Defaults to DEFAULT_REQUEST_TIMEOUT_SECONDS.
4346
user_agent (str, optional): Additional user agent string to
4447
append. Defaults to ''.
@@ -246,41 +249,49 @@ async def _handle_response( # pylint: disable=too-many-branches
246249

247250
async def _handle_request(self):
248251
try:
249-
async with aiohttp.ClientSession(
250-
timeout=aiohttp.ClientTimeout(total=self.timeout),
251-
headers=self.headers,
252-
) as session:
253-
logger.debug("Starting request: %s", self.url)
254-
if self.method == HTTPMethod.POST:
255-
is_form, obj = self.data.get_aiohttp_payload()
256-
if is_form:
257-
headers = {**self.headers, **obj.headers}
258-
response = await session.post(
259-
url=self.url,
260-
data=obj,
261-
headers=headers,
262-
)
263-
else:
264-
response = await session.request(
265-
"POST",
266-
url=self.url,
267-
json=obj,
268-
headers=self.headers,
269-
)
270-
elif self.method == HTTPMethod.GET:
271-
response = await session.get(
252+
session = await get_shared_aio_session()
253+
if self.stream:
254+
request_timeout = aiohttp.ClientTimeout(
255+
total=None,
256+
sock_read=self.timeout,
257+
)
258+
else:
259+
request_timeout = aiohttp.ClientTimeout(total=self.timeout)
260+
261+
logger.debug("Starting request: %s", self.url)
262+
if self.method == HTTPMethod.POST:
263+
is_form, obj = self.data.get_aiohttp_payload()
264+
if is_form:
265+
headers = {**self.headers, **obj.headers}
266+
response = await session.post(
272267
url=self.url,
273-
params=self.data.parameters,
274-
headers=self.headers,
268+
data=obj,
269+
headers=headers,
270+
timeout=request_timeout,
275271
)
276272
else:
277-
raise UnsupportedHTTPMethod(
278-
f"Unsupported http method: {self.method}",
273+
response = await session.request(
274+
"POST",
275+
url=self.url,
276+
json=obj,
277+
headers=self.headers,
278+
timeout=request_timeout,
279279
)
280-
logger.debug("Response returned: %s", self.url)
281-
async with response:
282-
async for rsp in self._handle_response(response):
283-
yield rsp
280+
elif self.method == HTTPMethod.GET:
281+
response = await session.get(
282+
url=self.url,
283+
params=self.data.parameters,
284+
headers=self.headers,
285+
timeout=request_timeout,
286+
)
287+
else:
288+
raise UnsupportedHTTPMethod(
289+
f"Unsupported http method: {self.method}",
290+
)
291+
logger.debug("Response returned: %s", self.url)
292+
async with response:
293+
async for rsp in self._handle_response(response):
294+
yield rsp
284295
except aiohttp.ClientConnectorError as e:
285296
logger.error(e)
286297
raise e

dashscope/api_entities/encryption.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,77 +115,77 @@ def _generate_iv():
115115

116116
@staticmethod
117117
def _encrypt_text_with_aes(plaintext, key, iv):
118-
"""使用AES-GCM加密数据"""
118+
"""Encrypt data with AES-GCM"""
119119

120-
# 创建AES-GCM加密器
120+
# Create AES-GCM encryptor
121121
aes_gcm = Cipher(
122122
algorithms.AES(key),
123123
modes.GCM(iv, tag=None),
124124
backend=default_backend(),
125125
).encryptor()
126126

127-
# 关联数据设为空(根据需求可调整)
127+
# Set associated data to empty (adjustable as needed)
128128
aes_gcm.authenticate_additional_data(b"")
129129

130-
# 加密数据
130+
# Encrypt data
131131
ciphertext = (
132132
aes_gcm.update(plaintext.encode("utf-8")) + aes_gcm.finalize()
133133
)
134134

135-
# 获取认证标签
135+
# Get authentication tag
136136
tag = aes_gcm.tag
137137

138-
# 组合密文和标签
138+
# Combine ciphertext and tag
139139
encrypted_data = ciphertext + tag
140140

141-
# 返回Base64编码结果
141+
# Return Base64 encoded result
142142
return base64.b64encode(encrypted_data).decode("utf-8")
143143

144144
@staticmethod
145145
def _decrypt_text_with_aes(base64_ciphertext, aes_key, iv):
146-
"""使用AES-GCM解密响应"""
146+
"""Decrypt response with AES-GCM"""
147147

148-
# 解码Base64数据
148+
# Decode Base64 data
149149
encrypted_data = base64.b64decode(base64_ciphertext)
150150

151-
# 分离密文和标签(标签长度16字节)
151+
# Separate ciphertext and tag (tag length is 16 bytes)
152152
ciphertext = encrypted_data[:-16]
153153
tag = encrypted_data[-16:]
154154

155-
# 创建AES-GCM解密器
155+
# Create AES-GCM decryptor
156156
aes_gcm = Cipher(
157157
algorithms.AES(aes_key),
158158
modes.GCM(iv, tag),
159159
backend=default_backend(),
160160
).decryptor()
161161

162-
# 验证关联数据(与加密时一致)
162+
# Verify associated data (same as during encryption)
163163
aes_gcm.authenticate_additional_data(b"")
164164

165-
# 解密数据
165+
# Decrypt data
166166
decrypted_bytes = aes_gcm.update(ciphertext) + aes_gcm.finalize()
167167

168-
# 明文
168+
# Plaintext
169169
plaintext = decrypted_bytes.decode("utf-8")
170170

171171
return json.loads(plaintext)
172172

173173
@staticmethod
174174
def _encrypt_aes_key_with_rsa(aes_key, public_key_str):
175-
"""使用RSA公钥加密AES密钥"""
175+
"""Encrypt AES key with RSA public key"""
176176

177-
# 解码Base64格式的公钥
177+
# Decode Base64 formatted public key
178178
public_key_bytes = base64.b64decode(public_key_str)
179179

180-
# 加载公钥
180+
# Load public key
181181
public_key = serialization.load_der_public_key(
182182
public_key_bytes,
183183
backend=default_backend(),
184184
)
185185

186186
base64_aes_key = base64.b64encode(aes_key).decode("utf-8")
187187

188-
# 使用RSA加密
188+
# Encrypt with RSA
189189
encrypted_bytes = public_key.encrypt(
190190
base64_aes_key.encode("utf-8"),
191191
padding.PKCS1v15(),

0 commit comments

Comments
 (0)