def __call__(self, action_request): response_body = self.run(action_request) if response_body is not None: return ActionResponse(action=action_request.action, body=response_body) else: return ActionResponse(action=action_request.action)
def execute_job(self, job_request): """ Processes and runs the ActionRequests on the Job. """ # Run the Job's Actions job_response = JobResponse() job_switches = RequestSwitchSet(job_request['context']['switches']) for i, raw_action_request in enumerate(job_request['actions']): action_request = EnrichedActionRequest( action=raw_action_request['action'], body=raw_action_request.get('body', None), switches=job_switches, context=job_request['context'], control=job_request['control'], client=job_request['client'], ) if action_request.action in self.action_class_map: # Get action to run action = self.action_class_map[action_request.action]( self.settings) # Wrap it in middleware wrapper = self.make_middleware_stack( [m.action for m in self.middleware], action, ) # Execute the middleware stack try: action_response = wrapper(action_request) except ActionError as e: # Error: an error was thrown while running the Action (or Action middleware) action_response = ActionResponse( action=action_request.action, errors=e.errors, ) else: # Error: Action not found. action_response = ActionResponse( action=action_request.action, errors=[ Error( code=ERROR_CODE_UNKNOWN, message= 'The action "{}" was not found on this server.'. format(action_request.action), field='action', ) ], ) job_response.actions.append(action_response) if (action_response.errors and not job_request['control'].get( 'continue_on_error', False)): # Quit running Actions if an error occurred and continue_on_error is False break return job_response
def __call__( self, action_request): # type: (EnrichedActionRequest) -> ActionResponse response_body = self.run(action_request) if response_body is not None: return ActionResponse(action=action_request.action, body=response_body) else: return ActionResponse(action=action_request.action)
def test_raises_only_error_codes_unexpected(test_error, auth_missing_error): errors = [test_error, auth_missing_error] with pytest.raises(pytest.raises.Exception): with raises_only_error_codes(['AUTH_MISSING']): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] )
def test_raises_error_codes_on_match(test_error): errors = [test_error] with raises_error_codes('TEST') as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)]) assert exc_info.soa_errors == errors
def test_raises_call_action_error_on_error(test_error): errors = [test_error] with raises_call_action_error() as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)]) assert exc_info.soa_errors == errors
def action_logic( request): # type: (EnrichedActionRequest) -> ActionResponse return ActionResponse(action='one', body={ 'animal_response': request.body['animal'], 'settings': settings })
def test_raises_field_errors_on_match(invalid_event_id_field_error): errors = [invalid_event_id_field_error] with raises_field_errors({'event_id': 'INVALID'}) as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)]) assert exc_info.soa_errors == errors
def test_raises_error_only_codes_unexpected_missing(test_error, auth_missing_error): errors = [test_error, auth_missing_error] with pytest.raises(pytest.raises.Exception): with raises_only_error_codes('UNAUTHORIZED'): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)])
def test_raises_error_only_codes_unexpected_field_error( invalid_event_id_field_error, auth_missing_error): errors = [invalid_event_id_field_error, auth_missing_error] with pytest.raises(pytest.raises.Exception): with raises_only_error_codes('AUTH_MISSING'): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)])
def test_raises_only_error_codes_match(test_error, auth_missing_error): errors = [test_error, auth_missing_error] with raises_only_error_codes(['AUTH_MISSING', 'TEST']) as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)]) assert exc_info.soa_errors == errors
def test_raises_error_codes_missing(codes): errors = [Error(code=code, message='bam') for code in codes] with pytest.raises(pytest.raises.Exception): with raises_error_codes(['AUTH_MISSING']): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] )
def test_two_stubs_with_parallel_calls_and_action_response_errors_not_raised( self, stub_test_action_1, stub_test_action_2, ): stub_test_action_1.return_value = ActionResponse( action='test_action_1', errors=[Error(code='BAD_ACTION', message='You are a bad actor')], ) job_responses = self.client.call_jobs_parallel( [ {'service_name': 'test_service', 'actions': [{'action': 'test_action_1', 'body': {'a': 'b'}}]}, {'service_name': 'test_service', 'actions': [{'action': 'test_action_2', 'body': {'c': 'd'}}]}, ], raise_action_errors=False, ) self.assertIsNotNone(job_responses) self.assertEqual(2, len(job_responses)) self.assertEqual(1, len(job_responses[0].actions)) self.assertEqual([Error(code='BAD_ACTION', message='You are a bad actor')], job_responses[0].actions[0].errors) self.assertEqual(1, len(job_responses[1].actions)) self.assertEqual({'three': 'four'}, job_responses[1].actions[0].body) stub_test_action_1.assert_called_once_with({'a': 'b'}) stub_test_action_2.assert_called_once_with({'c': 'd'})
def test_raises_error_codes_multiple(codes): errors = [Error(code=code, message='bam') for code in codes] with raises_error_codes(['TEST', 'AUTH_MISSING']) as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)]) assert exc_info.soa_errors == errors
def test_raises_field_errors_missing(code, field): errors = [ Error(code=code, message='test fail', field=field), ] with pytest.raises(pytest.raises.Exception): with raises_field_errors({'event_id': 'UNKNOWN'}): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)])
def test_raises_field_errors_match_multiple(codes): errors = [Error(code=code, field=field, message='bam') for field, code in codes] with raises_field_errors({'event_id': 'UNKNOWN', 'organization_id': 'INVALID'}) as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] ) assert exc_info.soa_errors == errors
def __call__(self, action_request): # type: (EnrichedActionRequest) -> ActionResponse """ Main entry point for actions from the `Server` (or potentially from tests). Validates that the request matches the `request_schema`, then calls `validate()`, then calls `run()` if `validate()` raised no errors, and then validates that the return value from `run()` matches the `response_schema` before returning it in an `ActionResponse`. :param action_request: The request object :return: The response object :raise: ActionError, ResponseValidationError """ # Validate the request if self.request_schema: errors = [ Error( code=error.code, message=error.message, field=error.pointer, is_caller_error=True, ) for error in (self.request_schema.errors(action_request.body) or []) ] if errors: raise ActionError(errors=errors, set_is_caller_error_to=None) # Run any custom validation self.validate(action_request) # Run the body of the action response_body = self.run(action_request) # Validate the response body. Errors in a response are the problem of # the service, and so we just raise a Python exception and let error # middleware catch it. The server will return a SERVER_ERROR response. if self.response_schema: conformity_errors = self.response_schema.errors(response_body) if conformity_errors: raise ResponseValidationError(action=action_request.action, errors=conformity_errors) # Make an ActionResponse and return it if response_body is not None: return ActionResponse( action=action_request.action, body=response_body, ) else: return ActionResponse(action=action_request.action)
class ErrorServer(Server): service_name = 'error_service' # noinspection PyTypeChecker action_class_map = { 'job_error': lambda *_, **__: (_ for _ in ()).throw( JobError(errors=[Error(code='BAD_JOB', message='You are a bad job')]) ), 'okay_action': lambda *_, **__: lambda *_, **__: ActionResponse(action='okay_action', body={'no_error': True}), }
def test_raises_field_errors_unexpected_only(invalid_event_id_field_error, unknown_event_id_field_error): errors = [ invalid_event_id_field_error, unknown_event_id_field_error, ] with pytest.raises(pytest.raises.Exception): with raises_field_errors({'event_id': ['UNKNOWN']}, only=True): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)])
def test_raises_only_field_errors_unexpected_error( auth_missing_error, invalid_organization_id_field_error): errors = [ auth_missing_error, invalid_organization_id_field_error, ] with pytest.raises(pytest.raises.Exception): with raises_only_field_errors({'organization_id': 'INVALID'}): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)])
def test_raises_only_field_errors_unexpected_missing( unknown_event_id_field_error, invalid_organization_id_field_error): errors = [ unknown_event_id_field_error, invalid_organization_id_field_error, ] with pytest.raises(pytest.raises.Exception): with raises_only_field_errors({'event_id': 'MISSING'}): raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)])
def test_raises_only_field_errors_match(invalid_event_id_field_error, unknown_event_id_field_error): errors = [ invalid_event_id_field_error, unknown_event_id_field_error, ] with raises_only_field_errors({'event_id': ['UNKNOWN', 'INVALID']}) as exc_info: raise Client.CallActionError( actions=[ActionResponse(action='', errors=errors)] ) assert exc_info.soa_errors == errors
def test_call_local_action_super_enriched_request(self): action = mock.MagicMock() action.return_value.return_value = ActionResponse( action='another_foo', body={'sweet': 'success'}) server = mock.MagicMock() server.settings = {'a_setting': 'a_value'} server.action_class_map = {'another_foo': action} logger = logging.getLogger('test') r = SuperEnrichedActionRequest( action='foo', body={'bar': 'baz'}, context={ 'auth_token': 'def456', 'auth': 'original' }, control={'repeat': True}, metrics='A custom object', analytics_logger=logger, ) r._server = server response = r.call_local_action('another_foo', {'entity_id': '1a8t27oh'}, context={ 'auth': 'new', 'foo': 'bar' }) assert response.action == 'another_foo' assert response.body == {'sweet': 'success'} action.assert_called_once_with(server.settings) assert action.return_value.call_count == 1 other_r = action.return_value.call_args[0][0] assert isinstance(other_r, SuperEnrichedActionRequest) assert other_r is not r assert other_r != r assert other_r.action == 'another_foo' assert other_r.body == {'entity_id': '1a8t27oh'} assert other_r.context == { 'auth_token': 'def456', 'auth': 'new', 'foo': 'bar' } assert other_r.control == {'repeat': True} assert other_r.metrics == 'A custom object' assert other_r.analytics_logger is logger assert getattr(other_r, '_server') is server
def test_call_local_action_other_request_details(self): action = mock.MagicMock() action.return_value.return_value = ActionResponse( action='another_foo', errors=[Error('BAR', 'Bar error')]) server = mock.MagicMock() server.settings = {'a_setting': 'a_value'} server.action_class_map = {'another_foo': action} r = EnrichedActionRequest(action='foo', body={'bar': 'baz'}, context={'auth': 'abc123'}, control={'speed': 5}) r._server = server with pytest.raises(ActionError) as error_context: r.call_local_action('another_foo', {'height': '10m'}) assert error_context.value.errors[0].code == 'BAR' action.assert_called_once_with(server.settings) assert action.return_value.call_count == 1 other_r = action.return_value.call_args[0][0] assert other_r is not r assert other_r != r assert other_r.action == 'another_foo' assert other_r.body == {'height': '10m'} assert other_r.context == {'auth': 'abc123'} assert other_r.control == {'speed': 5} assert getattr(other_r, '_server') is server action.reset_mock() response = r.call_local_action('another_foo', {'height': '10m'}, raise_action_errors=False) assert response.action == 'another_foo' assert response.errors assert response.errors[0].code == 'BAR' action.assert_called_once_with(server.settings) assert action.return_value.call_count == 1 other_r = action.return_value.call_args[0][0] assert other_r is not r assert other_r != r assert other_r.action == 'another_foo' assert other_r.body == {'height': '10m'} assert other_r.context == {'auth': 'abc123'} assert other_r.control == {'speed': 5} assert getattr(other_r, '_server') is server
def __call__(self, action_request): """ Main entry point for Actions from the Server (or potentially from tests) """ # Validate the request if self.request_schema: errors = [ Error( code=ERROR_CODE_INVALID, message=error.message, field=error.pointer, ) for error in ( self.request_schema.errors(action_request.body) or []) ] if errors: raise ActionError(errors=errors) # Run any custom validation self.validate(action_request) # Run the body of the action response_body = self.run(action_request) # Validate the response body. Errors in a response are the problem of # the service, and so we just raise a Python exception and let error # middleware catch it. The server will return a SERVER_ERROR response. if self.response_schema: errors = self.response_schema.errors(response_body) if errors: raise ResponseValidationError(action=action_request.action, errors=errors) # Make an ActionResponse and return it if response_body is not None: return ActionResponse( action=action_request.action, body=response_body, ) else: return ActionResponse(action=action_request.action)
def execute_job(self, job_request): """ Processes and runs the action requests contained in the job and returns a `JobResponse`. :param job_request: The job request :type job_request: dict :return: A `JobResponse` object :rtype: JobResponse """ # Run the Job's Actions job_response = JobResponse() job_switches = RequestSwitchSet(job_request['context']['switches']) for i, raw_action_request in enumerate(job_request['actions']): action_request = EnrichedActionRequest( action=raw_action_request['action'], body=raw_action_request.get('body', None), switches=job_switches, context=job_request['context'], control=job_request['control'], client=job_request['client'], async_event_loop=job_request['async_event_loop'], ) action_in_class_map = action_request.action in self.action_class_map if action_in_class_map or action_request.action in ('status', 'introspect'): # Get action to run if action_in_class_map: action = self.action_class_map[action_request.action](self.settings) elif action_request.action == 'introspect': from pysoa.server.action.introspection import IntrospectionAction action = IntrospectionAction(server=self) else: if not self._default_status_action_class: from pysoa.server.action.status import make_default_status_action_class self._default_status_action_class = make_default_status_action_class(self.__class__) action = self._default_status_action_class(self.settings) # Wrap it in middleware wrapper = self.make_middleware_stack( [m.action for m in self.middleware], action, ) # Execute the middleware stack try: action_response = wrapper(action_request) except ActionError as e: # Error: an error was thrown while running the Action (or Action middleware) action_response = ActionResponse( action=action_request.action, errors=e.errors, ) else: # Error: Action not found. action_response = ActionResponse( action=action_request.action, errors=[Error( code=ERROR_CODE_UNKNOWN, message='The action "{}" was not found on this server.'.format(action_request.action), field='action', )], ) job_response.actions.append(action_response) if ( action_response.errors and not job_request['control'].get('continue_on_error', False) ): # Quit running Actions if an error occurred and continue_on_error is False break return job_response
def wrapped_send_request(client, service_name, actions, *args, **kwargs): assert isinstance( service_name, six.text_type ), 'Called service name "{}" must be unicode'.format( service_name, ) actions_to_send_to_mock = OrderedDict() actions_to_send_to_wrapped_client = [] for i, action_request in enumerate(actions): action_name = getattr(action_request, 'action', None) or action_request['action'] assert isinstance( action_name, six.text_type ), 'Called action name "{}" must be unicode'.format( action_name, ) if service_name == self.service and action_name == self.action: # If the service AND action name match, we should send the request to our mocked client if not isinstance(action_request, ActionRequest): action_request = ActionRequest(**action_request) actions_to_send_to_mock[i] = action_request else: # If the service OR action name DO NOT match, we should delegate the request to the wrapped client actions_to_send_to_wrapped_client.append(action_request) request_id = _global_stub_action_request_counter.get_next() continue_on_error = kwargs.get('continue_on_error', False) if actions_to_send_to_wrapped_client: # If any un-stubbed actions need to be sent to the original client, send them self._services_with_calls_sent_to_wrapped_client.add( service_name) unwrapped_request_id = self._wrapped_client_send_request( client, service_name, actions_to_send_to_wrapped_client, *args, **kwargs) if not actions_to_send_to_mock: # If there are no stubbed actions to mock, just return the un-stubbed request ID return unwrapped_request_id self._stub_action_responses_to_merge[service_name][ unwrapped_request_id] = ( request_id, continue_on_error, ) ordered_actions_for_merging = OrderedDict() job_response_transport_exception = None job_response = JobResponse() for i, action_request in actions_to_send_to_mock.items(): mock_response = None try: mock_response = mock_action(action_request.body or {}) if isinstance(mock_response, JobResponse): job_response.errors.extend(mock_response.errors) if mock_response.actions: mock_response = mock_response.actions[0] elif isinstance(mock_response, dict): mock_response = ActionResponse(self.action, body=mock_response) elif not isinstance(mock_response, ActionResponse): mock_response = ActionResponse(self.action) except ActionError as e: mock_response = ActionResponse(self.action, errors=e.errors) except JobError as e: job_response.errors.extend(e.errors) except (MessageReceiveError, MessageReceiveTimeout) as e: job_response_transport_exception = e if mock_response: ordered_actions_for_merging[i] = mock_response job_response.actions.append(mock_response) if not continue_on_error and mock_response.errors: break if job_response.errors: break if actions_to_send_to_wrapped_client: # If the responses will have to be merged by get_all_responses, replace the list with the ordered dict job_response.actions = ordered_actions_for_merging self._stub_action_responses_outstanding[service_name][ request_id] = (job_response_transport_exception or job_response) return request_id
def __enter__(self): self._wrapped_client_send_request = Client.send_request self._wrapped_client_get_all_responses = Client.get_all_responses self._services_with_calls_sent_to_wrapped_client = set() mock_action = self._MockAction( name='{}.{}'.format(self.service, self.action)) if self.body or self.errors: mock_action.side_effect = lambda _: ActionResponse( self.action, errors=self.errors, body=self.body) @wraps(Client.send_request) def wrapped_send_request(client, service_name, actions, *args, **kwargs): assert isinstance( service_name, six.text_type ), 'Called service name "{}" must be unicode'.format( service_name, ) actions_to_send_to_mock = OrderedDict() actions_to_send_to_wrapped_client = [] for i, action_request in enumerate(actions): action_name = getattr(action_request, 'action', None) or action_request['action'] assert isinstance( action_name, six.text_type ), 'Called action name "{}" must be unicode'.format( action_name, ) if service_name == self.service and action_name == self.action: # If the service AND action name match, we should send the request to our mocked client if not isinstance(action_request, ActionRequest): action_request = ActionRequest(**action_request) actions_to_send_to_mock[i] = action_request else: # If the service OR action name DO NOT match, we should delegate the request to the wrapped client actions_to_send_to_wrapped_client.append(action_request) request_id = _global_stub_action_request_counter.get_next() continue_on_error = kwargs.get('continue_on_error', False) if actions_to_send_to_wrapped_client: # If any un-stubbed actions need to be sent to the original client, send them self._services_with_calls_sent_to_wrapped_client.add( service_name) unwrapped_request_id = self._wrapped_client_send_request( client, service_name, actions_to_send_to_wrapped_client, *args, **kwargs) if not actions_to_send_to_mock: # If there are no stubbed actions to mock, just return the un-stubbed request ID return unwrapped_request_id self._stub_action_responses_to_merge[service_name][ unwrapped_request_id] = ( request_id, continue_on_error, ) ordered_actions_for_merging = OrderedDict() job_response_transport_exception = None job_response = JobResponse() for i, action_request in actions_to_send_to_mock.items(): mock_response = None try: mock_response = mock_action(action_request.body or {}) if isinstance(mock_response, JobResponse): job_response.errors.extend(mock_response.errors) if mock_response.actions: mock_response = mock_response.actions[0] elif isinstance(mock_response, dict): mock_response = ActionResponse(self.action, body=mock_response) elif not isinstance(mock_response, ActionResponse): mock_response = ActionResponse(self.action) except ActionError as e: mock_response = ActionResponse(self.action, errors=e.errors) except JobError as e: job_response.errors.extend(e.errors) except (MessageReceiveError, MessageReceiveTimeout) as e: job_response_transport_exception = e if mock_response: ordered_actions_for_merging[i] = mock_response job_response.actions.append(mock_response) if not continue_on_error and mock_response.errors: break if job_response.errors: break if actions_to_send_to_wrapped_client: # If the responses will have to be merged by get_all_responses, replace the list with the ordered dict job_response.actions = ordered_actions_for_merging self._stub_action_responses_outstanding[service_name][ request_id] = (job_response_transport_exception or job_response) return request_id wrapped_send_request.description = '<stub {service}.{action} wrapper around {wrapped}>'.format( service=self.service, action=self.action, wrapped=getattr(Client.send_request, 'description', Client.send_request.__repr__()), ) # This description is a helpful debugging tool @wraps(Client.get_all_responses) def wrapped_get_all_responses(client, service_name, *args, **kwargs): if service_name in self._services_with_calls_sent_to_wrapped_client: # Check if the any requests were actually sent wrapped client for this service; we do this because # the service may exist solely as a stubbed service, and calling the wrapped get_all_responses # will result in an error in this case. for request_id, response in self._wrapped_client_get_all_responses( client, service_name, *args, **kwargs): if request_id in self._stub_action_responses_to_merge[ service_name]: request_id, continue_on_error = self._stub_action_responses_to_merge[ service_name].pop(request_id, ) response_to_merge = self._stub_action_responses_outstanding[ service_name].pop(request_id) if isinstance(response_to_merge, Exception): raise response_to_merge for i, action_response in six.iteritems( response_to_merge.actions): response.actions.insert(i, action_response) if not continue_on_error: # Simulate the server job halting on the first action error first_error_index = -1 for i, action_result in enumerate( response.actions): if action_result.errors: first_error_index = i break if first_error_index >= 0: response.actions = response.actions[: first_error_index + 1] response.errors.extend(response_to_merge.errors) yield request_id, response if self._stub_action_responses_to_merge[service_name]: raise Exception( 'Something very bad happened, and there are still stubbed responses to merge!' ) for request_id, response in six.iteritems( self._stub_action_responses_outstanding[service_name]): if isinstance(response, Exception): raise response yield request_id, response self._stub_action_responses_outstanding[service_name] = {} wrapped_get_all_responses.description = '<stub {service}.{action} wrapper around {wrapped}>'.format( service=self.service, action=self.action, wrapped=getattr(Client.get_all_responses, 'description', Client.get_all_responses.__repr__()), ) # This description is a helpful debugging tool # Wrap Client.send_request, whose original version was saved in self._wrapped_client_send_request (which itself # might be another wrapper if we have stubbed multiple actions). Client.send_request = wrapped_send_request # Wrap Client.get_all_responses, whose original version was saved in self._wrapped_client_get_all_responses # (which itself might be another wrapper if we have stubbed multiple actions). Client.get_all_responses = wrapped_get_all_responses self.enabled = True return mock_action
def action_logic( request): # type: (EnrichedActionRequest) -> ActionResponse return ActionResponse( action='one', body={'building_response': request.body['building']})
def action_logic(request): return ActionResponse( action='one', body={'building_response': request.body['building']})