diff --git a/HISTORY.rst b/HISTORY.rst index fce583b..cb68783 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,16 @@ History ------- +2.1.0 (2020-09-25) +++++++++++++++++++ + +* Added ``response.ip_address.traits.is_residential_proxy`` to the + minFraud Insights and Factors models. This indicates whether the IP + address is on a suspected anonymizing network and belongs to a + residential ISP. +* ``HTTPError`` now provides the decoded response content in the + ``decoded_content`` attribute. + 2.0.3 (2020-07-28) ++++++++++++++++++ diff --git a/minfraud/errors.py b/minfraud/errors.py index 4e8d93c..ad30b01 100644 --- a/minfraud/errors.py +++ b/minfraud/errors.py @@ -39,14 +39,29 @@ class HTTPError(MinFraudError): :type: str + .. attribute:: decoded_content: + + The decoded response content + + :type: str + """ + http_status: Optional[int] + uri: Optional[str] + decoded_content: Optional[str] + def __init__( - self, message: str, http_status: Optional[int] = None, uri: Optional[str] = None + self, + message: str, + http_status: Optional[int] = None, + uri: Optional[str] = None, + decoded_content: Optional[str] = None, ) -> None: super().__init__(message) self.http_status = http_status self.uri = uri + self.decoded_content = decoded_content class InvalidRequestError(MinFraudError): diff --git a/minfraud/webservice.py b/minfraud/webservice.py index 093feb1..00898c5 100644 --- a/minfraud/webservice.py +++ b/minfraud/webservice.py @@ -136,8 +136,8 @@ def _exception_for_error( if 400 <= status < 500: return self._exception_for_4xx_status(status, content_type, body, uri) if 500 <= status < 600: - return self._exception_for_5xx_status(status, uri) - return self._exception_for_unexpected_status(status, uri) + return self._exception_for_5xx_status(status, body, uri) + return self._exception_for_unexpected_status(status, body, uri) def _exception_for_4xx_status( self, status: int, content_type: str, body: str, uri: str @@ -151,13 +151,14 @@ def _exception_for_4xx_status( """Returns exception for error responses with 4xx status codes.""" if not body: return HTTPError( - "Received a {0} error with no body".format(status), status, uri + "Received a {0} error with no body".format(status), status, uri, body ) if content_type.find("json") == -1: return HTTPError( "Received a {0} with the following " "body: {1}".format(status, body), status, uri, + body, ) try: decoded_body = json.loads(body) @@ -169,6 +170,7 @@ def _exception_for_4xx_status( ), status, uri, + body, ) else: if "code" in decoded_body and "error" in decoded_body: @@ -180,6 +182,7 @@ def _exception_for_4xx_status( " or error keys: {0}".format(body), status, uri, + body, ) @staticmethod @@ -207,21 +210,31 @@ def _exception_for_web_service_error( return InvalidRequestError(message, code, status, uri) @staticmethod - def _exception_for_5xx_status(status: int, uri: str) -> HTTPError: + def _exception_for_5xx_status( + status: int, + body: Optional[str], + uri: str, + ) -> HTTPError: """Returns exception for error response with 5xx status codes.""" return HTTPError( u"Received a server error ({0}) for " u"{1}".format(status, uri), status, uri, + body, ) @staticmethod - def _exception_for_unexpected_status(status: int, uri: str) -> HTTPError: + def _exception_for_unexpected_status( + status: int, + body: Optional[str], + uri: str, + ) -> HTTPError: """Returns exception for responses with unexpected status codes.""" return HTTPError( u"Received an unexpected HTTP status " u"({0}) for {1}".format(status, uri), status, uri, + body, ) diff --git a/setup.py b/setup.py index 4478436..05ec838 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ requirements = [ "aiohttp>=3.6.2,<4.0.0", "email_validator>=1.1.1,<2.0.0", - "geoip2>=4.0.0,<5.0.0", + "geoip2>=4.1.0,<5.0.0", "requests>=2.24.0,<3.0.0", "urllib3>=1.25.2,<2.0.0", "voluptuous", diff --git a/tests/data/factors-response.json b/tests/data/factors-response.json index 7b93b66..acd4d86 100644 --- a/tests/data/factors-response.json +++ b/tests/data/factors-response.json @@ -94,6 +94,7 @@ "is_anonymous_vpn": true, "is_hosting_provider": true, "is_public_proxy": true, + "is_residential_proxy": true, "is_satellite_provider": true, "is_tor_exit_node": true, "isp": "Andrews & Arnold Ltd", diff --git a/tests/data/insights-response.json b/tests/data/insights-response.json index fe0043d..b9d73b0 100644 --- a/tests/data/insights-response.json +++ b/tests/data/insights-response.json @@ -94,6 +94,7 @@ "is_anonymous_vpn": true, "is_hosting_provider": true, "is_public_proxy": true, + "is_residential_proxy": true, "is_satellite_provider": true, "is_tor_exit_node": true, "isp": "Andrews & Arnold Ltd", diff --git a/tests/test_models.py b/tests/test_models.py index f8963e8..cbf96c6 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -160,6 +160,7 @@ def test_ip_address(self): "is_anonymous_vpn": True, "is_hosting_provider": True, "is_public_proxy": True, + "is_residential_proxy": True, "is_satellite_provider": True, "is_tor_exit_node": True, }, @@ -175,6 +176,7 @@ def test_ip_address(self): self.assertEqual(True, address.traits.is_anonymous_vpn) self.assertEqual(True, address.traits.is_hosting_provider) self.assertEqual(True, address.traits.is_public_proxy) + self.assertEqual(True, address.traits.is_residential_proxy) self.assertEqual(True, address.traits.is_satellite_provider) self.assertEqual(True, address.traits.is_tor_exit_node) self.assertEqual(True, address.country.is_high_risk) diff --git a/tests/test_webservice.py b/tests/test_webservice.py index e69f9c3..2d786e3 100644 --- a/tests/test_webservice.py +++ b/tests/test_webservice.py @@ -195,6 +195,7 @@ def test_200(self): self.assertEqual(self.cls(response), model) if self.has_ip_location(): self.assertEqual("United Kingdom", model.ip_address.country.name) + self.assertEqual(True, model.ip_address.traits.is_residential_proxy) @httprettified def test_200_on_request_with_nones(self):