tencentcloud-sdk-python-common 3.0.1450__tar.gz → 3.1.21__tar.gz

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 tencentcloud-sdk-python-common might be problematic. Click here for more details.

Files changed (33) hide show
  1. tencentcloud_sdk_python_common-3.1.21/PKG-INFO +48 -0
  2. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/setup.py +1 -0
  3. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/__init__.py +1 -1
  4. tencentcloud_sdk_python_common-3.1.21/tencentcloud/common/abstract_client_async.py +654 -0
  5. tencentcloud_sdk_python_common-3.1.21/tencentcloud/common/common_client_async.py +45 -0
  6. tencentcloud_sdk_python_common-3.1.21/tencentcloud/common/http/request_async.py +62 -0
  7. tencentcloud_sdk_python_common-3.1.21/tencentcloud/common/retry_async.py +87 -0
  8. tencentcloud_sdk_python_common-3.1.21/tencentcloud_sdk_python_common.egg-info/PKG-INFO +48 -0
  9. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud_sdk_python_common.egg-info/SOURCES.txt +4 -0
  10. tencentcloud_sdk_python_common-3.1.21/tencentcloud_sdk_python_common.egg-info/requires.txt +4 -0
  11. tencentcloud-sdk-python-common-3.0.1450/PKG-INFO +0 -45
  12. tencentcloud-sdk-python-common-3.0.1450/tencentcloud_sdk_python_common.egg-info/PKG-INFO +0 -45
  13. tencentcloud-sdk-python-common-3.0.1450/tencentcloud_sdk_python_common.egg-info/requires.txt +0 -1
  14. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/README.rst +0 -0
  15. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/setup.cfg +0 -0
  16. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/__init__.py +0 -0
  17. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/abstract_client.py +0 -0
  18. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/abstract_model.py +0 -0
  19. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/circuit_breaker.py +0 -0
  20. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/common_client.py +0 -0
  21. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/credential.py +0 -0
  22. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/exception/__init__.py +0 -0
  23. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/exception/tencent_cloud_sdk_exception.py +0 -0
  24. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/http/__init__.py +0 -0
  25. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/http/pre_conn.py +0 -0
  26. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/http/request.py +0 -0
  27. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/profile/__init__.py +0 -0
  28. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/profile/client_profile.py +0 -0
  29. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/profile/http_profile.py +0 -0
  30. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/retry.py +0 -0
  31. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud/common/sign.py +0 -0
  32. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud_sdk_python_common.egg-info/dependency_links.txt +0 -0
  33. {tencentcloud-sdk-python-common-3.0.1450 → tencentcloud_sdk_python_common-3.1.21}/tencentcloud_sdk_python_common.egg-info/top_level.txt +0 -0
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.1
2
+ Name: tencentcloud-sdk-python-common
3
+ Version: 3.1.21
4
+ Summary: Tencent Cloud Common SDK for Python
5
+ Home-page: https://github.com/TencentCloud/tencentcloud-sdk-python
6
+ Author: Tencent Cloud
7
+ Maintainer-email: tencentcloudapi@tencent.com
8
+ License: Apache License 2.0
9
+ Platform: any
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 2.7
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.6
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Requires-Dist: requests>=2.16.0
19
+ Provides-Extra: async
20
+ Requires-Dist: httpx>=0.22.0; extra == "async"
21
+
22
+ ============================
23
+ Tencent Cloud SDK for Python
24
+ ============================
25
+
26
+ Tencent Cloud Python Common SDK is the official software development kit, which allows Python developers to write software that makes use of Tencent Cloud services like CVM and CBS.
27
+ The SDK works on Python versions:
28
+
29
+ * 2.7 and greater, including 3.x
30
+
31
+ Quick Start
32
+ -----------
33
+
34
+ First, install the library:
35
+
36
+ .. code-block:: sh
37
+
38
+ $ pip install tencentcloud-sdk-python-common
39
+ $ pip install tencentcloud-sdk-python-common
40
+
41
+ or download source code from github and install:
42
+
43
+ .. code-block:: sh
44
+
45
+ $ git clone https://github.com/tencentcloud/tencentcloud-sdk-python.git
46
+ $ cd tencentcloud-sdk-python
47
+ $ python package.py --components common common
48
+
@@ -9,6 +9,7 @@ ROOT = os.path.dirname(__file__)
9
9
  setup(
10
10
  name='tencentcloud-sdk-python-common',
11
11
  install_requires=["requests>=2.16.0"],
12
+ extras_require={"async": ["httpx>=0.22.0"]},
12
13
  version=tencentcloud.__version__,
13
14
  description='Tencent Cloud Common SDK for Python',
14
15
  long_description=open('README.rst').read(),
@@ -14,4 +14,4 @@
14
14
  # limitations under the License.
15
15
 
16
16
 
17
- __version__ = '3.0.1450'
17
+ __version__ = '3.1.21'
@@ -0,0 +1,654 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 2017-2021 Tencent Ltd.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ import copy
17
+ import hashlib
18
+ import json
19
+ import logging
20
+ import logging.handlers
21
+ import random
22
+ import sys
23
+ import time
24
+ import uuid
25
+ import warnings
26
+ from datetime import datetime
27
+ from typing import Dict, Type, Union, List, Callable, Awaitable, Optional
28
+
29
+ import httpx
30
+
31
+ import tencentcloud
32
+ from tencentcloud.common.abstract_client import logger, urlparse, urlencode
33
+ from tencentcloud.common.abstract_model import AbstractModel
34
+ from tencentcloud.common.circuit_breaker import CircuitBreaker
35
+ from tencentcloud.common.credential import Credential
36
+ from tencentcloud.common.exception import TencentCloudSDKException
37
+ from tencentcloud.common.http.request_async import ApiRequest, ApiResponse, ResponsePrettyFormatter, \
38
+ RequestPrettyFormatter
39
+ from tencentcloud.common.profile.client_profile import ClientProfile, RegionBreakerProfile
40
+ from tencentcloud.common.retry_async import NoopRetryer
41
+ from tencentcloud.common.sign import Sign
42
+
43
+ warnings.filterwarnings("ignore", module="tencentcloud", category=UserWarning)
44
+
45
+ _json_content = 'application/json'
46
+ _multipart_content = 'multipart/form-data'
47
+ _form_urlencoded_content = 'application/x-www-form-urlencoded'
48
+ _octet_stream = "application/octet-stream"
49
+
50
+ LOGGER_NAME = "tencentcloud_sdk_common"
51
+
52
+ InterceptorType = Callable[["RequestChain"], Awaitable]
53
+ ParamsType = Union[Dict, str, bytes]
54
+
55
+
56
+ class RequestChain(object):
57
+ def __init__(self):
58
+ self.request: Optional[ApiRequest] = None
59
+
60
+ self._inters: List[InterceptorType] = []
61
+ self._idx = 0
62
+
63
+ async def proceed(self):
64
+ interceptor = self._inters[self._idx]
65
+ snapshot = self._new_snapshot()
66
+ snapshot._idx += 1
67
+ return await interceptor(snapshot)
68
+
69
+ def add_interceptor(self, inter: InterceptorType, idx=sys.maxsize):
70
+ self._inters.insert(idx, inter)
71
+ return self
72
+
73
+ def _new_snapshot(self) -> 'RequestChain':
74
+ new_chain = RequestChain()
75
+ new_chain.request = self.request
76
+ new_chain._inters = self._inters
77
+ new_chain._idx = self._idx
78
+ return new_chain
79
+
80
+
81
+ class AbstractClient(object):
82
+ _requestPath = '/'
83
+ _params = {}
84
+ _apiVersion = ''
85
+ _endpoint = ''
86
+ _service = ''
87
+ _sdkVersion = 'SDK_PYTHON_%s' % tencentcloud.__version__
88
+ _default_content_type = _form_urlencoded_content
89
+ FMT = '%(asctime)s %(process)d %(filename)s L%(lineno)s %(levelname)s %(message)s'
90
+
91
+ def __init__(self, credential: Credential, region: str, profile: ClientProfile = None):
92
+ self.credential = credential
93
+ self.region = region
94
+ self.profile = profile or ClientProfile()
95
+ self.circuit_breaker = None
96
+
97
+ kwargs: Dict = {"timeout": self.profile.httpProfile.reqTimeout}
98
+
99
+ if not self.profile.httpProfile.keepAlive:
100
+ kwargs["limits"] = httpx.Limits(max_keepalive_connections=0)
101
+
102
+ if self.profile.httpProfile.proxy:
103
+ kwargs["proxies"] = self.profile.httpProfile.proxy
104
+
105
+ if self.profile.httpProfile.certification is False:
106
+ kwargs["verify"] = False
107
+ elif isinstance(self.profile.httpProfile.certification, str) and self.profile.httpProfile.certification != "":
108
+ kwargs["cert"] = self.profile.httpProfile.certification
109
+
110
+ self.http_client = httpx.AsyncClient(**kwargs)
111
+
112
+ if not self.profile.disable_region_breaker:
113
+ if self.profile.region_breaker_profile is None:
114
+ self.profile.region_breaker_profile = RegionBreakerProfile()
115
+ self.circuit_breaker = CircuitBreaker(self.profile.region_breaker_profile)
116
+ if self.profile.request_client:
117
+ self.request_client = self._sdkVersion + "; " + self.profile.request_client
118
+ else:
119
+ self.request_client = self._sdkVersion
120
+
121
+ async def __aenter__(self):
122
+ return self
123
+
124
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
125
+ await self.close()
126
+
127
+ async def close(self):
128
+ await self.http_client.aclose()
129
+
130
+ async def call_and_deserialize(
131
+ self,
132
+ action: str,
133
+ params: ParamsType,
134
+ resp_cls: Type = dict,
135
+ headers: Dict[str, str] = None,
136
+ opts: Dict = None,
137
+ ):
138
+ opts = opts or {}
139
+ headers = headers or {}
140
+ chain = self._create_default_chain(action, params, resp_cls, headers, opts)
141
+ return await chain.proceed()
142
+
143
+ def _create_default_chain(
144
+ self,
145
+ action: str,
146
+ params: ParamsType,
147
+ resp_cls: Type,
148
+ headers: Dict[str, str],
149
+ opts: Dict,
150
+ ):
151
+ chain = RequestChain()
152
+ chain.add_interceptor(self._inter_retry)
153
+ chain.add_interceptor(self._inter_deserialize_resp(resp_cls))
154
+ if self.circuit_breaker:
155
+ chain.add_interceptor(self._inter_breaker(opts))
156
+ chain.add_interceptor(self._inter_build_request(action, params, headers, opts))
157
+ chain.add_interceptor(self._inter_send_request)
158
+ return chain
159
+
160
+ async def _inter_retry(self, chain: RequestChain):
161
+ retryer = self.profile.retryer or NoopRetryer()
162
+ return await retryer.send_request(chain.proceed)
163
+
164
+ def _inter_build_request(
165
+ self,
166
+ action: str,
167
+ params: ParamsType,
168
+ headers: Dict[str, str],
169
+ opts: Dict,
170
+ ):
171
+ async def inter(chain: RequestChain):
172
+ nonlocal action, params, opts, headers
173
+
174
+ if headers is None:
175
+ headers = {}
176
+
177
+ if not isinstance(headers, dict):
178
+ raise TencentCloudSDKException("ClientError", "headers must be a dict.")
179
+
180
+ if "x-tc-traceid" not in (k.lower() for k in headers.keys()):
181
+ headers["X-TC-TraceId"] = str(uuid.uuid4())
182
+
183
+ req = self._build_req(action, params, headers, opts)
184
+
185
+ if self.profile.httpProfile.apigw_endpoint:
186
+ req.host = self.profile.httpProfile.apigw_endpoint
187
+ req.headers["Host"] = req.host
188
+
189
+ # 低版本的 httpx 不会使用 Client.timeout, 需要 per request setup
190
+ req.extensions["timeout"] = self.http_client.timeout.as_dict()
191
+ chain.request = req
192
+
193
+ logger.debug("SendRequest:\n%s", RequestPrettyFormatter(req))
194
+
195
+ return await chain.proceed()
196
+
197
+ return inter
198
+
199
+ def _inter_breaker(self, opts: Dict):
200
+ async def inter(chain: RequestChain):
201
+ generation, need_break = self.circuit_breaker.before_requests()
202
+
203
+ endpoint = self._get_endpoint(opts=opts)
204
+ if need_break:
205
+ endpoint = self._service + "." + self.profile.region_breaker_profile.backup_endpoint
206
+
207
+ url = self.profile.httpProfile.scheme + endpoint + self._requestPath
208
+ chain.request.url = url
209
+
210
+ resp = None
211
+ try:
212
+ resp = await chain.proceed()
213
+ self.circuit_breaker.after_requests(generation, True)
214
+ return resp
215
+ except httpx.TransportError:
216
+ self.circuit_breaker.after_requests(generation, False)
217
+ raise
218
+ except TencentCloudSDKException as e:
219
+ success = resp and "RequestId" in (await resp.aread()) and e.code != "InternalError"
220
+ self.circuit_breaker.after_requests(generation, success)
221
+ raise
222
+
223
+ return inter
224
+
225
+ @staticmethod
226
+ def _inter_deserialize_resp(resp_cls: Type):
227
+ async def inter(chain: RequestChain):
228
+ resp = await chain.proceed()
229
+
230
+ await check_err(resp)
231
+
232
+ content_type = resp.headers["Content-Type"]
233
+ if content_type == "text/event-stream":
234
+ return deserialize_sse(resp)
235
+
236
+ return await deserialize_json(resp)
237
+
238
+ async def check_err(resp: ApiResponse):
239
+ if resp.status_code != 200:
240
+ if logger.isEnabledFor(logging.DEBUG):
241
+ logger.debug("GetResponse: %s", await ResponsePrettyFormatter(resp, format_body=True).astr())
242
+ raise TencentCloudSDKException("ServerNetworkError", await resp.aread())
243
+
244
+ ct = resp.headers.get('Content-Type')
245
+ if ct not in ('text/plain', _json_content):
246
+ return
247
+
248
+ content = await resp.aread()
249
+ data = json.loads(content)
250
+ if "Error" in data["Response"]:
251
+ code = data["Response"]["Error"]["Code"]
252
+ message = data["Response"]["Error"]["Message"]
253
+ reqid = data["Response"]["RequestId"]
254
+ if logger.isEnabledFor(logging.DEBUG):
255
+ logger.debug("GetResponse: %s", await ResponsePrettyFormatter(resp, format_body=True).astr())
256
+ raise TencentCloudSDKException(code, message, reqid)
257
+ if "DeprecatedWarning" in data["Response"]:
258
+ import warnings
259
+ warnings.filterwarnings("default")
260
+ warnings.warn("This action is deprecated, detail: %s" % data["Response"]["DeprecatedWarning"],
261
+ DeprecationWarning)
262
+
263
+ async def deserialize_json(resp: ApiResponse):
264
+ try:
265
+ if logger.isEnabledFor(logging.DEBUG):
266
+ logger.debug("GetResponse: %s", await ResponsePrettyFormatter(resp, format_body=True).astr())
267
+
268
+ content = await resp.aread()
269
+ if resp_cls == dict:
270
+ return json.loads(content)
271
+
272
+ if issubclass(resp_cls, AbstractModel):
273
+ resp_model = resp_cls()
274
+ resp_model._deserialize(json.loads(content)["Response"])
275
+ return resp_model
276
+
277
+ raise TencentCloudSDKException("ClientParamsError", "invalid resp_cls %s" % resp_cls)
278
+ finally:
279
+ await resp.aclose()
280
+
281
+ async def deserialize_sse(resp: ApiResponse):
282
+ logger.debug("GetResponse:\n%s", ResponsePrettyFormatter(resp, format_body=False))
283
+ e = {}
284
+
285
+ try:
286
+ async for line in resp.aiter_lines():
287
+ line = line.strip()
288
+ if not line:
289
+ yield e
290
+ e = {}
291
+ continue
292
+
293
+ logger.debug("GetResponse.Readline: %s", line)
294
+
295
+ # comment
296
+ if line[0] == ':':
297
+ continue
298
+
299
+ colon_idx = line.find(':')
300
+ key = line[:colon_idx]
301
+ val = line[colon_idx + 1:]
302
+ # If value starts with a U+0020 SPACE character, remove it from value.
303
+ if val and val[0] == " ":
304
+ val = val[1:]
305
+ if key == 'data':
306
+ # The spec allows for multiple data fields per event, concatenated them with "\n".
307
+ if 'data' not in e:
308
+ e['data'] = val
309
+ else:
310
+ e['data'] += '\n' + val
311
+ elif key in ('event', 'id'):
312
+ e[key] = val
313
+ elif key == 'retry':
314
+ e[key] = int(val)
315
+ finally:
316
+ await resp.aclose()
317
+
318
+ return inter
319
+
320
+ async def _inter_send_request(self, chain: RequestChain):
321
+ return await self.http_client.send(chain.request, stream=True)
322
+
323
+ def _get_service_domain(self):
324
+ root_domain = self.profile.httpProfile.rootDomain
325
+ return self._service + "." + root_domain
326
+
327
+ def _get_endpoint(self, opts=None):
328
+ endpoint = self.profile.httpProfile.endpoint
329
+ if not endpoint and opts:
330
+ endpoint = urlparse(opts.get("Endpoint", "")).hostname
331
+ if endpoint is None:
332
+ endpoint = self._get_service_domain()
333
+ return endpoint
334
+
335
+ def _build_req(self, action: str, params: ParamsType, headers: Dict[str, str], opts: Dict) -> ApiRequest:
336
+ if opts.get('SkipSign'):
337
+ return self._build_req_without_signature(action, params, headers, opts)
338
+ elif self.profile.signMethod == "TC3-HMAC-SHA256" or opts.get("IsMultipart") is True:
339
+ return self._build_req_with_tc3_signature(action, params, headers, opts)
340
+ elif self.profile.signMethod in ("HmacSHA1", "HmacSHA256"):
341
+ return self._build_req_with_old_signature(action, params, headers, opts)
342
+ else:
343
+ raise TencentCloudSDKException("ClientError", "Invalid signature method.")
344
+
345
+ def _build_req_without_signature(
346
+ self, action: str, params: ParamsType, headers: Dict[str, str], opts: Dict) -> ApiRequest:
347
+ method = self.profile.httpProfile.reqMethod
348
+ endpoint = self._get_endpoint(opts=opts)
349
+ url = "%s://%s%s" % (self.profile.httpProfile.scheme, endpoint, self._requestPath)
350
+ query = {}
351
+ body = ""
352
+
353
+ content_type = self._default_content_type
354
+ if method == 'GET':
355
+ content_type = _form_urlencoded_content
356
+ elif method == 'POST':
357
+ content_type = _json_content
358
+ if opts.get("IsMultipart"):
359
+ content_type = _multipart_content
360
+ if opts.get("IsOctetStream"):
361
+ content_type = _octet_stream
362
+ headers["Content-Type"] = content_type
363
+
364
+ if method == "GET" and content_type == _multipart_content:
365
+ raise TencentCloudSDKException("ClientError", "Invalid request method GET for multipart.")
366
+
367
+ endpoint = self._get_endpoint(opts=opts)
368
+ timestamp = int(time.time())
369
+ headers["Host"] = endpoint
370
+ headers["X-TC-Action"] = action[0].upper() + action[1:]
371
+ headers["X-TC-RequestClient"] = self.request_client
372
+ headers["X-TC-Timestamp"] = str(timestamp)
373
+ headers["X-TC-Version"] = self._apiVersion
374
+ if self.profile.unsignedPayload is True:
375
+ headers["X-TC-Content-SHA256"] = "UNSIGNED-PAYLOAD"
376
+ if self.region:
377
+ headers['X-TC-Region'] = self.region
378
+ if self.profile.language:
379
+ headers['X-TC-Language'] = self.profile.language
380
+
381
+ if method == 'GET':
382
+ params = copy.deepcopy(self._fix_params(params))
383
+ url += "?" + urlencode(params)
384
+ elif content_type == _json_content:
385
+ body = json.dumps(params)
386
+ elif content_type == _multipart_content:
387
+ boundary = uuid.uuid4().hex
388
+ headers["Content-Type"] = content_type + "; boundary=" + boundary
389
+ body = self._get_multipart_body(params, boundary, opts)
390
+
391
+ headers["Authorization"] = "SKIP"
392
+ return ApiRequest(method, url, params=query, content=body, headers=headers)
393
+
394
+ def _build_req_with_tc3_signature(
395
+ self, action: str, params: ParamsType, headers: Dict[str, str], opts: Dict) -> ApiRequest:
396
+ method = self.profile.httpProfile.reqMethod
397
+ endpoint = self._get_endpoint(opts=opts)
398
+ host = headers.get("Host", endpoint)
399
+ url = "%s://%s%s" % (self.profile.httpProfile.scheme, endpoint, self._requestPath)
400
+ query = ""
401
+ body = ""
402
+
403
+ content_type = self._default_content_type
404
+ if method == 'GET':
405
+ content_type = _form_urlencoded_content
406
+ elif method == 'POST':
407
+ content_type = _json_content
408
+ if opts.get("IsMultipart"):
409
+ content_type = _multipart_content
410
+ elif opts.get("IsOctetStream"):
411
+ if method != "POST":
412
+ raise TencentCloudSDKException("ClientError", "Invalid request method.")
413
+ content_type = _octet_stream
414
+ headers["Content-Type"] = content_type
415
+
416
+ if method == "GET" and content_type == _multipart_content:
417
+ raise TencentCloudSDKException("ClientError", "Invalid request method GET for multipart.")
418
+
419
+ timestamp = int(time.time())
420
+ cred_secret_id, cred_secret_key, cred_token = self.credential.get_credential_info()
421
+ headers["Host"] = host
422
+ headers["X-TC-Action"] = action[0].upper() + action[1:]
423
+ headers["X-TC-RequestClient"] = self.request_client
424
+ headers["X-TC-Timestamp"] = str(timestamp)
425
+ headers["X-TC-Version"] = self._apiVersion
426
+ if self.profile.unsignedPayload is True:
427
+ headers["X-TC-Content-SHA256"] = "UNSIGNED-PAYLOAD"
428
+ if self.region:
429
+ headers['X-TC-Region'] = self.region
430
+ if cred_token:
431
+ headers['X-TC-Token'] = cred_token
432
+ if self.profile.language:
433
+ headers['X-TC-Language'] = self.profile.language
434
+
435
+ if method == 'GET':
436
+ query = urlencode(copy.deepcopy(self._fix_params(params)))
437
+ elif content_type == _json_content:
438
+ body = json.dumps(params)
439
+ elif content_type == _multipart_content:
440
+ boundary = uuid.uuid4().hex
441
+ headers["Content-Type"] = content_type + "; boundary=" + boundary
442
+ body = self._get_multipart_body(params, boundary, opts)
443
+ elif content_type == _octet_stream:
444
+ body = params
445
+
446
+ if isinstance(body, str):
447
+ body = body.encode("utf-8")
448
+
449
+ service = self._service
450
+ date = datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d')
451
+ signature = self._get_tc3_signature(
452
+ method, self._requestPath, query, body, headers, date, service, cred_secret_key, opts)
453
+
454
+ auth = "TC3-HMAC-SHA256 Credential=%s/%s/%s/tc3_request, SignedHeaders=content-type;host, Signature=%s" % (
455
+ cred_secret_id, date, service, signature)
456
+ headers["Authorization"] = auth
457
+ # httpx 0.22.0 版本会过滤掉空 value 的 params 如 "a=&b=2"
458
+ # 不能使用 params 参数, 需要用 url 绕过, 后续版本已经修复, 但是 py36 最高只能安装 0.22.0
459
+ url += "?" + query
460
+ return ApiRequest(method, url, content=body, headers=headers)
461
+
462
+ def _build_req_with_old_signature(
463
+ self, action: str, params: ParamsType, headers: Dict[str, str], opts: Dict) -> ApiRequest:
464
+
465
+ if opts.get("IsOctetStream"):
466
+ raise TencentCloudSDKException("ClientError", "Invalid signature method.")
467
+
468
+ method = self.profile.httpProfile.reqMethod
469
+ endpoint = self._get_endpoint(opts=opts)
470
+ url = "%s://%s%s" % (self.profile.httpProfile.scheme, endpoint, self._requestPath)
471
+ query = {}
472
+ body = ""
473
+
474
+ params = copy.deepcopy(self._fix_params(params))
475
+ params['Action'] = action[0].upper() + action[1:]
476
+ params['RequestClient'] = self.request_client
477
+ params['Nonce'] = random.randint(1, sys.maxsize)
478
+ params['Timestamp'] = int(time.time())
479
+ params['Version'] = self._apiVersion
480
+
481
+ cred_secret_id, cred_secret_key, cred_token = self.credential.get_credential_info()
482
+
483
+ if self.region:
484
+ params['Region'] = self.region
485
+
486
+ if cred_token:
487
+ params['Token'] = cred_token
488
+
489
+ if cred_secret_id:
490
+ params['SecretId'] = cred_secret_id
491
+
492
+ if self.profile.signMethod:
493
+ params['SignatureMethod'] = self.profile.signMethod
494
+
495
+ if self.profile.language:
496
+ params['Language'] = self.profile.language
497
+
498
+ signInParam = self._format_sign_string(params, opts)
499
+ params['Signature'] = Sign.sign(str(cred_secret_key),
500
+ str(signInParam),
501
+ str(self.profile.signMethod))
502
+ if method == "GET":
503
+ query = params
504
+ else:
505
+ body = urlencode(params)
506
+
507
+ headers["Content-Type"] = "application/x-www-form-urlencoded"
508
+ return ApiRequest(method, url, params=query, content=body, headers=headers)
509
+
510
+ def _format_sign_string(self, params, opts=None):
511
+ formatParam = {}
512
+ for k in params:
513
+ formatParam[k.replace('_', '.')] = params[k]
514
+ strParam = '&'.join('%s=%s' % (k, formatParam[k]) for k in sorted(formatParam))
515
+ msg = '%s%s%s?%s' % (
516
+ self.profile.httpProfile.reqMethod, self._get_endpoint(opts=opts), self._requestPath, strParam)
517
+ return msg
518
+
519
+ def _fix_params(self, params):
520
+ if not isinstance(params, (dict,)):
521
+ return params
522
+ return self._format_params(None, params)
523
+
524
+ def _format_params(self, prefix, params):
525
+ d = {}
526
+ if params is None:
527
+ return d
528
+
529
+ if not isinstance(params, (tuple, list, dict)):
530
+ d[prefix] = params
531
+ return d
532
+
533
+ if isinstance(params, (list, tuple)):
534
+ for idx, item in enumerate(params):
535
+ if prefix:
536
+ key = "{0}.{1}".format(prefix, idx)
537
+ else:
538
+ key = "{0}".format(idx)
539
+ d.update(self._format_params(key, item))
540
+ return d
541
+
542
+ if isinstance(params, dict):
543
+ for k, v in params.items():
544
+ if prefix:
545
+ key = '{0}.{1}'.format(prefix, k)
546
+ else:
547
+ key = '{0}'.format(k)
548
+ d.update(self._format_params(key, v))
549
+ return d
550
+
551
+ raise TencentCloudSDKException("ClientParamsError", "some params type error")
552
+
553
+ def _get_multipart_body(self, params, boundary, options=None):
554
+ if options is None:
555
+ options = {}
556
+ # boundary and params key will never contain unicode characters
557
+ boundary = boundary.encode()
558
+ binparas = options.get("BinaryParams", [])
559
+ body = b''
560
+ for k, v in params.items():
561
+ kbytes = k.encode()
562
+ body += b'--%s\r\n' % boundary
563
+ body += b'Content-Disposition: form-data; name="%s"' % kbytes
564
+ if k in binparas:
565
+ body += b'; filename="%s"\r\n' % kbytes
566
+ else:
567
+ body += b"\r\n"
568
+ if isinstance(v, list) or isinstance(v, dict):
569
+ v = json.dumps(v)
570
+ body += b'Content-Type: application/json\r\n'
571
+ if sys.version_info[0] == 3 and isinstance(v, type("")):
572
+ v = v.encode()
573
+ body += b'\r\n%s\r\n' % v
574
+ if body != b'':
575
+ body += b'--%s--\r\n' % boundary
576
+ return body
577
+
578
+ @staticmethod
579
+ def _get_tc3_signature(method: str, path: str, query: str, body: bytes, headers: Dict, date: str, service: str,
580
+ secret_key: str, opts: Dict):
581
+ canonical_uri = path
582
+ canonical_querystring = query
583
+ payload = body
584
+
585
+ if headers.get("X-TC-Content-SHA256") == "UNSIGNED-PAYLOAD":
586
+ payload = b"UNSIGNED-PAYLOAD"
587
+
588
+ payload_hash = hashlib.sha256(payload).hexdigest()
589
+
590
+ canonical_headers = 'content-type:%s\nhost:%s\n' % (
591
+ headers["Content-Type"], headers["Host"])
592
+ signed_headers = 'content-type;host'
593
+ canonical_request = '%s\n%s\n%s\n%s\n%s\n%s' % (method,
594
+ canonical_uri,
595
+ canonical_querystring,
596
+ canonical_headers,
597
+ signed_headers,
598
+ payload_hash)
599
+
600
+ algorithm = 'TC3-HMAC-SHA256'
601
+ credential_scope = date + '/' + service + '/tc3_request'
602
+ if sys.version_info[0] == 3:
603
+ canonical_request = canonical_request.encode("utf8")
604
+ digest = hashlib.sha256(canonical_request).hexdigest()
605
+ string2sign = '%s\n%s\n%s\n%s' % (algorithm,
606
+ headers["X-TC-Timestamp"],
607
+ credential_scope,
608
+ digest)
609
+ return Sign.sign_tc3(secret_key, date, service, string2sign)
610
+
611
+ def set_stream_logger(self, stream=None, level=logging.DEBUG, log_format=None):
612
+ """Add a stream handler
613
+
614
+ :param stream: e.g. ``sys.stdout`` ``sys.stdin`` ``sys.stderr``
615
+ :type stream: IO[str]
616
+ :param level: Logging level, e.g. ``logging.INFO``
617
+ :type level: int
618
+ :param log_format: Log message format
619
+ :type log_format: str
620
+ """
621
+ log = logging.getLogger(LOGGER_NAME)
622
+ log.setLevel(level)
623
+ sh = logging.StreamHandler(stream)
624
+ sh.setLevel(level)
625
+ if log_format is None:
626
+ log_format = self.FMT
627
+ formatter = logging.Formatter(log_format)
628
+ sh.setFormatter(formatter)
629
+ log.addHandler(sh)
630
+
631
+ def set_file_logger(self, file_path, level=logging.DEBUG, log_format=None):
632
+ """Add a file handler
633
+
634
+ :param file_path: path of log file
635
+ :type file_path: str
636
+ :param level: Logging level, e.g. ``logging.INFO``
637
+ :type level: int
638
+ :param log_format: Log message format
639
+ :type log_format: str
640
+ """
641
+ log = logging.getLogger(LOGGER_NAME)
642
+ log.setLevel(level)
643
+ mb = 1024 * 1024
644
+ fh = logging.handlers.RotatingFileHandler(file_path, maxBytes=512 * mb, backupCount=10)
645
+ fh.setLevel(level)
646
+ if log_format is None:
647
+ log_format = self.FMT
648
+ formatter = logging.Formatter(log_format)
649
+ fh.setFormatter(formatter)
650
+ log.addHandler(fh)
651
+
652
+ def set_default_logger(self):
653
+ """Set default log handler"""
654
+ pass
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright 1999-2017 Tencent Ltd.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ from tencentcloud.common.abstract_client_async import AbstractClient
18
+ from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
19
+
20
+
21
+ class CommonClient(AbstractClient):
22
+ """General client for all products.
23
+
24
+ With CommonClient, you only need to install the tencentcloud-sdk-python-common package to access APIs of all products.
25
+ See GitHub examples for usage details: https://github.com/TencentCloud/tencentcloud-sdk-python/tree/master/examples/common_client
26
+
27
+ :param service: Product name
28
+ :type service: str
29
+ :param version: Version of API
30
+ :type version: str
31
+ :param credential: Request credential
32
+ :type credential: tencentcloud.common.credential.Credential or tencentcloud.common.credential.STSAssumeRoleCredential or None
33
+ :param region: Request region
34
+ :type region: str
35
+ :param profile: Request SDK profile
36
+ :type profile: tencentcloud.common.profile.client_profile.ClientProfile
37
+ """
38
+
39
+ def __init__(self, service, version, credential, region, profile=None):
40
+ if region is None or version is None or service is None:
41
+ raise TencentCloudSDKException("CommonClient Parameter Error, "
42
+ "credential region version service all required.")
43
+ self._apiVersion = version
44
+ self._service = service
45
+ super(CommonClient, self).__init__(credential, region, profile)
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/python
2
+ # -*- coding: utf-8 -*-
3
+ import httpx
4
+
5
+ __all__ = ["ApiRequest", "ApiResponse", "RequestPrettyFormatter", "ResponsePrettyFormatter"]
6
+
7
+ ApiRequest = httpx.Request
8
+ ApiResponse = httpx.Response
9
+
10
+
11
+ class RequestPrettyFormatter(object):
12
+ def __init__(self, req: ApiRequest, format_body=True, delimiter="\n"):
13
+ self._req = req
14
+ self._format_body = format_body
15
+ self._delimiter = delimiter
16
+
17
+ def __str__(self):
18
+ lines = ["%s %s" % (self._req.method, self._req.url)]
19
+ for k, v in self._req.headers.items():
20
+ lines.append("%s: %s" % (k, v))
21
+ lines.append("")
22
+ if self._format_body:
23
+ try:
24
+ lines.append(self._req.content.decode("utf-8"))
25
+ except UnicodeDecodeError:
26
+ # binary body
27
+ import base64
28
+ lines.append("base64_body:" + base64.standard_b64encode(self._req.content).decode())
29
+ return self._delimiter.join(lines)
30
+
31
+
32
+ class ResponsePrettyFormatter(object):
33
+ def __init__(self, resp: ApiResponse, format_body=True, delimiter="\n"):
34
+ self._resp = resp
35
+ self._format_body = format_body
36
+ self._delimiter = delimiter
37
+
38
+ def __str__(self):
39
+ lines = ['%s %d %s' % (self.str_ver(self._resp.http_version), self._resp.status_code, self._resp.reason_phrase)]
40
+ for k, v in self._resp.headers.items():
41
+ lines.append('%s: %s' % (k, v))
42
+ return self._delimiter.join(lines)
43
+
44
+ async def astr(self):
45
+ lines = ['%s %d %s' % (self.str_ver(self._resp.http_version), self._resp.status_code, self._resp.reason_phrase)]
46
+ for k, v in self._resp.headers.items():
47
+ lines.append('%s: %s' % (k, v))
48
+ if self._format_body:
49
+ lines.append('')
50
+ lines.append((await self._resp.aread()).decode("utf-8"))
51
+ return self._delimiter.join(lines)
52
+
53
+ @staticmethod
54
+ def str_ver(ver):
55
+ if ver == 10:
56
+ return "HTTP/1.0"
57
+ elif ver == 11:
58
+ return "HTTP/1.1"
59
+ elif ver == 20:
60
+ return "HTTP/2.0"
61
+ else:
62
+ return str(ver)
@@ -0,0 +1,87 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ import httpx
5
+
6
+ from tencentcloud.common.exception import TencentCloudSDKException
7
+
8
+
9
+ class NoopRetryer(object):
10
+ """configuration without retry
11
+
12
+ NoopRetryer is a retry policy that does nothing.
13
+ It is useful when you don't want to retry.
14
+ """
15
+
16
+ async def send_request(self, fn):
17
+ return await fn()
18
+
19
+
20
+ class StandardRetryer(object):
21
+ """Retry configuration
22
+
23
+ StandardRetryer is a retry policy that retries on network errors or frequency limitation.
24
+ :param max_attempts: Maximum number of attempts.
25
+ :type max_attempts: int
26
+ :param backoff_fn: A function that takes the number of attempts and returns the number of seconds to sleep before the next retry.
27
+ Default sleep time is 2^n seconds, n is the number of attempts.
28
+ :type backoff_fn: function
29
+ :param logger: A logger to log retry attempts. If not provided, no logging will be performed.
30
+ :type logger: logging.Logger
31
+ """
32
+
33
+ def __init__(self, max_attempts=3, backoff_fn=None, logger=None):
34
+ self._max_attempts = max_attempts
35
+ self._backoff_fn = backoff_fn or self.backoff
36
+ self._logger = logger
37
+
38
+ async def send_request(self, fn):
39
+ resp = None
40
+ err = None
41
+
42
+ for n in range(self._max_attempts):
43
+ try:
44
+ resp = await fn()
45
+ except TencentCloudSDKException as e:
46
+ err = e
47
+ except httpx.TransportError as e:
48
+ err = e
49
+
50
+ if not await self.should_retry(resp, err):
51
+ if err:
52
+ raise err
53
+ return resp
54
+
55
+ sleep = await self._backoff_fn(n)
56
+ await self.on_retry(n, sleep, resp, err)
57
+ await asyncio.sleep(sleep)
58
+
59
+ raise err
60
+
61
+ @staticmethod
62
+ async def should_retry(resp, err):
63
+ if not err:
64
+ return False
65
+
66
+ if isinstance(err, httpx.TransportError):
67
+ return True
68
+
69
+ if not isinstance(err, TencentCloudSDKException):
70
+ return False
71
+
72
+ ec = err.get_code()
73
+ if ec in (
74
+ "ClientNetworkError", "ServerNetworkError", "RequestLimitExceeded",
75
+ "RequestLimitExceeded.UinLimitExceeded", "RequestLimitExceeded.GlobalRegionUinLimitExceeded"
76
+ ):
77
+ return True
78
+
79
+ return False
80
+
81
+ @staticmethod
82
+ async def backoff(n):
83
+ return 2 ** n
84
+
85
+ async def on_retry(self, n, sleep, resp, err):
86
+ if self._logger:
87
+ self._logger.debug("retry: n=%d sleep=%ss err=%s", n, sleep, err)
@@ -0,0 +1,48 @@
1
+ Metadata-Version: 2.1
2
+ Name: tencentcloud-sdk-python-common
3
+ Version: 3.1.21
4
+ Summary: Tencent Cloud Common SDK for Python
5
+ Home-page: https://github.com/TencentCloud/tencentcloud-sdk-python
6
+ Author: Tencent Cloud
7
+ Maintainer-email: tencentcloudapi@tencent.com
8
+ License: Apache License 2.0
9
+ Platform: any
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 2.7
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.6
17
+ Classifier: Programming Language :: Python :: 3.7
18
+ Requires-Dist: requests>=2.16.0
19
+ Provides-Extra: async
20
+ Requires-Dist: httpx>=0.22.0; extra == "async"
21
+
22
+ ============================
23
+ Tencent Cloud SDK for Python
24
+ ============================
25
+
26
+ Tencent Cloud Python Common SDK is the official software development kit, which allows Python developers to write software that makes use of Tencent Cloud services like CVM and CBS.
27
+ The SDK works on Python versions:
28
+
29
+ * 2.7 and greater, including 3.x
30
+
31
+ Quick Start
32
+ -----------
33
+
34
+ First, install the library:
35
+
36
+ .. code-block:: sh
37
+
38
+ $ pip install tencentcloud-sdk-python-common
39
+ $ pip install tencentcloud-sdk-python-common
40
+
41
+ or download source code from github and install:
42
+
43
+ .. code-block:: sh
44
+
45
+ $ git clone https://github.com/tencentcloud/tencentcloud-sdk-python.git
46
+ $ cd tencentcloud-sdk-python
47
+ $ python package.py --components common common
48
+
@@ -4,17 +4,21 @@ setup.py
4
4
  tencentcloud/__init__.py
5
5
  tencentcloud/common/__init__.py
6
6
  tencentcloud/common/abstract_client.py
7
+ tencentcloud/common/abstract_client_async.py
7
8
  tencentcloud/common/abstract_model.py
8
9
  tencentcloud/common/circuit_breaker.py
9
10
  tencentcloud/common/common_client.py
11
+ tencentcloud/common/common_client_async.py
10
12
  tencentcloud/common/credential.py
11
13
  tencentcloud/common/retry.py
14
+ tencentcloud/common/retry_async.py
12
15
  tencentcloud/common/sign.py
13
16
  tencentcloud/common/exception/__init__.py
14
17
  tencentcloud/common/exception/tencent_cloud_sdk_exception.py
15
18
  tencentcloud/common/http/__init__.py
16
19
  tencentcloud/common/http/pre_conn.py
17
20
  tencentcloud/common/http/request.py
21
+ tencentcloud/common/http/request_async.py
18
22
  tencentcloud/common/profile/__init__.py
19
23
  tencentcloud/common/profile/client_profile.py
20
24
  tencentcloud/common/profile/http_profile.py
@@ -0,0 +1,4 @@
1
+ requests>=2.16.0
2
+
3
+ [async]
4
+ httpx>=0.22.0
@@ -1,45 +0,0 @@
1
- Metadata-Version: 1.2
2
- Name: tencentcloud-sdk-python-common
3
- Version: 3.0.1450
4
- Summary: Tencent Cloud Common SDK for Python
5
- Home-page: https://github.com/TencentCloud/tencentcloud-sdk-python
6
- Author: Tencent Cloud
7
- Maintainer-email: tencentcloudapi@tencent.com
8
- License: Apache License 2.0
9
- Description: ============================
10
- Tencent Cloud SDK for Python
11
- ============================
12
-
13
- Tencent Cloud Python Common SDK is the official software development kit, which allows Python developers to write software that makes use of Tencent Cloud services like CVM and CBS.
14
- The SDK works on Python versions:
15
-
16
- * 2.7 and greater, including 3.x
17
-
18
- Quick Start
19
- -----------
20
-
21
- First, install the library:
22
-
23
- .. code-block:: sh
24
-
25
- $ pip install tencentcloud-sdk-python-common
26
- $ pip install tencentcloud-sdk-python-common
27
-
28
- or download source code from github and install:
29
-
30
- .. code-block:: sh
31
-
32
- $ git clone https://github.com/tencentcloud/tencentcloud-sdk-python.git
33
- $ cd tencentcloud-sdk-python
34
- $ python package.py --components common common
35
-
36
-
37
- Platform: any
38
- Classifier: Development Status :: 5 - Production/Stable
39
- Classifier: Intended Audience :: Developers
40
- Classifier: License :: OSI Approved :: Apache Software License
41
- Classifier: Programming Language :: Python
42
- Classifier: Programming Language :: Python :: 2.7
43
- Classifier: Programming Language :: Python :: 3
44
- Classifier: Programming Language :: Python :: 3.6
45
- Classifier: Programming Language :: Python :: 3.7
@@ -1,45 +0,0 @@
1
- Metadata-Version: 1.2
2
- Name: tencentcloud-sdk-python-common
3
- Version: 3.0.1450
4
- Summary: Tencent Cloud Common SDK for Python
5
- Home-page: https://github.com/TencentCloud/tencentcloud-sdk-python
6
- Author: Tencent Cloud
7
- Maintainer-email: tencentcloudapi@tencent.com
8
- License: Apache License 2.0
9
- Description: ============================
10
- Tencent Cloud SDK for Python
11
- ============================
12
-
13
- Tencent Cloud Python Common SDK is the official software development kit, which allows Python developers to write software that makes use of Tencent Cloud services like CVM and CBS.
14
- The SDK works on Python versions:
15
-
16
- * 2.7 and greater, including 3.x
17
-
18
- Quick Start
19
- -----------
20
-
21
- First, install the library:
22
-
23
- .. code-block:: sh
24
-
25
- $ pip install tencentcloud-sdk-python-common
26
- $ pip install tencentcloud-sdk-python-common
27
-
28
- or download source code from github and install:
29
-
30
- .. code-block:: sh
31
-
32
- $ git clone https://github.com/tencentcloud/tencentcloud-sdk-python.git
33
- $ cd tencentcloud-sdk-python
34
- $ python package.py --components common common
35
-
36
-
37
- Platform: any
38
- Classifier: Development Status :: 5 - Production/Stable
39
- Classifier: Intended Audience :: Developers
40
- Classifier: License :: OSI Approved :: Apache Software License
41
- Classifier: Programming Language :: Python
42
- Classifier: Programming Language :: Python :: 2.7
43
- Classifier: Programming Language :: Python :: 3
44
- Classifier: Programming Language :: Python :: 3.6
45
- Classifier: Programming Language :: Python :: 3.7