From 1ca7de27472504accc9a7e1154208f04571095f9 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 25 Jan 2023 12:10:07 -0700 Subject: [PATCH 1/3] feat: add retrieve_stateless_rates function --- CHANGELOG.md | 4 + easypost/beta/__init__.py | 1 + easypost/beta/rate.py | 26 +++++ .../test_retrieve_stateless_rates.yaml | 108 ++++++++++++++++++ tests/test_beta_rate.py | 12 ++ 5 files changed, 151 insertions(+) create mode 100644 easypost/beta/rate.py create mode 100644 tests/cassettes/test_retrieve_stateless_rates.yaml create mode 100644 tests/test_beta_rate.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 91690d49..01b54453 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## Next Release + +- Adds `retrieve_stateless_rates` function to pull stateless rates when shipment data is provided + ## v7.9.0 (2023-01-18) - Adds `all` function to `Pickup` to retrieve all pickups diff --git a/easypost/beta/__init__.py b/easypost/beta/__init__.py index ff8952fb..e439a0e4 100644 --- a/easypost/beta/__init__.py +++ b/easypost/beta/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa +from easypost.beta.rate import Rate from easypost.beta.referral import Referral diff --git a/easypost/beta/rate.py b/easypost/beta/rate.py new file mode 100644 index 00000000..92342baf --- /dev/null +++ b/easypost/beta/rate.py @@ -0,0 +1,26 @@ +from typing import ( + Any, + Dict, + Optional, +) + +from easypost.easypost_object import convert_to_easypost_object +from easypost.requestor import ( + RequestMethod, + Requestor, +) +from easypost.resource import Resource + + +class Rate(Resource): + @classmethod + def retrieve_stateless_rates(cls, api_key: Optional[str] = None, **params) -> Dict[str, Any]: + """Retrieves stateless rates by passing shipment data.""" + requestor = Requestor(local_api_key=api_key) + url = cls.class_url() + wrapped_params = { + "shipment": params, + } + response, api_key = requestor.request(method=RequestMethod.POST, url=url, params=wrapped_params, beta=True) + + return convert_to_easypost_object(response=response, api_key=api_key) diff --git a/tests/cassettes/test_retrieve_stateless_rates.yaml b/tests/cassettes/test_retrieve_stateless_rates.yaml new file mode 100644 index 00000000..f173742f --- /dev/null +++ b/tests/cassettes/test_retrieve_stateless_rates.yaml @@ -0,0 +1,108 @@ +interactions: +- request: + body: '{"shipment": {"from_address": {"name": "Jack Sparrow", "street1": "388 + Townsend St", "street2": "Apt 20", "city": "San Francisco", "state": "CA", "zip": + "94107", "country": "US", "email": "test@example.com", "phone": "5555555555"}, + "to_address": {"name": "Elizabeth Swan", "street1": "179 N Harbor Dr", "city": + "Redondo Beach", "state": "CA", "zip": "90277", "country": "US", "email": "test@example.com", + "phone": "5555555555"}, "parcel": {"length": 10, "width": 8, "height": 4, "weight": + 15.4}}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '496' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/beta/rates + response: + body: + string: '{"from_address": {"object": "Address", "name": "Jack Sparrow", "street1": + "388 Townsend St", "street2": "Apt 20", "city": "San Francisco", "state": + "CA", "zip": "94107", "country": "US", "phone": "", "email": ""}, + "to_address": {"object": "Address", "name": "Elizabeth Swan", "street1": "179 + N Harbor Dr", "city": "Redondo Beach", "state": "CA", "zip": "90277", "country": + "US", "phone": "", "email": ""}, "rates": [{"object": + "Rate", "mode": "test", "service": "ParcelSelect", "carrier": "USPS", "rate": + "7.75", "currency": "USD", "retail_rate": "7.75", "retail_currency": "USD", + "list_rate": "7.75", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + 5, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + 5, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": + "Rate", "mode": "test", "service": "Priority", "carrier": "USPS", "rate": + "8.24", "currency": "USD", "retail_rate": "10.20", "retail_currency": "USD", + "list_rate": "8.24", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + 2, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + 2, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": + "Rate", "mode": "test", "service": "First", "carrier": "USPS", "rate": "6.07", + "currency": "USD", "retail_rate": "6.07", "retail_currency": "USD", "list_rate": + "6.07", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + 3, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + 3, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": + "Rate", "mode": "test", "service": "Express", "carrier": "USPS", "rate": "31.25", + "currency": "USD", "retail_rate": "35.80", "retail_currency": "USD", "list_rate": + "31.25", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + null, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + null, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}], "options": + {"currency": "USD", "payment": {"type": "SENDER"}, "date_advance": 0}, "parcel": + {"object": "Parcel", "length": 10.0, "width": 8.0, "height": 4.0, "weight": + 15.4}, "messages": [{"carrier": "DhlEcs", "carrier_account_id": "ca_c3cbbd21bc97400bbbaed6d030909476", + "type": "rate_error", "message": "Unauthorized. Please check credentials and + try again"}]}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '2236' + content-type: + - application/json; charset=utf-8 + etag: + - W/"5730ea5b8b4ede80bf42e209b8b4d9df" + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-canary: + - direct + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 974d0bc763d17e0be78859a4000435bb + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb7nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb1nuq 29913d444b + - intlb1wdc 29913d444b + - extlb3wdc 29913d444b + x-runtime: + - '0.692670' + x-version-label: + - easypost-202301251848-057c9f927b-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_beta_rate.py b/tests/test_beta_rate.py new file mode 100644 index 00000000..cce2abf1 --- /dev/null +++ b/tests/test_beta_rate.py @@ -0,0 +1,12 @@ +import pytest + +import easypost + + +@pytest.mark.vcr() +def test_retrieve_stateless_rates(basic_shipment): + response = easypost.beta.Rate.retrieve_stateless_rates(**basic_shipment) + + stateless_rates = response["rates"] + + assert all(rate["object"] == "Rate" for rate in stateless_rates) From 8261a678b2a21f29e3b34e7ae77f31151bc9f0f3 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 25 Jan 2023 12:43:45 -0700 Subject: [PATCH 2/3] feat: adds get_lowest_stateless_rate function --- CHANGELOG.md | 1 + easypost/beta/rate.py | 30 ++++- .../test_beta_get_lowest_stateless_rate.yaml | 105 ++++++++++++++++++ ...> test_beta_retrieve_stateless_rates.yaml} | 21 ++-- tests/test_beta_rate.py | 17 ++- 5 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 tests/cassettes/test_beta_get_lowest_stateless_rate.yaml rename tests/cassettes/{test_retrieve_stateless_rates.yaml => test_beta_retrieve_stateless_rates.yaml} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b54453..41b4a38e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Next Release - Adds `retrieve_stateless_rates` function to pull stateless rates when shipment data is provided +- Adds `get_lowest_stateless_rate` function to filter the lowest stateless rate ## v7.9.0 (2023-01-18) diff --git a/easypost/beta/rate.py b/easypost/beta/rate.py index 92342baf..6088649e 100644 --- a/easypost/beta/rate.py +++ b/easypost/beta/rate.py @@ -1,10 +1,12 @@ from typing import ( Any, Dict, + List, Optional, ) from easypost.easypost_object import convert_to_easypost_object +from easypost.error import Error from easypost.requestor import ( RequestMethod, Requestor, @@ -23,4 +25,30 @@ def retrieve_stateless_rates(cls, api_key: Optional[str] = None, **params) -> Di } response, api_key = requestor.request(method=RequestMethod.POST, url=url, params=wrapped_params, beta=True) - return convert_to_easypost_object(response=response, api_key=api_key) + return convert_to_easypost_object(response=response.get("rates", None), api_key=api_key) + + @classmethod + def get_lowest_stateless_rate( + cls, stateless_rates: List[Dict[str, Any]], carriers: List[str] = None, services: List[str] = None + ) -> Dict[str, Any]: + """Get the lowest stateless rate.""" + carriers = carriers or [] + services = services or [] + lowest_rate = None + + carriers = [carrier.lower() for carrier in carriers] + services = [service.lower() for service in services] + + for rate in stateless_rates: + if (carriers and rate["carrier"].lower() not in carriers) or ( + services and rate["service"].lower() not in services + ): + continue + + if lowest_rate is None or float(rate.rate) < float(lowest_rate.rate): + lowest_rate = rate + + if lowest_rate is None: + raise Error(message="No rates found.") + + return lowest_rate diff --git a/tests/cassettes/test_beta_get_lowest_stateless_rate.yaml b/tests/cassettes/test_beta_get_lowest_stateless_rate.yaml new file mode 100644 index 00000000..73ead700 --- /dev/null +++ b/tests/cassettes/test_beta_get_lowest_stateless_rate.yaml @@ -0,0 +1,105 @@ +interactions: +- request: + body: '{"shipment": {"from_address": {"name": "Jack Sparrow", "street1": "388 + Townsend St", "street2": "Apt 20", "city": "San Francisco", "state": "CA", "zip": + "94107", "country": "US", "email": "test@example.com", "phone": "5555555555"}, + "to_address": {"name": "Elizabeth Swan", "street1": "179 N Harbor Dr", "city": + "Redondo Beach", "state": "CA", "zip": "90277", "country": "US", "email": "test@example.com", + "phone": "5555555555"}, "parcel": {"length": 10, "width": 8, "height": 4, "weight": + 15.4}}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '496' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/beta/rates + response: + body: + string: '{"from_address": {"object": "Address", "name": "Jack Sparrow", "street1": + "388 Townsend St", "street2": "Apt 20", "city": "San Francisco", "state": + "CA", "zip": "94107", "country": "US", "phone": "", "email": ""}, + "to_address": {"object": "Address", "name": "Elizabeth Swan", "street1": "179 + N Harbor Dr", "city": "Redondo Beach", "state": "CA", "zip": "90277", "country": + "US", "phone": "", "email": ""}, "rates": [{"object": + "Rate", "mode": "test", "service": "Express", "carrier": "USPS", "rate": "31.25", + "currency": "USD", "retail_rate": "35.80", "retail_currency": "USD", "list_rate": + "31.25", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + null, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + null, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": + "Rate", "mode": "test", "service": "ParcelSelect", "carrier": "USPS", "rate": + "7.75", "currency": "USD", "retail_rate": "7.75", "retail_currency": "USD", + "list_rate": "7.75", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + 5, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + 5, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": + "Rate", "mode": "test", "service": "Priority", "carrier": "USPS", "rate": + "8.24", "currency": "USD", "retail_rate": "10.20", "retail_currency": "USD", + "list_rate": "8.24", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + 2, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + 2, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": + "Rate", "mode": "test", "service": "First", "carrier": "USPS", "rate": "6.07", + "currency": "USD", "retail_rate": "6.07", "retail_currency": "USD", "list_rate": + "6.07", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + 3, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + 3, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}], "options": + {"currency": "USD", "payment": {"type": "SENDER"}, "date_advance": 0}, "parcel": + {"object": "Parcel", "length": 10.0, "width": 8.0, "height": 4.0, "weight": + 15.4}, "messages": [{"carrier": "DhlEcs", "carrier_account_id": "ca_c3cbbd21bc97400bbbaed6d030909476", + "type": "rate_error", "message": "Unauthorized. Please check credentials and + try again"}]}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '2236' + content-type: + - application/json; charset=utf-8 + etag: + - W/"3625cd1b5602ffe486a6855ff1125560" + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - afb1143263d185b6e788ea6c00073ea7 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb5nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb2nuq 29913d444b + - extlb1nuq 29913d444b + x-runtime: + - '0.719252' + x-version-label: + - easypost-202301251848-057c9f927b-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_retrieve_stateless_rates.yaml b/tests/cassettes/test_beta_retrieve_stateless_rates.yaml similarity index 95% rename from tests/cassettes/test_retrieve_stateless_rates.yaml rename to tests/cassettes/test_beta_retrieve_stateless_rates.yaml index f173742f..db49520e 100644 --- a/tests/cassettes/test_retrieve_stateless_rates.yaml +++ b/tests/cassettes/test_beta_retrieve_stateless_rates.yaml @@ -32,11 +32,6 @@ interactions: "to_address": {"object": "Address", "name": "Elizabeth Swan", "street1": "179 N Harbor Dr", "city": "Redondo Beach", "state": "CA", "zip": "90277", "country": "US", "phone": "", "email": ""}, "rates": [{"object": - "Rate", "mode": "test", "service": "ParcelSelect", "carrier": "USPS", "rate": - "7.75", "currency": "USD", "retail_rate": "7.75", "retail_currency": "USD", - "list_rate": "7.75", "list_currency": "USD", "billing_type": "easypost", "delivery_days": - 5, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": - 5, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": "Rate", "mode": "test", "service": "Priority", "carrier": "USPS", "rate": "8.24", "currency": "USD", "retail_rate": "10.20", "retail_currency": "USD", "list_rate": "8.24", "list_currency": "USD", "billing_type": "easypost", "delivery_days": @@ -51,7 +46,12 @@ interactions: "currency": "USD", "retail_rate": "35.80", "retail_currency": "USD", "list_rate": "31.25", "list_currency": "USD", "billing_type": "easypost", "delivery_days": null, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": - null, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}], "options": + null, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}, {"object": + "Rate", "mode": "test", "service": "ParcelSelect", "carrier": "USPS", "rate": + "7.75", "currency": "USD", "retail_rate": "7.75", "retail_currency": "USD", + "list_rate": "7.75", "list_currency": "USD", "billing_type": "easypost", "delivery_days": + 5, "delivery_date": null, "delivery_date_guaranteed": false, "est_delivery_days": + 5, "carrier_account_id": "ca_b25657e9896e4d63ac8151ac346ac41e"}], "options": {"currency": "USD", "payment": {"type": "SENDER"}, "date_advance": 0}, "parcel": {"object": "Parcel", "length": 10.0, "width": 8.0, "height": 4.0, "weight": 15.4}, "messages": [{"carrier": "DhlEcs", "carrier_account_id": "ca_c3cbbd21bc97400bbbaed6d030909476", @@ -65,7 +65,7 @@ interactions: content-type: - application/json; charset=utf-8 etag: - - W/"5730ea5b8b4ede80bf42e209b8b4d9df" + - W/"c0cf07a188c4d565d28a80c1841efc5f" expires: - '0' pragma: @@ -85,7 +85,7 @@ interactions: x-download-options: - noopen x-ep-request-uuid: - - 974d0bc763d17e0be78859a4000435bb + - afb1142f63d185b5e788ea6b00073e5c x-frame-options: - SAMEORIGIN x-node: @@ -94,10 +94,9 @@ interactions: - none x-proxied: - intlb1nuq 29913d444b - - intlb1wdc 29913d444b - - extlb3wdc 29913d444b + - extlb1nuq 29913d444b x-runtime: - - '0.692670' + - '0.720155' x-version-label: - easypost-202301251848-057c9f927b-master x-xss-protection: diff --git a/tests/test_beta_rate.py b/tests/test_beta_rate.py index cce2abf1..99cb67e8 100644 --- a/tests/test_beta_rate.py +++ b/tests/test_beta_rate.py @@ -4,9 +4,18 @@ @pytest.mark.vcr() -def test_retrieve_stateless_rates(basic_shipment): - response = easypost.beta.Rate.retrieve_stateless_rates(**basic_shipment) - - stateless_rates = response["rates"] +def test_beta_retrieve_stateless_rates(basic_shipment): + """Tests that we can retrieve stateless rates when basic shipment data.""" + stateless_rates = easypost.beta.Rate.retrieve_stateless_rates(**basic_shipment) assert all(rate["object"] == "Rate" for rate in stateless_rates) + + +@pytest.mark.vcr() +def test_beta_get_lowest_stateless_rate(basic_shipment): + """Tests that we can return the lowest stateless rate from a list of stateless rates.""" + stateless_rates = easypost.beta.Rate.retrieve_stateless_rates(**basic_shipment) + + lowest_stateless_rate = easypost.beta.Rate.get_lowest_stateless_rate(stateless_rates) + + assert lowest_stateless_rate["service"] == "First" From a2a930d00dbd6ef365e0b45bc850cdb2205fdfb0 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:03:43 -0700 Subject: [PATCH 3/3] chore: move get_lowest_stateless_rate to utils --- easypost/beta/rate.py | 28 ---------------------------- easypost/util.py | 32 +++++++++++++++++++++++++++++++- tests/test_beta_rate.py | 3 ++- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/easypost/beta/rate.py b/easypost/beta/rate.py index 6088649e..a3a67c5f 100644 --- a/easypost/beta/rate.py +++ b/easypost/beta/rate.py @@ -1,12 +1,10 @@ from typing import ( Any, Dict, - List, Optional, ) from easypost.easypost_object import convert_to_easypost_object -from easypost.error import Error from easypost.requestor import ( RequestMethod, Requestor, @@ -26,29 +24,3 @@ def retrieve_stateless_rates(cls, api_key: Optional[str] = None, **params) -> Di response, api_key = requestor.request(method=RequestMethod.POST, url=url, params=wrapped_params, beta=True) return convert_to_easypost_object(response=response.get("rates", None), api_key=api_key) - - @classmethod - def get_lowest_stateless_rate( - cls, stateless_rates: List[Dict[str, Any]], carriers: List[str] = None, services: List[str] = None - ) -> Dict[str, Any]: - """Get the lowest stateless rate.""" - carriers = carriers or [] - services = services or [] - lowest_rate = None - - carriers = [carrier.lower() for carrier in carriers] - services = [service.lower() for service in services] - - for rate in stateless_rates: - if (carriers and rate["carrier"].lower() not in carriers) or ( - services and rate["service"].lower() not in services - ): - continue - - if lowest_rate is None or float(rate.rate) < float(lowest_rate.rate): - lowest_rate = rate - - if lowest_rate is None: - raise Error(message="No rates found.") - - return lowest_rate diff --git a/easypost/util.py b/easypost/util.py index 1f35fc42..c7592b51 100644 --- a/easypost/util.py +++ b/easypost/util.py @@ -1,4 +1,8 @@ -from typing import List +from typing import ( + Any, + Dict, + List, +) from easypost.easypost_object import EasyPostObject from easypost.error import Error @@ -29,3 +33,29 @@ def get_lowest_object_rate( raise Error(message="No rates found.") return lowest_rate + + +def get_lowest_stateless_rate( + stateless_rates: List[Dict[str, Any]], carriers: List[str] = None, services: List[str] = None +) -> Dict[str, Any]: + """Get the lowest stateless rate.""" + carriers = carriers or [] + services = services or [] + lowest_rate = None + + carriers = [carrier.lower() for carrier in carriers] + services = [service.lower() for service in services] + + for rate in stateless_rates: + if (carriers and rate["carrier"].lower() not in carriers) or ( + services and rate["service"].lower() not in services + ): + continue + + if lowest_rate is None or float(rate.rate) < float(lowest_rate.rate): + lowest_rate = rate + + if lowest_rate is None: + raise Error(message="No rates found.") + + return lowest_rate diff --git a/tests/test_beta_rate.py b/tests/test_beta_rate.py index 99cb67e8..77afa048 100644 --- a/tests/test_beta_rate.py +++ b/tests/test_beta_rate.py @@ -1,6 +1,7 @@ import pytest import easypost +from easypost.util import get_lowest_stateless_rate @pytest.mark.vcr() @@ -16,6 +17,6 @@ def test_beta_get_lowest_stateless_rate(basic_shipment): """Tests that we can return the lowest stateless rate from a list of stateless rates.""" stateless_rates = easypost.beta.Rate.retrieve_stateless_rates(**basic_shipment) - lowest_stateless_rate = easypost.beta.Rate.get_lowest_stateless_rate(stateless_rates) + lowest_stateless_rate = get_lowest_stateless_rate(stateless_rates) assert lowest_stateless_rate["service"] == "First"