From e6aab26c6ad94a8cb34e30a64e6c79c3648242af Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:32:36 -0700 Subject: [PATCH 1/2] feat: create/delete/enable/disable API keys --- CHANGELOG.md | 8 + easypost/constant.py | 2 +- easypost/services/api_key_service.py | 54 +++- pyproject.toml | 2 +- ...ll_api_keys.yaml => test_api_key_all.yaml} | 0 ...l => test_api_key_authenticated_user.yaml} | 0 tests/cassettes/test_api_key_lifecycle.yaml | 257 ++++++++++++++++++ ...t_api_key_retrieve_authenticated_user.yaml | 177 ++++++++++++ ... => test_api_key_retrieve_child_user.yaml} | 0 tests/test_api_key.py | 49 +++- 10 files changed, 520 insertions(+), 29 deletions(-) rename tests/cassettes/{test_all_api_keys.yaml => test_api_key_all.yaml} (100%) rename tests/cassettes/{test_authenticated_user_api_keys.yaml => test_api_key_authenticated_user.yaml} (100%) create mode 100644 tests/cassettes/test_api_key_lifecycle.yaml create mode 100644 tests/cassettes/test_api_key_retrieve_authenticated_user.yaml rename tests/cassettes/{test_child_user_api_keys.yaml => test_api_key_retrieve_child_user.yaml} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c91ce6cb..118a7b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v10.4.0 (2026-02-03) + +- Adds the following functions usable by child and referral customer users: + - `api_key.create` + - `api_key.delete` + - `api_key.enable` + - `api_key.disable` + ## v10.3.0 (2025-11-24) - Adds the following functions: diff --git a/easypost/constant.py b/easypost/constant.py index 6736abc7..2e8585e4 100644 --- a/easypost/constant.py +++ b/easypost/constant.py @@ -1,6 +1,6 @@ # flake8: noqa # Library version -VERSION = "10.3.0" +VERSION = "10.4.0" VERSION_INFO = [str(number) for number in VERSION.split(".")] # Client defaults diff --git a/easypost/services/api_key_service.py b/easypost/services/api_key_service.py index f142cf88..f59247d2 100644 --- a/easypost/services/api_key_service.py +++ b/easypost/services/api_key_service.py @@ -1,15 +1,10 @@ -from typing import ( - Any, -) +from typing import Any from easypost.constant import NO_USER_FOUND from easypost.easypost_object import convert_to_easypost_object from easypost.errors import FilteringError from easypost.models import ApiKey -from easypost.requestor import ( - RequestMethod, - Requestor, -) +from easypost.requestor import RequestMethod, Requestor from easypost.services.base_service import BaseService @@ -18,14 +13,6 @@ def __init__(self, client): self._client = client self._model_class = ApiKey.__name__ - def all(self) -> dict[str, Any]: - """Retrieve a list of all API keys.""" - url = "/api_keys" - - response = Requestor(self._client).request(method=RequestMethod.GET, url=url) - - return convert_to_easypost_object(response=response) - def retrieve_api_keys_for_user(self, id: str) -> list[ApiKey]: """Retrieve a list of API keys (works for the authenticated User or a child User).""" api_keys = self.all() @@ -41,3 +28,40 @@ def retrieve_api_keys_for_user(self, id: str) -> list[ApiKey]: return child.keys raise FilteringError(message=NO_USER_FOUND) + + def all(self) -> dict[str, Any]: + """Retrieve a list of all API keys.""" + url = "/api_keys" + + response = Requestor(self._client).request(method=RequestMethod.GET, url=url) + + return convert_to_easypost_object(response=response) + + def create(self, mode: str) -> ApiKey: + """Create an API key for a child or referral customer user.""" + url = "/api_keys" + params = {"mode": mode} + + response = Requestor(self._client).request(method=RequestMethod.POST, url=url, params=params) + + return convert_to_easypost_object(response=response) + + def delete(self, id: str) -> None: + """Delete an API key for a child or referral customer user.""" + self._delete_resource(self._model_class, id) + + def enable(self, id: str) -> ApiKey: + """Enable a child or referral customer API key.""" + url = f"/api_keys/{id}/enable" + + response = Requestor(self._client).request(method=RequestMethod.POST, url=url) + + return convert_to_easypost_object(response=response) + + def disable(self, id: str) -> ApiKey: + """Disable a child or referral customer API key.""" + url = f"/api_keys/{id}/disable" + + response = Requestor(self._client).request(method=RequestMethod.POST, url=url) + + return convert_to_easypost_object(response=response) diff --git a/pyproject.toml b/pyproject.toml index ba66b4f5..44f6a1a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "easypost" description = "EasyPost Shipping API Client Library for Python" -version = "10.3.0" +version = "10.4.0" readme = "README.md" requires-python = ">=3.9" license = { file = "LICENSE" } diff --git a/tests/cassettes/test_all_api_keys.yaml b/tests/cassettes/test_api_key_all.yaml similarity index 100% rename from tests/cassettes/test_all_api_keys.yaml rename to tests/cassettes/test_api_key_all.yaml diff --git a/tests/cassettes/test_authenticated_user_api_keys.yaml b/tests/cassettes/test_api_key_authenticated_user.yaml similarity index 100% rename from tests/cassettes/test_authenticated_user_api_keys.yaml rename to tests/cassettes/test_api_key_authenticated_user.yaml diff --git a/tests/cassettes/test_api_key_lifecycle.yaml b/tests/cassettes/test_api_key_lifecycle.yaml new file mode 100644 index 00000000..ed1aabba --- /dev/null +++ b/tests/cassettes/test_api_key_lifecycle.yaml @@ -0,0 +1,257 @@ +interactions: +- request: + body: '{"mode": "production"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '22' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/v2/api_keys + response: + body: + string: '{"object": "ApiKey", "key": "", "mode": "production", "created_at": + "2026-02-02T21:31:26Z", "active": true, "id": "ak_e87ac0a105ea4760a5df59a757230365"}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '199' + 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: + - 2e56ccf3698117aee2bb247402d1e34b + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb57nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb4nuq d9379ca146 + - extlb2nuq cbbd141214 + x-runtime: + - '0.111460' + x-version-label: + - easypost-202602022023-e2aafa4ad5-master + x-xss-protection: + - 1; mode=block + status: + code: 201 + message: Created +- request: + body: '{}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '2' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/v2/api_keys/ak_e87ac0a105ea4760a5df59a757230365/disable + response: + body: + string: '{"object": "ApiKey", "key": "", "mode": "production", "created_at": + "2026-02-02T21:31:26Z", "active": false, "id": "ak_e87ac0a105ea4760a5df59a757230365"}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '200' + 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: + - 2e56cced698117aee2bb247502d1e3ae + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb53nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb4nuq d9379ca146 + - extlb2nuq cbbd141214 + x-runtime: + - '0.125884' + x-version-label: + - easypost-202602022023-e2aafa4ad5-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: '{}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '2' + Content-Type: + - application/json + authorization: + - + user-agent: + - + method: POST + uri: https://api.easypost.com/v2/api_keys/ak_e87ac0a105ea4760a5df59a757230365/enable + response: + body: + string: '{"object": "ApiKey", "key": "", "mode": "production", "created_at": + "2026-02-02T21:31:26Z", "active": true, "id": "ak_e87ac0a105ea4760a5df59a757230365"}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '199' + 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: + - 2e56ccee698117afe2bb247602d1e40f + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb42nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb6nuq d9379ca146 + - extlb2nuq cbbd141214 + x-runtime: + - '0.120192' + x-version-label: + - easypost-202602022023-e2aafa4ad5-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + authorization: + - + user-agent: + - + method: DELETE + uri: https://api.easypost.com/v2/api_keys/ak_e87ac0a105ea4760a5df59a757230365 + response: + body: + string: '' + headers: + cache-control: + - private, no-cache, no-store + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 2e56ccf1698117afe2bb247702d1e476 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb42nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb4nuq d9379ca146 + - extlb2nuq cbbd141214 + x-runtime: + - '0.105366' + x-version-label: + - easypost-202602022023-e2aafa4ad5-master + x-xss-protection: + - 1; mode=block + status: + code: 204 + message: No Content +version: 1 diff --git a/tests/cassettes/test_api_key_retrieve_authenticated_user.yaml b/tests/cassettes/test_api_key_retrieve_authenticated_user.yaml new file mode 100644 index 00000000..f49d1064 --- /dev/null +++ b/tests/cassettes/test_api_key_retrieve_authenticated_user.yaml @@ -0,0 +1,177 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/v2/users + response: + body: + string: '{"id": "user_54356a6b96c940d8913961a3c7b909f2", "object": "User", "parent_id": + null, "name": "New Name", "phone_number": "", "verified": true, + "created_at": "2017-03-01T03:01:34Z", "default_carbon_offset": false, "has_elevate_access": + false, "balance": "0.00000", "price_per_shipment": "0.00000", "recharge_amount": + "100.00", "secondary_recharge_amount": "100.00", "recharge_threshold": "0.00", + "has_billing_method": false, "cc_fee_rate": "0.03", "default_insurance_amount": + null, "insurance_fee_rate": "0.01", "insurance_fee_minimum": "1.00", "email": + "", "children": [{"id": "user_584be78af2f141e988b6c60dda9dd8fd", + "object": "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": + "Test User2", "phone_number": "8005550100", "verified": true, "created_at": + "2023-12-11T17:13:38Z", "default_carbon_offset": false, "has_elevate_access": + false, "children": []}, {"id": "user_437e724f37de412db6df8821968d8d3c", "object": + "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test + User2", "phone_number": "8005550100", "verified": true, "created_at": "2023-12-11T17:13:43Z", + "default_carbon_offset": false, "has_elevate_access": false, "children": []}, + {"id": "user_2553102e90de43dea0b9fa7f8c8d7e33", "object": "User", "parent_id": + "user_54356a6b96c940d8913961a3c7b909f2", "name": "Child Account Name22", "phone_number": + "8005550100", "verified": true, "created_at": "2024-01-04T19:56:21Z", "default_carbon_offset": + false, "has_elevate_access": false, "children": []}, {"id": "user_31754439d4524fbb8867d138de537e12", + "object": "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": + "Test User", "phone_number": "8005550100", "verified": true, "created_at": + "2024-01-19T21:42:21Z", "default_carbon_offset": false, "has_elevate_access": + false, "children": []}, {"id": "user_d26377e80676431d88c17d8b140d365a", "object": + "User", "parent_id": "user_54356a6b96c940d8913961a3c7b909f2", "name": "Test + User", "phone_number": "8005550100", "verified": true, "created_at": "2024-01-19T21:44:18Z", + "default_carbon_offset": false, "has_elevate_access": false, "children": []}]}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '2010' + 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: + - 2e56ccf0698116b8e2bb1c7902d05b35 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb41nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb4nuq d9379ca146 + - extlb2nuq cbbd141214 + x-runtime: + - '0.148339' + x-version-label: + - easypost-202602022023-e2aafa4ad5-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/v2/api_keys + response: + body: + string: '{"id": "user_54356a6b96c940d8913961a3c7b909f2", "keys": [{"object": + "ApiKey", "active": true, "key": "", "mode": "test", "created_at": + "2022-02-24T20:59:05Z", "id": "ak_16f993eae7984eab94e0957a78bee407"}, {"object": + "ApiKey", "active": true, "key": "", "mode": "production", "created_at": + "2022-02-24T20:59:14Z", "id": "ak_62524a9c3f684f65ab9eccbf70950df8"}], "children": + [{"id": "user_584be78af2f141e988b6c60dda9dd8fd", "keys": [{"object": "ApiKey", + "active": true, "key": "", "mode": "test", "created_at": "2023-12-11T17:13:38Z", + "id": "ak_90e045d452174b3e99198138dc7938f5"}, {"object": "ApiKey", "active": + true, "key": "", "mode": "production", "created_at": "2023-12-11T17:13:38Z", + "id": "ak_7ef472842fda4256b72779e74653ca79"}], "children": []}, {"id": "user_437e724f37de412db6df8821968d8d3c", + "keys": [{"object": "ApiKey", "active": true, "key": "", "mode": + "test", "created_at": "2023-12-11T17:13:43Z", "id": "ak_726be943161e4028a347dd2389284d7d"}, + {"object": "ApiKey", "active": true, "key": "", "mode": "production", + "created_at": "2023-12-11T17:13:43Z", "id": "ak_dd9f2e1f14d342679191eee4b69b6ad0"}], + "children": []}, {"id": "user_2553102e90de43dea0b9fa7f8c8d7e33", "keys": [{"object": + "ApiKey", "active": true, "key": "", "mode": "test", "created_at": + "2024-01-04T19:56:21Z", "id": "ak_e14b2c164b95497a8b9141b2ad212855"}, {"object": + "ApiKey", "active": true, "key": "", "mode": "production", "created_at": + "2024-01-04T19:56:21Z", "id": "ak_a35fabeaec96494694ec0c8726103a80"}], "children": + []}, {"id": "user_31754439d4524fbb8867d138de537e12", "keys": [{"object": "ApiKey", + "active": true, "key": "", "mode": "test", "created_at": "2024-01-19T21:42:21Z", + "id": "ak_dffa08301f7d4a54895f7840d54d05e0"}, {"object": "ApiKey", "active": + true, "key": "", "mode": "production", "created_at": "2024-01-19T21:42:21Z", + "id": "ak_d386ae3ceee04b0880002cd39dca2587"}], "children": []}, {"id": "user_d26377e80676431d88c17d8b140d365a", + "keys": [{"object": "ApiKey", "active": true, "key": "", "mode": + "test", "created_at": "2024-01-19T21:44:18Z", "id": "ak_92d46dfa461245bba1ec48aac1f2090d"}, + {"object": "ApiKey", "active": true, "key": "", "mode": "production", + "created_at": "2024-01-19T21:44:18Z", "id": "ak_8637c80064224594b9dfdf82efbb27b7"}], + "children": []}]}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '2782' + 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: + - 2e56ccef698116b9e2bb1c7c02d05ba9 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb38nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb6nuq d9379ca146 + - extlb2nuq cbbd141214 + x-runtime: + - '0.086400' + x-version-label: + - easypost-202602022023-e2aafa4ad5-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_child_user_api_keys.yaml b/tests/cassettes/test_api_key_retrieve_child_user.yaml similarity index 100% rename from tests/cassettes/test_child_user_api_keys.yaml rename to tests/cassettes/test_api_key_retrieve_child_user.yaml diff --git a/tests/test_api_key.py b/tests/test_api_key.py index 07764d6e..8817e6d2 100644 --- a/tests/test_api_key.py +++ b/tests/test_api_key.py @@ -1,20 +1,13 @@ +import os + import pytest +from easypost import EasyPostClient from easypost.models import ApiKey @pytest.mark.vcr() -def test_all_api_keys(prod_client): - """Tests that we can retrieve all API keys.""" - api_keys = prod_client.api_keys.all() - - assert all(isinstance(key, ApiKey) for key in api_keys.keys) - for child in api_keys.children: - assert all(isinstance(key, ApiKey) for key in child["keys"]) - - -@pytest.mark.vcr() -def test_authenticated_user_api_keys(prod_client): +def test_api_key_retrieve_authenticated_user(prod_client): """Tests that we can retrieve the authenticated user's API keys.""" user = prod_client.user.retrieve_me() api_keys = prod_client.api_keys.retrieve_api_keys_for_user(user.id) @@ -23,7 +16,7 @@ def test_authenticated_user_api_keys(prod_client): @pytest.mark.vcr() -def test_child_user_api_keys(prod_client): +def test_api_key_retrieve_child_user(prod_client): """Tests that we can retrieve a child user's API keys as the parent.""" user = prod_client.user.create(name="Test User") child_user = prod_client.user.retrieve(user.id) @@ -34,3 +27,35 @@ def test_child_user_api_keys(prod_client): # Delete the user so we don't clutter up the test environment prod_client.user.delete(child_user.id) + + +@pytest.mark.vcr() +def test_api_key_all(prod_client): + """Tests that we can retrieve all API keys.""" + api_keys = prod_client.api_keys.all() + + assert all(isinstance(key, ApiKey) for key in api_keys.keys) + for child in api_keys.children: + assert all(isinstance(key, ApiKey) for key in child["keys"]) + + +@pytest.mark.vcr() +def test_api_key_lifecycle(): + """Tests creating an API key for a child user.""" + # Create an API key + referral_client = EasyPostClient(os.getenv("REFERRAL_CUSTOMER_PROD_API_KEY")) + api_key = referral_client.api_keys.create("production") + assert isinstance(api_key, ApiKey) + assert api_key.id.startswith("ak_") + assert api_key.mode == "production" + + # Disable the API key + api_key = referral_client.api_keys.disable(api_key.id) + assert api_key.active is False + + # Enable the API key + api_key = referral_client.api_keys.enable(api_key.id) + assert api_key.active is True + + # Delete the API key + referral_client.api_keys.delete(api_key.id) From 9c8c076f9d9a470125215fae83431833cba0fb3b Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:53:10 -0700 Subject: [PATCH 2/2] chore: trigger GitHub Actions