From 7ee8f292bbdd8ba88d5f4f391add770cd6adc057 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Thu, 12 Feb 2026 10:19:03 -0700 Subject: [PATCH] feat: add a generic API request interface --- easypost/easypost_client.py | 15 ++++ .../cassettes/test_client_make_api_call.yaml | 74 +++++++++++++++++++ tests/test_easypost_client.py | 22 ++++-- 3 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 tests/cassettes/test_client_make_api_call.yaml diff --git a/easypost/easypost_client.py b/easypost/easypost_client.py index 2da85038..026b67bc 100644 --- a/easypost/easypost_client.py +++ b/easypost/easypost_client.py @@ -1,3 +1,5 @@ +from typing import Any + from easypost.constant import ( API_BASE, API_VERSION, @@ -5,7 +7,9 @@ SUPPORT_EMAIL, TIMEOUT, ) +from easypost.easypost_object import convert_to_easypost_object from easypost.hooks import RequestHook, ResponseHook +from easypost.requestor import RequestMethod, Requestor from easypost.services import ( AddressService, ApiKeyService, @@ -138,3 +142,14 @@ def subscribe_to_response_hook(self, function): def unsubscribe_from_response_hook(self, function): """Unsubscribe functions from running when a response occurs.""" self._response_hook -= function + + def make_api_call(self, method: RequestMethod, endpoint: str, params: dict[str, Any]) -> dict[str, Any]: + """Make an API call to the EasyPost API. + + This public, generic interface is useful for making arbitrary API calls to the EasyPost API that + are not yet supported by the client library's services. When possible, the service for your use case + should be used instead as it provides a more convenient and higher-level interface depending on the endpoint. + """ + response = Requestor(self).request(method=method, url=endpoint, params=params) + + return convert_to_easypost_object(response=response) diff --git a/tests/cassettes/test_client_make_api_call.yaml b/tests/cassettes/test_client_make_api_call.yaml new file mode 100644 index 00000000..4b1cf691 --- /dev/null +++ b/tests/cassettes/test_client_make_api_call.yaml @@ -0,0 +1,74 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/v2/addresses?page_size=1 + response: + body: + string: '{"addresses": [{"id": "adr_b532f4f1bc0311f0b86eac1f6bc539aa", "object": + "Address", "created_at": "2025-11-07T18:00:55Z", "updated_at": "2025-11-07T18:00:55Z", + "name": null, "company": "EASYPOST", "street1": "000 UNKNOWN STREET", "street2": + null, "city": "NOT A CITY", "state": "ZZ", "zip": "00001", "country": "US", + "phone": "", "email": "", "mode": "test", "carrier_facility": + null, "residential": null, "federal_tax_id": null, "state_tax_id": null, "verifications": + {"zip4": {"success": true, "errors": [{"code": "E.ADDRESS.NOT_FOUND", "field": + "address", "message": "Address not found", "suggestion": null}], "details": + null}, "delivery": {"success": true, "errors": [{"code": "E.ADDRESS.NOT_FOUND", + "field": "address", "message": "Address not found", "suggestion": null}], + "details": {"latitude": null, "longitude": null, "time_zone": null}}, "verify_carrier": + "ups"}}], "has_more": true}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '842' + content-type: + - application/json; charset=utf-8 + 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: + - f785caeb698e03c9e786b0840112d88f + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb56nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb3nuq 0dcc3a6efb + - extlb2nuq c01291cd8f + x-runtime: + - '0.061229' + x-version-label: + - easypost-202602121634-14fe075b27-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_easypost_client.py b/tests/test_easypost_client.py index 20721d83..65de3f54 100644 --- a/tests/test_easypost_client.py +++ b/tests/test_easypost_client.py @@ -6,9 +6,10 @@ from easypost.easypost_client import EasyPostClient from easypost.errors import TimeoutError +from easypost.requestor import RequestMethod -def test_api_key(): +def test_easypost_client_api_key(): """Tests setting and getting API keys from different client objects.""" client1 = EasyPostClient(api_key="123") assert client1.api_key == "123" @@ -17,7 +18,7 @@ def test_api_key(): assert client2.api_key == "456" -def test_no_api_key(): +def test_easypost_client_no_api_key(): """Tests that we raise an error when no API key is passed to the client.""" with pytest.raises(TypeError) as error: EasyPostClient() @@ -25,7 +26,7 @@ def test_no_api_key(): assert "missing 1 required positional argument: 'api_key'" in str(error.value) -def test_invalid_client_property(): +def test_easypost_client_invalid_client_property(): """Tests that we throw an error when attempting to use an invalid property of a client.""" with pytest.raises(AttributeError) as error: EasyPostClient("123").invalid_property() @@ -33,7 +34,7 @@ def test_invalid_client_property(): assert str(error.value) == "'EasyPostClient' object has no attribute 'invalid_property'" -def test_api_base(): +def test_easypost_client_api_base(): """Tests that we can override the API base of the client object.""" client1 = EasyPostClient(api_key="123") assert client1.api_base == "https://api.easypost.com/v2" @@ -43,7 +44,7 @@ def test_api_base(): @patch("requests.Session") -def test_client_timeout(mock_session, basic_shipment): +def test_easypost_client_timeout(mock_session, basic_shipment): """Tests that the timeout gets used properly in requests when set.""" mock_session().request.side_effect = requests.exceptions.Timeout() client = EasyPostClient(api_key=os.getenv("EASYPOST_TEST_API_KEY"), timeout=0.1) @@ -53,3 +54,14 @@ def test_client_timeout(mock_session, basic_shipment): assert False except TimeoutError as error: assert error.message == "Request timed out." + + +@pytest.mark.vcr() +def test_client_make_api_call(): + """Tests that we can make an API call using the generic make_api_call method.""" + client = EasyPostClient(api_key=os.getenv("EASYPOST_TEST_API_KEY")) + + response = client.make_api_call(method=RequestMethod.GET, endpoint="/addresses", params={"page_size": 1}) + + assert len(response["addresses"]) == 1 + assert response["addresses"][0]["object"] == "Address"