diff --git a/shipengine_sdk/__init__.py b/shipengine_sdk/__init__.py index ef1b01b..0bbbcc5 100644 --- a/shipengine_sdk/__init__.py +++ b/shipengine_sdk/__init__.py @@ -4,8 +4,6 @@ import logging from logging import NullHandler -from .models import * # noqa - # SDK imports here from .shipengine import ShipEngine from .shipengine_config import ShipEngineConfig diff --git a/shipengine_sdk/models/enums/error_type.py b/shipengine_sdk/models/enums/error_type.py index 64b0608..f496ee1 100644 --- a/shipengine_sdk/models/enums/error_type.py +++ b/shipengine_sdk/models/enums/error_type.py @@ -47,3 +47,6 @@ class ErrorType(Enum): AUTHORIZATION = "authorization" """General authorization error type.""" + + ERROR = "error" + """Generic error.""" diff --git a/shipengine_sdk/services/address_validation.py b/shipengine_sdk/services/address_validation.py index 0b4a435..a7fc851 100644 --- a/shipengine_sdk/services/address_validation.py +++ b/shipengine_sdk/services/address_validation.py @@ -5,6 +5,7 @@ from ..models.address import Address, AddressValidateResult from ..models.enums import RPCMethods from ..shipengine_config import ShipEngineConfig +from ..util import does_normalized_address_have_errors def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResult: @@ -33,5 +34,13 @@ def validate(address: Address, config: ShipEngineConfig) -> AddressValidateResul def normalize(address: Address, config: ShipEngineConfig) -> Address: + """ + Normalize a given address into a standardized format. + + :param Address address: The address to be validate. + :param ShipEngineConfig config: The global ShipEngine configuration object. + :returns: :class:`Address`: The normalized address returned from ShipEngine API. + """ validation_result: AddressValidateResult = validate(address=address, config=config) + does_normalized_address_have_errors(result=validation_result) return validation_result.normalized_address diff --git a/shipengine_sdk/shipengine.py b/shipengine_sdk/shipengine.py index 4e701c2..75b906f 100644 --- a/shipengine_sdk/shipengine.py +++ b/shipengine_sdk/shipengine.py @@ -2,7 +2,7 @@ from typing import Dict, Union from .models.address import Address, AddressValidateResult -from .services.address_validation import validate +from .services.address_validation import normalize, validate from .shipengine_config import ShipEngineConfig @@ -40,3 +40,10 @@ def validate_address( """ config: ShipEngineConfig = self.config.merge(new_config=config) return validate(address, config) + + def normalize_address( + self, address: Address, config: Union[Dict[str, any], ShipEngineConfig] = None + ) -> Address: + """Normalize a given address into a standardized format used by carriers.""" + config: ShipEngineConfig = self.config.merge(new_config=config) + return normalize(address=address, config=config) diff --git a/shipengine_sdk/util/sdk_assertions.py b/shipengine_sdk/util/sdk_assertions.py index ceb5da3..e8bffa6 100644 --- a/shipengine_sdk/util/sdk_assertions.py +++ b/shipengine_sdk/util/sdk_assertions.py @@ -226,3 +226,40 @@ def is_response_500(status_code: int, response_body: Dict[str, any]) -> None: error_type=error_data["type"], error_code=error_data["code"], ) + + +def does_normalized_address_have_errors(result) -> None: + """ + Assertions to check if the returned normalized address has any errors. If errors + are present an exception is thrown. + + :param AddressValidateResult result: The address validation response from ShipEngine API. + """ + if len(result.errors) > 1: + error_list = list() + map(lambda msg: error_list.append(msg), result.errors) + str_errors = "\n".join(error_list) + + raise ShipEngineError( + message=f"Invalid address.\n {str_errors}", + request_id=result.request_id, + source=ErrorSource.SHIPENGINE.value, + error_type=ErrorType.ERROR.value, + error_code=ErrorCode.INVALID_ADDRESS.value, + ) + elif len(result.errors) == 1: + raise ShipEngineError( + message=f"Invalid address.\n {result.errors[0]['message']}", + request_id=result.request_id, + source=ErrorSource.SHIPENGINE.value, + error_type=ErrorType.ERROR.value, + error_code=result.errors[0]["code"], + ) + elif result.is_valid is False: + raise ShipEngineError( + message="Invalid address - The address provided could not be normalized.", + request_id=result.request_id, + source=ErrorSource.SHIPENGINE.value, + error_type=ErrorType.ERROR.value, + error_code=ErrorCode.INVALID_ADDRESS.value, + ) diff --git a/tests/jsonrpc/test_process_request.py b/tests/jsonrpc/test_process_request.py index 25ddfe0..5d8dce8 100644 --- a/tests/jsonrpc/test_process_request.py +++ b/tests/jsonrpc/test_process_request.py @@ -1,7 +1,6 @@ """Testing the process request and response functions.""" import pytest -from shipengine_sdk import ErrorCode, ErrorSource, ErrorType, RPCMethods from shipengine_sdk.errors import ( AccountStatusError, BusinessRuleError, @@ -11,6 +10,7 @@ ValidationError, ) from shipengine_sdk.jsonrpc import handle_response, wrap_request +from shipengine_sdk.models import ErrorCode, ErrorSource, ErrorType, RPCMethods def handle_response_errors(error_source: str, error_code: str, error_type: str): diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 0000000..b5bcf10 --- /dev/null +++ b/tests/services/__init__.py @@ -0,0 +1 @@ +"""Initial Docstring""" diff --git a/tests/services/test_address_validation.py b/tests/services/test_address_validation.py index e36b9be..eb5eba9 100644 --- a/tests/services/test_address_validation.py +++ b/tests/services/test_address_validation.py @@ -1,224 +1,31 @@ -"""Initial Docstring""" +"""Test the validate address method of the ShipEngine SDK.""" import re -from typing import Dict -from shipengine_sdk import ShipEngine from shipengine_sdk.errors import ClientSystemError, ValidationError from shipengine_sdk.models import ( Address, AddressValidateResult, - Endpoints, ErrorCode, ErrorSource, ErrorType, ) - -def stub_config() -> Dict[str, any]: - """ - Return a test configuration dictionary to be used - when instantiating the ShipEngine object. - """ - return dict( - api_key="baz", base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=15 - ) - - -def stub_shipengine_instance() -> ShipEngine: - """Return a test instance of the ShipEngine object.""" - return ShipEngine(stub_config()) - - -def valid_residential_address() -> Address: - """ - Return a test Address object with valid residential - address information. - """ - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def valid_commercial_address() -> Address: - """ - Return a test Address object with valid commercial - address information. - """ - return Address( - street=["4 Jersey St", "ste 200"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def address_with_warnings() -> Address: - """Return a test Address object that will cause the server to return warning messages.""" - return Address( - street=["170 Warning Blvd", "Apartment 32-B"], - city_locality="Toronto", - state_province="ON", - postal_code="M6K 3C3", - country_code="CA", - ) - - -def address_with_errors() -> Address: - """Return a test Address object that will cause the server to return an error message.""" - return Address( - street=["4 Invalid St"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def valid_canadian_address() -> Address: - """Return an Address object with a valid canadian address.""" - return Address( - street=["170 Princes Blvd", "Ste 200"], - city_locality="Toronto", - state_province="ON", - postal_code="M6K 3C3", - country_code="CA", - ) - - -def multi_line_address() -> Address: - """Returns a valid multiline address.""" - return Address( - street=["4 Jersey St", "ste 200", "1st Floor"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def non_latin_address() -> Address: - """Return an address with non-latin characters.""" - return Address( - street=["上鳥羽角田町68"], - city_locality="南区", - state_province="京都", - postal_code="601-8104", - country_code="JP", - ) - - -def unknown_address() -> Address: - """ - Return an address that will make the server respond with an - address with an unknown residential flag. - """ - return Address( - street=["4 Unknown St"], - city_locality="Toronto", - state_province="ON", - postal_code="M6K 3C3", - country_code="CA", - ) - - -def address_missing_required_fields() -> Address: - """Return an address that is missing a state, city, and postal_code to return a ValidationError..""" - return Address( - street=["4 Jersey St"], - city_locality="", - state_province="", - postal_code="", - country_code="US", - ) - - -def address_missing_country() -> Address: - """Return an address that is only missing the country_code.""" - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="", - ) - - -def address_with_invalid_country() -> Address: - """Return an address that has an invalid country_code specified.""" - return Address( - street=["4 Jersey St", "Apt. 2b"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="RZ", - ) - - -def get_server_side_error() -> Address: - """Return an address that will cause the server to return a 500 server error.""" - return Address( - street=["500 Server Error"], - city_locality="Boston", - state_province="MA", - postal_code="02215", - country_code="US", - ) - - -def validate_an_address(address: Address) -> AddressValidateResult: - """ - Helper function that passes a config dictionary into the ShipEngine object to instantiate - it and calls the `validate_address` method, providing it the `address` that is passed into - this function. - """ - return stub_shipengine_instance().validate_address(address=address) - - -def us_valid_address_assertions( - original_address: Address, - validated_address: AddressValidateResult, - expected_residential_indicator, -) -> None: - """A set of common assertions that are regularly made on the commercial US address used for testing.""" - address = validated_address.normalized_address - assert type(validated_address) is AddressValidateResult - assert validated_address.is_valid is True - assert type(address) is Address - assert len(validated_address.info) == 0 - assert len(validated_address.warnings) == 0 - assert len(validated_address.errors) == 0 - assert address is not None - assert address.city_locality == original_address.city_locality.upper() - assert address.state_province == original_address.state_province.upper() - assert address.postal_code == original_address.postal_code - assert address.country_code == original_address.country_code.upper() - assert address.is_residential is expected_residential_indicator - - -def canada_valid_address_assertions( - original_address: Address, - validated_address: AddressValidateResult, - expected_residential_indicator, -) -> None: - """A set of common assertions that are regularly made on the canadian_address used for testing.""" - address = validated_address.normalized_address - assert type(validated_address) is AddressValidateResult - assert validated_address.is_valid is True - assert type(address) is Address - assert len(validated_address.info) == 0 - assert len(validated_address.warnings) == 0 - assert len(validated_address.errors) == 0 - assert address is not None - assert address.city_locality == original_address.city_locality - assert address.state_province == original_address.state_province.title() - assert address.postal_code == "M6 K 3 C3" - assert address.country_code == original_address.country_code.upper() - assert address.is_residential is expected_residential_indicator +from ..util.test_data import ( + address_missing_required_fields, + address_with_errors, + address_with_invalid_country, + address_with_warnings, + canada_valid_avs_assertions, + get_server_side_error, + multi_line_address, + non_latin_address, + unknown_address, + us_valid_avs_assertions, + valid_canadian_address, + valid_commercial_address, + valid_residential_address, + validate_an_address, +) class TestValidateAddress: @@ -228,7 +35,7 @@ def test_valid_residential_address(self) -> None: validated_address = validate_an_address(residential_address) address = validated_address.normalized_address - us_valid_address_assertions( + us_valid_avs_assertions( original_address=residential_address, validated_address=validated_address, expected_residential_indicator=True, @@ -246,7 +53,7 @@ def test_valid_commercial_address(self) -> None: validated_address = validate_an_address(commercial_address) address = validated_address.normalized_address - us_valid_address_assertions( + us_valid_avs_assertions( original_address=commercial_address, validated_address=validated_address, expected_residential_indicator=False, @@ -264,7 +71,7 @@ def test_multi_line_address(self) -> None: validated_address = validate_an_address(valid_multi_line_address) address = validated_address.normalized_address - us_valid_address_assertions( + us_valid_avs_assertions( original_address=valid_multi_line_address, validated_address=validated_address, expected_residential_indicator=False, @@ -281,7 +88,7 @@ def test_numeric_postal_code(self) -> None: """DX-1028 - Validate numeric postal code.""" residential_address = valid_residential_address() validated_address = validate_an_address(residential_address) - us_valid_address_assertions( + us_valid_avs_assertions( original_address=residential_address, validated_address=validated_address, expected_residential_indicator=True, @@ -292,7 +99,7 @@ def test_alpha_postal_code(self): """DX-1029 - Alpha postal code.""" canadian_address = valid_canadian_address() validated_address = validate_an_address(canadian_address) - canada_valid_address_assertions( + canada_valid_avs_assertions( original_address=canadian_address, validated_address=validated_address, expected_residential_indicator=False, @@ -302,7 +109,7 @@ def test_unknown_address(self): """DX-1026 - Validate address of unknown address.""" address = unknown_address() validated_address = validate_an_address(address) - canada_valid_address_assertions( + canada_valid_avs_assertions( original_address=address, validated_address=validated_address, expected_residential_indicator=None, diff --git a/tests/services/test_normalize_address.py b/tests/services/test_normalize_address.py new file mode 100644 index 0000000..c0da67d --- /dev/null +++ b/tests/services/test_normalize_address.py @@ -0,0 +1,22 @@ +"""Test the normalize address method of the ShipEngine SDK.""" +from shipengine_sdk.models import Address + +from ..util.test_data import ( + normalize_an_address, + us_valid_normalize_assertions, + valid_residential_address, +) + + +class TestNormalizeAddress: + def test_normalize_valid_residential_address(self) -> None: + """DX-1041 - Normalize valid residential address.""" + residential_address = valid_residential_address() + normalized = normalize_an_address(residential_address) + + us_valid_normalize_assertions( + original_address=residential_address, + normalized_address=normalized, + expected_residential_indicator=True, + ) + assert type(normalized) is Address diff --git a/tests/util/__init__.py b/tests/util/__init__.py new file mode 100644 index 0000000..b5bcf10 --- /dev/null +++ b/tests/util/__init__.py @@ -0,0 +1 @@ +"""Initial Docstring""" diff --git a/tests/util/test_data.py b/tests/util/test_data.py new file mode 100644 index 0000000..1e32e86 --- /dev/null +++ b/tests/util/test_data.py @@ -0,0 +1,247 @@ +"""Test data as functions.""" +from typing import Dict + +from shipengine_sdk import ShipEngine +from shipengine_sdk.models import Address, AddressValidateResult, Endpoints + + +def stub_config() -> Dict[str, any]: + """ + Return a test configuration dictionary to be used + when instantiating the ShipEngine object. + """ + return dict( + api_key="baz", base_uri=Endpoints.TEST_RPC_URL.value, page_size=50, retries=2, timeout=15 + ) + + +def stub_shipengine_instance() -> ShipEngine: + """Return a test instance of the ShipEngine object.""" + return ShipEngine(stub_config()) + + +def valid_residential_address() -> Address: + """ + Return a test Address object with valid residential + address information. + """ + return Address( + street=["4 Jersey St", "Apt. 2b"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + +def valid_commercial_address() -> Address: + """ + Return a test Address object with valid commercial + address information. + """ + return Address( + street=["4 Jersey St", "ste 200"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + +def address_with_warnings() -> Address: + """Return a test Address object that will cause the server to return warning messages.""" + return Address( + street=["170 Warning Blvd", "Apartment 32-B"], + city_locality="Toronto", + state_province="ON", + postal_code="M6K 3C3", + country_code="CA", + ) + + +def address_with_errors() -> Address: + """Return a test Address object that will cause the server to return an error message.""" + return Address( + street=["4 Invalid St"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + +def valid_canadian_address() -> Address: + """Return an Address object with a valid canadian address.""" + return Address( + street=["170 Princes Blvd", "Ste 200"], + city_locality="Toronto", + state_province="ON", + postal_code="M6K 3C3", + country_code="CA", + ) + + +def multi_line_address() -> Address: + """Returns a valid multiline address.""" + return Address( + street=["4 Jersey St", "ste 200", "1st Floor"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + +def non_latin_address() -> Address: + """Return an address with non-latin characters.""" + return Address( + street=["上鳥羽角田町68"], + city_locality="南区", + state_province="京都", + postal_code="601-8104", + country_code="JP", + ) + + +def unknown_address() -> Address: + """ + Return an address that will make the server respond with an + address with an unknown residential flag. + """ + return Address( + street=["4 Unknown St"], + city_locality="Toronto", + state_province="ON", + postal_code="M6K 3C3", + country_code="CA", + ) + + +def address_missing_required_fields() -> Address: + """Return an address that is missing a state, city, and postal_code to return a ValidationError..""" + return Address( + street=["4 Jersey St"], + city_locality="", + state_province="", + postal_code="", + country_code="US", + ) + + +def address_missing_country() -> Address: + """Return an address that is only missing the country_code.""" + return Address( + street=["4 Jersey St", "Apt. 2b"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="", + ) + + +def address_with_invalid_country() -> Address: + """Return an address that has an invalid country_code specified.""" + return Address( + street=["4 Jersey St", "Apt. 2b"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="RZ", + ) + + +def get_server_side_error() -> Address: + """Return an address that will cause the server to return a 500 server error.""" + return Address( + street=["500 Server Error"], + city_locality="Boston", + state_province="MA", + postal_code="02215", + country_code="US", + ) + + +def validate_an_address(address: Address) -> AddressValidateResult: + """ + Helper function that passes a config dictionary into the ShipEngine object to instantiate + it and calls the `validate_address` method, providing it the `address` that is passed into + this function. + """ + return stub_shipengine_instance().validate_address(address=address) + + +def normalize_an_address(address: Address) -> Address: + """ + Helper function that passes a config dictionary into the ShipEngine object to instantiate + it and calls the `normalize_address` method, providing it the `address` that is passed into + this function. + """ + return stub_shipengine_instance().normalize_address(address) + + +# Assertion helper functions + + +def us_valid_avs_assertions( + original_address: Address, + validated_address: AddressValidateResult, + expected_residential_indicator, +) -> None: + """ + A set of common assertions that are regularly made on the commercial US address + used for testing `validate_address`. + """ + address = validated_address.normalized_address + assert type(validated_address) is AddressValidateResult + assert validated_address.is_valid is True + assert type(address) is Address + assert len(validated_address.info) == 0 + assert len(validated_address.warnings) == 0 + assert len(validated_address.errors) == 0 + assert address is not None + assert address.city_locality == original_address.city_locality.upper() + assert address.state_province == original_address.state_province.upper() + assert address.postal_code == original_address.postal_code + assert address.country_code == original_address.country_code.upper() + assert address.is_residential is expected_residential_indicator + + +def canada_valid_avs_assertions( + original_address: Address, + validated_address: AddressValidateResult, + expected_residential_indicator, +) -> None: + """ + A set of common assertions that are regularly made on the canadian_address + used for testing `validate_address`. + """ + address = validated_address.normalized_address + assert type(validated_address) is AddressValidateResult + assert validated_address.is_valid is True + assert type(address) is Address + assert len(validated_address.info) == 0 + assert len(validated_address.warnings) == 0 + assert len(validated_address.errors) == 0 + assert address is not None + assert address.city_locality == original_address.city_locality + assert address.state_province == original_address.state_province.title() + assert address.postal_code == "M6 K 3 C3" + assert address.country_code == original_address.country_code.upper() + assert address.is_residential is expected_residential_indicator + + +def us_valid_normalize_assertions( + original_address: Address, + normalized_address: Address, + expected_residential_indicator, +) -> None: + """ + A set of common assertions that are regularly made on the commercial US address + used for `normalized_address` testing. + """ + assert type(normalized_address) is Address + assert normalized_address.city_locality == original_address.city_locality.upper() + assert normalized_address.state_province == original_address.state_province.upper() + assert normalized_address.postal_code == original_address.postal_code + assert normalized_address.country_code == original_address.country_code.upper() + assert normalized_address.is_residential is expected_residential_indicator