From f629404bd704c482be79371fda46687f2d41bcb5 Mon Sep 17 00:00:00 2001 From: Mae Evans Date: Fri, 12 Jun 2026 13:16:47 -0600 Subject: [PATCH 1/5] Add US Autocomplete (V2) API client Co-Authored-By: Claude Opus 4.8 (1M context) --- Makefile | 7 +- examples/us_autocomplete_example.py | 78 ++++++++ smartystreets_python_sdk/client_builder.py | 6 + .../us_autocomplete/__init__.py | 3 + .../us_autocomplete/client.py | 68 +++++++ .../us_autocomplete/geolocation_type.py | 3 + .../us_autocomplete/lookup.py | 82 +++++++++ .../us_autocomplete/suggestion.py | 13 ++ test/us_autocomplete/__init__.py | 0 test/us_autocomplete/client_test.py | 173 ++++++++++++++++++ 10 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 examples/us_autocomplete_example.py create mode 100644 smartystreets_python_sdk/us_autocomplete/__init__.py create mode 100644 smartystreets_python_sdk/us_autocomplete/client.py create mode 100644 smartystreets_python_sdk/us_autocomplete/geolocation_type.py create mode 100644 smartystreets_python_sdk/us_autocomplete/lookup.py create mode 100644 smartystreets_python_sdk/us_autocomplete/suggestion.py create mode 100644 test/us_autocomplete/__init__.py create mode 100644 test/us_autocomplete/client_test.py diff --git a/Makefile b/Makefile index 651d103..f756b4d 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,9 @@ international_street_api: us_autocomplete_pro_api: PYTHONPATH=. python3 examples/us_autocomplete_pro_example.py +us_autocomplete_api: + PYTHONPATH=. python3 examples/us_autocomplete_example.py + us_enrichment_api: PYTHONPATH=. python3 examples/us_enrichment_example.py @@ -54,7 +57,7 @@ us_street_api: us_zipcode_api: PYTHONPATH=. python3 examples/us_zipcode_single_lookup_example.py && PYTHONPATH=. python3 examples/us_zipcode_multiple_lookups_example.py -examples: international_autocomplete_api international_postal_code_api international_street_api us_autocomplete_pro_api us_enrichment_api us_extract_api us_reverse_geo_api us_street_api us_street_iana_timezone_api us_zipcode_api +examples: international_autocomplete_api international_postal_code_api international_street_api us_autocomplete_pro_api us_autocomplete_api us_enrichment_api us_extract_api us_reverse_geo_api us_street_api us_street_iana_timezone_api us_zipcode_api -.PHONY: clean test dependencies package publish examples international_autocomplete_api international_postal_code_api international_street_api us_autocomplete_pro_api us_enrichment_api us_extract_api us_reverse_geo_api us_street_api us_street_iana_timezone_api us_zipcode_api +.PHONY: clean test dependencies package publish examples international_autocomplete_api international_postal_code_api international_street_api us_autocomplete_pro_api us_autocomplete_api us_enrichment_api us_extract_api us_reverse_geo_api us_street_api us_street_iana_timezone_api us_zipcode_api diff --git a/examples/us_autocomplete_example.py b/examples/us_autocomplete_example.py new file mode 100644 index 0000000..5688630 --- /dev/null +++ b/examples/us_autocomplete_example.py @@ -0,0 +1,78 @@ +import os + +from smartystreets_python_sdk import SharedCredentials, BasicAuthCredentials, ClientBuilder +from smartystreets_python_sdk.us_autocomplete import Lookup as AutocompleteLookup, geolocation_type + +# This example is for us-autocomplete (V2). It has the same name as a previous product +# which has been deprecated since 2022 which we refer to as US Autocomplete Basic. +# +# If you are still using US Autocomplete Basic, this SDK will not work. + +def run(): + # key = "Your SmartyStreets Key here" + # hostname = "Your Hostname here" + + # We recommend storing your secret keys in environment variables instead---it's safer! + # for client-side requests (browser/mobile), use this code: + # key = os.environ['SMARTY_AUTH_WEB'] + # hostname = os.environ['SMARTY_WEBSITE_DOMAIN'] + # + # credentials = SharedCredentials(key, hostname) + + # for server-to-server requests, use this code: + auth_id = os.environ['SMARTY_AUTH_ID'] + auth_token = os.environ['SMARTY_AUTH_TOKEN'] + + credentials = BasicAuthCredentials(auth_id, auth_token) + + client = ClientBuilder(credentials).build_us_autocomplete_api_client() + + lookup = AutocompleteLookup('1042 W Center') + + # Uncomment the below line to add a custom parameter + # lookup.add_custom_parameter("parameter", "value") + + client.send(lookup) + + print('*** Result with no filter ***') + print() + for suggestion in lookup.result: + print(suggestion.street_line + " " + suggestion.city, suggestion.state, sep=", ") + + # Documentation for input fields can be found at: + # https://www.smarty.com/docs/apis/us-autocomplete-v2/reference#http-request-input-fields + + lookup.add_city_filter('Denver,Aurora,CO') + lookup.add_city_filter('Orem,UT') + lookup.add_state_preference('CO') + # lookup.selected = '1042 W Center St Apt A (24) Orem UT 84057' + lookup.max_results = 5 + lookup.prefer_geo = geolocation_type.NONE + lookup.prefer_ratio = 33 + lookup.source = 'all' + + suggestions = client.send(lookup) # The client will also return the suggestions directly + + print() + print('*** Result with some filters ***') + entry_id = '' + address_with_secondaries = '' + for suggestion in suggestions: + print(suggestion.street_line + " " + suggestion.city + ", " + suggestion.state) + if suggestion.entry_id: + address_with_secondaries = suggestion.street_line + " " + suggestion.city + " " + suggestion.state + entry_id = suggestion.entry_id + + # Expand the secondaries of a result that has an entry_id by passing it back as the selected address. + if entry_id: + lookup.selected = entry_id + suggestions = client.send(lookup) + + print() + print('*** Secondaries for: [' + address_with_secondaries + '] ***') + for suggestion in suggestions: + print(suggestion.street_line + " " + suggestion.city + ", " + suggestion.state) + + +if __name__ == "__main__": + run() diff --git a/smartystreets_python_sdk/client_builder.py b/smartystreets_python_sdk/client_builder.py index c89b987..06837f2 100644 --- a/smartystreets_python_sdk/client_builder.py +++ b/smartystreets_python_sdk/client_builder.py @@ -3,6 +3,7 @@ from smartystreets_python_sdk.us_zipcode import Client as USZIPClient from smartystreets_python_sdk.us_extract import Client as USExtractClient from smartystreets_python_sdk.us_autocomplete_pro import Client as USAutocompleteProClient +from smartystreets_python_sdk.us_autocomplete import Client as USAutocompleteClient from smartystreets_python_sdk.us_reverse_geo import Client as USReverseGeoClient from smartystreets_python_sdk.international_street import Client as InternationalStreetClient from smartystreets_python_sdk.international_autocomplete import Client as InternationalAutocompleteClient @@ -34,6 +35,7 @@ def __init__(self, signer): self.INTERNATIONAL_STREET_API_URL = "https://international-street.api.smarty.com/verify" self.INTERNATIONAL_AUTOCOMPLETE_API_URL = "https://international-autocomplete.api.smarty.com/v2/lookup" self.US_AUTOCOMPLETE_PRO_API_URL = "https://us-autocomplete-pro.api.smarty.com/lookup" + self.US_AUTOCOMPLETE_API_URL = "https://us-autocomplete.api.smarty.com/v2/lookup" self.US_EXTRACT_API_URL = "https://us-extract.api.smarty.com" self.US_STREET_API_URL = "https://us-street.api.smarty.com/street-address" self.US_ZIP_CODE_API_URL = "https://us-zipcode.api.smarty.com/lookup" @@ -229,6 +231,10 @@ def build_us_autocomplete_pro_api_client(self): self.ensure_url_prefix_not_null(self.US_AUTOCOMPLETE_PRO_API_URL) return USAutocompleteProClient(self.build_sender(), self.serializer) + def build_us_autocomplete_api_client(self): + self.ensure_url_prefix_not_null(self.US_AUTOCOMPLETE_API_URL) + return USAutocompleteClient(self.build_sender(), self.serializer) + def build_us_extract_api_client(self): self.ensure_url_prefix_not_null(self.US_EXTRACT_API_URL) return USExtractClient(self.build_sender(), self.serializer) diff --git a/smartystreets_python_sdk/us_autocomplete/__init__.py b/smartystreets_python_sdk/us_autocomplete/__init__.py new file mode 100644 index 0000000..fea7586 --- /dev/null +++ b/smartystreets_python_sdk/us_autocomplete/__init__.py @@ -0,0 +1,3 @@ +from .suggestion import Suggestion +from .lookup import Lookup +from .client import Client diff --git a/smartystreets_python_sdk/us_autocomplete/client.py b/smartystreets_python_sdk/us_autocomplete/client.py new file mode 100644 index 0000000..85ff83b --- /dev/null +++ b/smartystreets_python_sdk/us_autocomplete/client.py @@ -0,0 +1,68 @@ +from smartystreets_python_sdk import Request +from smartystreets_python_sdk.exceptions import SmartyException +from smartystreets_python_sdk.us_autocomplete import Suggestion, geolocation_type + + +class Client: + def __init__(self, sender, serializer): + """ + It is recommended to instantiate this class using ClientBuilder.build_us_autocomplete_api_client() + """ + self.sender = sender + self.serializer = serializer + + def send(self, lookup): + """ + Sends a Lookup object to the US Autocomplete API and stores the result in the Lookup's result field. + """ + if not lookup or not lookup.search: + raise SmartyException('Send() must be passed a Lookup with the search field set.') + + request = self.build_request(lookup) + + response = self.sender.send(request) + + if response.error: + raise response.error + + result = self.serializer.deserialize(response.payload) + suggestions = self.convert_suggestions(result.get('suggestions') or []) + lookup.result = suggestions + + return suggestions + + def build_request(self, lookup): + request = Request() + + self.add_parameter(request, 'search', lookup.search) + self.add_parameter(request, 'max_results', lookup.max_results) + self.add_parameter(request, 'include_only_cities', self.build_filter_string(lookup.city_filter)) + self.add_parameter(request, 'include_only_states', self.build_filter_string(lookup.state_filter)) + self.add_parameter(request, 'include_only_zip_codes', self.build_filter_string(lookup.zip_filter)) + self.add_parameter(request, 'exclude_states', self.build_filter_string(lookup.exclude_states)) + self.add_parameter(request, 'prefer_cities', self.build_filter_string(lookup.prefer_cities)) + self.add_parameter(request, 'prefer_states', self.build_filter_string(lookup.prefer_states)) + self.add_parameter(request, 'prefer_zip_codes', self.build_filter_string(lookup.prefer_zips)) + self.add_parameter(request, 'prefer_ratio', lookup.prefer_ratio) + self.add_parameter(request, 'prefer_geolocation', lookup.prefer_geo) + self.add_parameter(request, 'source', lookup.source) + self.add_parameter(request, 'selected', lookup.selected) + self.add_parameter(request, 'exclude', lookup.exclude) + + for parameter in lookup.custom_parameter_array: + self.add_parameter(request, parameter, lookup.custom_parameter_array[parameter]) + + return request + + @staticmethod + def build_filter_string(filter_list): + return ';'.join(filter_list or []) or None + + @staticmethod + def convert_suggestions(suggestion_dictionaries): + return [Suggestion(suggestion) for suggestion in suggestion_dictionaries] + + @staticmethod + def add_parameter(request, key, value): + if value and value != 'none': + request.parameters[key] = value diff --git a/smartystreets_python_sdk/us_autocomplete/geolocation_type.py b/smartystreets_python_sdk/us_autocomplete/geolocation_type.py new file mode 100644 index 0000000..4288999 --- /dev/null +++ b/smartystreets_python_sdk/us_autocomplete/geolocation_type.py @@ -0,0 +1,3 @@ +CITY = 'city' + +NONE = 'none' diff --git a/smartystreets_python_sdk/us_autocomplete/lookup.py b/smartystreets_python_sdk/us_autocomplete/lookup.py new file mode 100644 index 0000000..7947798 --- /dev/null +++ b/smartystreets_python_sdk/us_autocomplete/lookup.py @@ -0,0 +1,82 @@ +from smartystreets_python_sdk.us_autocomplete import geolocation_type + + +class Lookup: + def __init__(self, search=None, max_results=None, city_filter=None, state_filter=None, zip_filter=None, + exclude_states=None, prefer_cities=None, prefer_states=None, prefer_zips=None, prefer_ratio=None, + prefer_geo=None, selected=None, exclude=None, source=None): + + """ + In addition to holding all of the input data for this lookup, this class also will contain the result + of the lookup after it comes back from the API. + + See "https://www.smarty.com/docs/apis/us-autocomplete-v2/reference#http-request-input-fields" + + :param search: The part of the address that has already been typed (required) + :param max_results: Maximum number of address suggestions to return + :param city_filter: Limit the results to only those cities listed, as well as those in state_filter + :param state_filter: Limit the results to only those states listed, as well as those in city_filter + :param zip_filter: Limit the result to only those ZIP Codes listed. When this parameter is used, + no other _cities, _states parameters can be used + :param exclude_states: Exclude the following states from the results. When this parameter is used, + no other include_ parameters can be used + :param prefer_cities: Display suggestions with the listed cities and states at the top of the suggestion list + :param prefer_states: Display suggestions with the listed states at the top of the suggestion list + :param prefer_zips: Display suggestions with the listed ZIP Codes at the top of the suggestion list. + When this parameter is used, no other _cities or _state parameters can be used + :param prefer_ratio: Specifies the percentage of address suggestions that should be preferred + and will appear at the top of the suggestion list + :param prefer_geo: If omitted or set to city it uses the sender's IP address to determine location, + then automatically adds the city and state to the prefer_cities value. + This parameter takes precedence over other _include or exclude parameters, + meaning that if it is not set to none, you may see addresses from the customer's area + when you may not desire it + :param selected: Used by UI components to request a list of secondaries (up to 100) for the specified address + :param exclude: Removes the given partial address from the suggestion results + :param source: Include results from alternate data sources. If no value is passed, the default will be `postal`. + Allowed values are: + all - will include non-postal addresses in the results + postal - will limit the results to postal addresses only + """ + self.result = [] + self.custom_parameter_array = {} + self.search = search + self.max_results = max_results + self.city_filter = city_filter or [] + self.state_filter = state_filter or [] + self.zip_filter = zip_filter or [] + self.exclude_states = exclude_states or [] + self.prefer_cities = prefer_cities or [] + self.prefer_states = prefer_states or [] + self.prefer_zips = prefer_zips or [] + self.prefer_ratio = prefer_ratio + self.prefer_geo = prefer_geo + self.selected = selected + self.exclude = exclude + self.source = source + + def add_city_filter(self, city): + self.city_filter.append(city) + + def add_state_filter(self, state): + self.state_filter.append(state) + + def add_zip_filter(self, zipcode): + self.prefer_geo = geolocation_type.NONE + self.zip_filter.append(zipcode) + + def add_state_exclusion(self, state): + self.exclude_states.append(state) + + def add_city_preference(self, city): + self.prefer_cities.append(city) + + def add_state_preference(self, state): + self.prefer_states.append(state) + + def add_zip_preference(self, zipcode): + self.prefer_geo = geolocation_type.NONE + self.prefer_zips.append(zipcode) + + def add_custom_parameter(self, parameter, value): + self.custom_parameter_array[parameter] = value diff --git a/smartystreets_python_sdk/us_autocomplete/suggestion.py b/smartystreets_python_sdk/us_autocomplete/suggestion.py new file mode 100644 index 0000000..0817cca --- /dev/null +++ b/smartystreets_python_sdk/us_autocomplete/suggestion.py @@ -0,0 +1,13 @@ +class Suggestion: + def __init__(self, obj=None): + """ + See "https://www.smarty.com/docs/apis/us-autocomplete-v2/reference#http-response-status" + """ + self.smarty_key = obj.get('smarty_key', None) + self.entry_id = obj.get('entry_id', None) + self.street_line = obj.get('street_line', None) + self.secondary = obj.get('secondary', None) + self.city = obj.get('city', None) + self.state = obj.get('state', None) + self.zipcode = obj.get('zipcode', None) + self.entries = obj.get('entries', None) diff --git a/test/us_autocomplete/__init__.py b/test/us_autocomplete/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/us_autocomplete/client_test.py b/test/us_autocomplete/client_test.py new file mode 100644 index 0000000..4e6301c --- /dev/null +++ b/test/us_autocomplete/client_test.py @@ -0,0 +1,173 @@ +import unittest + +from smartystreets_python_sdk import Response, exceptions +from test.mocks import * +from smartystreets_python_sdk.us_autocomplete import Client, Lookup, geolocation_type + + +class TestClient(unittest.TestCase): + def test_sending_search_only_lookup(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + + client.send(Lookup('1')) + + self.assertEqual('1', sender.request.parameters['search']) + + def test_sending_fully_populated_lookup(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.max_results = 7 + lookup.add_city_filter('city1') + lookup.add_city_filter('city2') + lookup.add_state_filter('state1') + lookup.add_state_filter('state2') + lookup.add_state_exclusion('state3') + lookup.add_city_preference('city3') + lookup.add_state_preference('state4') + lookup.prefer_ratio = 3 + lookup.prefer_geo = geolocation_type.CITY + lookup.source = 'all' + lookup.selected = 'selectedAddress' + lookup.exclude = 'excludedAddress' + lookup.add_custom_parameter('custom', '6') + + client.send(lookup) + + self.assertEqual('1', sender.request.parameters['search']) + self.assertEqual(7, sender.request.parameters['max_results']) + self.assertEqual('city1;city2', sender.request.parameters['include_only_cities']) + self.assertEqual('state1;state2', sender.request.parameters['include_only_states']) + self.assertEqual('state3', sender.request.parameters['exclude_states']) + self.assertEqual('city3', sender.request.parameters['prefer_cities']) + self.assertEqual('state4', sender.request.parameters['prefer_states']) + self.assertEqual(3, sender.request.parameters['prefer_ratio']) + self.assertEqual('city', sender.request.parameters['prefer_geolocation']) + self.assertEqual('all', sender.request.parameters['source']) + self.assertEqual('selectedAddress', sender.request.parameters['selected']) + self.assertEqual('excludedAddress', sender.request.parameters['exclude']) + self.assertEqual('6', sender.request.parameters['custom']) + + def test_sending_exclude(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.exclude = 'excludedAddress' + + client.send(lookup) + + self.assertEqual('excludedAddress', sender.request.parameters['exclude']) + + def test_prefer_geolocation_none_is_omitted(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.prefer_geo = geolocation_type.NONE + + client.send(lookup) + + self.assertNotIn('prefer_geolocation', sender.request.parameters) + + def test_zip_filter_serialized(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.add_zip_filter('11111') + + client.send(lookup) + + self.assertEqual('11111', sender.request.parameters['include_only_zip_codes']) + self.assertNotIn('prefer_geolocation', sender.request.parameters) + + def test_prefer_zip_serialized(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + lookup = Lookup('1') + lookup.add_zip_preference('22222') + + client.send(lookup) + + self.assertEqual('22222', sender.request.parameters['prefer_zip_codes']) + self.assertNotIn('prefer_geolocation', sender.request.parameters) + + def test_source_omitted_when_unset(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + + client.send(Lookup('1')) + + self.assertNotIn('source', sender.request.parameters) + + def test_deserialize_called_with_response_body(self): + response = Response('Hello, World!', 0) + + sender = MockSender(response) + deserializer = FakeDeserializer({}) + client = Client(sender, deserializer) + + client.send(Lookup('1')) + + self.assertEqual(response.payload, deserializer.input) + + def test_result_correctly_assigned_to_corresponding_lookup(self): + lookup = Lookup('1') + expected_result = {"suggestions": [{"street_line": "2", "entry_id": "3"}]} + + sender = MockSender(Response('{[]}', 0)) + deserializer = FakeDeserializer(expected_result) + client = Client(sender, deserializer) + + client.send(lookup) + + self.assertEqual('2', lookup.result[0].street_line) + self.assertEqual('3', lookup.result[0].entry_id) + + def test_all_suggestion_fields_deserialized(self): + lookup = Lookup('1') + expected_result = {"suggestions": [{ + "smarty_key": "key", + "entry_id": "entry", + "street_line": "street", + "secondary": "secondary", + "city": "city", + "state": "state", + "zipcode": "zip", + "entries": 5, + }]} + + sender = MockSender(Response('{[]}', 0)) + deserializer = FakeDeserializer(expected_result) + client = Client(sender, deserializer) + + client.send(lookup) + + suggestion = lookup.result[0] + self.assertEqual('key', suggestion.smarty_key) + self.assertEqual('entry', suggestion.entry_id) + self.assertEqual('street', suggestion.street_line) + self.assertEqual('secondary', suggestion.secondary) + self.assertEqual('city', suggestion.city) + self.assertEqual('state', suggestion.state) + self.assertEqual('zip', suggestion.zipcode) + self.assertEqual(5, suggestion.entries) + + def test_rejects_blank_lookup(self): + sender = RequestCapturingSender() + serializer = FakeSerializer({}) + client = Client(sender, serializer) + + self.assertRaises(exceptions.SmartyException, client.send, Lookup()) + + def test_raises_exception_when_response_has_error(self): + exception = exceptions.BadCredentialsError + client = Client(MockExceptionSender(exception), FakeSerializer(None)) + + self.assertRaises(exception, client.send, Lookup('test')) From c446df80cf4ffb0a3e914916ee8e5d782603c8ab Mon Sep 17 00:00:00 2001 From: Andrea Wait Date: Thu, 18 Jun 2026 13:53:53 -0600 Subject: [PATCH 2/5] Refactored source to enum --- examples/us_autocomplete_example.py | 3 +- .../us_autocomplete/__init__.py | 1 + .../us_autocomplete/client.py | 4 ++- .../us_autocomplete/lookup.py | 6 ++-- .../us_autocomplete/source.py | 6 ++++ test/us_autocomplete/client_test.py | 34 ++++++++++++++++++- 6 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 smartystreets_python_sdk/us_autocomplete/source.py diff --git a/examples/us_autocomplete_example.py b/examples/us_autocomplete_example.py index 5688630..456bce6 100644 --- a/examples/us_autocomplete_example.py +++ b/examples/us_autocomplete_example.py @@ -2,6 +2,7 @@ from smartystreets_python_sdk import SharedCredentials, BasicAuthCredentials, ClientBuilder from smartystreets_python_sdk.us_autocomplete import Lookup as AutocompleteLookup, geolocation_type +from smartystreets_python_sdk.us_autocomplete.source import Source # This example is for us-autocomplete (V2). It has the same name as a previous product # which has been deprecated since 2022 which we refer to as US Autocomplete Basic. @@ -49,7 +50,7 @@ def run(): lookup.max_results = 5 lookup.prefer_geo = geolocation_type.NONE lookup.prefer_ratio = 33 - lookup.source = 'all' + lookup.source = Source.ALL suggestions = client.send(lookup) # The client will also return the suggestions directly diff --git a/smartystreets_python_sdk/us_autocomplete/__init__.py b/smartystreets_python_sdk/us_autocomplete/__init__.py index fea7586..8d45937 100644 --- a/smartystreets_python_sdk/us_autocomplete/__init__.py +++ b/smartystreets_python_sdk/us_autocomplete/__init__.py @@ -1,3 +1,4 @@ from .suggestion import Suggestion from .lookup import Lookup from .client import Client +from .source import Source diff --git a/smartystreets_python_sdk/us_autocomplete/client.py b/smartystreets_python_sdk/us_autocomplete/client.py index 85ff83b..c3b0cdf 100644 --- a/smartystreets_python_sdk/us_autocomplete/client.py +++ b/smartystreets_python_sdk/us_autocomplete/client.py @@ -1,3 +1,5 @@ +from enum import Enum + from smartystreets_python_sdk import Request from smartystreets_python_sdk.exceptions import SmartyException from smartystreets_python_sdk.us_autocomplete import Suggestion, geolocation_type @@ -65,4 +67,4 @@ def convert_suggestions(suggestion_dictionaries): @staticmethod def add_parameter(request, key, value): if value and value != 'none': - request.parameters[key] = value + request.parameters[key] = value.value if isinstance(value, Enum) else value diff --git a/smartystreets_python_sdk/us_autocomplete/lookup.py b/smartystreets_python_sdk/us_autocomplete/lookup.py index 7947798..18c265e 100644 --- a/smartystreets_python_sdk/us_autocomplete/lookup.py +++ b/smartystreets_python_sdk/us_autocomplete/lookup.py @@ -33,10 +33,8 @@ def __init__(self, search=None, max_results=None, city_filter=None, state_filter when you may not desire it :param selected: Used by UI components to request a list of secondaries (up to 100) for the specified address :param exclude: Removes the given partial address from the suggestion results - :param source: Include results from alternate data sources. If no value is passed, the default will be `postal`. - Allowed values are: - all - will include non-postal addresses in the results - postal - will limit the results to postal addresses only + :param source: Include results from alternate data sources. Defaults to None (API default: postal). + Use Source.ALL to include non-postal addresses, Source.POSTAL to limit to postal only. """ self.result = [] self.custom_parameter_array = {} diff --git a/smartystreets_python_sdk/us_autocomplete/source.py b/smartystreets_python_sdk/us_autocomplete/source.py new file mode 100644 index 0000000..2d151c2 --- /dev/null +++ b/smartystreets_python_sdk/us_autocomplete/source.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class Source(str, Enum): + ALL = "all" + POSTAL = "postal" diff --git a/test/us_autocomplete/client_test.py b/test/us_autocomplete/client_test.py index 4e6301c..2e56463 100644 --- a/test/us_autocomplete/client_test.py +++ b/test/us_autocomplete/client_test.py @@ -3,6 +3,7 @@ from smartystreets_python_sdk import Response, exceptions from test.mocks import * from smartystreets_python_sdk.us_autocomplete import Client, Lookup, geolocation_type +from smartystreets_python_sdk.us_autocomplete.source import Source class TestClient(unittest.TestCase): @@ -30,7 +31,7 @@ def test_sending_fully_populated_lookup(self): lookup.add_state_preference('state4') lookup.prefer_ratio = 3 lookup.prefer_geo = geolocation_type.CITY - lookup.source = 'all' + lookup.source = Source.ALL lookup.selected = 'selectedAddress' lookup.exclude = 'excludedAddress' lookup.add_custom_parameter('custom', '6') @@ -106,6 +107,37 @@ def test_source_omitted_when_unset(self): self.assertNotIn('source', sender.request.parameters) + def test_source_all_enum_serializes_as_string_value(self): + sender = RequestCapturingSender() + client = Client(sender, FakeDeserializer({})) + lookup = Lookup('1') + lookup.source = Source.ALL + + client.send(lookup) + + self.assertIs(type(sender.request.parameters['source']), str) + self.assertEqual('all', sender.request.parameters['source']) + + def test_source_postal_enum_serializes_as_string_value(self): + sender = RequestCapturingSender() + client = Client(sender, FakeDeserializer({})) + lookup = Lookup('1') + lookup.source = Source.POSTAL + + client.send(lookup) + + self.assertIs(type(sender.request.parameters['source']), str) + self.assertEqual('postal', sender.request.parameters['source']) + + def test_source_none_omits_parameter(self): + sender = RequestCapturingSender() + client = Client(sender, FakeDeserializer({})) + lookup = Lookup('1') + + client.send(lookup) + + self.assertNotIn('source', sender.request.parameters) + def test_deserialize_called_with_response_body(self): response = Response('Hello, World!', 0) From 204dcc19d3987c3ccff138927e3015087dbc787d Mon Sep 17 00:00:00 2001 From: Mae Evans Date: Mon, 22 Jun 2026 13:29:53 -0600 Subject: [PATCH 3/5] add source --- smartystreets_python_sdk/us_autocomplete/suggestion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smartystreets_python_sdk/us_autocomplete/suggestion.py b/smartystreets_python_sdk/us_autocomplete/suggestion.py index 0817cca..e9d6308 100644 --- a/smartystreets_python_sdk/us_autocomplete/suggestion.py +++ b/smartystreets_python_sdk/us_autocomplete/suggestion.py @@ -11,3 +11,4 @@ def __init__(self, obj=None): self.state = obj.get('state', None) self.zipcode = obj.get('zipcode', None) self.entries = obj.get('entries', None) + self.source = obj.get('source', None) From 0b4ef23ecd18818b09ac08200812723146656d7a Mon Sep 17 00:00:00 2001 From: Andy Johnson Date: Tue, 23 Jun 2026 10:21:47 -0600 Subject: [PATCH 4/5] Change exclude to list of strings, rename prefer_geolocation. --- examples/us_autocomplete_example.py | 2 +- smartystreets_python_sdk/us_autocomplete/client.py | 8 ++++---- smartystreets_python_sdk/us_autocomplete/lookup.py | 12 ++++++------ test/us_autocomplete/client_test.py | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/us_autocomplete_example.py b/examples/us_autocomplete_example.py index 456bce6..2577925 100644 --- a/examples/us_autocomplete_example.py +++ b/examples/us_autocomplete_example.py @@ -48,7 +48,7 @@ def run(): lookup.add_state_preference('CO') # lookup.selected = '1042 W Center St Apt A (24) Orem UT 84057' lookup.max_results = 5 - lookup.prefer_geo = geolocation_type.NONE + lookup.prefer_geolocation = geolocation_type.NONE lookup.prefer_ratio = 33 lookup.source = Source.ALL diff --git a/smartystreets_python_sdk/us_autocomplete/client.py b/smartystreets_python_sdk/us_autocomplete/client.py index c3b0cdf..b17a31a 100644 --- a/smartystreets_python_sdk/us_autocomplete/client.py +++ b/smartystreets_python_sdk/us_autocomplete/client.py @@ -46,10 +46,10 @@ def build_request(self, lookup): self.add_parameter(request, 'prefer_states', self.build_filter_string(lookup.prefer_states)) self.add_parameter(request, 'prefer_zip_codes', self.build_filter_string(lookup.prefer_zips)) self.add_parameter(request, 'prefer_ratio', lookup.prefer_ratio) - self.add_parameter(request, 'prefer_geolocation', lookup.prefer_geo) + self.add_parameter(request, 'prefer_geolocation', lookup.prefer_geolocation) self.add_parameter(request, 'source', lookup.source) self.add_parameter(request, 'selected', lookup.selected) - self.add_parameter(request, 'exclude', lookup.exclude) + self.add_parameter(request, 'exclude', self.build_filter_string(lookup.exclude, ',')) for parameter in lookup.custom_parameter_array: self.add_parameter(request, parameter, lookup.custom_parameter_array[parameter]) @@ -57,8 +57,8 @@ def build_request(self, lookup): return request @staticmethod - def build_filter_string(filter_list): - return ';'.join(filter_list or []) or None + def build_filter_string(filter_list, separator=';'): + return separator.join(filter_list or []) or None @staticmethod def convert_suggestions(suggestion_dictionaries): diff --git a/smartystreets_python_sdk/us_autocomplete/lookup.py b/smartystreets_python_sdk/us_autocomplete/lookup.py index 18c265e..9c24f2f 100644 --- a/smartystreets_python_sdk/us_autocomplete/lookup.py +++ b/smartystreets_python_sdk/us_autocomplete/lookup.py @@ -4,7 +4,7 @@ class Lookup: def __init__(self, search=None, max_results=None, city_filter=None, state_filter=None, zip_filter=None, exclude_states=None, prefer_cities=None, prefer_states=None, prefer_zips=None, prefer_ratio=None, - prefer_geo=None, selected=None, exclude=None, source=None): + prefer_geolocation=None, selected=None, exclude=None, source=None): """ In addition to holding all of the input data for this lookup, this class also will contain the result @@ -26,7 +26,7 @@ def __init__(self, search=None, max_results=None, city_filter=None, state_filter When this parameter is used, no other _cities or _state parameters can be used :param prefer_ratio: Specifies the percentage of address suggestions that should be preferred and will appear at the top of the suggestion list - :param prefer_geo: If omitted or set to city it uses the sender's IP address to determine location, + :param prefer_geolocation: If omitted or set to city it uses the sender's IP address to determine location, then automatically adds the city and state to the prefer_cities value. This parameter takes precedence over other _include or exclude parameters, meaning that if it is not set to none, you may see addresses from the customer's area @@ -48,9 +48,9 @@ def __init__(self, search=None, max_results=None, city_filter=None, state_filter self.prefer_states = prefer_states or [] self.prefer_zips = prefer_zips or [] self.prefer_ratio = prefer_ratio - self.prefer_geo = prefer_geo + self.prefer_geolocation = prefer_geolocation self.selected = selected - self.exclude = exclude + self.exclude = exclude or [] self.source = source def add_city_filter(self, city): @@ -60,7 +60,7 @@ def add_state_filter(self, state): self.state_filter.append(state) def add_zip_filter(self, zipcode): - self.prefer_geo = geolocation_type.NONE + self.prefer_geolocation = geolocation_type.NONE self.zip_filter.append(zipcode) def add_state_exclusion(self, state): @@ -73,7 +73,7 @@ def add_state_preference(self, state): self.prefer_states.append(state) def add_zip_preference(self, zipcode): - self.prefer_geo = geolocation_type.NONE + self.prefer_geolocation = geolocation_type.NONE self.prefer_zips.append(zipcode) def add_custom_parameter(self, parameter, value): diff --git a/test/us_autocomplete/client_test.py b/test/us_autocomplete/client_test.py index 2e56463..33f669a 100644 --- a/test/us_autocomplete/client_test.py +++ b/test/us_autocomplete/client_test.py @@ -30,10 +30,10 @@ def test_sending_fully_populated_lookup(self): lookup.add_city_preference('city3') lookup.add_state_preference('state4') lookup.prefer_ratio = 3 - lookup.prefer_geo = geolocation_type.CITY + lookup.prefer_geolocation = geolocation_type.CITY lookup.source = Source.ALL lookup.selected = 'selectedAddress' - lookup.exclude = 'excludedAddress' + lookup.exclude = ['excludedAddress'] lookup.add_custom_parameter('custom', '6') client.send(lookup) @@ -57,18 +57,18 @@ def test_sending_exclude(self): serializer = FakeSerializer({}) client = Client(sender, serializer) lookup = Lookup('1') - lookup.exclude = 'excludedAddress' + lookup.exclude = ['excluded1', 'excluded2', 'excluded3'] client.send(lookup) - self.assertEqual('excludedAddress', sender.request.parameters['exclude']) + self.assertEqual('excluded1,excluded2,excluded3', sender.request.parameters['exclude']) def test_prefer_geolocation_none_is_omitted(self): sender = RequestCapturingSender() serializer = FakeSerializer({}) client = Client(sender, serializer) lookup = Lookup('1') - lookup.prefer_geo = geolocation_type.NONE + lookup.prefer_geolocation = geolocation_type.NONE client.send(lookup) From 9e4f6c237391cf4e92c5e52c8a06146f0d9df208 Mon Sep 17 00:00:00 2001 From: Andy Johnson Date: Tue, 23 Jun 2026 10:47:25 -0600 Subject: [PATCH 5/5] Add add_exclude func. --- smartystreets_python_sdk/us_autocomplete/lookup.py | 3 +++ test/us_autocomplete/client_test.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/smartystreets_python_sdk/us_autocomplete/lookup.py b/smartystreets_python_sdk/us_autocomplete/lookup.py index 9c24f2f..dc398e8 100644 --- a/smartystreets_python_sdk/us_autocomplete/lookup.py +++ b/smartystreets_python_sdk/us_autocomplete/lookup.py @@ -76,5 +76,8 @@ def add_zip_preference(self, zipcode): self.prefer_geolocation = geolocation_type.NONE self.prefer_zips.append(zipcode) + def add_exclude(self, exclude_type): + self.exclude.append(exclude_type) + def add_custom_parameter(self, parameter, value): self.custom_parameter_array[parameter] = value diff --git a/test/us_autocomplete/client_test.py b/test/us_autocomplete/client_test.py index 33f669a..f555049 100644 --- a/test/us_autocomplete/client_test.py +++ b/test/us_autocomplete/client_test.py @@ -33,7 +33,7 @@ def test_sending_fully_populated_lookup(self): lookup.prefer_geolocation = geolocation_type.CITY lookup.source = Source.ALL lookup.selected = 'selectedAddress' - lookup.exclude = ['excludedAddress'] + lookup.add_exclude('excludedAddress') lookup.add_custom_parameter('custom', '6') client.send(lookup)