You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

152 lines
5.6KB

  1. # -*- coding: utf-8 -*-
  2. """
  3. oss2.http
  4. ~~~~~~~~
  5. 这个模块包含了HTTP Adapters。尽管OSS Python SDK内部使用requests库进行HTTP通信,但是对使用者是透明的。
  6. 该模块中的 `Session` 、 `Request` 、`Response` 对requests的对应的类做了简单的封装。
  7. """
  8. import platform
  9. import requests
  10. from loguru import logger
  11. from requests.structures import CaseInsensitiveDict
  12. from . import __version__, defaults
  13. from .compat import to_bytes
  14. from .exceptions import RequestError
  15. from .utils import file_object_remaining_bytes, SizedFileAdapter
  16. USER_AGENT = 'aliyun-sdk-python/{0}({1}/{2}/{3};{4})'.format(
  17. __version__, platform.system(), platform.release(), platform.machine(), platform.python_version())
  18. class Session(object):
  19. """属于同一个Session的请求共享一组连接池,如有可能也会重用HTTP连接。"""
  20. def __init__(self, pool_size=None):
  21. self.session = requests.Session()
  22. psize = pool_size or defaults.connection_pool_size
  23. self.session.mount('http://', requests.adapters.HTTPAdapter(pool_connections=psize, pool_maxsize=psize))
  24. self.session.mount('https://', requests.adapters.HTTPAdapter(pool_connections=psize, pool_maxsize=psize))
  25. def do_request(self, req, timeout):
  26. try:
  27. logger.debug("Send request, method: {0}, url: {1}, params: {2}, headers: {3}, timeout: {4}, proxies: {5}".format(
  28. req.method, req.url, req.params, req.headers, timeout, req.proxies))
  29. return Response(self.session.request(req.method, req.url,
  30. data=req.data,
  31. params=req.params,
  32. headers=req.headers,
  33. stream=True,
  34. timeout=timeout,
  35. proxies=req.proxies))
  36. except requests.RequestException as e:
  37. raise RequestError(e)
  38. class Request(object):
  39. def __init__(self, method, url,
  40. data=None,
  41. params=None,
  42. headers=None,
  43. app_name='',
  44. proxies=None,
  45. region=None,
  46. product=None,
  47. cloudbox_id=None):
  48. self.method = method
  49. self.url = url
  50. self.data = _convert_request_body(data)
  51. self.params = params or {}
  52. self.proxies = proxies
  53. self.region = region
  54. self.product = product
  55. self.cloudbox_id = cloudbox_id
  56. if not isinstance(headers, CaseInsensitiveDict):
  57. self.headers = CaseInsensitiveDict(headers)
  58. else:
  59. self.headers = headers
  60. # tell requests not to add 'Accept-Encoding: gzip, deflate' by default
  61. if 'Accept-Encoding' not in self.headers:
  62. self.headers['Accept-Encoding'] = None
  63. if 'User-Agent' not in self.headers:
  64. if app_name:
  65. self.headers['User-Agent'] = USER_AGENT + '/' + app_name
  66. else:
  67. self.headers['User-Agent'] = USER_AGENT
  68. logger.debug("Init request, method: {0}, url: {1}, params: {2}, headers: {3}".format(method, url, params,
  69. headers))
  70. _CHUNK_SIZE = 8 * 1024
  71. class Response(object):
  72. def __init__(self, response):
  73. self.response = response
  74. self.status = response.status_code
  75. self.headers = response.headers
  76. self.request_id = response.headers.get('x-oss-request-id', '')
  77. # When a response contains no body, iter_content() cannot
  78. # be run twice (requests.exceptions.StreamConsumedError will be raised).
  79. # For details of the issue, please see issue #82
  80. #
  81. # To work around this issue, we simply return b'' when everything has been read.
  82. #
  83. # Note you cannot use self.response.raw.read() to implement self.read(), because
  84. # raw.read() does not uncompress response body when the encoding is gzip etc., and
  85. # we try to avoid depends on details of self.response.raw.
  86. self.__all_read = False
  87. logger.debug("Get response headers, req-id:{0}, status: {1}, headers: {2}".format(self.request_id, self.status,
  88. self.headers))
  89. def read(self, amt=None):
  90. if self.__all_read:
  91. return b''
  92. if amt is None:
  93. content_list = []
  94. for chunk in self.response.iter_content(_CHUNK_SIZE):
  95. content_list.append(chunk)
  96. content = b''.join(content_list)
  97. self.__all_read = True
  98. return content
  99. else:
  100. try:
  101. return next(self.response.iter_content(amt))
  102. except StopIteration:
  103. self.__all_read = True
  104. return b''
  105. def __iter__(self):
  106. return self.response.iter_content(_CHUNK_SIZE)
  107. # requests对于具有fileno()方法的file object,会用fileno()的返回值作为Content-Length。
  108. # 这对于已经读取了部分内容,或执行了seek()的file object是不正确的。
  109. #
  110. # _convert_request_body()对于支持seek()和tell() file object,确保是从
  111. # 当前位置读取,且只读取当前位置到文件结束的内容。
  112. def _convert_request_body(data):
  113. data = to_bytes(data)
  114. if hasattr(data, '__len__'):
  115. return data
  116. if hasattr(data, 'seek') and hasattr(data, 'tell'):
  117. return SizedFileAdapter(data, file_object_remaining_bytes(data))
  118. return data