382 lines
13 KiB
Python

import asyncio
import logging
import os
import ssl
import time
from enum import Enum
from typing import Any, Dict, Optional
from urllib.parse import urlencode, urlparse
import aiohttp
import certifi
from requests import Session, PreparedRequest, adapters, status_codes
from Tea.exceptions import RequiredArgumentException, RetryError
from Tea.model import TeaModel
from Tea.request import TeaRequest
from Tea.response import TeaResponse
from Tea.stream import BaseStream
DEFAULT_CONNECT_TIMEOUT = 5000
DEFAULT_READ_TIMEOUT = 10000
DEFAULT_POOL_SIZE = 10
logger = logging.getLogger('alibabacloud-tea')
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
logger.addHandler(ch)
class TLSVersion(Enum):
TLSv1 = 'TLSv1'
TLSv1_1 = 'TLSv1.1'
TLSv1_2 = 'TLSv1.2'
TLSv1_3 = 'TLSv1.3'
class _TLSAdapter(adapters.HTTPAdapter):
"""A HTTPAdapter that uses an arbitrary TLS version."""
def __init__(self, ssl_context=None, **kwargs):
self.ssl_context = ssl_context
super().__init__(**kwargs)
def init_poolmanager(self, *args, **kwargs):
"""Override the init_poolmanager method to set the SSL."""
kwargs['ssl_context'] = self.ssl_context
super().init_poolmanager(*args, **kwargs)
class TeaCore:
_sessions = {}
http_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
https_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
@staticmethod
def _set_tls_minimum_version(sls_context, tls_min_version):
context = sls_context
if tls_min_version is not None:
if tls_min_version == 'TLSv1':
context.minimum_version = ssl.TLSVersion.TLSv1
elif tls_min_version == 'TLSv1.1':
context.minimum_version = ssl.TLSVersion.TLSv1_1
elif tls_min_version == 'TLSv1.2':
context.minimum_version = ssl.TLSVersion.TLSv1_2
elif tls_min_version == 'TLSv1.3':
context.minimum_version = ssl.TLSVersion.TLSv1_3
return context
@staticmethod
def get_adapter(prefix, tls_min_version: str = None):
ca_cert = certifi.where()
context = ssl.create_default_context()
if ca_cert and prefix.upper() == 'HTTPS':
context = TeaCore._set_tls_minimum_version(context, tls_min_version)
context.load_verify_locations(ca_cert)
adapter = _TLSAdapter(ssl_context=context, pool_connections=DEFAULT_POOL_SIZE,
pool_maxsize=DEFAULT_POOL_SIZE * 4)
return adapter
@staticmethod
def _get_session(session_key: str, protocol: str, tls_min_version: str = None, verify: bool = True):
if session_key not in TeaCore._sessions:
session = Session()
adapter = TeaCore.get_adapter(protocol, tls_min_version)
if protocol.upper() == 'HTTPS':
if verify:
session.mount('https://', adapter)
else:
session.mount('https://', TeaCore.https_adapter)
else:
session.mount('http://', adapter)
TeaCore._sessions[session_key] = session
return TeaCore._sessions[session_key]
@staticmethod
def _prepare_http_debug(request, symbol):
base = ''
for key, value in request.headers.items():
base += f'\n{symbol} {key} : {value}'
return base
@staticmethod
def _do_http_debug(request, response):
# logger the request
url = urlparse(request.url)
request_base = f'\n> {request.method.upper()} {url.path + url.query} HTTP/1.1'
logger.debug(request_base + TeaCore._prepare_http_debug(request, '>'))
# logger the response
response_base = f'\n< HTTP/1.1 {response.status_code}' \
f' {status_codes._codes.get(response.status_code)[0].upper()}'
logger.debug(response_base + TeaCore._prepare_http_debug(response, '<'))
@staticmethod
def compose_url(request):
host = request.headers.get('host')
if not host:
raise RequiredArgumentException('endpoint')
else:
host = host.rstrip('/')
protocol = f'{request.protocol.lower()}://'
pathname = request.pathname
if host.startswith(('http://', 'https://')):
protocol = ''
if request.port == 80:
port = ''
else:
port = f':{request.port}'
url = protocol + host + port + pathname
if request.query:
if "?" in url:
if not url.endswith("&"):
url += "&"
else:
url += "?"
encode_query = {}
for key in request.query:
value = request.query[key]
if value is not None:
encode_query[key] = str(value)
url += urlencode(encode_query)
return url.rstrip("?&")
@staticmethod
async def async_do_action(
request: TeaRequest,
runtime_option=None
) -> TeaResponse:
runtime_option = runtime_option or {}
url = TeaCore.compose_url(request)
verify = not runtime_option.get('ignoreSSL', False)
tls_min_version = runtime_option.get('tlsMinVersion')
if isinstance(tls_min_version, Enum):
tls_min_version = tls_min_version.value
timeout = runtime_option.get('timeout')
connect_timeout = runtime_option.get('connectTimeout') or timeout or DEFAULT_CONNECT_TIMEOUT
read_timeout = runtime_option.get('readTimeout') or timeout or DEFAULT_READ_TIMEOUT
connect_timeout, read_timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
proxy = None
if request.protocol.upper() == 'HTTP':
proxy = runtime_option.get('httpProxy')
if not proxy:
proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
elif request.protocol.upper() == 'HTTPS':
proxy = runtime_option.get('httpsProxy')
if not proxy:
proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
connector = None
ca_cert = certifi.where()
if ca_cert and request.protocol.upper() == 'HTTPS':
ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
ssl_context = TeaCore._set_tls_minimum_version(ssl_context, tls_min_version)
ssl_context.load_verify_locations(ca_cert)
connector = aiohttp.TCPConnector(
ssl=ssl_context,
)
else:
verify = False
timeout = aiohttp.ClientTimeout(
sock_read=read_timeout,
sock_connect=connect_timeout
)
async with aiohttp.ClientSession(
connector=connector
) as s:
body = b''
if isinstance(request.body, BaseStream):
for content in request.body:
body += content
elif isinstance(request.body, str):
body = request.body.encode('utf-8')
else:
body = request.body
try:
async with s.request(request.method, url,
data=body,
headers=request.headers,
ssl=verify,
proxy=proxy,
timeout=timeout) as response:
tea_resp = TeaResponse()
tea_resp.body = await response.read()
tea_resp.headers = {k.lower(): v for k, v in response.headers.items()}
tea_resp.status_code = response.status
tea_resp.status_message = response.reason
tea_resp.response = response
except IOError as e:
raise RetryError(str(e))
return tea_resp
@staticmethod
def do_action(
request: TeaRequest,
runtime_option=None
) -> TeaResponse:
url = TeaCore.compose_url(request)
runtime_option = runtime_option or {}
verify = not runtime_option.get('ignoreSSL', False)
tls_min_version = runtime_option.get('tlsMinVersion')
if isinstance(tls_min_version, Enum):
tls_min_version = tls_min_version.value
if verify:
verify = runtime_option.get('ca', True) if runtime_option.get('ca', True) is not None else True
cert = runtime_option.get('cert', None)
timeout = runtime_option.get('timeout')
connect_timeout = runtime_option.get('connectTimeout') or timeout or DEFAULT_CONNECT_TIMEOUT
read_timeout = runtime_option.get('readTimeout') or timeout or DEFAULT_READ_TIMEOUT
timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
if isinstance(request.body, str):
request.body = request.body.encode('utf-8')
p = PreparedRequest()
p.prepare(
method=request.method.upper(),
url=url,
data=request.body,
headers=request.headers,
)
proxies = {}
http_proxy = runtime_option.get('httpProxy')
https_proxy = runtime_option.get('httpsProxy')
no_proxy = runtime_option.get('noProxy')
if not http_proxy:
http_proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
if not https_proxy:
https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
if http_proxy:
proxies['http'] = http_proxy
if https_proxy:
proxies['https'] = https_proxy
if no_proxy:
proxies['no_proxy'] = no_proxy
host = request.headers.get('host')
host = host.rstrip('/')
session_key = f'{request.protocol.lower()}://{host}:{request.port}'
session = TeaCore._get_session(session_key=session_key, protocol=request.protocol,
tls_min_version=tls_min_version, verify=verify)
try:
resp = session.send(
p,
proxies=proxies,
timeout=timeout,
verify=verify,
cert=cert,
)
except IOError as e:
raise RetryError(str(e))
debug = runtime_option.get('debug') or os.getenv('DEBUG')
if debug and debug.lower() == 'sdk':
TeaCore._do_http_debug(p, resp)
response = TeaResponse()
response.status_message = resp.reason
response.status_code = resp.status_code
response.headers = {k.lower(): v for k, v in resp.headers.items()}
response.body = resp.content
response.response = resp
return response
@staticmethod
def get_response_body(resp) -> str:
return resp.content.decode("utf-8")
@staticmethod
def allow_retry(dic, retry_times, now=None) -> bool:
if retry_times == 0:
return True
if dic is None or not dic.__contains__("maxAttempts") or \
dic.get('retryable') is not True and retry_times >= 1:
return False
else:
retry = 0 if dic.get("maxAttempts") is None else int(
dic.get("maxAttempts"))
return retry >= retry_times
@staticmethod
def get_backoff_time(dic, retry_times) -> int:
default_back_off_time = 0
if dic is None or not dic.get("policy") or dic.get("policy") == "no":
return default_back_off_time
back_off_time = dic.get('period', default_back_off_time)
if not isinstance(back_off_time, int) and \
not (isinstance(back_off_time, str) and back_off_time.isdigit()):
return default_back_off_time
back_off_time = int(back_off_time)
if back_off_time < 0:
return retry_times
return back_off_time
@staticmethod
async def sleep_async(t):
await asyncio.sleep(t)
@staticmethod
def sleep(t):
time.sleep(t)
@staticmethod
def is_retryable(ex) -> bool:
return isinstance(ex, RetryError)
@staticmethod
def bytes_readable(body):
return body
@staticmethod
def merge(*dic_list) -> dict:
dic_result = {}
for item in dic_list:
if isinstance(item, dict):
dic_result.update(item)
elif isinstance(item, TeaModel):
dic_result.update(item.to_map())
return dic_result
@staticmethod
def to_map(model: Optional[TeaModel]) -> Dict[str, Any]:
if isinstance(model, TeaModel):
return model.to_map()
else:
return dict()
@staticmethod
def from_map(
model: TeaModel,
dic: Dict[str, Any]
) -> TeaModel:
if isinstance(model, TeaModel):
try:
return model.from_map(dic)
except Exception:
model._map = dic
return model
else:
return model