Repository: boto/botocore
Pull request #743 (2015-12-17 22:19:11)

Use before-call event to allow short-circuiting the endpoint call.

kyleknap: The code change looks fine. The only issue that I can forsee is that if we ever realize that we want to exercise more of stack (the endpoint/retry logic) before we short-circuit the sending of the request to the http session we may muddy the code more with event-based if statement trees. So @jamesls @mtdowling @rayluo are we sure using the ``before-call`` event is the direction we want to go to short-circuit the sending of the request over the wire?
jamesls: I don't think it's necessarily bad if in the future we end up wanting to allow customers to drop down to the http layer and expose some event in endpoints.  Right now, using this event is the lowest level we have before dropping down to endpoints/http and it doesn't require adding any new events.  It also allows libraries such as @garnaat's placebo to use entirely public APIs to achieve their desired functionality.

The only thing this doesn't allow customers to do is to specify HTTP responses.  I don't really see that being all that common of a use case for customers (it's arguable more interesting for us botocore devs), I'd prefer to be very cautious about not exposing internals of either the Endpoint class or the Request/Response classes in emitted events in such a way that we can't refactor the internals in the future because of backwards compatibility concerns.  This is why I'd suggest holding off on adding events to endpoints.py for now until we feel more confident about the endpoint and http interfaces.

So for me, I'm inclined to :shipit: as is.
garnaat: FWIW, this looks pretty much ideal for what I was looking for.  As long as I can inject the response data and an HTTP response code (which I think I can do with this) that would solve all of my problems.
kyleknap: I agree that we should not expose any of the internals of the endpoint. So I guess the way I was seeing it was that the arguments emitted in this event would not expose any of the internals of the endpoint, but I have not really thought too much about how that would look.

I do feel that we will need to support the endpoint event at some point for whatever reason, but I guess for now I am fine with making the ``before-call`` event a switch as I can see it being it intuitive that if you return something from the ``before-call`` event, botocore would use that instead of making the request.
kyleknap: :ship:

Patch:
diff --git a/botocore/client.py b/botocore/client.py
index d66d8a489..96119cb9c 100644
--- a/botocore/client.py
+++ b/botocore/client.py
@@ -381,8 +381,19 @@ class BaseClient(object):
         operation_model = self._service_model.operation_model(operation_name)
         request_dict = self._convert_to_request_dict(
             api_params, operation_model, context=request_context)
-        http, parsed_response = self._endpoint.make_request(
-            operation_model, request_dict)
+
+        handler, event_response = self.meta.events.emit_until_response(
+            'before-call.{endpoint_prefix}.{operation_name}'.format(
+                endpoint_prefix=self._service_model.endpoint_prefix,
+                operation_name=operation_name),
+            model=operation_model, params=request_dict,
+            request_signer=self._request_signer, context=request_context)
+
+        if event_response is not None:
+            http, parsed_response = event_response
+        else:
+            http, parsed_response = self._endpoint.make_request(
+                operation_model, request_dict)
 
         self.meta.events.emit(
             'after-call.{endpoint_prefix}.{operation_name}'.format(
@@ -425,13 +436,6 @@ class BaseClient(object):
             api_params, operation_model)
         prepare_request_dict(request_dict, endpoint_url=self._endpoint.host,
                              user_agent=self._client_config.user_agent)
-        self.meta.events.emit(
-            'before-call.{endpoint_prefix}.{operation_name}'.format(
-                endpoint_prefix=self._service_model.endpoint_prefix,
-                operation_name=operation_name),
-            model=operation_model, params=request_dict,
-            request_signer=self._request_signer, context=context
-        )
         return request_dict
 
     def get_paginator(self, operation_name):


Test patch:
diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py
index 29e158b8b..a26b43b41 100644
--- a/tests/unit/test_client.py
+++ b/tests/unit/test_client.py
@@ -1109,6 +1109,20 @@ class TestAutoGeneratedClient(unittest.TestCase):
         call_kwargs = self.endpoint_creator.create_endpoint.call_args[1]
         self.assertEqual(call_kwargs['verify'], 'verify.pem')
 
+    def test_before_call_short_circuits_request(self):
+        def return_mock_tuple(**kwargs):
+            http_mock = mock.Mock()
+            http_mock.status_code = 200
+            return http_mock, mock.Mock()
+        emitter = hooks.HierarchicalEmitter()
+        emitter.register_last('before-call.*.*', return_mock_tuple)
+        creator = self.create_client_creator(event_emitter=emitter)
+        service_client = creator.create_client(
+            'myservice', 'us-west-2')
+
+        service_client.test_operation(Foo='one')
+        self.assertFalse(self.endpoint.make_request.called)
+
 
 class TestConfig(unittest.TestCase):
     def test_can_use_args_to_construct(self):