diff --git a/smartystreets_python_sdk/international_street/__init__.py b/smartystreets_python_sdk/international_street/__init__.py index 844fa21..6a669eb 100644 --- a/smartystreets_python_sdk/international_street/__init__.py +++ b/smartystreets_python_sdk/international_street/__init__.py @@ -4,3 +4,4 @@ from .analysis import Analysis from .candidate import Candidate from .client import Client +from .language_mode import LanguageMode diff --git a/smartystreets_python_sdk/international_street/client.py b/smartystreets_python_sdk/international_street/client.py index b84c310..82d2977 100644 --- a/smartystreets_python_sdk/international_street/client.py +++ b/smartystreets_python_sdk/international_street/client.py @@ -1,5 +1,6 @@ from smartystreets_python_sdk import Request from smartystreets_python_sdk.international_street import Candidate +from smartystreets_python_sdk.international_street.language_mode import LanguageMode class Client: @@ -32,7 +33,7 @@ def build_request(self, lookup): self.add_parameter(request, 'input_id', lookup.input_id) self.add_parameter(request, 'country', lookup.country) self.add_parameter(request, 'geocode', str(lookup.geocode).lower()) - self.add_parameter(request, 'language', lookup.language) + self.add_parameter(request, 'language', self.normalized_language(lookup.language)) self.add_parameter(request, 'freeform', lookup.freeform) self.add_parameter(request, 'address1', lookup.address1) self.add_parameter(request, 'address2', lookup.address2) @@ -57,3 +58,9 @@ def convert_candidates(raw_candidates): 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_street/language_mode.py b/smartystreets_python_sdk/international_street/language_mode.py index 17d27b4..c5f3fab 100644 --- a/smartystreets_python_sdk/international_street/language_mode.py +++ b/smartystreets_python_sdk/international_street/language_mode.py @@ -1,3 +1,27 @@ -NATIVE = 'native' +from enum import Enum -LATIN = 'latin' +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_street/lookup.py b/smartystreets_python_sdk/international_street/lookup.py index 49dbdbc..3b7be1d 100644 --- a/smartystreets_python_sdk/international_street/lookup.py +++ b/smartystreets_python_sdk/international_street/lookup.py @@ -1,4 +1,5 @@ from smartystreets_python_sdk.exceptions import UnprocessableEntityError +from smartystreets_python_sdk.international_street.language_mode import LanguageMode class Lookup: @@ -68,6 +69,9 @@ def ensure_enough_info(self): if self.field_is_missing(self.freeform) and self.field_is_missing(self.address1): raise UnprocessableEntityError('Either freeform or address1 is required.') - + + if self.language is not None: + LanguageMode.from_value(self.language) + def add_custom_parameter(self, parameter, value): self.custom_parameter_array[parameter] = value diff --git a/test/international_street/client_test.py b/test/international_street/client_test.py index 0e56f0a..70347c1 100644 --- a/test/international_street/client_test.py +++ b/test/international_street/client_test.py @@ -2,7 +2,7 @@ from smartystreets_python_sdk import Response from smartystreets_python_sdk.exceptions import UnprocessableEntityError -from smartystreets_python_sdk.international_street import Lookup, Client, language_mode, Candidate +from smartystreets_python_sdk.international_street import Lookup, Client, LanguageMode, Candidate from test.mocks import * @@ -25,7 +25,7 @@ def test_sending_single_fully_populated_lookup(self): lookup = Lookup() lookup.country = '0' lookup.geocode = True - lookup.language = language_mode.NATIVE + lookup.language = LanguageMode.NATIVE lookup.freeform = '1' lookup.address1 = '2' lookup.address2 = '3' @@ -42,7 +42,7 @@ def test_sending_single_fully_populated_lookup(self): self.assertEqual('0', sender.request.parameters['country']) self.assertEqual('true', sender.request.parameters['geocode']) - self.assertEqual(language_mode.NATIVE, sender.request.parameters['language']) + self.assertEqual(LanguageMode.NATIVE.value, sender.request.parameters['language']) self.assertEqual('1', sender.request.parameters['freeform']) self.assertEqual('2', sender.request.parameters['address1']) self.assertEqual('3', sender.request.parameters['address2']) @@ -55,6 +55,36 @@ def test_sending_single_fully_populated_lookup(self): self.assertEqual('10', sender.request.parameters['features']) self.assertEqual('11', sender.request.parameters['custom']) + def test_sending_lookup_with_mixed_case_language_value(self): + sender = RequestCapturingSender() + serializer = FakeDeserializer({}) + client = Client(sender, serializer) + lookup = Lookup('1', '0') + lookup.language = 'Latin' + + client.send(lookup) + + self.assertEqual('latin', sender.request.parameters['language']) + + def test_mixed_case_language_not_mutated(self): + sender = RequestCapturingSender() + serializer = FakeDeserializer({}) + client = Client(sender, serializer) + lookup = Lookup('1', '0') + 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', '0') + lookup.language = 'Klingon' + + self.assertRaises(UnprocessableEntityError, client.send, lookup) + def test_empty_lookup_rejected(self): sender = MockSender(None) client = Client(sender, None) diff --git a/test/international_street/language_mode_test.py b/test/international_street/language_mode_test.py new file mode 100644 index 0000000..ad4e3ff --- /dev/null +++ b/test/international_street/language_mode_test.py @@ -0,0 +1,21 @@ +import unittest + +from smartystreets_python_sdk.exceptions import UnprocessableEntityError +from smartystreets_python_sdk.international_street import LanguageMode + + +class TestLanguageMode(unittest.TestCase): + def test_from_value_resolves_mixed_case(self): + self.assertEqual(LanguageMode.LATIN, LanguageMode.from_value('Latin')) + self.assertEqual(LanguageMode.NATIVE, LanguageMode.from_value('NATIVE')) + self.assertEqual(LanguageMode.LATIN, LanguageMode.from_value('latin')) + + def test_from_value_returns_language_mode_instance_unchanged(self): + self.assertEqual(LanguageMode.NATIVE, LanguageMode.from_value(LanguageMode.NATIVE)) + + def test_from_value_rejects_invalid_value(self): + self.assertRaises(UnprocessableEntityError, LanguageMode.from_value, 'Klingon') + + def test_values_are_lowercase(self): + self.assertEqual('native', LanguageMode.NATIVE.value) + self.assertEqual('latin', LanguageMode.LATIN.value)