about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/botocore/waiter.py
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/botocore/waiter.py')
-rw-r--r--.venv/lib/python3.12/site-packages/botocore/waiter.py396
1 files changed, 396 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/botocore/waiter.py b/.venv/lib/python3.12/site-packages/botocore/waiter.py
new file mode 100644
index 00000000..5c1e8f1c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/botocore/waiter.py
@@ -0,0 +1,396 @@
+# Copyright 2012-2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"). You
+# may not use this file except in compliance with the License. A copy of
+# the License is located at
+#
+# http://aws.amazon.com/apache2.0/
+#
+# or in the "license" file accompanying this file. This file is
+# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
+# ANY KIND, either express or implied. See the License for the specific
+# language governing permissions and limitations under the License.
+import logging
+import time
+from functools import partial
+
+import jmespath
+
+from botocore.context import with_current_context
+from botocore.docs.docstring import WaiterDocstring
+from botocore.useragent import register_feature_id
+from botocore.utils import get_service_module_name
+
+from . import xform_name
+from .exceptions import ClientError, WaiterConfigError, WaiterError
+
+logger = logging.getLogger(__name__)
+
+
+def create_waiter_with_client(waiter_name, waiter_model, client):
+    """
+
+    :type waiter_name: str
+    :param waiter_name: The name of the waiter.  The name should match
+        the name (including the casing) of the key name in the waiter
+        model file (typically this is CamelCasing).
+
+    :type waiter_model: botocore.waiter.WaiterModel
+    :param waiter_model: The model for the waiter configuration.
+
+    :type client: botocore.client.BaseClient
+    :param client: The botocore client associated with the service.
+
+    :rtype: botocore.waiter.Waiter
+    :return: The waiter object.
+
+    """
+    single_waiter_config = waiter_model.get_waiter(waiter_name)
+    operation_name = xform_name(single_waiter_config.operation)
+    operation_method = NormalizedOperationMethod(
+        getattr(client, operation_name)
+    )
+
+    # Create a new wait method that will serve as a proxy to the underlying
+    # Waiter.wait method. This is needed to attach a docstring to the
+    # method.
+    def wait(self, **kwargs):
+        Waiter.wait(self, **kwargs)
+
+    wait.__doc__ = WaiterDocstring(
+        waiter_name=waiter_name,
+        event_emitter=client.meta.events,
+        service_model=client.meta.service_model,
+        service_waiter_model=waiter_model,
+        include_signature=False,
+    )
+
+    # Rename the waiter class based on the type of waiter.
+    waiter_class_name = str(
+        f'{get_service_module_name(client.meta.service_model)}.Waiter.{waiter_name}'
+    )
+
+    # Create the new waiter class
+    documented_waiter_cls = type(waiter_class_name, (Waiter,), {'wait': wait})
+
+    # Return an instance of the new waiter class.
+    return documented_waiter_cls(
+        waiter_name, single_waiter_config, operation_method
+    )
+
+
+def is_valid_waiter_error(response):
+    error = response.get('Error')
+    if isinstance(error, dict) and 'Code' in error:
+        return True
+    return False
+
+
+class NormalizedOperationMethod:
+    def __init__(self, client_method):
+        self._client_method = client_method
+
+    def __call__(self, **kwargs):
+        try:
+            return self._client_method(**kwargs)
+        except ClientError as e:
+            return e.response
+
+
+class WaiterModel:
+    SUPPORTED_VERSION = 2
+
+    def __init__(self, waiter_config):
+        """
+
+        Note that the WaiterModel takes ownership of the waiter_config.
+        It may or may not mutate the waiter_config.  If this is a concern,
+        it is best to make a copy of the waiter config before passing it to
+        the WaiterModel.
+
+        :type waiter_config: dict
+        :param waiter_config: The loaded waiter config
+            from the <service>*.waiters.json file.  This can be
+            obtained from a botocore Loader object as well.
+
+        """
+        self._waiter_config = waiter_config['waiters']
+
+        # These are part of the public API.  Changing these
+        # will result in having to update the consuming code,
+        # so don't change unless you really need to.
+        version = waiter_config.get('version', 'unknown')
+        self._verify_supported_version(version)
+        self.version = version
+        self.waiter_names = list(sorted(waiter_config['waiters'].keys()))
+
+    def _verify_supported_version(self, version):
+        if version != self.SUPPORTED_VERSION:
+            raise WaiterConfigError(
+                error_msg=(
+                    "Unsupported waiter version, supported version "
+                    f"must be: {self.SUPPORTED_VERSION}, but version "
+                    f"of waiter config is: {version}"
+                )
+            )
+
+    def get_waiter(self, waiter_name):
+        try:
+            single_waiter_config = self._waiter_config[waiter_name]
+        except KeyError:
+            raise ValueError(f"Waiter does not exist: {waiter_name}")
+        return SingleWaiterConfig(single_waiter_config)
+
+
+class SingleWaiterConfig:
+    """Represents the waiter configuration for a single waiter.
+
+    A single waiter is considered the configuration for a single
+    value associated with a named waiter (i.e TableExists).
+
+    """
+
+    def __init__(self, single_waiter_config):
+        self._config = single_waiter_config
+
+        # These attributes are part of the public API.
+        self.description = single_waiter_config.get('description', '')
+        # Per the spec, these three fields are required.
+        self.operation = single_waiter_config['operation']
+        self.delay = single_waiter_config['delay']
+        self.max_attempts = single_waiter_config['maxAttempts']
+
+    @property
+    def acceptors(self):
+        acceptors = []
+        for acceptor_config in self._config['acceptors']:
+            acceptor = AcceptorConfig(acceptor_config)
+            acceptors.append(acceptor)
+        return acceptors
+
+
+class AcceptorConfig:
+    def __init__(self, config):
+        self.state = config['state']
+        self.matcher = config['matcher']
+        self.expected = config['expected']
+        self.argument = config.get('argument')
+        self.matcher_func = self._create_matcher_func()
+
+    @property
+    def explanation(self):
+        if self.matcher == 'path':
+            return f'For expression "{self.argument}" we matched expected path: "{self.expected}"'
+        elif self.matcher == 'pathAll':
+            return (
+                f'For expression "{self.argument}" all members matched '
+                f'expected path: "{self.expected}"'
+            )
+        elif self.matcher == 'pathAny':
+            return (
+                f'For expression "{self.argument}" we matched expected '
+                f'path: "{self.expected}" at least once'
+            )
+        elif self.matcher == 'status':
+            return f'Matched expected HTTP status code: {self.expected}'
+        elif self.matcher == 'error':
+            return f'Matched expected service error code: {self.expected}'
+        else:
+            return f'No explanation for unknown waiter type: "{self.matcher}"'
+
+    def _create_matcher_func(self):
+        # An acceptor function is a callable that takes a single value.  The
+        # parsed AWS response.  Note that the parsed error response is also
+        # provided in the case of errors, so it's entirely possible to
+        # handle all the available matcher capabilities in the future.
+        # There's only three supported matchers, so for now, this is all
+        # contained to a single method.  If this grows, we can expand this
+        # out to separate methods or even objects.
+
+        if self.matcher == 'path':
+            return self._create_path_matcher()
+        elif self.matcher == 'pathAll':
+            return self._create_path_all_matcher()
+        elif self.matcher == 'pathAny':
+            return self._create_path_any_matcher()
+        elif self.matcher == 'status':
+            return self._create_status_matcher()
+        elif self.matcher == 'error':
+            return self._create_error_matcher()
+        else:
+            raise WaiterConfigError(
+                error_msg=f"Unknown acceptor: {self.matcher}"
+            )
+
+    def _create_path_matcher(self):
+        expression = jmespath.compile(self.argument)
+        expected = self.expected
+
+        def acceptor_matches(response):
+            if is_valid_waiter_error(response):
+                return
+            return expression.search(response) == expected
+
+        return acceptor_matches
+
+    def _create_path_all_matcher(self):
+        expression = jmespath.compile(self.argument)
+        expected = self.expected
+
+        def acceptor_matches(response):
+            if is_valid_waiter_error(response):
+                return
+            result = expression.search(response)
+            if not isinstance(result, list) or not result:
+                # pathAll matcher must result in a list.
+                # Also we require at least one element in the list,
+                # that is, an empty list should not result in this
+                # acceptor match.
+                return False
+            for element in result:
+                if element != expected:
+                    return False
+            return True
+
+        return acceptor_matches
+
+    def _create_path_any_matcher(self):
+        expression = jmespath.compile(self.argument)
+        expected = self.expected
+
+        def acceptor_matches(response):
+            if is_valid_waiter_error(response):
+                return
+            result = expression.search(response)
+            if not isinstance(result, list) or not result:
+                # pathAny matcher must result in a list.
+                # Also we require at least one element in the list,
+                # that is, an empty list should not result in this
+                # acceptor match.
+                return False
+            for element in result:
+                if element == expected:
+                    return True
+            return False
+
+        return acceptor_matches
+
+    def _create_status_matcher(self):
+        expected = self.expected
+
+        def acceptor_matches(response):
+            # We don't have any requirements on the expected incoming data
+            # other than it is a dict, so we don't assume there's
+            # a ResponseMetadata.HTTPStatusCode.
+            status_code = response.get('ResponseMetadata', {}).get(
+                'HTTPStatusCode'
+            )
+            return status_code == expected
+
+        return acceptor_matches
+
+    def _create_error_matcher(self):
+        expected = self.expected
+
+        def acceptor_matches(response):
+            # When the client encounters an error, it will normally raise
+            # an exception.  However, the waiter implementation will catch
+            # this exception, and instead send us the parsed error
+            # response.  So response is still a dictionary, and in the case
+            # of an error response will contain the "Error" and
+            # "ResponseMetadata" key.
+            # When expected is True, accept any error code.
+            # When expected is False, check if any errors were encountered.
+            # Otherwise, check for a specific AWS error code.
+            if expected is True:
+                return "Error" in response and "Code" in response["Error"]
+            elif expected is False:
+                return "Error" not in response
+            else:
+                return response.get("Error", {}).get("Code", "") == expected
+
+        return acceptor_matches
+
+
+class Waiter:
+    def __init__(self, name, config, operation_method):
+        """
+
+        :type name: string
+        :param name: The name of the waiter
+
+        :type config: botocore.waiter.SingleWaiterConfig
+        :param config: The configuration for the waiter.
+
+        :type operation_method: callable
+        :param operation_method: A callable that accepts **kwargs
+            and returns a response.  For example, this can be
+            a method from a botocore client.
+
+        """
+        self._operation_method = operation_method
+        # The two attributes are exposed to allow for introspection
+        # and documentation.
+        self.name = name
+        self.config = config
+
+    @with_current_context(partial(register_feature_id, 'WAITER'))
+    def wait(self, **kwargs):
+        acceptors = list(self.config.acceptors)
+        current_state = 'waiting'
+        # pop the invocation specific config
+        config = kwargs.pop('WaiterConfig', {})
+        sleep_amount = config.get('Delay', self.config.delay)
+        max_attempts = config.get('MaxAttempts', self.config.max_attempts)
+        last_matched_acceptor = None
+        num_attempts = 0
+
+        while True:
+            response = self._operation_method(**kwargs)
+            num_attempts += 1
+            for acceptor in acceptors:
+                if acceptor.matcher_func(response):
+                    last_matched_acceptor = acceptor
+                    current_state = acceptor.state
+                    break
+            else:
+                # If none of the acceptors matched, we should
+                # transition to the failure state if an error
+                # response was received.
+                if is_valid_waiter_error(response):
+                    # Transition to a failure state, which we
+                    # can just handle here by raising an exception.
+                    raise WaiterError(
+                        name=self.name,
+                        reason='An error occurred ({}): {}'.format(
+                            response['Error'].get('Code', 'Unknown'),
+                            response['Error'].get('Message', 'Unknown'),
+                        ),
+                        last_response=response,
+                    )
+            if current_state == 'success':
+                logger.debug(
+                    "Waiting complete, waiter matched the " "success state."
+                )
+                return
+            if current_state == 'failure':
+                reason = f'Waiter encountered a terminal failure state: {acceptor.explanation}'
+                raise WaiterError(
+                    name=self.name,
+                    reason=reason,
+                    last_response=response,
+                )
+            if num_attempts >= max_attempts:
+                if last_matched_acceptor is None:
+                    reason = 'Max attempts exceeded'
+                else:
+                    reason = (
+                        f'Max attempts exceeded. Previously accepted state: '
+                        f'{acceptor.explanation}'
+                    )
+                raise WaiterError(
+                    name=self.name,
+                    reason=reason,
+                    last_response=response,
+                )
+            time.sleep(sleep_amount)