Skip to content
Draft
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.

## [Unreleased]
### Added
- Add Address Validation feedback submission with the first response ID and a validation conclusion.

### Fixed
- Raise API or HTTP exceptions for unsuccessful Address Validation responses instead of returning error payloads.

Expand Down
46 changes: 46 additions & 0 deletions googlemaps/addressvalidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ def _addressvalidation_extract(response):
raise exceptions.HTTPError(response.status_code)


def _addressvalidation_feedback_extract(response):
"""Extract feedback without parsing an empty successful response."""
if 200 <= response.status_code < 300:
return {}
return _addressvalidation_extract(response)


def addressvalidation(client, addressLines, regionCode=None , locality=None, enableUspsCass=None):
"""
The Google Maps Address Validation API returns a verification of an address
Expand Down Expand Up @@ -76,4 +83,43 @@ def addressvalidation(client, addressLines, regionCode=None , locality=None, ena
base_url=_ADDRESSVALIDATION_BASE_URL,
extract_body=_addressvalidation_extract,
post_json=params)


def addressvalidation_feedback(client, response_id, conclusion):
"""Provide the final outcome of an address validation sequence.

Send feedback once, after the transaction concludes. Use the response ID
from the first validation response, not a later attempt in the sequence.
See https://developers.google.com/maps/documentation/address-validation/reference/rest/v1/TopLevel/provideValidationFeedback.

:param response_id: The ``responseId`` from the first validation response.
:type response_id: str

:param conclusion: One of ``VALIDATED_VERSION_USED``, ``USER_VERSION_USED``,
``UNVALIDATED_VERSION_USED``, or ``UNUSED``.
:type conclusion: str

:raises ValueError: If the response ID is not a nonempty string or the
conclusion is not supported.
:raises googlemaps.exceptions.ApiError: If the API returns a structured error.
:raises googlemaps.exceptions.HTTPError: If an unsuccessful response does
not contain a structured API error.

:returns: An empty dictionary on success.
:rtype: dict
"""
if not isinstance(response_id, str) or not response_id:
raise ValueError("response_id must be a nonempty string")
if conclusion not in (
"VALIDATED_VERSION_USED", "USER_VERSION_USED",
"UNVALIDATED_VERSION_USED", "UNUSED",
):
raise ValueError("conclusion must be a supported validation conclusion")

return client._request(
"/v1:provideValidationFeedback", {},
base_url=_ADDRESSVALIDATION_BASE_URL,
extract_body=_addressvalidation_feedback_extract,
post_json={"responseId": response_id, "conclusion": conclusion},
)

2 changes: 2 additions & 0 deletions googlemaps/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ def _generate_auth_url(self, path, params, accepts_clientid):
from googlemaps.places import places_autocomplete_query
from googlemaps.maps import static_map
from googlemaps.addressvalidation import addressvalidation
from googlemaps.addressvalidation import addressvalidation_feedback

def make_api_method(func):
"""
Expand Down Expand Up @@ -472,6 +473,7 @@ def wrapper(*args, **kwargs):
Client.places_autocomplete_query = make_api_method(places_autocomplete_query)
Client.static_map = make_api_method(static_map)
Client.addressvalidation = make_api_method(addressvalidation)
Client.addressvalidation_feedback = make_api_method(addressvalidation_feedback)


def sign_hmac(secret, payload):
Expand Down
104 changes: 103 additions & 1 deletion tests/test_addressvalidation.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,106 @@ def test_transient_error_exhausts_retry_timeout(self):
with self.assertRaises(googlemaps.exceptions.Timeout):
self.client.addressvalidation(["1600 Amphitheatre Parkway"])

self.assertEqual(1, len(responses.calls))
self.assertEqual(1, len(responses.calls))


class AddressValidationFeedbackTest(TestCase):
def setUp(self):
self.client = googlemaps.Client("AIza-feedback-tests-non-secret")
self.url = (
"https://addressvalidation.googleapis.com/v1:provideValidationFeedback"
)

def test_feedback_submits_supported_conclusions(self):
for conclusion in (
"VALIDATED_VERSION_USED", "USER_VERSION_USED",
"UNVALIDATED_VERSION_USED", "UNUSED",
):
with self.subTest(conclusion=conclusion):
with responses.RequestsMock() as http:
http.add(
responses.POST, self.url, body="", status=200,
match=[responses.matchers.json_params_matcher({
"responseId": "initial-response-id",
"conclusion": conclusion,
})],
)

self.assertEqual({}, self.client.addressvalidation_feedback(
"initial-response-id", conclusion
))

@responses.activate
def test_feedback_accepts_no_content(self):
responses.add(responses.POST, self.url, status=204)

self.assertEqual({}, self.client.addressvalidation_feedback(
"initial-response-id", "UNUSED"
))

@responses.activate
def test_feedback_rejects_invalid_response_ids_before_transport(self):
for response_id in ("", None, 123, []):
with self.subTest(response_id=response_id):
with self.assertRaises(ValueError):
self.client.addressvalidation_feedback(response_id, "UNUSED")

self.assertEqual(0, len(responses.calls))

@responses.activate
def test_feedback_rejects_invalid_conclusions_before_transport(self):
for conclusion in (
"VALIDATION_CONCLUSION_UNSPECIFIED", "UNKNOWN",
"validated_version_used", None, [],
):
with self.subTest(conclusion=conclusion):
with self.assertRaises(ValueError):
self.client.addressvalidation_feedback(
"initial-response-id", conclusion
)

self.assertEqual(0, len(responses.calls))

@responses.activate
def test_feedback_raises_structured_api_errors(self):
responses.add(
responses.POST, self.url, status=400,
json={"error": {
"status": "INVALID_ARGUMENT",
"message": "The response ID is unknown.",
}},
)

with self.assertRaises(googlemaps.exceptions.ApiError) as error:
self.client.addressvalidation_feedback("unknown-response-id", "UNUSED")

self.assertEqual("INVALID_ARGUMENT", error.exception.status)
self.assertEqual("The response ID is unknown.", error.exception.message)

@responses.activate
def test_feedback_raises_http_errors_for_non_json_bodies(self):
responses.add(
responses.POST, self.url, status=403,
body="<html>Access denied</html>", content_type="text/html",
)

with self.assertRaises(googlemaps.exceptions.HTTPError) as error:
self.client.addressvalidation_feedback("initial-response-id", "UNUSED")

self.assertEqual(403, error.exception.status_code)

@responses.activate
def test_feedback_retries_transient_errors_before_extraction(self):
responses.add(
responses.POST, self.url, body="Unavailable", status=503,
)
responses.add(responses.POST, self.url, body="", status=200)

with mock.patch("googlemaps.client.time.sleep"):
self.assertEqual({}, self.client.addressvalidation_feedback(
"initial-response-id", "USER_VERSION_USED"
))

self.assertEqual([503, 200], [
call.response.status_code for call in responses.calls
])