From 2331490d7e6e61fe9d845f31f573503d3500e02c Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 7 Aug 2015 13:17:55 -0700 Subject: [PATCH 1/2] Copy request dict and remove keys with None values --- minfraud/validation.py | 5 +++-- minfraud/webservice.py | 23 ++++++++++++++++++----- tests/test_webservice.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/minfraud/validation.py b/minfraud/validation.py index 38ba21c..df309ce 100644 --- a/minfraud/validation.py +++ b/minfraud/validation.py @@ -214,8 +214,9 @@ def _uri(s): 'last_4_digits': _credit_card_last_4, }, Required('device'): { - 'accept_language': _unicode_or_printable_ascii, Required('ip_address'): - _ip_address, 'user_agent': _unicode_or_printable_ascii + 'accept_language': _unicode_or_printable_ascii, + Required('ip_address'): _ip_address, + 'user_agent': _unicode_or_printable_ascii }, 'email': {'address': _email_or_md5, 'domain': _hostname, }, diff --git a/minfraud/webservice.py b/minfraud/webservice.py index d8051dd..9303a63 100644 --- a/minfraud/webservice.py +++ b/minfraud/webservice.py @@ -93,16 +93,17 @@ def score(self, transaction, validate=True): def _response_for(self, path, model_class, request, validate): """Send request and create response object""" + cleaned_request = self._copy_and_clean(request) if validate: try: - validate_transaction(request) + validate_transaction(cleaned_request) except MultipleInvalid as ex: raise InvalidRequestError( "Invalid transaction data: {0}".format(ex)) uri = '/'.join([self._base_uri, path]) response = requests.post( uri, - json=request, + json=cleaned_request, auth=(self._user_id, self._license_key), headers= {'Accept': 'application/json', @@ -113,6 +114,18 @@ def _response_for(self, path, model_class, request, validate): else: self._handle_error(response, uri) + def _copy_and_clean(self, data): + """This returns a copy of the data structure with Nones removed""" + if isinstance(data, dict): + return { + k: self._copy_and_clean(v) + for (k, v) in data.items() if v is not None + } + elif isinstance(data, (list, set, tuple)): + return [self._copy_and_clean(x) for x in data if x is not None] + else: + return data + def _user_agent(self): """Create User-Agent header""" return 'minFraud-API/%s %s' % (__version__, default_user_agent()) @@ -154,9 +167,9 @@ def _handle_4xx_status(self, response, status, uri): except ValueError: raise HTTPError( 'Received a {status:d} error but it did not include' - ' the expected JSON body: {content}' - .format(status=status, - content=response.content), status, uri) + ' the expected JSON body: {content}'.format( + status=status, + content=response.content), status, uri) else: if 'code' in body and 'error' in body: self._handle_web_service_error(body.get('error'), diff --git a/tests/test_webservice.py b/tests/test_webservice.py index 212a1a0..b5384d9 100644 --- a/tests/test_webservice.py +++ b/tests/test_webservice.py @@ -41,6 +41,24 @@ def test_200(self): if self.type == 'insights': self.assertEqual('United Kingdom', model.ip_address.country.name) + def test_200_on_request_with_nones(self): + model = self.create_success( + request={ + 'device': { + 'ip_address': '81.2.69.160', + 'accept_language': None + }, + 'event': { + 'shop_id': None + }, + 'shopping_cart': [{ + 'category': None, + 'quantity': 2, + }, None], + }) + response = self.response + self.assertEqual(0.01, model.risk_score) + def test_200_with_locales(self): locales = ('fr', ) client = Client(42, 'abcdef123456', locales=locales) @@ -142,7 +160,11 @@ def create_error(self, mock, status_code=400, text='', headers=None): return getattr(self.client, self.type)(self.full_request) @requests_mock.mock() - def create_success(self, mock, text=None, headers=None, client=None): + def create_success(self, mock, + text=None, + headers=None, + client=None, + request=None): if headers is None: headers = { 'Content-Type': @@ -158,7 +180,9 @@ def create_success(self, mock, text=None, headers=None, client=None): headers=headers) if client is None: client = self.client - return getattr(client, self.type)(self.full_request) + if request is None: + request = self.full_request + return getattr(client, self.type)(request) class TestInsights(BaseTest, unittest.TestCase): From b433a4a99c0c4c90e6c86c61461b98092ede3cf4 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Fri, 7 Aug 2015 13:22:11 -0700 Subject: [PATCH 2/2] Fix dictionary comprehension in 2.6 --- minfraud/webservice.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/minfraud/webservice.py b/minfraud/webservice.py index 9303a63..8b9b2e3 100644 --- a/minfraud/webservice.py +++ b/minfraud/webservice.py @@ -117,10 +117,8 @@ def _response_for(self, path, model_class, request, validate): def _copy_and_clean(self, data): """This returns a copy of the data structure with Nones removed""" if isinstance(data, dict): - return { - k: self._copy_and_clean(v) - for (k, v) in data.items() if v is not None - } + return dict((k, self._copy_and_clean(v)) for (k, v) in data.items() + if v is not None) elif isinstance(data, (list, set, tuple)): return [self._copy_and_clean(x) for x in data if x is not None] else: