client.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. # Copyright 2014 ksyun.com, Inc. or its affiliates. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"). You
  4. # may not use this file except in compliance with the License. A copy of
  5. # the License is located at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # or in the "license" file accompanying this file. This file is
  10. # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  11. # ANY KIND, either express or implied. See the License for the specific
  12. # language governing permissions and limitations under the License.
  13. import collections
  14. import copy
  15. import logging
  16. import kscore.serialize
  17. import kscore.validate
  18. from kscore import waiter, xform_name
  19. from kscore.auth import AUTH_TYPE_MAPS
  20. from kscore.ksrequest import prepare_request_dict
  21. from kscore.config import Config
  22. from kscore.docs.docstring import ClientMethodDocstring
  23. from kscore.docs.docstring import PaginatorDocstring
  24. from kscore.endpoint import EndpointCreator
  25. from kscore.exceptions import ClientError, DataNotFoundError
  26. from kscore.exceptions import OperationNotPageableError
  27. from kscore.exceptions import UnknownSignatureVersionError
  28. from kscore.hooks import first_non_none_response
  29. from kscore.model import ServiceModel
  30. from kscore.paginate import Paginator
  31. from kscore.signers import RequestSigner
  32. from kscore.utils import CachedProperty
  33. from kscore.utils import fix_s3_host
  34. from kscore.utils import get_service_module_name
  35. from kscore.utils import switch_to_virtual_host_style
  36. from kscore.utils import switch_host_s3_accelerate
  37. from kscore.utils import S3_ACCELERATE_ENDPOINT
  38. logger = logging.getLogger(__name__)
  39. class ClientCreator(object):
  40. """Creates client objects for a service."""
  41. def __init__(self, loader, endpoint_resolver, user_agent, event_emitter,
  42. retry_handler_factory, retry_config_translator,
  43. response_parser_factory=None):
  44. self._loader = loader
  45. self._endpoint_resolver = endpoint_resolver
  46. self._user_agent = user_agent
  47. self._event_emitter = event_emitter
  48. self._retry_handler_factory = retry_handler_factory
  49. self._retry_config_translator = retry_config_translator
  50. self._response_parser_factory = response_parser_factory
  51. def create_client(self, service_name, region_name, is_secure=True,
  52. endpoint_url=None, verify=None,
  53. credentials=None, scoped_config=None,
  54. api_version=None,
  55. client_config=None,
  56. customer_path=None):
  57. service_model = self._load_service_model(service_name, api_version)
  58. customer_model = self._load_customer_model(customer_path, service_name, api_version)
  59. cls = self._create_client_class(service_name, service_model)
  60. client_args = self._get_client_args(customer_model, service_model, region_name, is_secure, endpoint_url,
  61. verify, credentials, scoped_config, client_config)
  62. return cls(**client_args)
  63. def create_client_class(self, service_name, api_version=None):
  64. service_model = self._load_service_model(service_name, api_version)
  65. return self._create_client_class(service_name, service_model)
  66. def _create_client_class(self, service_name, service_model):
  67. class_attributes = self._create_methods(service_model)
  68. py_name_to_operation_name = self._create_name_mapping(service_model)
  69. class_attributes['_PY_TO_OP_NAME'] = py_name_to_operation_name
  70. bases = [BaseClient]
  71. self._event_emitter.emit('creating-client-class.%s' % service_name,
  72. class_attributes=class_attributes,
  73. base_classes=bases)
  74. class_name = get_service_module_name(service_model)
  75. cls = type(str(class_name), tuple(bases), class_attributes)
  76. return cls
  77. def _load_customer_model(self, path, service_name, api_version=None):
  78. if path:
  79. json_model = self._loader.load_customer_model(path, service_name, 'service-2', 'customer',
  80. api_version=api_version)
  81. else:
  82. json_model = collections.OrderedDict()
  83. return json_model
  84. def _load_service_model(self, service_name, api_version=None):
  85. json_model = self._loader.load_service_model(service_name, 'service-2',
  86. api_version=api_version)
  87. service_model = ServiceModel(json_model, service_name=service_name)
  88. self._register_retries(service_model)
  89. return service_model
  90. def _register_retries(self, service_model):
  91. endpoint_prefix = service_model.endpoint_prefix
  92. # First, we load the entire retry config for all services,
  93. # then pull out just the information we need.
  94. original_config = self._loader.load_data('_retry')
  95. if not original_config:
  96. return
  97. retry_config = self._retry_config_translator.build_retry_config(
  98. endpoint_prefix, original_config.get('retry', {}),
  99. original_config.get('definitions', {}))
  100. logger.debug("Registering retry handlers for service: %s",
  101. service_model.service_name)
  102. handler = self._retry_handler_factory.create_retry_handler(
  103. retry_config, endpoint_prefix)
  104. unique_id = 'retry-config-%s' % endpoint_prefix
  105. self._event_emitter.register('needs-retry.%s' % endpoint_prefix,
  106. handler, unique_id=unique_id)
  107. def _inject_s3_configuration(self, config_kwargs, scoped_config,
  108. client_config):
  109. s3_configuration = None
  110. # Check the scoped config first.
  111. if scoped_config is not None:
  112. s3_configuration = scoped_config.get('s3')
  113. # Until we have proper validation of the config file (including
  114. # nested types), we have to account for the fact that the s3
  115. # key could be parsed as a string, e.g 's3 = foo'.
  116. # In the case we'll ignore the key for now.
  117. if not isinstance(s3_configuration, dict):
  118. logger.debug("The s3 config key is not a dictionary type, "
  119. "ignoring its value of: %s", s3_configuration)
  120. s3_configuration = None
  121. # Convert logic for s3 accelerate options in the scoped config
  122. # so that the various strings map to the appropriate boolean value.
  123. if s3_configuration and \
  124. 'use_accelerate_endpoint' in s3_configuration:
  125. # Make sure any further modifications to the s3 section will
  126. # not affect the scoped config by making a copy of it.
  127. s3_configuration = s3_configuration.copy()
  128. # Normalize on different possible values of True
  129. if s3_configuration['use_accelerate_endpoint'] in [
  130. True, 'True', 'true']:
  131. s3_configuration['use_accelerate_endpoint'] = True
  132. else:
  133. s3_configuration['use_accelerate_endpoint'] = False
  134. # Next specfic client config values takes precedence over
  135. # specific values in the scoped config.
  136. if client_config is not None:
  137. if client_config.s3 is not None:
  138. if s3_configuration is None:
  139. s3_configuration = client_config.s3
  140. else:
  141. # The current s3_configuration dictionary may be
  142. # from a source that only should be read from so
  143. # we want to be safe and just make a copy of it to modify
  144. # before it actually gets updated.
  145. s3_configuration = s3_configuration.copy()
  146. s3_configuration.update(client_config.s3)
  147. config_kwargs['s3'] = s3_configuration
  148. def _get_client_args(self, customer_model, service_model, region_name, is_secure,
  149. endpoint_url, verify, credentials,
  150. scoped_config, client_config):
  151. service_name = service_model.endpoint_prefix
  152. protocol = service_model.metadata['protocol']
  153. parameter_validation = True
  154. if client_config:
  155. parameter_validation = client_config.parameter_validation
  156. serializer = kscore.serialize.create_serializer(
  157. protocol, parameter_validation)
  158. event_emitter = copy.copy(self._event_emitter)
  159. response_parser = kscore.parsers.create_parser(protocol)
  160. endpoint_bridge = ClientEndpointBridge(
  161. self._endpoint_resolver, scoped_config, client_config,
  162. service_signing_name=service_model.metadata.get('signingName'))
  163. endpoint_config = endpoint_bridge.resolve(
  164. service_name, region_name, endpoint_url, is_secure)
  165. # Override the user agent if specified in the client config.
  166. user_agent = self._user_agent
  167. if client_config is not None:
  168. if client_config.user_agent is not None:
  169. user_agent = client_config.user_agent
  170. if client_config.user_agent_extra is not None:
  171. user_agent += ' %s' % client_config.user_agent_extra
  172. signer = RequestSigner(
  173. service_name, endpoint_config['signing_region'],
  174. endpoint_config['signing_name'],
  175. endpoint_config['signature_version'],
  176. credentials, event_emitter)
  177. # Create a new client config to be passed to the client based
  178. # on the final values. We do not want the user to be able
  179. # to try to modify an existing client with a client config.
  180. config_kwargs = dict(
  181. region_name=endpoint_config['region_name'],
  182. signature_version=endpoint_config['signature_version'],
  183. user_agent=user_agent)
  184. if client_config is not None:
  185. config_kwargs.update(
  186. connect_timeout=client_config.connect_timeout,
  187. read_timeout=client_config.read_timeout)
  188. # Add any additional s3 configuration for client
  189. self._inject_s3_configuration(
  190. config_kwargs, scoped_config, client_config)
  191. new_config = Config(**config_kwargs)
  192. endpoint_creator = EndpointCreator(event_emitter)
  193. endpoint = endpoint_creator.create_endpoint(
  194. service_model, region_name=endpoint_config['region_name'],
  195. endpoint_url=endpoint_config['endpoint_url'], verify=verify,
  196. response_parser_factory=self._response_parser_factory,
  197. timeout=(new_config.connect_timeout, new_config.read_timeout))
  198. return {
  199. 'serializer': serializer,
  200. 'endpoint': endpoint,
  201. 'response_parser': response_parser,
  202. 'event_emitter': event_emitter,
  203. 'request_signer': signer,
  204. 'service_model': service_model,
  205. 'loader': self._loader,
  206. 'client_config': new_config,
  207. 'customer_model': customer_model
  208. }
  209. def _create_methods(self, service_model):
  210. op_dict = {}
  211. for operation_name in service_model.operation_names:
  212. py_operation_name = xform_name(operation_name)
  213. op_dict[py_operation_name] = self._create_api_method(
  214. py_operation_name, operation_name, service_model)
  215. return op_dict
  216. def _create_name_mapping(self, service_model):
  217. # py_name -> OperationName, for every operation available
  218. # for a service.
  219. mapping = {}
  220. for operation_name in service_model.operation_names:
  221. py_operation_name = xform_name(operation_name)
  222. mapping[py_operation_name] = operation_name
  223. return mapping
  224. def _create_api_method(self, py_operation_name, operation_name,
  225. service_model):
  226. def _api_call(self, *args, **kwargs):
  227. # We're accepting *args so that we can give a more helpful
  228. # error message than TypeError: _api_call takes exactly
  229. # 1 argument.
  230. if args:
  231. raise TypeError(
  232. "%s() only accepts keyword arguments." % py_operation_name)
  233. # The "self" in this scope is referring to the BaseClient.
  234. return self._make_api_call(operation_name, kwargs)
  235. _api_call.__name__ = str(py_operation_name)
  236. # Add the docstring to the client method
  237. operation_model = service_model.operation_model(operation_name)
  238. docstring = ClientMethodDocstring(
  239. operation_model=operation_model,
  240. method_name=operation_name,
  241. event_emitter=self._event_emitter,
  242. method_description=operation_model.documentation,
  243. example_prefix='response = client.%s' % py_operation_name,
  244. include_signature=False
  245. )
  246. _api_call.__doc__ = docstring
  247. return _api_call
  248. class ClientEndpointBridge(object):
  249. """Bridges endpoint data and client creation"""
  250. DEFAULT_ENDPOINT = '{service}.{region}.api.ksyun.com'
  251. def __init__(self, endpoint_resolver, scoped_config=None,
  252. client_config=None, default_endpoint=None,
  253. service_signing_name=None):
  254. self.service_signing_name = service_signing_name
  255. self.endpoint_resolver = endpoint_resolver
  256. self.scoped_config = scoped_config
  257. self.client_config = client_config
  258. self.default_endpoint = default_endpoint or self.DEFAULT_ENDPOINT
  259. def resolve(self, service_name, region_name=None, endpoint_url=None,
  260. is_secure=True):
  261. region_name = self._check_default_region(service_name, region_name)
  262. resolved = self.endpoint_resolver.construct_endpoint(
  263. service_name, region_name)
  264. if resolved:
  265. return self._create_endpoint(
  266. resolved, service_name, region_name, endpoint_url, is_secure)
  267. else:
  268. return self._assume_endpoint(service_name, region_name,
  269. endpoint_url, is_secure)
  270. def _check_default_region(self, service_name, region_name):
  271. if region_name is not None:
  272. return region_name
  273. # Use the client_config region if no explicit region was provided.
  274. if self.client_config and self.client_config.region_name is not None:
  275. return self.client_config.region_name
  276. def _create_endpoint(self, resolved, service_name, region_name,
  277. endpoint_url, is_secure):
  278. region_name, signing_region = self._pick_region_values(
  279. resolved, region_name, endpoint_url)
  280. if endpoint_url is None:
  281. # Use the sslCommonName over the hostname for Python 2.6 compat.
  282. hostname = resolved.get('sslCommonName', resolved.get('hostname'))
  283. endpoint_url = self._make_url(hostname, is_secure,
  284. resolved.get('protocols', []))
  285. signature_version = self._resolve_signature_version(
  286. service_name, resolved)
  287. signing_name = self._resolve_signing_name(service_name, resolved)
  288. return self._create_result(
  289. service_name=service_name, region_name=region_name,
  290. signing_region=signing_region, signing_name=signing_name,
  291. endpoint_url=endpoint_url, metadata=resolved,
  292. signature_version=signature_version)
  293. def _assume_endpoint(self, service_name, region_name, endpoint_url,
  294. is_secure):
  295. if endpoint_url is None:
  296. # Expand the default hostname URI template.
  297. hostname = self.default_endpoint.format(
  298. service=service_name, region=region_name)
  299. endpoint_url = self._make_url(hostname, is_secure,
  300. ['http', 'https'])
  301. logger.debug('Assuming an endpoint for %s, %s: %s',
  302. service_name, region_name, endpoint_url)
  303. # We still want to allow the user to provide an explicit version.
  304. signature_version = self._resolve_signature_version(
  305. service_name, {'signatureVersions': ['v4']})
  306. signing_name = self._resolve_signing_name(service_name, resolved={})
  307. return self._create_result(
  308. service_name=service_name, region_name=region_name,
  309. signing_region=region_name, signing_name=signing_name,
  310. signature_version=signature_version, endpoint_url=endpoint_url,
  311. metadata={})
  312. def _create_result(self, service_name, region_name, signing_region,
  313. signing_name, endpoint_url, signature_version,
  314. metadata):
  315. return {
  316. 'service_name': service_name,
  317. 'region_name': region_name,
  318. 'signing_region': signing_region,
  319. 'signing_name': signing_name,
  320. 'endpoint_url': endpoint_url,
  321. 'signature_version': signature_version,
  322. 'metadata': metadata
  323. }
  324. def _make_url(self, hostname, is_secure, supported_protocols):
  325. if is_secure and 'https' in supported_protocols:
  326. scheme = 'https'
  327. else:
  328. scheme = 'http'
  329. return '%s://%s' % (scheme, hostname)
  330. def _resolve_signing_name(self, service_name, resolved):
  331. # CredentialScope overrides everything else.
  332. if 'credentialScope' in resolved \
  333. and 'service' in resolved['credentialScope']:
  334. return resolved['credentialScope']['service']
  335. # Use the signingName from the model if present.
  336. if self.service_signing_name:
  337. return self.service_signing_name
  338. # Just assume is the same as the service name.
  339. return service_name
  340. def _pick_region_values(self, resolved, region_name, endpoint_url):
  341. signing_region = region_name
  342. if endpoint_url is None:
  343. # Do not use the region name or signing name from the resolved
  344. # endpoint if the user explicitly provides an endpoint_url. This
  345. # would happen if we resolve to an endpoint where the service has
  346. # a "defaults" section that overrides all endpoint with a single
  347. # hostname and credentialScope. This has been the case historically
  348. # for how STS has worked. The only way to resolve an STS endpoint
  349. # was to provide a region_name and an endpoint_url. In that case,
  350. # we would still resolve an endpoint, but we would not use the
  351. # resolved endpointName or signingRegion because we want to allow
  352. # custom endpoints.
  353. region_name = resolved['endpointName']
  354. signing_region = region_name
  355. if 'credentialScope' in resolved \
  356. and 'region' in resolved['credentialScope']:
  357. signing_region = resolved['credentialScope']['region']
  358. return region_name, signing_region
  359. def _resolve_signature_version(self, service_name, resolved):
  360. # Client config overrides everything.
  361. client = self.client_config
  362. if client and client.signature_version is not None:
  363. return client.signature_version
  364. # Scoped config overrides picking from the endpoint metadata.
  365. scoped = self.scoped_config
  366. if scoped is not None:
  367. service_config = scoped.get(service_name)
  368. if service_config is not None and isinstance(service_config, dict):
  369. version = service_config.get('signature_version')
  370. if version:
  371. logger.debug(
  372. "Switching signature version for service %s "
  373. "to version %s based on config file override.",
  374. service_name, version)
  375. return version
  376. # Pick a signature version from the endpoint metadata if present.
  377. if 'signatureVersions' in resolved:
  378. potential_versions = resolved['signatureVersions']
  379. if service_name == 's3':
  380. # We currently prefer s3 over s3v4.
  381. if 's3' in potential_versions:
  382. return 's3'
  383. elif 's3v4' in potential_versions:
  384. return 's3v4'
  385. if 'v4' in potential_versions:
  386. return 'v4'
  387. # Now just iterate over the signature versions in order until we
  388. # find the first one that is known to KSCore.
  389. for known in AUTH_TYPE_MAPS:
  390. if known in potential_versions:
  391. return known
  392. raise UnknownSignatureVersionError(
  393. signature_version=resolved.get('signatureVersions'))
  394. class BaseClient(object):
  395. # This is actually reassigned with the py->op_name mapping
  396. # when the client creator creates the subclass. This value is used
  397. # because calls such as client.get_paginator('list_objects') use the
  398. # snake_case name, but we need to know the ListObjects form.
  399. # xform_name() does the ListObjects->list_objects conversion, but
  400. # we need the reverse mapping here.
  401. _PY_TO_OP_NAME = {}
  402. def __init__(self, serializer, endpoint, response_parser,
  403. event_emitter, request_signer, service_model, loader,
  404. client_config, customer_model):
  405. self._serializer = serializer
  406. self._endpoint = endpoint
  407. self._response_parser = response_parser
  408. self._request_signer = request_signer
  409. self._cache = {}
  410. self._loader = loader
  411. self._client_config = client_config
  412. self.meta = ClientMeta(event_emitter, self._client_config,
  413. endpoint.host, service_model,
  414. self._PY_TO_OP_NAME, customer_model)
  415. self._register_handlers()
  416. def _register_handlers(self):
  417. # Register the handler required to sign requests.
  418. self.meta.events.register('request-created.%s' %
  419. self.meta.service_model.endpoint_prefix,
  420. self._request_signer.handler)
  421. self._register_s3_specific_handlers()
  422. def _register_s3_specific_handlers(self):
  423. # Register all of the s3 specific handlers
  424. if self.meta.config.s3 is None:
  425. s3_addressing_style = None
  426. s3_accelerate = None
  427. else:
  428. s3_addressing_style = self.meta.config.s3.get('addressing_style')
  429. s3_accelerate = self.meta.config.s3.get('use_accelerate_endpoint')
  430. # Enable accelerate if the configuration is set to to true or the
  431. # endpoint being used matches one of the Accelerate endpoints.
  432. if s3_accelerate or S3_ACCELERATE_ENDPOINT in self._endpoint.host:
  433. self._force_virtual_style_s3_addressing()
  434. self.meta.events.register_first(
  435. 'request-created.s3', switch_host_s3_accelerate)
  436. elif s3_addressing_style:
  437. # Otherwise go ahead with the style the user may have specified.
  438. if s3_addressing_style == 'path':
  439. self._force_path_style_s3_addressing()
  440. elif s3_addressing_style == 'virtual':
  441. self._force_virtual_style_s3_addressing()
  442. def _force_path_style_s3_addressing(self):
  443. # Do not try to modify the host if path is specified. The
  444. # ``fix_s3_host`` usually switches the addresing style to virtual.
  445. self.meta.events.unregister('before-sign.s3', fix_s3_host)
  446. def _force_virtual_style_s3_addressing(self):
  447. # If the virtual host addressing style is being forced,
  448. # switch the default fix_s3_host handler for the more general
  449. # switch_to_virtual_host_style handler that does not have opt out
  450. # cases (other than throwing an error if the name is DNS incompatible)
  451. self.meta.events.unregister('before-sign.s3', fix_s3_host)
  452. self.meta.events.register(
  453. 'before-sign.s3', switch_to_virtual_host_style)
  454. @property
  455. def _service_model(self):
  456. return self.meta.service_model
  457. def _make_api_call(self, operation_name, api_params):
  458. request_context = {}
  459. operation_model = self._service_model.operation_model(operation_name)
  460. request_dict = self._convert_to_request_dict(
  461. api_params, operation_model, context=request_context)
  462. handler, event_response = self.meta.events.emit_until_response(
  463. 'before-call.{endpoint_prefix}.{operation_name}'.format(
  464. endpoint_prefix=self._service_model.endpoint_prefix,
  465. operation_name=operation_name),
  466. model=operation_model, params=request_dict,
  467. request_signer=self._request_signer, context=request_context)
  468. if event_response is not None:
  469. http, parsed_response = event_response
  470. else:
  471. http, parsed_response = self._endpoint.make_request(
  472. operation_model, request_dict)
  473. self.meta.events.emit(
  474. 'after-call.{endpoint_prefix}.{operation_name}'.format(
  475. endpoint_prefix=self._service_model.endpoint_prefix,
  476. operation_name=operation_name),
  477. http_response=http, parsed=parsed_response,
  478. model=operation_model, context=request_context
  479. )
  480. # result mapping to customer
  481. self._mapping_to_customer_resp(parsed_response, operation_name)
  482. if (operation_name == 'ListMetrics' or
  483. operation_name == 'GetMetricStatistics' or
  484. operation_name == 'GetMetricStatisticsBatch' or
  485. operation_name == 'GetMetricStatisticsBatchV2' or
  486. operation_name == 'ListMetricsV3' or
  487. operation_name == 'GetMetricStatisticsV3'):
  488. return parsed_response
  489. if http.status_code >= 300:
  490. raise ClientError(parsed_response, operation_name)
  491. else:
  492. return parsed_response
  493. def _mapping_to_customer_resp(self, parsed_response, operation_name):
  494. if parsed_response and type(parsed_response) == dict and self.meta.customer_model.__contains__(operation_name):
  495. self._mapping_recursion(parsed_response, self.meta.customer_model.get(operation_name))
  496. def _mapping_recursion(self, parsed_response, _dict):
  497. _del = []
  498. if type(parsed_response) == dict:
  499. _p = dict(parsed_response)
  500. for _item in _p.items():
  501. if _dict.__contains__(_item[0]):
  502. parsed_response[_dict.get(_item[0])] = _item[1]
  503. _del.append(_item[0])
  504. if type(_item[1]) == list or type(_item[1]) == dict:
  505. self._mapping_recursion(_item[1], _dict)
  506. for _d in _del:
  507. del parsed_response[_d]
  508. else:
  509. for _item in list(parsed_response):
  510. if type(_item) == dict:
  511. self._mapping_recursion(_item, _dict)
  512. def _convert_to_request_dict(self, api_params, operation_model,
  513. context=None):
  514. # Given the API params provided by the user and the operation_model
  515. # we can serialize the request to a request_dict.
  516. operation_name = operation_model.name
  517. # Emit an event that allows users to modify the parameters at the
  518. # beginning of the method. It allows handlers to modify existing
  519. # parameters or return a new set of parameters to use.
  520. responses = self.meta.events.emit(
  521. 'provide-client-params.{endpoint_prefix}.{operation_name}'.format(
  522. endpoint_prefix=self._service_model.endpoint_prefix,
  523. operation_name=operation_name),
  524. params=api_params, model=operation_model, context=context)
  525. api_params = first_non_none_response(responses, default=api_params)
  526. event_name = (
  527. 'before-parameter-build.{endpoint_prefix}.{operation_name}')
  528. self.meta.events.emit(
  529. event_name.format(
  530. endpoint_prefix=self._service_model.endpoint_prefix,
  531. operation_name=operation_name),
  532. params=api_params, model=operation_model, context=context)
  533. serializer = self._serializer
  534. if operation_model.is_rewrite_protocol:
  535. serializer = kscore.serialize.create_serializer(operation_model.protocol, True)
  536. request_dict = serializer.serialize_to_request(api_params, operation_model)
  537. prepare_request_dict(request_dict, endpoint_url=self._endpoint.host,
  538. user_agent=self._client_config.user_agent)
  539. return request_dict
  540. def get_paginator(self, operation_name):
  541. """Create a paginator for an operation.
  542. :type operation_name: string
  543. :param operation_name: The operation name. This is the same name
  544. as the method name on the client. For example, if the
  545. method name is ``create_foo``, and you'd normally invoke the
  546. operation as ``client.create_foo(**kwargs)``, if the
  547. ``create_foo`` operation can be paginated, you can use the
  548. call ``client.get_paginator("create_foo")``.
  549. :raise OperationNotPageableError: Raised if the operation is not
  550. pageable. You can use the ``client.can_paginate`` method to
  551. check if an operation is pageable.
  552. :rtype: L{kscore.paginate.Paginator}
  553. :return: A paginator object.
  554. """
  555. if not self.can_paginate(operation_name):
  556. raise OperationNotPageableError(operation_name=operation_name)
  557. else:
  558. actual_operation_name = self._PY_TO_OP_NAME[operation_name]
  559. # Create a new paginate method that will serve as a proxy to
  560. # the underlying Paginator.paginate method. This is needed to
  561. # attach a docstring to the method.
  562. def paginate(self, **kwargs):
  563. return Paginator.paginate(self, **kwargs)
  564. paginator_config = self._cache['page_config'][
  565. actual_operation_name]
  566. # Add the docstring for the paginate method.
  567. paginate.__doc__ = PaginatorDocstring(
  568. paginator_name=actual_operation_name,
  569. event_emitter=self.meta.events,
  570. service_model=self.meta.service_model,
  571. paginator_config=paginator_config,
  572. include_signature=False
  573. )
  574. # Rename the paginator class based on the type of paginator.
  575. paginator_class_name = str('%s.Paginator.%s' % (
  576. get_service_module_name(self.meta.service_model),
  577. actual_operation_name))
  578. # Create the new paginator class
  579. documented_paginator_cls = type(
  580. paginator_class_name, (Paginator,), {'paginate': paginate})
  581. paginator = documented_paginator_cls(
  582. getattr(self, operation_name),
  583. paginator_config)
  584. return paginator
  585. def can_paginate(self, operation_name):
  586. """Check if an operation can be paginated.
  587. :type operation_name: string
  588. :param operation_name: The operation name. This is the same name
  589. as the method name on the client. For example, if the
  590. method name is ``create_foo``, and you'd normally invoke the
  591. operation as ``client.create_foo(**kwargs)``, if the
  592. ``create_foo`` operation can be paginated, you can use the
  593. call ``client.get_paginator("create_foo")``.
  594. :return: ``True`` if the operation can be paginated,
  595. ``False`` otherwise.
  596. """
  597. if 'page_config' not in self._cache:
  598. try:
  599. page_config = self._loader.load_service_model(
  600. self._service_model.service_name,
  601. 'paginators-1',
  602. self._service_model.api_version)['pagination']
  603. self._cache['page_config'] = page_config
  604. except DataNotFoundError:
  605. self._cache['page_config'] = {}
  606. actual_operation_name = self._PY_TO_OP_NAME[operation_name]
  607. return actual_operation_name in self._cache['page_config']
  608. def _get_waiter_config(self):
  609. if 'waiter_config' not in self._cache:
  610. try:
  611. waiter_config = self._loader.load_service_model(
  612. self._service_model.service_name,
  613. 'waiters-2',
  614. self._service_model.api_version)
  615. self._cache['waiter_config'] = waiter_config
  616. except DataNotFoundError:
  617. self._cache['waiter_config'] = {}
  618. return self._cache['waiter_config']
  619. def get_waiter(self, waiter_name):
  620. config = self._get_waiter_config()
  621. if not config:
  622. raise ValueError("Waiter does not exist: %s" % waiter_name)
  623. model = waiter.WaiterModel(config)
  624. mapping = {}
  625. for name in model.waiter_names:
  626. mapping[xform_name(name)] = name
  627. if waiter_name not in mapping:
  628. raise ValueError("Waiter does not exist: %s" % waiter_name)
  629. return waiter.create_waiter_with_client(
  630. mapping[waiter_name], model, self)
  631. @CachedProperty
  632. def waiter_names(self):
  633. """Returns a list of all available waiters."""
  634. config = self._get_waiter_config()
  635. if not config:
  636. return []
  637. model = waiter.WaiterModel(config)
  638. # Waiter configs is a dict, we just want the waiter names
  639. # which are the keys in the dict.
  640. return [xform_name(name) for name in model.waiter_names]
  641. class ClientMeta(object):
  642. """Holds additional client methods.
  643. This class holds additional information for clients. It exists for
  644. two reasons:
  645. * To give advanced functionality to clients
  646. * To namespace additional client attributes from the operation
  647. names which are mapped to methods at runtime. This avoids
  648. ever running into collisions with operation names.
  649. """
  650. def __init__(self, events, client_config, endpoint_url, service_model,
  651. method_to_api_mapping, customer_model):
  652. self.events = events
  653. self._client_config = client_config
  654. self._endpoint_url = endpoint_url
  655. self._service_model = service_model
  656. self._method_to_api_mapping = method_to_api_mapping
  657. self._customer_model = customer_model
  658. @property
  659. def service_model(self):
  660. return self._service_model
  661. @property
  662. def region_name(self):
  663. return self._client_config.region_name
  664. @property
  665. def endpoint_url(self):
  666. return self._endpoint_url
  667. @property
  668. def customer_model(self):
  669. return self._customer_model
  670. @property
  671. def config(self):
  672. return self._client_config
  673. @property
  674. def method_to_api_mapping(self):
  675. return self._method_to_api_mapping