1
# Copyright 2014 Amazon.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://aws.amazon.com/apache2.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 10
import logging
14 10
import functools
15

16 10
from botocore import waiter, xform_name
17 10
from botocore.args import ClientArgsCreator
18 10
from botocore.auth import AUTH_TYPE_MAPS
19 10
from botocore.awsrequest import prepare_request_dict
20 10
from botocore.docs.docstring import ClientMethodDocstring
21 10
from botocore.docs.docstring import PaginatorDocstring
22 10
from botocore.exceptions import (
23
    ClientError, DataNotFoundError, OperationNotPageableError,
24
    UnknownSignatureVersionError, InvalidEndpointDiscoveryConfigurationError
25
)
26 10
from botocore.hooks import first_non_none_response
27 10
from botocore.model import ServiceModel
28 10
from botocore.paginate import Paginator
29 10
from botocore.utils import (
30
    CachedProperty, get_service_module_name, S3RegionRedirector,
31
    S3ArnParamHandler, S3EndpointSetter, ensure_boolean,
32
    S3ControlArnParamHandler, S3ControlEndpointSetter,
33
)
34 10
from botocore.args import ClientArgsCreator
35 10
from botocore import UNSIGNED
36
# Keep this imported.  There's pre-existing code that uses
37
# "from botocore.client import Config".
38 10
from botocore.config import Config
39 10
from botocore.history import get_global_history_recorder
40 10
from botocore.discovery import (
41
    EndpointDiscoveryHandler, EndpointDiscoveryManager,
42
    block_endpoint_discovery_required_operations
43
)
44 10
from botocore.retries import standard
45 10
from botocore.retries import adaptive
46

47

48 10
logger = logging.getLogger(__name__)
49 10
history_recorder = get_global_history_recorder()
50

51

52 10
class ClientCreator(object):
53
    """Creates client objects for a service."""
54 10
    def __init__(self, loader, endpoint_resolver, user_agent, event_emitter,
55
                 retry_handler_factory, retry_config_translator,
56
                 response_parser_factory=None, exceptions_factory=None,
57
                 config_store=None):
58 10
        self._loader = loader
59 10
        self._endpoint_resolver = endpoint_resolver
60 10
        self._user_agent = user_agent
61 10
        self._event_emitter = event_emitter
62 10
        self._retry_handler_factory = retry_handler_factory
63 10
        self._retry_config_translator = retry_config_translator
64 10
        self._response_parser_factory = response_parser_factory
65 10
        self._exceptions_factory = exceptions_factory
66
        # TODO: Migrate things away from scoped_config in favor of the
67
        # config_store.  The config store can pull things from both the scoped
68
        # config and environment variables (and potentially more in the
69
        # future).
70 10
        self._config_store = config_store
71

72 10
    def create_client(self, service_name, region_name, is_secure=True,
73
                      endpoint_url=None, verify=None,
74
                      credentials=None, scoped_config=None,
75
                      api_version=None,
76
                      client_config=None):
77 10
        responses = self._event_emitter.emit(
78
            'choose-service-name', service_name=service_name)
79 10
        service_name = first_non_none_response(responses, default=service_name)
80 10
        service_model = self._load_service_model(service_name, api_version)
81 10
        cls = self._create_client_class(service_name, service_model)
82 10
        endpoint_bridge = ClientEndpointBridge(
83
            self._endpoint_resolver, scoped_config, client_config,
84
            service_signing_name=service_model.metadata.get('signingName'))
85 10
        client_args = self._get_client_args(
86
            service_model, region_name, is_secure, endpoint_url,
87
            verify, credentials, scoped_config, client_config, endpoint_bridge)
88 10
        service_client = cls(**client_args)
89 10
        self._register_retries(service_client)
90 10
        self._register_s3_events(
91
            service_client, endpoint_bridge, endpoint_url, client_config,
92
            scoped_config)
93 10
        self._register_s3_control_events(
94
            service_client, endpoint_bridge, endpoint_url, client_config,
95
            scoped_config)
96 10
        self._register_endpoint_discovery(
97
            service_client, endpoint_url, client_config
98
        )
99 10
        return service_client
100

101 10
    def create_client_class(self, service_name, api_version=None):
102 10
        service_model = self._load_service_model(service_name, api_version)
103 10
        return self._create_client_class(service_name, service_model)
104

105 10
    def _create_client_class(self, service_name, service_model):
106 10
        class_attributes = self._create_methods(service_model)
107 10
        py_name_to_operation_name = self._create_name_mapping(service_model)
108 10
        class_attributes['_PY_TO_OP_NAME'] = py_name_to_operation_name
109 10
        bases = [BaseClient]
110 10
        service_id = service_model.service_id.hyphenize()
111 10
        self._event_emitter.emit(
112
            'creating-client-class.%s' % service_id,
113
            class_attributes=class_attributes,
114
            base_classes=bases)
115 10
        class_name = get_service_module_name(service_model)
116 10
        cls = type(str(class_name), tuple(bases), class_attributes)
117 10
        return cls
118

119 10
    def _load_service_model(self, service_name, api_version=None):
120 10
        json_model = self._loader.load_service_model(service_name, 'service-2',
121
                                                     api_version=api_version)
122 10
        service_model = ServiceModel(json_model, service_name=service_name)
123 10
        return service_model
124

125 10
    def _register_retries(self, client):
126 10
        retry_mode = client.meta.config.retries['mode']
127 10
        if retry_mode == 'standard':
128 10
            self._register_v2_standard_retries(client)
129 10
        elif retry_mode == 'adaptive':
130 10
            self._register_v2_standard_retries(client)
131 10
            self._register_v2_adaptive_retries(client)
132 10
        elif retry_mode == 'legacy':
133 10
            self._register_legacy_retries(client)
134

135 10
    def _register_v2_standard_retries(self, client):
136 10
        max_attempts = client.meta.config.retries.get('total_max_attempts')
137 10
        kwargs = {'client': client}
138 10
        if max_attempts is not None:
139 10
            kwargs['max_attempts'] = max_attempts
140 10
        standard.register_retry_handler(**kwargs)
141

142 10
    def _register_v2_adaptive_retries(self, client):
143 10
        adaptive.register_retry_handler(client)
144

145 10
    def _register_legacy_retries(self, client):
146 10
        endpoint_prefix = client.meta.service_model.endpoint_prefix
147 10
        service_id = client.meta.service_model.service_id
148 10
        service_event_name = service_id.hyphenize()
149

150
        # First, we load the entire retry config for all services,
151
        # then pull out just the information we need.
152 10
        original_config = self._loader.load_data('_retry')
153 10
        if not original_config:
154 10
            return
155

156 10
        retries = self._transform_legacy_retries(client.meta.config.retries)
157 10
        retry_config = self._retry_config_translator.build_retry_config(
158
            endpoint_prefix, original_config.get('retry', {}),
159
            original_config.get('definitions', {}),
160
            retries
161
        )
162

163 10
        logger.debug("Registering retry handlers for service: %s",
164
                     client.meta.service_model.service_name)
165 10
        handler = self._retry_handler_factory.create_retry_handler(
166
            retry_config, endpoint_prefix)
167 10
        unique_id = 'retry-config-%s' % service_event_name
168 10
        client.meta.events.register(
169
            'needs-retry.%s' % service_event_name, handler,
170
            unique_id=unique_id
171
        )
172

173 10
    def _transform_legacy_retries(self, retries):
174 10
        if retries is None:
175 0
            return
176 10
        copied_args = retries.copy()
177 10
        if 'total_max_attempts' in retries:
178 10
            copied_args = retries.copy()
179 10
            copied_args['max_attempts'] = (
180
                copied_args.pop('total_max_attempts') - 1)
181 10
        return copied_args
182

183 10
    def _get_retry_mode(self, client, config_store):
184 0
        client_retries = client.meta.config.retries
185 0
        if client_retries is not None and \
186
                client_retries.get('mode') is not None:
187 0
            return client_retries['mode']
188 0
        return config_store.get_config_variable('retry_mode') or 'legacy'
189

190 10
    def _register_endpoint_discovery(self, client, endpoint_url, config):
191 10
        if endpoint_url is not None:
192
            # Don't register any handlers in the case of a custom endpoint url
193 10
            return
194
        # Only attach handlers if the service supports discovery
195 10
        if client.meta.service_model.endpoint_discovery_operation is None:
196 10
            return
197 10
        events = client.meta.events
198 10
        service_id = client.meta.service_model.service_id.hyphenize()
199 10
        enabled = False
200 10
        if config and config.endpoint_discovery_enabled is not None:
201 10
            enabled = config.endpoint_discovery_enabled
202 10
        elif self._config_store:
203 10
            enabled = self._config_store.get_config_variable(
204
                'endpoint_discovery_enabled')
205

206 10
        enabled = self._normalize_endpoint_discovery_config(enabled)
207 10
        if enabled and self._requires_endpoint_discovery(client, enabled):
208 10
            discover = enabled is True
209 10
            manager = EndpointDiscoveryManager(client, always_discover=discover)
210 10
            handler = EndpointDiscoveryHandler(manager)
211 10
            handler.register(events, service_id)
212
        else:
213 10
            events.register('before-parameter-build',
214
                            block_endpoint_discovery_required_operations)
215

216 10
    def _normalize_endpoint_discovery_config(self, enabled):
217
        """Config must either be a boolean-string or string-literal 'auto'"""
218 10
        if isinstance(enabled, str):
219 10
            enabled = enabled.lower().strip()
220 10
            if enabled == 'auto':
221 10
                return enabled
222 10
            elif enabled in ('true', 'false'):
223 10
                return ensure_boolean(enabled)
224 10
        elif isinstance(enabled, bool):
225 10
            return enabled
226

227 10
        raise InvalidEndpointDiscoveryConfigurationError(config_value=enabled)
228

229 10
    def _requires_endpoint_discovery(self, client, enabled):
230 10
        if enabled == "auto":
231 10
            return client.meta.service_model.endpoint_discovery_required
232 10
        return enabled
233

234 10
    def _register_s3_events(self, client, endpoint_bridge, endpoint_url,
235
                            client_config, scoped_config):
236 10
        if client.meta.service_model.service_name != 's3':
237 10
            return
238 10
        S3RegionRedirector(endpoint_bridge, client).register()
239 10
        S3ArnParamHandler().register(client.meta.events)
240 10
        S3EndpointSetter(
241
            endpoint_resolver=self._endpoint_resolver,
242
            region=client.meta.region_name,
243
            s3_config=client.meta.config.s3,
244
            endpoint_url=endpoint_url,
245
            partition=client.meta.partition
246
        ).register(client.meta.events)
247 10
        self._set_s3_presign_signature_version(
248
            client.meta, client_config, scoped_config)
249

250 10
    def _register_s3_control_events(
251
        self, client, endpoint_bridge,
252
        endpoint_url, client_config, scoped_config
253
    ):
254 10
        if client.meta.service_model.service_name != 's3control':
255 10
            return
256 10
        S3ControlArnParamHandler().register(client.meta.events)
257 10
        S3ControlEndpointSetter(
258
            endpoint_resolver=self._endpoint_resolver,
259
            region=client.meta.region_name,
260
            s3_config=client.meta.config.s3,
261
            endpoint_url=endpoint_url,
262
            partition=client.meta.partition
263
        ).register(client.meta.events)
264

265 10
    def _set_s3_presign_signature_version(self, client_meta,
266
                                          client_config, scoped_config):
267
        # This will return the manually configured signature version, or None
268
        # if none was manually set. If a customer manually sets the signature
269
        # version, we always want to use what they set.
270 10
        provided_signature_version = _get_configured_signature_version(
271
            's3', client_config, scoped_config)
272 10
        if provided_signature_version is not None:
273 10
            return
274

275
        # Check to see if the region is a region that we know about. If we
276
        # don't know about a region, then we can safely assume it's a new
277
        # region that is sigv4 only, since all new S3 regions only allow sigv4.
278
        # The only exception is aws-global. This is a pseudo-region for the
279
        # global endpoint, we should respect the signature versions it
280
        # supports, which includes v2.
281 10
        regions = self._endpoint_resolver.get_available_endpoints(
282
            's3', client_meta.partition)
283 10
        if client_meta.region_name != 'aws-global' and \
284
                client_meta.region_name not in regions:
285 10
            return
286

287
        # If it is a region we know about, we want to default to sigv2, so here
288
        # we check to see if it is available.
289 10
        endpoint = self._endpoint_resolver.construct_endpoint(
290
            's3', client_meta.region_name)
291 10
        signature_versions = endpoint['signatureVersions']
292 10
        if 's3' not in signature_versions:
293 10
            return
294

295
        # We now know that we're in a known region that supports sigv2 and
296
        # the customer hasn't set a signature version so we default the
297
        # signature version to sigv2.
298 10
        client_meta.events.register(
299
            'choose-signer.s3', self._default_s3_presign_to_sigv2)
300

301 10
    def _default_s3_presign_to_sigv2(self, signature_version, **kwargs):
302
        """
303
        Returns the 's3' (sigv2) signer if presigning an s3 request. This is
304
        intended to be used to set the default signature version for the signer
305
        to sigv2.
306

307
        :type signature_version: str
308
        :param signature_version: The current client signature version.
309

310
        :type signing_name: str
311
        :param signing_name: The signing name of the service.
312

313
        :return: 's3' if the request is an s3 presign request, None otherwise
314
        """
315 10
        for suffix in ['-query', '-presign-post']:
316 10
            if signature_version.endswith(suffix):
317 10
                return 's3' + suffix
318

319 10
    def _get_client_args(self, service_model, region_name, is_secure,
320
                         endpoint_url, verify, credentials,
321
                         scoped_config, client_config, endpoint_bridge):
322 10
        args_creator = ClientArgsCreator(
323
            self._event_emitter, self._user_agent,
324
            self._response_parser_factory, self._loader,
325
            self._exceptions_factory, config_store=self._config_store)
326 10
        return args_creator.get_client_args(
327
            service_model, region_name, is_secure, endpoint_url,
328
            verify, credentials, scoped_config, client_config, endpoint_bridge)
329

330 10
    def _create_methods(self, service_model):
331 10
        op_dict = {}
332 10
        for operation_name in service_model.operation_names:
333 10
            py_operation_name = xform_name(operation_name)
334 10
            op_dict[py_operation_name] = self._create_api_method(
335
                py_operation_name, operation_name, service_model)
336 10
        return op_dict
337

338 10
    def _create_name_mapping(self, service_model):
339
        # py_name -> OperationName, for every operation available
340
        # for a service.
341 10
        mapping = {}
342 10
        for operation_name in service_model.operation_names:
343 10
            py_operation_name = xform_name(operation_name)
344 10
            mapping[py_operation_name] = operation_name
345 10
        return mapping
346

347 10
    def _create_api_method(self, py_operation_name, operation_name,
348
                           service_model):
349 10
        def _api_call(self, *args, **kwargs):
350
            # We're accepting *args so that we can give a more helpful
351
            # error message than TypeError: _api_call takes exactly
352
            # 1 argument.
353 10
            if args:
354 10
                raise TypeError(
355
                    "%s() only accepts keyword arguments." % py_operation_name)
356
            # The "self" in this scope is referring to the BaseClient.
357 10
            return self._make_api_call(operation_name, kwargs)
358

359 10
        _api_call.__name__ = str(py_operation_name)
360

361
        # Add the docstring to the client method
362 10
        operation_model = service_model.operation_model(operation_name)
363 10
        docstring = ClientMethodDocstring(
364
            operation_model=operation_model,
365
            method_name=operation_name,
366
            event_emitter=self._event_emitter,
367
            method_description=operation_model.documentation,
368
            example_prefix='response = client.%s' % py_operation_name,
369
            include_signature=False
370
        )
371 10
        _api_call.__doc__ = docstring
372 10
        return _api_call
373

374

375 10
class ClientEndpointBridge(object):
376
    """Bridges endpoint data and client creation
377

378
    This class handles taking out the relevant arguments from the endpoint
379
    resolver and determining which values to use, taking into account any
380
    client configuration options and scope configuration options.
381

382
    This class also handles determining what, if any, region to use if no
383
    explicit region setting is provided. For example, Amazon S3 client will
384
    utilize "us-east-1" by default if no region can be resolved."""
385

386 10
    DEFAULT_ENDPOINT = '{service}.{region}.amazonaws.com'
387 10
    _DUALSTACK_ENABLED_SERVICES = ['s3', 's3-control']
388

389 10
    def __init__(self, endpoint_resolver, scoped_config=None,
390
                 client_config=None, default_endpoint=None,
391
                 service_signing_name=None):
392 10
        self.service_signing_name = service_signing_name
393 10
        self.endpoint_resolver = endpoint_resolver
394 10
        self.scoped_config = scoped_config
395 10
        self.client_config = client_config
396 10
        self.default_endpoint = default_endpoint or self.DEFAULT_ENDPOINT
397

398 10
    def resolve(self, service_name, region_name=None, endpoint_url=None,
399
                is_secure=True):
400 10
        region_name = self._check_default_region(service_name, region_name)
401 10
        resolved = self.endpoint_resolver.construct_endpoint(
402
            service_name, region_name)
403

404
        # If we can't resolve the region, we'll attempt to get a global
405
        # endpoint for non-regionalized services (iam, route53, etc)
406 10
        if not resolved:
407
            # TODO: fallback partition_name should be configurable in the
408
            # future for users to define as needed.
409 10
            resolved = self.endpoint_resolver.construct_endpoint(
410
                service_name, region_name, partition_name='aws')
411

412 10
        if resolved:
413 10
            return self._create_endpoint(
414
                resolved, service_name, region_name, endpoint_url, is_secure)
415
        else:
416 10
            return self._assume_endpoint(service_name, region_name,
417
                                         endpoint_url, is_secure)
418

419 10
    def _check_default_region(self, service_name, region_name):
420 10
        if region_name is not None:
421 10
            return region_name
422
        # Use the client_config region if no explicit region was provided.
423 10
        if self.client_config and self.client_config.region_name is not None:
424 10
            return self.client_config.region_name
425

426 10
    def _create_endpoint(self, resolved, service_name, region_name,
427
                         endpoint_url, is_secure):
428 10
        explicit_region = region_name is not None
429 10
        region_name, signing_region = self._pick_region_values(
430
            resolved, region_name, endpoint_url)
431 10
        if endpoint_url is None:
432 10
            if self._is_s3_dualstack_mode(service_name):
433 10
                endpoint_url = self._create_dualstack_endpoint(
434
                    service_name, region_name,
435
                    resolved['dnsSuffix'], is_secure, explicit_region)
436
            else:
437
                # Use the sslCommonName over the hostname for Python 2.6 compat.
438 10
                hostname = resolved.get('sslCommonName', resolved.get('hostname'))
439 10
                endpoint_url = self._make_url(hostname, is_secure,
440
                                            resolved.get('protocols', []))
441 10
        signature_version = self._resolve_signature_version(
442
            service_name, resolved)
443 10
        signing_name = self._resolve_signing_name(service_name, resolved)
444 10
        return self._create_result(
445
            service_name=service_name, region_name=region_name,
446
            signing_region=signing_region, signing_name=signing_name,
447
            endpoint_url=endpoint_url, metadata=resolved,
448
            signature_version=signature_version)
449

450 10
    def _is_s3_dualstack_mode(self, service_name):
451 10
        if service_name not in self._DUALSTACK_ENABLED_SERVICES:
452 10
            return False
453
        # TODO: This normalization logic is duplicated from the
454
        # ClientArgsCreator class.  Consolidate everything to
455
        # ClientArgsCreator.  _resolve_signature_version also has similarly
456
        # duplicated logic.
457 10
        client_config = self.client_config
458 10
        if client_config is not None and client_config.s3 is not None and \
459
                'use_dualstack_endpoint' in client_config.s3:
460
            # Client config trumps scoped config.
461 10
            return client_config.s3['use_dualstack_endpoint']
462 10
        if self.scoped_config is None:
463 10
            return False
464 10
        enabled = self.scoped_config.get('s3', {}).get(
465
            'use_dualstack_endpoint', False)
466 10
        if enabled in [True, 'True', 'true']:
467 10
            return True
468 10
        return False
469

470 10
    def _create_dualstack_endpoint(self, service_name, region_name,
471
                                   dns_suffix, is_secure, explicit_region):
472 10
        if not explicit_region and region_name == 'aws-global':
473
            # If the region_name passed was not explicitly set, default to
474
            # us-east-1 instead of the modeled default aws-global. Dualstack
475
            # does not support aws-global
476 10
            region_name = 'us-east-1'
477 10
        hostname = '{service}.dualstack.{region}.{dns_suffix}'.format(
478
            service=service_name, region=region_name,
479
            dns_suffix=dns_suffix)
480
        # Dualstack supports http and https so were hardcoding this value for
481
        # now.  This can potentially move into the endpoints.json file.
482 10
        return self._make_url(hostname, is_secure, ['http', 'https'])
483

484 10
    def _assume_endpoint(self, service_name, region_name, endpoint_url,
485
                         is_secure):
486 10
        if endpoint_url is None:
487
            # Expand the default hostname URI template.
488 10
            hostname = self.default_endpoint.format(
489
                service=service_name, region=region_name)
490 10
            endpoint_url = self._make_url(hostname, is_secure,
491
                                          ['http', 'https'])
492 10
        logger.debug('Assuming an endpoint for %s, %s: %s',
493
                     service_name, region_name, endpoint_url)
494
        # We still want to allow the user to provide an explicit version.
495 10
        signature_version = self._resolve_signature_version(
496
            service_name, {'signatureVersions': ['v4']})
497 10
        signing_name = self._resolve_signing_name(service_name, resolved={})
498 10
        return self._create_result(
499
            service_name=service_name, region_name=region_name,
500
            signing_region=region_name, signing_name=signing_name,
501
            signature_version=signature_version, endpoint_url=endpoint_url,
502
            metadata={})
503

504 10
    def _create_result(self, service_name, region_name, signing_region,
505
                       signing_name, endpoint_url, signature_version,
506
                       metadata):
507 10
        return {
508
            'service_name': service_name,
509
            'region_name': region_name,
510
            'signing_region': signing_region,
511
            'signing_name': signing_name,
512
            'endpoint_url': endpoint_url,
513
            'signature_version': signature_version,
514
            'metadata': metadata
515
        }
516

517 10
    def _make_url(self, hostname, is_secure, supported_protocols):
518 10
        if is_secure and 'https' in supported_protocols:
519 10
            scheme = 'https'
520
        else:
521 10
            scheme = 'http'
522 10
        return '%s://%s' % (scheme, hostname)
523

524 10
    def _resolve_signing_name(self, service_name, resolved):
525
        # CredentialScope overrides everything else.
526 10
        if 'credentialScope' in resolved \
527
                and 'service' in resolved['credentialScope']:
528 10
            return resolved['credentialScope']['service']
529
        # Use the signingName from the model if present.
530 10
        if self.service_signing_name:
531 10
            return self.service_signing_name
532
        # Just assume is the same as the service name.
533 10
        return service_name
534

535 10
    def _pick_region_values(self, resolved, region_name, endpoint_url):
536 10
        signing_region = region_name
537 10
        if endpoint_url is None:
538
            # Do not use the region name or signing name from the resolved
539
            # endpoint if the user explicitly provides an endpoint_url. This
540
            # would happen if we resolve to an endpoint where the service has
541
            # a "defaults" section that overrides all endpoint with a single
542
            # hostname and credentialScope. This has been the case historically
543
            # for how STS has worked. The only way to resolve an STS endpoint
544
            # was to provide a region_name and an endpoint_url. In that case,
545
            # we would still resolve an endpoint, but we would not use the
546
            # resolved endpointName or signingRegion because we want to allow
547
            # custom endpoints.
548 10
            region_name = resolved['endpointName']
549 10
            signing_region = region_name
550 10
            if 'credentialScope' in resolved \
551
                    and 'region' in resolved['credentialScope']:
552 10
                signing_region = resolved['credentialScope']['region']
553 10
        return region_name, signing_region
554

555 10
    def _resolve_signature_version(self, service_name, resolved):
556 10
        configured_version = _get_configured_signature_version(
557
            service_name, self.client_config, self.scoped_config)
558 10
        if configured_version is not None:
559 10
            return configured_version
560

561
        # Pick a signature version from the endpoint metadata if present.
562 10
        if 'signatureVersions' in resolved:
563 10
            potential_versions = resolved['signatureVersions']
564 10
            if service_name == 's3':
565 10
                return 's3v4'
566 10
            if 'v4' in potential_versions:
567 10
                return 'v4'
568
            # Now just iterate over the signature versions in order until we
569
            # find the first one that is known to Botocore.
570 10
            for known in potential_versions:
571 10
                if known in AUTH_TYPE_MAPS:
572 10
                    return known
573 10
        raise UnknownSignatureVersionError(
574
            signature_version=resolved.get('signatureVersions'))
575

576

577 10
class BaseClient(object):
578

579
    # This is actually reassigned with the py->op_name mapping
580
    # when the client creator creates the subclass.  This value is used
581
    # because calls such as client.get_paginator('list_objects') use the
582
    # snake_case name, but we need to know the ListObjects form.
583
    # xform_name() does the ListObjects->list_objects conversion, but
584
    # we need the reverse mapping here.
585 10
    _PY_TO_OP_NAME = {}
586

587 10
    def __init__(self, serializer, endpoint, response_parser,
588
                 event_emitter, request_signer, service_model, loader,
589
                 client_config, partition, exceptions_factory):
590 10
        self._serializer = serializer
591 10
        self._endpoint = endpoint
592 10
        self._response_parser = response_parser
593 10
        self._request_signer = request_signer
594 10
        self._cache = {}
595 10
        self._loader = loader
596 10
        self._client_config = client_config
597 10
        self.meta = ClientMeta(event_emitter, self._client_config,
598
                               endpoint.host, service_model,
599
                               self._PY_TO_OP_NAME, partition)
600 10
        self._exceptions_factory = exceptions_factory
601 10
        self._exceptions = None
602 10
        self._register_handlers()
603

604 10
    def __getattr__(self, item):
605 10
        event_name = 'getattr.%s.%s' % (
606
            self._service_model.service_id.hyphenize(), item
607
        )
608 10
        handler, event_response = self.meta.events.emit_until_response(
609
            event_name, client=self)
610

611 10
        if event_response is not None:
612 10
            return event_response
613

614 10
        raise AttributeError(
615
            "'%s' object has no attribute '%s'" % (
616
                self.__class__.__name__, item)
617
        )
618

619 10
    def _register_handlers(self):
620
        # Register the handler required to sign requests.
621 10
        service_id = self.meta.service_model.service_id.hyphenize()
622 10
        self.meta.events.register(
623
            'request-created.%s' % service_id,
624
            self._request_signer.handler
625
        )
626

627 10
    @property
628 2
    def _service_model(self):
629 10
        return self.meta.service_model
630

631 10
    def _make_api_call(self, operation_name, api_params):
632 10
        operation_model = self._service_model.operation_model(operation_name)
633 10
        service_name = self._service_model.service_name
634 10
        history_recorder.record('API_CALL', {
635
            'service': service_name,
636
            'operation': operation_name,
637
            'params': api_params,
638
        })
639 10
        if operation_model.deprecated:
640 10
            logger.debug('Warning: %s.%s() is deprecated',
641
                         service_name, operation_name)
642 10
        request_context = {
643
            'client_region': self.meta.region_name,
644
            'client_config': self.meta.config,
645
            'has_streaming_input': operation_model.has_streaming_input,
646
            'auth_type': operation_model.auth_type,
647
        }
648 10
        request_dict = self._convert_to_request_dict(
649
            api_params, operation_model, context=request_context)
650

651 10
        service_id = self._service_model.service_id.hyphenize()
652 10
        handler, event_response = self.meta.events.emit_until_response(
653
            'before-call.{service_id}.{operation_name}'.format(
654
                service_id=service_id,
655
                operation_name=operation_name),
656
            model=operation_model, params=request_dict,
657
            request_signer=self._request_signer, context=request_context)
658

659 10
        if event_response is not None:
660 10
            http, parsed_response = event_response
661
        else:
662 10
            http, parsed_response = self._make_request(
663
                operation_model, request_dict, request_context)
664

665 10
        self.meta.events.emit(
666
            'after-call.{service_id}.{operation_name}'.format(
667
                service_id=service_id,
668
                operation_name=operation_name),
669
            http_response=http, parsed=parsed_response,
670
            model=operation_model, context=request_context
671
        )
672

673 10
        if http.status_code >= 300:
674 10
            error_code = parsed_response.get("Error", {}).get("Code")
675 10
            error_class = self.exceptions.from_code(error_code)
676 10
            raise error_class(parsed_response, operation_name)
677
        else:
678 10
            return parsed_response
679

680 10
    def _make_request(self, operation_model, request_dict, request_context):
681 10
        try:
682 10
            return self._endpoint.make_request(operation_model, request_dict)
683 10
        except Exception as e:
684 10
            self.meta.events.emit(
685
                'after-call-error.{service_id}.{operation_name}'.format(
686
                    service_id=self._service_model.service_id.hyphenize(),
687
                    operation_name=operation_model.name),
688
                exception=e, context=request_context
689
            )
690 10
            raise
691

692 10
    def _convert_to_request_dict(self, api_params, operation_model,
693
                                 context=None):
694 10
        api_params = self._emit_api_params(
695
            api_params, operation_model, context)
696 10
        request_dict = self._serializer.serialize_to_request(
697
            api_params, operation_model)
698 10
        if not self._client_config.inject_host_prefix:
699 10
            request_dict.pop('host_prefix', None)
700 10
        prepare_request_dict(request_dict, endpoint_url=self._endpoint.host,
701
                             user_agent=self._client_config.user_agent,
702
                             context=context)
703 10
        return request_dict
704

705 10
    def _emit_api_params(self, api_params, operation_model, context):
706
        # Given the API params provided by the user and the operation_model
707
        # we can serialize the request to a request_dict.
708 10
        operation_name = operation_model.name
709

710
        # Emit an event that allows users to modify the parameters at the
711
        # beginning of the method. It allows handlers to modify existing
712
        # parameters or return a new set of parameters to use.
713 10
        service_id = self._service_model.service_id.hyphenize()
714 10
        responses = self.meta.events.emit(
715
            'provide-client-params.{service_id}.{operation_name}'.format(
716
                service_id=service_id,
717
                operation_name=operation_name),
718
            params=api_params, model=operation_model, context=context)
719 10
        api_params = first_non_none_response(responses, default=api_params)
720

721 10
        event_name = (
722
            'before-parameter-build.{service_id}.{operation_name}')
723 10
        self.meta.events.emit(
724
            event_name.format(
725
                service_id=service_id,
726
                operation_name=operation_name),
727
            params=api_params, model=operation_model, context=context)
728 10
        return api_params
729

730 10
    def get_paginator(self, operation_name):
731
        """Create a paginator for an operation.
732

733
        :type operation_name: string
734
        :param operation_name: The operation name.  This is the same name
735
            as the method name on the client.  For example, if the
736
            method name is ``create_foo``, and you'd normally invoke the
737
            operation as ``client.create_foo(**kwargs)``, if the
738
            ``create_foo`` operation can be paginated, you can use the
739
            call ``client.get_paginator("create_foo")``.
740

741
        :raise OperationNotPageableError: Raised if the operation is not
742
            pageable.  You can use the ``client.can_paginate`` method to
743
            check if an operation is pageable.
744

745
        :rtype: L{botocore.paginate.Paginator}
746
        :return: A paginator object.
747

748
        """
749 10
        if not self.can_paginate(operation_name):
750 10
            raise OperationNotPageableError(operation_name=operation_name)
751
        else:
752 10
            actual_operation_name = self._PY_TO_OP_NAME[operation_name]
753

754
            # Create a new paginate method that will serve as a proxy to
755
            # the underlying Paginator.paginate method. This is needed to
756
            # attach a docstring to the method.
757 10
            def paginate(self, **kwargs):
758 10
                return Paginator.paginate(self, **kwargs)
759

760 10
            paginator_config = self._cache['page_config'][
761
                actual_operation_name]
762
            # Add the docstring for the paginate method.
763 10
            paginate.__doc__ = PaginatorDocstring(
764
                paginator_name=actual_operation_name,
765
                event_emitter=self.meta.events,
766
                service_model=self.meta.service_model,
767
                paginator_config=paginator_config,
768
                include_signature=False
769
            )
770

771
            # Rename the paginator class based on the type of paginator.
772 10
            paginator_class_name = str('%s.Paginator.%s' % (
773
                get_service_module_name(self.meta.service_model),
774
                actual_operation_name))
775

776
            # Create the new paginator class
777 10
            documented_paginator_cls = type(
778
                paginator_class_name, (Paginator,), {'paginate': paginate})
779

780 10
            operation_model = self._service_model.operation_model(actual_operation_name)
781 10
            paginator = documented_paginator_cls(
782
                getattr(self, operation_name),
783
                paginator_config,
784
                operation_model)
785 10
            return paginator
786

787 10
    def can_paginate(self, operation_name):
788
        """Check if an operation can be paginated.
789

790
        :type operation_name: string
791
        :param operation_name: The operation name.  This is the same name
792
            as the method name on the client.  For example, if the
793
            method name is ``create_foo``, and you'd normally invoke the
794
            operation as ``client.create_foo(**kwargs)``, if the
795
            ``create_foo`` operation can be paginated, you can use the
796
            call ``client.get_paginator("create_foo")``.
797

798
        :return: ``True`` if the operation can be paginated,
799
            ``False`` otherwise.
800

801
        """
802 10
        if 'page_config' not in self._cache:
803 10
            try:
804 10
                page_config = self._loader.load_service_model(
805
                    self._service_model.service_name,
806
                    'paginators-1',
807
                    self._service_model.api_version)['pagination']
808 10
                self._cache['page_config'] = page_config
809 10
            except DataNotFoundError:
810 10
                self._cache['page_config'] = {}
811 10
        actual_operation_name = self._PY_TO_OP_NAME[operation_name]
812 10
        return actual_operation_name in self._cache['page_config']
813

814 10
    def _get_waiter_config(self):
815 10
        if 'waiter_config' not in self._cache:
816 10
            try:
817 10
                waiter_config = self._loader.load_service_model(
818
                    self._service_model.service_name,
819
                    'waiters-2',
820
                    self._service_model.api_version)
821 10
                self._cache['waiter_config'] = waiter_config
822 10
            except DataNotFoundError:
823 10
                self._cache['waiter_config'] = {}
824 10
        return self._cache['waiter_config']
825

826 10
    def get_waiter(self, waiter_name):
827
        """Returns an object that can wait for some condition.
828

829
        :type waiter_name: str
830
        :param waiter_name: The name of the waiter to get. See the waiters
831
            section of the service docs for a list of available waiters.
832

833
        :returns: The specified waiter object.
834
        :rtype: botocore.waiter.Waiter
835
        """
836 10
        config = self._get_waiter_config()
837 10
        if not config:
838 10
            raise ValueError("Waiter does not exist: %s" % waiter_name)
839 10
        model = waiter.WaiterModel(config)
840 10
        mapping = {}
841 10
        for name in model.waiter_names:
842 10
            mapping[xform_name(name)] = name
843 10
        if waiter_name not in mapping:
844 0
            raise ValueError("Waiter does not exist: %s" % waiter_name)
845

846 10
        return waiter.create_waiter_with_client(
847
            mapping[waiter_name], model, self)
848

849 10
    @CachedProperty
850 2
    def waiter_names(self):
851
        """Returns a list of all available waiters."""
852 10
        config = self._get_waiter_config()
853 10
        if not config:
854 10
            return []
855 10
        model = waiter.WaiterModel(config)
856
        # Waiter configs is a dict, we just want the waiter names
857
        # which are the keys in the dict.
858 10
        return [xform_name(name) for name in model.waiter_names]
859

860 10
    @property
861 2
    def exceptions(self):
862 10
        if self._exceptions is None:
863 10
            self._exceptions = self._load_exceptions()
864 10
        return self._exceptions
865

866 10
    def _load_exceptions(self):
867 10
        return self._exceptions_factory.create_client_exceptions(
868
            self._service_model)
869

870

871 10
class ClientMeta(object):
872
    """Holds additional client methods.
873

874
    This class holds additional information for clients.  It exists for
875
    two reasons:
876

877
        * To give advanced functionality to clients
878
        * To namespace additional client attributes from the operation
879
          names which are mapped to methods at runtime.  This avoids
880
          ever running into collisions with operation names.
881

882
    """
883

884 10
    def __init__(self, events, client_config, endpoint_url, service_model,
885
                 method_to_api_mapping, partition):
886 10
        self.events = events
887 10
        self._client_config = client_config
888 10
        self._endpoint_url = endpoint_url
889 10
        self._service_model = service_model
890 10
        self._method_to_api_mapping = method_to_api_mapping
891 10
        self._partition = partition
892

893 10
    @property
894 2
    def service_model(self):
895 10
        return self._service_model
896

897 10
    @property
898 2
    def region_name(self):
899 10
        return self._client_config.region_name
900

901 10
    @property
902 2
    def endpoint_url(self):
903 10
        return self._endpoint_url
904

905 10
    @property
906 2
    def config(self):
907 10
        return self._client_config
908

909 10
    @property
910 2
    def method_to_api_mapping(self):
911 10
        return self._method_to_api_mapping
912

913 10
    @property
914 2
    def partition(self):
915 10
        return self._partition
916

917

918 10
def _get_configured_signature_version(service_name, client_config,
919
                                      scoped_config):
920
    """
921
    Gets the manually configured signature version.
922

923
    :returns: the customer configured signature version, or None if no
924
        signature version was configured.
925
    """
926
    # Client config overrides everything.
927 10
    if client_config and client_config.signature_version is not None:
928 10
        return client_config.signature_version
929

930
    # Scoped config overrides picking from the endpoint metadata.
931 10
    if scoped_config is not None:
932
        # A given service may have service specific configuration in the
933
        # config file, so we need to check there as well.
934 10
        service_config = scoped_config.get(service_name)
935 10
        if service_config is not None and isinstance(service_config, dict):
936 10
            version = service_config.get('signature_version')
937 10
            if version:
938 10
                logger.debug(
939
                    "Switching signature version for service %s "
940
                    "to version %s based on config file override.",
941
                    service_name, version)
942 10
                return version
943 10
    return None

Read our documentation on viewing source code .

Loading