diff --git a/examples/international_autocomplete_example.py b/examples/international_autocomplete_example.py index 8456a18..a724254 100644 --- a/examples/international_autocomplete_example.py +++ b/examples/international_autocomplete_example.py @@ -2,6 +2,7 @@ from smartystreets_python_sdk import SharedCredentials, ClientBuilder, BasicAuthCredentials from smartystreets_python_sdk.international_autocomplete import Lookup as InternationalAutocompleteLookup +from smartystreets_python_sdk.international_autocomplete import LanguageMode def run(): @@ -27,6 +28,7 @@ def run(): lookup.country = "FRA" lookup.max_group_results = 50 lookup.geolocation = True + lookup.language = LanguageMode.NATIVE # Uncomment the below line to add a custom parameter # lookup.add_custom_parameter("parameter", "value") diff --git a/smartystreets_python_sdk/international_autocomplete/__init__.py b/smartystreets_python_sdk/international_autocomplete/__init__.py index c1c21b8..9b1d882 100644 --- a/smartystreets_python_sdk/international_autocomplete/__init__.py +++ b/smartystreets_python_sdk/international_autocomplete/__init__.py @@ -1,3 +1,4 @@ from .candidate import Candidate from .lookup import Lookup from .client import Client +from .language_mode import LanguageMode diff --git a/smartystreets_python_sdk/international_autocomplete/client.py b/smartystreets_python_sdk/international_autocomplete/client.py index 50755f5..61c5d8c 100644 --- a/smartystreets_python_sdk/international_autocomplete/client.py +++ b/smartystreets_python_sdk/international_autocomplete/client.py @@ -1,6 +1,7 @@ from smartystreets_python_sdk import Request from smartystreets_python_sdk.exceptions import SmartyException from smartystreets_python_sdk.international_autocomplete import Candidate +from smartystreets_python_sdk.international_autocomplete.language_mode import LanguageMode class Client: @@ -45,6 +46,7 @@ def build_request(self, lookup): self.add_parameter(request, 'geolocation', 'on') self.add_parameter(request, 'include_only_locality', lookup.locality) self.add_parameter(request, 'include_only_postal_code', lookup.postal_code) + self.add_parameter(request, 'language', self.normalized_language(lookup.language)) for parameter in lookup.custom_parameter_array: self.add_parameter(request, parameter, lookup.custom_parameter_array[parameter]) @@ -59,3 +61,9 @@ def convert_candidates(candidate_dictionaries): def add_parameter(request, key, value): if value and value != 'none': request.parameters[key] = value + + @staticmethod + def normalized_language(language): + if language is None: + return None + return LanguageMode.from_value(language).value diff --git a/smartystreets_python_sdk/international_autocomplete/language_mode.py b/smartystreets_python_sdk/international_autocomplete/language_mode.py new file mode 100644 index 0000000..c5f3fab --- /dev/null +++ b/smartystreets_python_sdk/international_autocomplete/language_mode.py @@ -0,0 +1,27 @@ +from enum import Enum + +from smartystreets_python_sdk.exceptions import UnprocessableEntityError + + +class LanguageMode(Enum): + """ + A closed set of valid Language values, the closest Python equivalent to an enum-constrained field. + """ + NATIVE = 'native' + LATIN = 'latin' + + @staticmethod + def from_value(value): + """ + Resolves a LanguageMode instance or a raw value (eg. from user input or config) into a LanguageMode, + matching 'native'/'latin' regardless of case. + """ + if isinstance(value, LanguageMode): + return value + + for mode in LanguageMode: + if str(value).lower() == mode.value: + return mode + + raise UnprocessableEntityError( + "invalid Language value; must be unset, 'native', or 'latin' (case-insensitive)") diff --git a/smartystreets_python_sdk/international_autocomplete/lookup.py b/smartystreets_python_sdk/international_autocomplete/lookup.py index bf6a656..ebc05a5 100644 --- a/smartystreets_python_sdk/international_autocomplete/lookup.py +++ b/smartystreets_python_sdk/international_autocomplete/lookup.py @@ -1,6 +1,6 @@ class Lookup: def __init__(self, search=None, address_id=None, country=None, max_results=5, max_group_results=100, - geolocation=False, locality=None, postal_code=None): + geolocation=False, locality=None, postal_code=None, language=None): """ In addition to holding all the input data for this lookup, this class also will contain the result of the lookup after it comes back from the API. @@ -10,6 +10,7 @@ def __init__(self, search=None, address_id=None, country=None, max_results=5, ma :param administrative_area: Limit the results to only the administrative area provided :param locality: Limit the results to only the locality provided :param postal_code: Limit the results to only the postal code provided + :param language: The output language, Native or Latin (defaults to the country's language) """ self.result = [] @@ -22,6 +23,7 @@ def __init__(self, search=None, address_id=None, country=None, max_results=5, ma self.geolocation = geolocation self.locality = locality self.postal_code = postal_code + self.language = language def add_custom_parameter(self, parameter, value): self.custom_parameter_array[parameter] = value diff --git a/test/international_autocomplete/client_test.py b/test/international_autocomplete/client_test.py index b5232ac..f3fe732 100644 --- a/test/international_autocomplete/client_test.py +++ b/test/international_autocomplete/client_test.py @@ -2,7 +2,8 @@ from smartystreets_python_sdk import Response, exceptions from test.mocks import * -from smartystreets_python_sdk.international_autocomplete import Client, Lookup +from smartystreets_python_sdk.international_autocomplete import Client, LanguageMode, Lookup +from smartystreets_python_sdk.international_street import LanguageMode as StreetLanguageMode class TestClient(unittest.TestCase): @@ -29,6 +30,7 @@ def test_sending_fully_populated_lookup(self): lookup.locality = '3' lookup.postal_code = '4' lookup.address_id = '5' + lookup.language = LanguageMode.NATIVE lookup.add_custom_parameter('custom', '6') client.send(lookup) @@ -41,6 +43,7 @@ def test_sending_fully_populated_lookup(self): self.assertEqual('3', sender.request.parameters['include_only_locality']) self.assertEqual('4', sender.request.parameters['include_only_postal_code']) self.assertEqual('/5', sender.request.url_components) + self.assertEqual(LanguageMode.NATIVE.value, sender.request.parameters['language']) self.assertEqual('6', sender.request.parameters['custom']) def test_sending_lookup_with_custom_max_group_results(self): @@ -65,6 +68,59 @@ def test_sending_lookup_with_geolocation(self): self.assertEqual('on', sender.request.parameters['geolocation']) + def test_sending_lookup_with_language(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.language = LanguageMode.NATIVE + + client.send(lookup) + + self.assertEqual(LanguageMode.NATIVE.value, sender.request.parameters['language']) + + def test_sending_lookup_with_mixed_case_language_value(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.language = 'Latin' + + client.send(lookup) + + self.assertEqual('latin', sender.request.parameters['language']) + + def test_mixed_case_language_not_mutated(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.language = 'Latin' + + client.send(lookup) + + self.assertEqual('Latin', lookup.language) + + def test_rejects_invalid_mixed_case_language_value(self): + sender = MockSender(None) + client = Client(sender, None) + lookup = Lookup('1') + lookup.language = 'Klingon' + + self.assertRaises(exceptions.UnprocessableEntityError, client.send, lookup) + + def test_language_omitted_when_not_set(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + + client.send(Lookup('1')) + + self.assertNotIn('language', sender.request.parameters) + + def test_language_mode_not_shared_with_international_street(self): + self.assertIsNot(LanguageMode, StreetLanguageMode) + def test_deserialize_called_with_response_body(self): response = Response('Hello, World!', 0)