From e1f0b7ea3693faa99c91dfbc7e052fccd69c50e0 Mon Sep 17 00:00:00 2001 From: James Brown Date: Tue, 1 Jun 2021 22:50:31 +0000 Subject: [PATCH] clean up address verify property and some miscellaneous request logic allows users to just pass `True` for verify and verify_strict instead of having to pass `[True]`. Also properly quotes in case a user accidentally passes something icky. as always, I am a firm believer that functions should be functions, not classmethods --- easypost/__init__.py | 66 +++++++++------ ...est_address_creation_with_verify_bool.yaml | 83 +++++++++++++++++++ tests/test_address.py | 21 +++++ 3 files changed, 143 insertions(+), 27 deletions(-) create mode 100644 tests/cassettes/test_address_creation_with_verify_bool.yaml diff --git a/easypost/__init__.py b/easypost/__init__.py index 22bd6dca..70b28c2f 100644 --- a/easypost/__init__.py +++ b/easypost/__init__.py @@ -152,6 +152,31 @@ def convert_to_easypost_object(response, api_key, parent=None, name=None): return response +def _utf8(value): + if six.PY2: + # Python2's urlencode wants bytestrings, not unicode + if isinstance(value, six.text_type): + return value.encode('utf-8') + return value + elif isinstance(value, six.binary_type): + # Python3's six.text_type(bytestring) returns "b'bytestring'" + # So, have to decode it to unicode + return value.decode('utf-8') + else: + # Python3's urlencode can handle unicode + return value + + +def _urlencode_list(params): + encoded = [] + for key, values in sorted(params.items()): + for value in values: + if isinstance(value, bool): + value = str(value).lower() + encoded.append('{0}[]={1}'.format(key, quote_plus(_utf8(value)))) + return '&'.join(encoded) + + class Requestor(object): def __init__(self, local_api_key=None): self._api_key = local_api_key @@ -161,27 +186,12 @@ def api_url(cls, url=None): url = url or '' return '%s%s' % (api_base, url) - @classmethod - def _utf8(cls, value): - if six.PY2: - # Python2's urlencode wants bytestrings, not unicode - if isinstance(value, six.text_type): - return value.encode('utf-8') - return value - elif isinstance(value, six.binary_type): - # Python3's six.text_type(bytestring) returns "b'bytestring'" - # So, have to decode it to unicode - return value.decode('utf-8') - else: - # Python3's urlencode can handle unicode - return value - @classmethod def encode_dict(cls, out, key, dict_value): n = {} for k, v in sorted(six.iteritems(dict_value)): - k = cls._utf8(k) - v = cls._utf8(v) + k = _utf8(k) + v = _utf8(v) n["%s[%s]" % (key, k)] = v out.extend(cls._encode_inner(n)) @@ -189,7 +199,7 @@ def encode_dict(cls, out, key, dict_value): def encode_list(cls, out, key, list_value): n = {} for k, v in enumerate(list_value): - v = cls._utf8(v) + v = _utf8(v) n["%s[%s]" % (key, k)] = v out.extend(cls._encode_inner(n)) @@ -217,7 +227,7 @@ def _encode_inner(cls, params): out = [] for key, value in sorted(six.iteritems(params)): - key = cls._utf8(key) + key = _utf8(key) try: encoder = ENCODERS[value.__class__] encoder(out, key, value) @@ -574,7 +584,7 @@ def instance_url(self): easypost_id = self.get('id') if not easypost_id: raise Error('%s instance has invalid ID: %r' % (type(self).__name__, easypost_id)) - easypost_id = Requestor._utf8(easypost_id) + easypost_id = _utf8(easypost_id) base = self.class_url() param = quote_plus(easypost_id) return "{base}/{param}".format(base=base, param=param) @@ -634,13 +644,15 @@ def create(cls, api_key=None, verify=None, verify_strict=None, **params): requestor = Requestor(api_key) url = cls.class_url() - if verify or verify_strict: - verify = verify or [] - verify_strict = verify_strict or [] - url += '?' + '&'.join( - ['verify[]={0}'.format(opt) for opt in verify] + - ['verify_strict[]={0}'.format(opt) for opt in verify_strict] - ) + verify_params = {} + for key, value in (('verify', verify), ('verify_strict', verify_strict)): + if not value: + continue + elif isinstance(value, (bool, str)): + value = [value] + verify_params[key] = value + if verify_params: + url += '?' + _urlencode_list(verify_params) wrapped_params = {cls.class_name(): params} response, api_key = requestor.request('post', url, wrapped_params) diff --git a/tests/cassettes/test_address_creation_with_verify_bool.yaml b/tests/cassettes/test_address_creation_with_verify_bool.yaml new file mode 100644 index 00000000..3c770240 --- /dev/null +++ b/tests/cassettes/test_address_creation_with_verify_bool.yaml @@ -0,0 +1,83 @@ +interactions: +- request: + body: address%5Bcity%5D=San+Francisco&address%5Bcompany%5D=EasyPost&address%5Bcountry%5D=US&address%5Bphone%5D=415-456-7890&address%5Bstate%5D=CA&address%5Bstreet1%5D=118+2&address%5Bstreet2%5D=FLoor+4&address%5Bzip%5D=94105 + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '218' + Content-type: + - application/x-www-form-urlencoded + authorization: + - EZTK-NONE + user-agent: + - easypost/v2 pythonclient/suppressed + x-client-user-agent: + - suppressed + method: POST + uri: https://api.easypost.com/v2/addresses?verify%5B%5D=true + response: + body: + string: !!binary | + H4sIAAAAAAAAA4ySzU7jMBSFXyXylhRsN2nS7KIySCOhgkhZjEYocuwbxiPHrmwHARXvznULs5hV + s3Hu8Xeuj38ORCvSEKF8v+IDH0ZVg2R1IYWoGZMg1uuCVeuqHgaSEzf8BRmRb5XyEAJK0oOIoHqR + ZE45W9DVgrId503JG0YvKG0oRXDeq/NAKyYgjZ2Nwe5u2gv7ho4fbffr/q7bIRCiB4gMRcbqjG+v + s26X3dxmxb85jnMpm47J2rXb7Oah3W5+dpu7I4NBUN+0WLzrPf7iJmm5WK54CiDdbKNPzscOy/0f + ZxNesLIoV1W9TgxMQpvvlJNTCYgQYrIL7zX4fhRSm2OCE4UHphXYqAUaR2EC5GQEBV6YPorXPl3E + iTwG/E97Aa9HLUXUzgbSHFLwIo1hljJdRRP9jB3Be+ex+v2UEwURU4ZTi49UG4193s61HYjB9eKc + dresLqu6qsqcGGefv8QF4/xyiU+kyknUE/Tvp6NqJ0wrxdWtC31rn8FAIB/4fQIAAP//AwDow9Tp + bgIAAA== + headers: + cache-control: + - no-cache, no-store + content-encoding: + - gzip + content-type: + - application/json; charset=utf-8 + etag: + - W/"b7d8f69ebe4ba9aee3182e54d399f72a" + expires: + - '0' + location: + - /api/v2/addresses/adr_62b2bfd8ec184caa811cea99417978bb + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=15768000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - d6502afc60b6ba1afb32a2c100f6fc6a + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb5nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb1nuq 15c8815ace + - extlb1nuq 15c8815ace + x-request-id: + - 1d5229af-27c8-4ffb-a2f9-b2fa88d35b9e + x-runtime: + - '0.056680' + x-version-label: + - easypost-202106012134-e838b9cad4-master + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +version: 1 diff --git a/tests/test_address.py b/tests/test_address.py index d1f64ec6..0c7d0965 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -49,6 +49,27 @@ def test_address_creation_with_verify(): assert address.country == 'US' +@pytest.mark.vcr() +def test_address_creation_with_verify_bool(): + # Create an address with a verify parameter to test that it verifies accurately + address = easypost.Address.create( + verify=True, + street1='118 2', + street2='FLoor 4', + city='San Francisco', + state='CA', + zip='94105', + country='US', + company='EasyPost', + phone='415-456-7890' + ) + + assert address.id is not None + assert address.street1 == '118 2ND ST FL 4' + assert address.street2 == '' + assert address.country == 'US' + + @pytest.mark.vcr() def test_address_creation_with_verify_failure(): # Create an address with a verify parameter to test that it fails elegantly