diff --git a/CHANGELOG.md b/CHANGELOG.md index b3c2dc99..06fd311b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # CHANGELOG +## v7.15.0 (2024-01-08) + +- Adds `all_children` function to the User class for retrieving paginated lists of children users, and removes the beta function +- Adds `get_next_page_of_children` function to the User class for retrieving next paginated lists of children users, and removes the beta function + +## v7.14.1 (2023-10-30) + +- Fixes a bug where `get_next_page` functions threw an error, preventing users from retrieving the final page of results + +## v7.14.0 (2023-10-23) + +- Adds beta `all_children` function to the User class for retrieving paginated lists of children users + +## v7.13.1 (2023-05-31) + +- Fixes a bug where `Payload` didn't inherit `EasyPostObject` which could throw errors when retrieving event payloads (closes #284) + ## v7.13.0 (2023-05-02) - Adds `retrieve_estimated_delivery_date` function to the Shipment class diff --git a/easypost/payload.py b/easypost/payload.py index 075e7263..1f5ab5ec 100644 --- a/easypost/payload.py +++ b/easypost/payload.py @@ -1,2 +1,5 @@ -class Payload: +from easypost.resource import Resource + + +class Payload(Resource): pass diff --git a/easypost/requestor.py b/easypost/requestor.py index f230e4cd..2373a8bd 100644 --- a/easypost/requestor.py +++ b/easypost/requestor.py @@ -42,7 +42,7 @@ def _objects_to_ids(cls, param: Dict[str, Any]) -> Dict[str, Any]: return {"id": param.id} elif isinstance(param, dict): data = {} - for (k, v) in param.items(): + for k, v in param.items(): if isinstance(v, list): data[k] = [cls._objects_to_ids(item) for item in v] # type: ignore else: diff --git a/easypost/resource.py b/easypost/resource.py index e6050d17..dbd93f73 100644 --- a/easypost/resource.py +++ b/easypost/resource.py @@ -93,7 +93,7 @@ def get_next_page( response, api_key = requestor.request(method=RequestMethod.GET, url=url, params=params) response_array: List[Any] = response.get(url[1:]) # type: ignore - if response is None or len(response_array) == 0 or not response.get("has_more"): + if response is None or len(response_array) == 0: raise Error(message="There are no more pages to retrieve.") return convert_to_easypost_object(response=response, api_key=api_key) diff --git a/easypost/user.py b/easypost/user.py index 0810f1bf..e721cadb 100644 --- a/easypost/user.py +++ b/easypost/user.py @@ -7,6 +7,7 @@ from easypost.api_key import ApiKey from easypost.easypost_object import convert_to_easypost_object +from easypost.error import Error from easypost.requestor import ( RequestMethod, Requestor, @@ -83,3 +84,38 @@ def update_brand(self, api_key: Optional[str] = None, **params) -> "User": method=RequestMethod.PATCH, url=self.instance_url() + "/brand", params=params ) return convert_to_easypost_object(response=response, api_key=api_key) + + @classmethod + def all_children(cls, api_key: Optional[str] = None, **params) -> Dict[str, Any]: + """Retrieve a paginated list of children from the API.""" + requestor = Requestor(local_api_key=api_key) + url = "/users/children" + response, api_key = requestor.request(method=RequestMethod.GET, url=url, params=params) + return convert_to_easypost_object(response=response, api_key=api_key) + + @classmethod + def get_next_page_of_children( + cls, + children: Dict[str, Any], + page_size: int, + api_key: Optional[str] = None, + ) -> List["User"]: + """Get next page of children.""" + requestor = Requestor(local_api_key=api_key) + url = "/users/children" + children_array = children.get("children", []) + + if len(children_array) == 0 or not children.get("has_more", False): + raise Error(message="There are no more pages to retrieve.") + + params = { + "after_id": children_array[-1].id, + "page_size": page_size, + } + + data, api_key = requestor.request(method=RequestMethod.GET, url=url, params=params) + next_children_array: List[Any] = data.get("children", []) + if len(next_children_array) == 0: + raise Error(message="There are no more pages to retrieve.") + + return convert_to_easypost_object(response=data, api_key=api_key) diff --git a/easypost/version.py b/easypost/version.py index 505fb15f..f5852f2c 100644 --- a/easypost/version.py +++ b/easypost/version.py @@ -1,4 +1,4 @@ -VERSION = "7.13.0" +VERSION = "7.15.0" numbers = [str(number) for number in VERSION.split(".")] VERSION_INFO = numbers diff --git a/setup.py b/setup.py index b31c8f4a..b4436760 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( name="easypost", - version="7.13.0", + version="7.15.0", description="EasyPost Shipping API Client Library for Python", author="EasyPost", author_email="support@easypost.com", diff --git a/tests/cassettes/test_beta_user_all_children.yaml b/tests/cassettes/test_beta_user_all_children.yaml new file mode 100644 index 00000000..14ad3068 --- /dev/null +++ b/tests/cassettes/test_beta_user_all_children.yaml @@ -0,0 +1,67 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/beta/users/children?page_size=5 + response: + body: + string: '{"children": [], "has_more": false}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '32' + content-type: + - application/json; charset=utf-8 + etag: + - W/"3e2ec6a518ba813e2b978a9e1e34e8f6" + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + vary: + - Origin + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 9c098db26532e374e786ab5e0050ccad + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb38nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb2nuq 5a08ab70b2 + - extlb2nuq 003ad9bca0 + x-runtime: + - '0.037394' + x-version-label: + - easypost-202310201725-2922e4b728-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_beta_user_get_next_page.yaml b/tests/cassettes/test_beta_user_get_next_page.yaml new file mode 100644 index 00000000..23fce0e8 --- /dev/null +++ b/tests/cassettes/test_beta_user_get_next_page.yaml @@ -0,0 +1,67 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/beta/users/children?page_size=5 + response: + body: + string: '{"children": [], "has_more": false}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '32' + content-type: + - application/json; charset=utf-8 + etag: + - W/"3e2ec6a518ba813e2b978a9e1e34e8f6" + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + vary: + - Origin + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - 9c098db16532e6e0e786b4a20051d0f3 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb39nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb1nuq 5a08ab70b2 + - extlb2nuq 003ad9bca0 + x-runtime: + - '0.034338' + x-version-label: + - easypost-202310201725-2922e4b728-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_beta_user_get_next_page_collect_all.yaml b/tests/cassettes/test_beta_user_get_next_page_collect_all.yaml new file mode 100644 index 00000000..4482191c --- /dev/null +++ b/tests/cassettes/test_beta_user_get_next_page_collect_all.yaml @@ -0,0 +1,67 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + authorization: + - + user-agent: + - + method: GET + uri: https://api.easypost.com/beta/users/children?page_size=1 + response: + body: + string: '{"children": [], "has_more": false}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '32' + content-type: + - application/json; charset=utf-8 + etag: + - W/"3e2ec6a518ba813e2b978a9e1e34e8f6" + expires: + - '0' + pragma: + - no-cache + referrer-policy: + - strict-origin-when-cross-origin + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + transfer-encoding: + - chunked + vary: + - Origin + x-backend: + - easypost + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - e7025b87653f1d16f42cf30400b96021 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb35nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb2nuq b3de2c47ef + - extlb2nuq 003ad9bca0 + x-runtime: + - '0.036347' + x-version-label: + - easypost-202310271808-bc7d2a481d-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_user_all_children.yaml b/tests/cassettes/test_user_all_children.yaml new file mode 100644 index 00000000..c4c73c2d --- /dev/null +++ b/tests/cassettes/test_user_all_children.yaml @@ -0,0 +1,71 @@ +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/children?page_size=5 + response: + body: + string: '{"children": [{"id": "user_484dd58db70a4f31b4bb862998cf0e04", "object": + "User", "parent_id": "user_0f6b83e3530b401cb1e8aeaa6a250d4d", "name": "Test + User", "phone_number": "", "verified": true, "created_at": "2023-05-16T22:01:20Z"}, + {"id": "user_14e894c0d541459395f4456e7cf4f175", "object": "User", "parent_id": + "user_0f6b83e3530b401cb1e8aeaa6a250d4d", "name": "Test User", "phone_number": + "", "verified": true, "created_at": "2023-09-27T22:05:26Z"}, {"id": + "user_f04df3dad13848339a7975d295d6629f", "object": "User", "parent_id": "user_0f6b83e3530b401cb1e8aeaa6a250d4d", + "name": "Test User", "phone_number": "", "verified": true, "created_at": + "2023-11-30T19:23:22Z"}], "has_more": false}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '673' + 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: + - ec7851e965970bd2e79a7e400013d8db + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb39nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb1nuq 2c48984abf + - extlb1nuq 003ad9bca0 + x-runtime: + - '0.039816' + x-version-label: + - easypost-202401041812-437974c716-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_user_get_next_page.yaml b/tests/cassettes/test_user_get_next_page.yaml new file mode 100644 index 00000000..111918a3 --- /dev/null +++ b/tests/cassettes/test_user_get_next_page.yaml @@ -0,0 +1,73 @@ +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/children?page_size=5 + response: + body: + string: '{"children": [{"id": "user_484dd58db70a4f31b4bb862998cf0e04", "object": + "User", "parent_id": "user_0f6b83e3530b401cb1e8aeaa6a250d4d", "name": "Test + User", "phone_number": "", "verified": true, "created_at": "2023-05-16T22:01:20Z"}, + {"id": "user_14e894c0d541459395f4456e7cf4f175", "object": "User", "parent_id": + "user_0f6b83e3530b401cb1e8aeaa6a250d4d", "name": "Test User", "phone_number": + "", "verified": true, "created_at": "2023-09-27T22:05:26Z"}, {"id": + "user_f04df3dad13848339a7975d295d6629f", "object": "User", "parent_id": "user_0f6b83e3530b401cb1e8aeaa6a250d4d", + "name": "Test User", "phone_number": "", "verified": true, "created_at": + "2023-11-30T19:23:22Z"}], "has_more": false}' + headers: + cache-control: + - private, no-cache, no-store + content-length: + - '673' + 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-canary: + - direct + x-content-type-options: + - nosniff + x-download-options: + - noopen + x-ep-request-uuid: + - ec7851e765970bd2e79a7e410013d944 + x-frame-options: + - SAMEORIGIN + x-node: + - bigweb32nuq + x-permitted-cross-domain-policies: + - none + x-proxied: + - intlb1nuq 2c48984abf + - extlb1nuq 003ad9bca0 + x-runtime: + - '0.177179' + x-version-label: + - easypost-202401041812-437974c716-master + x-xss-protection: + - 1; mode=block + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_user.py b/tests/test_user.py index db9bf428..c35556af 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -109,3 +109,30 @@ def test_user_update_brand(prod_api_key): assert isinstance(brand, easypost.Brand) assert str.startswith(brand.id, "brd_") assert brand.color == color + + +@pytest.mark.vcr() +def test_user_all_children(prod_api_key, page_size): + children_data = easypost.User.all_children(page_size=page_size) + + children_array = children_data["children"] + assert len(children_array) <= page_size + assert all(isinstance(child, easypost.User) for child in children_array) + + has_more = children_data["has_more"] + assert isinstance(has_more, bool) + + +@pytest.mark.vcr() +def test_user_get_next_page(prod_api_key, page_size): + try: + children = easypost.User.all_children(page_size=page_size) + next_page = easypost.User.get_next_page_of_children(children=children, page_size=page_size) + + first_id_of_first_page = children["children"][0].id + first_id_of_second_page = next_page["children"][0].id + + assert first_id_of_first_page != first_id_of_second_page + except easypost.Error as e: + if e.message != "There are no more pages to retrieve.": + raise easypost.Error(message="Test failed intentionally.")