Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
++++++++++++++++++

Expand Down
17 changes: 16 additions & 1 deletion minfraud/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
23 changes: 18 additions & 5 deletions minfraud/webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -169,6 +170,7 @@ def _exception_for_4xx_status(
),
status,
uri,
body,
)
else:
if "code" in decoded_body and "error" in decoded_body:
Expand All @@ -180,6 +182,7 @@ def _exception_for_4xx_status(
" or error keys: {0}".format(body),
status,
uri,
body,
)

@staticmethod
Expand Down Expand Up @@ -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,
)


Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions tests/data/factors-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions tests/data/insights-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tests/test_webservice.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down