From 613d06078e23ec482ae97e36ef9586279f115373 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Mon, 4 Nov 2024 17:53:20 -0500 Subject: [PATCH 01/11] Initial pass --- .../classes/cloudlibrary_client.py | 119 ++++++++++++++++++ tests/test_cloudlibrary_client.py | 37 ++++++ 2 files changed, 156 insertions(+) create mode 100644 src/nypl_py_utils/classes/cloudlibrary_client.py create mode 100644 tests/test_cloudlibrary_client.py diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py new file mode 100644 index 0000000..3b51057 --- /dev/null +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -0,0 +1,119 @@ +import base64 +import hashlib +import hmac +import requests + +from datetime import datetime, timedelta +from nypl_py_utils.functions.log_helper import create_log +from requests.adapters import HTTPAdapter, Retry + +_API_URL = "https://partner.yourcloudlibrary.com/cirrus/library" +_VERSION = "3.0.2" + + +class CloudLibraryClient: + """Client for interacting with CloudLibrary API v3.0.2""" + + def __init__(self, library_id, account_id, account_key): + self.logger = create_log("cloudlibrary_client") + self.library_id = library_id + self.account_id = account_id + self.account_key = account_key + self.base_url = f"{_API_URL}/{self.library_id}" + self.setup_session() + + def setup_session(self): + """Authenticate and set up HTTP session""" + retry_policy = Retry(total=3, backoff_factor=45, + status_forcelist=[500, 502, 503, 504], + allowed_methods=frozenset(["GET"])) + self.session = requests.Session() + self.session.mount("https://", + HTTPAdapter(max_retries=retry_policy)) + + def get_library_events(self, start_date: str, end_date: str) -> requests.Response: + """ + Retrieves all the events related to library-owned items within the + optional timeframe. Pulls yesterday's events by default. + """ + yesterday = datetime.now() - timedelta(1) + yesterday_formatted = datetime.strftime(yesterday, "%Y-%m-%d") + start_date = yesterday_formatted if start_date is None else start_date + end_date = yesterday_formatted if end_date is None else end_date + + path = f"data/cloudevents?startdate{start_date}&enddate={end_date}" + response = self.request(path, "GET") + return response + + def create_request_body_with_filter(self, request_type: str, item_id: str, patron_id: str) -> str: + """ + Helper function to generate request body when performing item + and/or patron-specific functions (ex. checking out a title). + """ + request_template = "<%(request_type)s>%(item_id)s%(patron_id)s" + return request_template % { + "request_type": request_type, + "item_id": item_id, + "patron_id": patron_id, + } + + def request(self, path, body=None, method_type="GET") -> requests.Response: + headers = self._build_headers(method_type, path) + url = f"{self.base_url}/{path}" + method_type = method_type.upper() + + try: + if method_type == "GET": + response = self.session.get(url=url, + data=body, + headers=headers, + timeout=60) + elif method_type == "PUT": + response = self.session.put(url=url, + data=body, + headers=headers, + timeout=60) + else: + response = self.session.post(url=url, + data=body, + headers=headers, + timeout=60) + response.raise_for_status() + except Exception as e: + error_message = f"Failed to retrieve response from {url}: {e}" + self.logger.error(error_message) + raise CloudLibraryClientError(error_message) + + return response + + def _build_headers(self, method_type: str, path: str) -> dict: + time, authorization = self._build_authorization(method_type, path) + headers = { + "3mcl-Datetime": time, + "3mcl-Authorization": authorization, + "3mcl-Version": _VERSION, + } + + if method_type == "GET": + headers["Accept"] = "application/xml" + else: + headers["Content-Type"] = "application/xml" + + return headers + + def _build_authorization(self, method_type: str, path: str) -> tuple[str, str]: + now = datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT") + message = "\n".join([now, method_type, path]) + digest = hmac.new( + self.account_key.encode("utf-8"), + msg=message.encode("utf-8"), + digestmod=hashlib.sha256 + ).digest() + signature = base64.standard_b64decode(digest).decode() + + return now, f"3MCLAUTH {self.account_id}:{signature}" + + +class CloudLibraryClientError(Exception): + def __init__(self, message=None): + self.message = message diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py new file mode 100644 index 0000000..187801f --- /dev/null +++ b/tests/test_cloudlibrary_client.py @@ -0,0 +1,37 @@ +from unittest import mock +import pytest + +from nypl_py_utils.classes.cloudlibrary_client import CloudLibraryClient + +_TEST_LIBRARY_EVENTS_RESPONSE = """ +4302fcca-ef99-49bf-bd29-d673e990f765 +2012-06-29T17:35:18 +2012-06-26T13:58:52.055 + + +4302fcca-ef99-49bf-bd29-d673e990f4a7 +CHECKIN +2012-06-26T05:07:56 +2012-06-26T07:50:59 +edbz9 +1234 +9780307238405 +TestUser1 +1234 +2012-06-29T17:35:18 + + + +""" + + +class TestCloudLibraryClient: + @pytest.fixture + def test_instance(self): + client = CloudLibraryClient("library_id", "account_id", "account_key") + client.request = mock.MagicMock() + return client + + def test_get_library_events_success(self, test_instance): + return From e9a3feae5a0f93bc2599aad87cf32170baadba01 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Thu, 7 Nov 2024 15:11:11 -0500 Subject: [PATCH 02/11] Added tests for cloudLibrary client --- CHANGELOG.md | 3 + .../classes/cloudlibrary_client.py | 61 ++++++--- tests/test_cloudlibrary_client.py | 119 ++++++++++++++---- 3 files changed, 136 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdc66c8..5ba6640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## v1.5.0 11/4/24 +- Added cloudLibrary client + ## v1.4.0 9/23/24 - Added SFTP client diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py index 3b51057..e5bc57b 100644 --- a/src/nypl_py_utils/classes/cloudlibrary_client.py +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -3,11 +3,11 @@ import hmac import requests -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from nypl_py_utils.functions.log_helper import create_log from requests.adapters import HTTPAdapter, Retry -_API_URL = "https://partner.yourcloudlibrary.com/cirrus/library" +_API_URL = "https://partner.yourcloudlibrary.com" _VERSION = "3.0.2" @@ -19,7 +19,6 @@ def __init__(self, library_id, account_id, account_key): self.library_id = library_id self.account_id = account_id self.account_key = account_key - self.base_url = f"{_API_URL}/{self.library_id}" self.setup_session() def setup_session(self): @@ -31,21 +30,35 @@ def setup_session(self): self.session.mount("https://", HTTPAdapter(max_retries=retry_policy)) - def get_library_events(self, start_date: str, end_date: str) -> requests.Response: + def get_library_events(self, start_date=None, end_date=None) -> requests.Response: """ Retrieves all the events related to library-owned items within the optional timeframe. Pulls yesterday's events by default. + + start_date and end_date are optional parameters, and must be + formatted either YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS """ - yesterday = datetime.now() - timedelta(1) - yesterday_formatted = datetime.strftime(yesterday, "%Y-%m-%d") - start_date = yesterday_formatted if start_date is None else start_date - end_date = yesterday_formatted if end_date is None else end_date + date_format = "%Y-%m-%dT%H:%M:%S" + today = datetime.now(timezone.utc) + yesterday = today - timedelta(1) + start_date = datetime.strftime( + yesterday, date_format) if start_date is None else start_date + end_date = datetime.strftime( + today, date_format) if end_date is None else end_date + + if start_date > end_date: + error_message = f"Start date {start_date} is greater than end date {end_date}, cannot retrieve library events" + self.logger.error(error_message) + raise CloudLibraryClientError(error_message) - path = f"data/cloudevents?startdate{start_date}&enddate={end_date}" - response = self.request(path, "GET") + self.logger.info( + f"Fetching all library events in time frame {start_date} to {end_date}...") + + path = f"data/cloudevents?startdate={start_date}&enddate={end_date}" + response = self.request(path=path, method_type="GET") return response - def create_request_body_with_filter(self, request_type: str, item_id: str, patron_id: str) -> str: + def create_request_body(self, request_type, item_id, patron_id) -> str: """ Helper function to generate request body when performing item and/or patron-specific functions (ex. checking out a title). @@ -57,9 +70,15 @@ def create_request_body_with_filter(self, request_type: str, item_id: str, patro "patron_id": patron_id, } - def request(self, path, body=None, method_type="GET") -> requests.Response: - headers = self._build_headers(method_type, path) - url = f"{self.base_url}/{path}" + def request(self, path, method_type="POST", body=None) -> requests.Response: + """ + Use this method to call specific paths in the cloudLibrary API. + This method is necessary for building headers/authorization. + Example usage of this method is in the get_library_events function. + """ + extended_path = f"/cirrus/library/{self.library_id}/{path}" + headers = self._build_headers(method_type, extended_path) + url = f"{_API_URL}{extended_path}" method_type = method_type.upper() try: @@ -86,12 +105,13 @@ def request(self, path, body=None, method_type="GET") -> requests.Response: return response - def _build_headers(self, method_type: str, path: str) -> dict: - time, authorization = self._build_authorization(method_type, path) + def _build_headers(self, method_type, path) -> dict: + time, authorization = self._build_authorization( + method_type, path) headers = { "3mcl-Datetime": time, "3mcl-Authorization": authorization, - "3mcl-Version": _VERSION, + "3mcl-APIVersion": _VERSION, } if method_type == "GET": @@ -101,15 +121,16 @@ def _build_headers(self, method_type: str, path: str) -> dict: return headers - def _build_authorization(self, method_type: str, path: str) -> tuple[str, str]: - now = datetime.now().strftime("%a, %d %b %Y %H:%M:%S GMT") + def _build_authorization(self, method_type, path) -> tuple[str, str]: + now = datetime.now(timezone.utc).strftime( + "%a, %d %b %Y %H:%M:%S GMT") message = "\n".join([now, method_type, path]) digest = hmac.new( self.account_key.encode("utf-8"), msg=message.encode("utf-8"), digestmod=hashlib.sha256 ).digest() - signature = base64.standard_b64decode(digest).decode() + signature = base64.standard_b64encode(digest).decode() return now, f"3MCLAUTH {self.account_id}:{signature}" diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py index 187801f..db4fc81 100644 --- a/tests/test_cloudlibrary_client.py +++ b/tests/test_cloudlibrary_client.py @@ -1,37 +1,102 @@ -from unittest import mock import pytest -from nypl_py_utils.classes.cloudlibrary_client import CloudLibraryClient - -_TEST_LIBRARY_EVENTS_RESPONSE = """ -4302fcca-ef99-49bf-bd29-d673e990f765 -2012-06-29T17:35:18 -2012-06-26T13:58:52.055 - - -4302fcca-ef99-49bf-bd29-d673e990f4a7 -CHECKIN -2012-06-26T05:07:56 -2012-06-26T07:50:59 -edbz9 -1234 -9780307238405 -TestUser1 -1234 -2012-06-29T17:35:18 - - +from freezegun import freeze_time +from unittest import mock + +from requests import ConnectTimeout +from nypl_py_utils.classes.cloudlibrary_client import CloudLibraryClient, CloudLibraryClientError + +_API_URL = "https://partner.yourcloudlibrary.com/cirrus/library/" + +# catch-all API response since we're not testing actual data +_TEST_LIBRARY_EVENTS_RESPONSE = """ + 4302fcca-ef99-49bf-bd29-d673e990f765 + 2024-11-10T17:35:18 + 2012-11-11T13:58:52.055 + + + 4302fcca-ef99-49bf-bd29-d673e990f4a7 + CHECKIN + 2024-11-10T05:07:56 + 2024-11-10T07:50:59 + edbz9 + 1234 + 9780307238405 + TestUser1 + 1234 + 2024-11-10T17:35:18 + + """ +@freeze_time("2024-11-11 10:00:00") class TestCloudLibraryClient: @pytest.fixture def test_instance(self): - client = CloudLibraryClient("library_id", "account_id", "account_key") - client.request = mock.MagicMock() - return client + return CloudLibraryClient( + "library_id", "account_id", "account_key") + + def test_get_library_events_success_no_args(self, test_instance, requests_mock, caplog): + start = "2024-11-10T10:00:00" + end = "2024-11-11T10:00:00" + requests_mock.get( + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + text=_TEST_LIBRARY_EVENTS_RESPONSE) + response = test_instance.get_library_events() + + assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + assert f"Fetching all library events in time frame {start} to {end}..." in caplog.text + + def test_get_library_events_success_with_start_and_end_date(self, test_instance, requests_mock, caplog): + start = "2024-11-01T10:00:00" + end = "2024-11-05T10:00:00" + requests_mock.get( + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + text=_TEST_LIBRARY_EVENTS_RESPONSE) + response = test_instance.get_library_events(start, end) + + assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + assert f"Fetching all library events in time frame {start} to {end}..." in caplog.text + + def test_get_library_events_success_with_no_end_date(self, test_instance, requests_mock, caplog): + start = "2024-11-01T09:00:00" + end = "2024-11-11T10:00:00" + requests_mock.get( + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + text=_TEST_LIBRARY_EVENTS_RESPONSE) + response = test_instance.get_library_events(start) + + assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + assert f"Fetching all library events in time frame {start} to {end}..." in caplog.text + + def test_get_library_events_exception_when_start_date_greater_than_end(self, test_instance, caplog): + start = "2024-11-11T09:00:00" + end = "2024-11-01T10:00:00" + + with pytest.raises(CloudLibraryClientError): + test_instance.get_library_events(start, end) + assert f"Start date {start} is greater than end date {end}, cannot retrieve library events" in caplog.text + + def test_get_library_events_failure(self, test_instance, requests_mock): + start = "2024-11-10T10:00:00" + end = "2024-11-11T10:00:00" + requests_mock.get( + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + exc=ConnectTimeout) + + with pytest.raises(CloudLibraryClientError): + test_instance.get_library_events() + + def test_create_request_body_success(self, test_instance): + request_type = "CheckoutRequest" + item_id = "df45qw" + patron_id = "215555602845" + EXPECTED_REQUEST_BODY = f"<{request_type}>{item_id}{patron_id}" + request_body = test_instance.create_request_body( + request_type, item_id, patron_id) - def test_get_library_events_success(self, test_instance): - return + assert request_body == EXPECTED_REQUEST_BODY From 747f3c20e8328a17da037429d3f3ce391e18130e Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Thu, 7 Nov 2024 15:59:16 -0500 Subject: [PATCH 03/11] Update linting --- README.md | 1 + .../classes/cloudlibrary_client.py | 28 ++++--- tests/test_cloudlibrary_client.py | 79 +++++++++++-------- 3 files changed, 62 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index f695d8b..a355df4 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This package contains common Python utility classes and functions. * Connecting to and querying a PostgreSQL database using a connection pool * Connecting to and querying Redshift * Making requests to the Oauth2 authenticated APIs such as NYPL Platform API and Sierra +* Interacting with vendor APIs such as cloudLibrary ## Functions * Reading a YAML config file and putting the contents in os.environ -- see `config/sample.yaml` for an example of how the config file should be formatted diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py index e5bc57b..dcc4524 100644 --- a/src/nypl_py_utils/classes/cloudlibrary_client.py +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -30,9 +30,10 @@ def setup_session(self): self.session.mount("https://", HTTPAdapter(max_retries=retry_policy)) - def get_library_events(self, start_date=None, end_date=None) -> requests.Response: + def get_library_events(self, start_date=None, + end_date=None) -> requests.Response: """ - Retrieves all the events related to library-owned items within the + Retrieves all the events related to library-owned items within the optional timeframe. Pulls yesterday's events by default. start_date and end_date are optional parameters, and must be @@ -47,32 +48,36 @@ def get_library_events(self, start_date=None, end_date=None) -> requests.Respons today, date_format) if end_date is None else end_date if start_date > end_date: - error_message = f"Start date {start_date} is greater than end date {end_date}, cannot retrieve library events" + error_message = (f"Start date {start_date} greater than end date " + f"{end_date}, cannot retrieve library events") self.logger.error(error_message) raise CloudLibraryClientError(error_message) self.logger.info( - f"Fetching all library events in time frame {start_date} to {end_date}...") + (f"Fetching all library events in " + f"time frame {start_date} to {end_date}...")) path = f"data/cloudevents?startdate={start_date}&enddate={end_date}" response = self.request(path=path, method_type="GET") return response - def create_request_body(self, request_type, item_id, patron_id) -> str: + def create_request_body(self, request_type, + item_id, patron_id) -> str: """ - Helper function to generate request body when performing item - and/or patron-specific functions (ex. checking out a title). + Helper function to generate request body when performing item + and/or patron-specific functions (ex. checking out a title). """ - request_template = "<%(request_type)s>%(item_id)s%(patron_id)s" + request_template = "<%(request_type)s>%(item_id)s%(patron_id)s" # noqa return request_template % { "request_type": request_type, "item_id": item_id, "patron_id": patron_id, } - def request(self, path, method_type="POST", body=None) -> requests.Response: + def request(self, path, method_type="POST", + body=None) -> requests.Response: """ - Use this method to call specific paths in the cloudLibrary API. + Use this method to call specific paths in the cloudLibrary API. This method is necessary for building headers/authorization. Example usage of this method is in the get_library_events function. """ @@ -121,7 +126,8 @@ def _build_headers(self, method_type, path) -> dict: return headers - def _build_authorization(self, method_type, path) -> tuple[str, str]: + def _build_authorization(self, method_type, + path) -> tuple[str, str]: now = datetime.now(timezone.utc).strftime( "%a, %d %b %Y %H:%M:%S GMT") message = "\n".join([now, method_type, path]) diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py index db4fc81..786c17c 100644 --- a/tests/test_cloudlibrary_client.py +++ b/tests/test_cloudlibrary_client.py @@ -1,34 +1,33 @@ import pytest from freezegun import freeze_time -from unittest import mock - from requests import ConnectTimeout -from nypl_py_utils.classes.cloudlibrary_client import CloudLibraryClient, CloudLibraryClientError +from nypl_py_utils.classes.cloudlibrary_client import ( + CloudLibraryClient, CloudLibraryClientError) _API_URL = "https://partner.yourcloudlibrary.com/cirrus/library/" # catch-all API response since we're not testing actual data _TEST_LIBRARY_EVENTS_RESPONSE = """ - 4302fcca-ef99-49bf-bd29-d673e990f765 - 2024-11-10T17:35:18 - 2012-11-11T13:58:52.055 - - - 4302fcca-ef99-49bf-bd29-d673e990f4a7 - CHECKIN - 2024-11-10T05:07:56 - 2024-11-10T07:50:59 - edbz9 - 1234 - 9780307238405 - TestUser1 - 1234 - 2024-11-10T17:35:18 - - +xmlns:xsd="http://www.w3.org/2001/XMLSchema" +xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> +4302fcca-ef99-49bf-bd29-d673e990f765 +2024-11-10T17:35:18 +2012-11-11T13:58:52.055 + + +4302fcca-ef99-49bf-bd29-d673e990f4a7 +CHECKIN +2024-11-10T05:07:56 +2024-11-10T07:50:59 +edbz9 +1234 +9780307238405 +TestUser1 +1234 +2024-11-10T17:35:18 + + """ @@ -40,52 +39,60 @@ def test_instance(self): return CloudLibraryClient( "library_id", "account_id", "account_key") - def test_get_library_events_success_no_args(self, test_instance, requests_mock, caplog): + def test_get_library_events_success_no_args( + self, test_instance, requests_mock, caplog): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa text=_TEST_LIBRARY_EVENTS_RESPONSE) response = test_instance.get_library_events() assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE - assert f"Fetching all library events in time frame {start} to {end}..." in caplog.text + assert (f"Fetching all library events in time frame " + f"{start} to {end}...") in caplog.text - def test_get_library_events_success_with_start_and_end_date(self, test_instance, requests_mock, caplog): + def test_get_library_events_success_with_start_and_end_date( + self, test_instance, requests_mock, caplog): start = "2024-11-01T10:00:00" end = "2024-11-05T10:00:00" requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa text=_TEST_LIBRARY_EVENTS_RESPONSE) response = test_instance.get_library_events(start, end) assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE - assert f"Fetching all library events in time frame {start} to {end}..." in caplog.text + assert (f"Fetching all library events in time frame " + f"{start} to {end}...") in caplog.text - def test_get_library_events_success_with_no_end_date(self, test_instance, requests_mock, caplog): + def test_get_library_events_success_with_no_end_date( + self, test_instance, requests_mock, caplog): start = "2024-11-01T09:00:00" end = "2024-11-11T10:00:00" requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa text=_TEST_LIBRARY_EVENTS_RESPONSE) response = test_instance.get_library_events(start) assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE - assert f"Fetching all library events in time frame {start} to {end}..." in caplog.text + assert (f"Fetching all library events in time frame " + f"{start} to {end}...") in caplog.text - def test_get_library_events_exception_when_start_date_greater_than_end(self, test_instance, caplog): + def test_get_library_events_exception_when_start_date_greater_than_end( + self, test_instance, caplog): start = "2024-11-11T09:00:00" end = "2024-11-01T10:00:00" with pytest.raises(CloudLibraryClientError): test_instance.get_library_events(start, end) - assert f"Start date {start} is greater than end date {end}, cannot retrieve library events" in caplog.text + assert (f"Start date {start} greater than end date " + f"{end}, cannot retrieve library events") in caplog.text def test_get_library_events_failure(self, test_instance, requests_mock): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa exc=ConnectTimeout) with pytest.raises(CloudLibraryClientError): @@ -95,7 +102,9 @@ def test_create_request_body_success(self, test_instance): request_type = "CheckoutRequest" item_id = "df45qw" patron_id = "215555602845" - EXPECTED_REQUEST_BODY = f"<{request_type}>{item_id}{patron_id}" + EXPECTED_REQUEST_BODY = (f"<{request_type}>{item_id}" + f"{patron_id}" + f"") request_body = test_instance.create_request_body( request_type, item_id, patron_id) From d636a52ba6b36583bc2e46f337501f9d84d3c0a4 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Sun, 10 Nov 2024 16:33:41 -0500 Subject: [PATCH 04/11] Update pyproject.toml --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ed151cd..4c68601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,9 @@ avro-client = [ "avro>=1.11.1", "requests>=2.28.1" ] +cloudlibrary-client = [ + "requests>=2.28.1" +] kinesis-client = [ "boto3>=1.26.5", "botocore>=1.29.5" From 604ec0e0f088abac2951e8f92922b06252405458 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Mon, 18 Nov 2024 10:45:46 -0500 Subject: [PATCH 05/11] Update session initialization --- .../classes/cloudlibrary_client.py | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py index dcc4524..5e5f646 100644 --- a/src/nypl_py_utils/classes/cloudlibrary_client.py +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -4,6 +4,7 @@ import requests from datetime import datetime, timedelta, timezone +from enum import Enum from nypl_py_utils.functions.log_helper import create_log from requests.adapters import HTTPAdapter, Retry @@ -19,10 +20,8 @@ def __init__(self, library_id, account_id, account_key): self.library_id = library_id self.account_id = account_id self.account_key = account_key - self.setup_session() - def setup_session(self): - """Authenticate and set up HTTP session""" + # authenticate & set up HTTP session retry_policy = Retry(total=3, backoff_factor=45, status_forcelist=[500, 502, 503, 504], allowed_methods=frozenset(["GET"])) @@ -74,7 +73,7 @@ def create_request_body(self, request_type, "patron_id": patron_id, } - def request(self, path, method_type="POST", + def request(self, path, method_type="GET", body=None) -> requests.Response: """ Use this method to call specific paths in the cloudLibrary API. @@ -87,21 +86,21 @@ def request(self, path, method_type="POST", method_type = method_type.upper() try: - if method_type == "GET": - response = self.session.get(url=url, - data=body, - headers=headers, - timeout=60) - elif method_type == "PUT": + if method_type == "PUT": response = self.session.put(url=url, data=body, headers=headers, timeout=60) - else: + elif method_type == "POST": response = self.session.post(url=url, data=body, headers=headers, timeout=60) + else: + response = self.session.get(url=url, + data=body, + headers=headers, + timeout=60) response.raise_for_status() except Exception as e: error_message = f"Failed to retrieve response from {url}: {e}" @@ -144,3 +143,11 @@ def _build_authorization(self, method_type, class CloudLibraryClientError(Exception): def __init__(self, message=None): self.message = message + +class CloudLibraryEventType(Enum): + CHECKIN = "Patron checked in or returned an item" + CHECKOUT = "Patron checked out or borrowed an item" + HOLD = "Patron placed a hold request on a book" + PURCHASE = "Library purchased an item (this could be another copy of a currently owned item)" + RESERVED = "The title which is currently on hold for the patron is now available for checkout" + REMOVED = "A copy of a item has expired or was deliberately removed from library stock" From e76724f38ff57438e1924d018d876ce84ebbea13 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Mon, 18 Nov 2024 16:09:48 -0500 Subject: [PATCH 06/11] Updated tests --- .../classes/cloudlibrary_client.py | 12 +-- tests/test_cloudlibrary_client.py | 75 +++++++++++++++---- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py index 5e5f646..f751b7e 100644 --- a/src/nypl_py_utils/classes/cloudlibrary_client.py +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -4,7 +4,6 @@ import requests from datetime import datetime, timedelta, timezone -from enum import Enum from nypl_py_utils.functions.log_helper import create_log from requests.adapters import HTTPAdapter, Retry @@ -46,7 +45,8 @@ def get_library_events(self, start_date=None, end_date = datetime.strftime( today, date_format) if end_date is None else end_date - if start_date > end_date: + if (datetime.strptime(start_date, date_format) > + datetime.strptime(end_date, date_format)): error_message = (f"Start date {start_date} greater than end date " f"{end_date}, cannot retrieve library events") self.logger.error(error_message) @@ -143,11 +143,3 @@ def _build_authorization(self, method_type, class CloudLibraryClientError(Exception): def __init__(self, message=None): self.message = message - -class CloudLibraryEventType(Enum): - CHECKIN = "Patron checked in or returned an item" - CHECKOUT = "Patron checked out or borrowed an item" - HOLD = "Patron placed a hold request on a book" - PURCHASE = "Library purchased an item (this could be another copy of a currently owned item)" - RESERVED = "The title which is currently on hold for the patron is now available for checkout" - REMOVED = "A copy of a item has expired or was deliberately removed from library stock" diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py index 786c17c..7d838c4 100644 --- a/tests/test_cloudlibrary_client.py +++ b/tests/test_cloudlibrary_client.py @@ -40,41 +40,50 @@ def test_instance(self): "library_id", "account_id", "account_key") def test_get_library_events_success_no_args( - self, test_instance, requests_mock, caplog): + self, test_instance, mocker, caplog): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" - requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa - text=_TEST_LIBRARY_EVENTS_RESPONSE) + mock_request = mocker.patch( + "nypl_py_utils.classes.cloudlibrary_client.CloudLibraryClient.request", # noqa + return_value=_TEST_LIBRARY_EVENTS_RESPONSE) response = test_instance.get_library_events() - assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + mock_request.assert_called_once_with( + path=f"data/cloudevents?startdate={start}&enddate={end}", + method_type="GET") + assert response == _TEST_LIBRARY_EVENTS_RESPONSE assert (f"Fetching all library events in time frame " f"{start} to {end}...") in caplog.text def test_get_library_events_success_with_start_and_end_date( - self, test_instance, requests_mock, caplog): + self, test_instance, mocker, caplog): start = "2024-11-01T10:00:00" end = "2024-11-05T10:00:00" - requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa - text=_TEST_LIBRARY_EVENTS_RESPONSE) + mock_request = mocker.patch( + "nypl_py_utils.classes.cloudlibrary_client.CloudLibraryClient.request", # noqa + return_value=_TEST_LIBRARY_EVENTS_RESPONSE) response = test_instance.get_library_events(start, end) - assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + mock_request.assert_called_once_with( + path=f"data/cloudevents?startdate={start}&enddate={end}", + method_type="GET") + assert response == _TEST_LIBRARY_EVENTS_RESPONSE assert (f"Fetching all library events in time frame " f"{start} to {end}...") in caplog.text def test_get_library_events_success_with_no_end_date( - self, test_instance, requests_mock, caplog): + self, test_instance, mocker, caplog): start = "2024-11-01T09:00:00" end = "2024-11-11T10:00:00" - requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa - text=_TEST_LIBRARY_EVENTS_RESPONSE) + mock_request = mocker.patch( + "nypl_py_utils.classes.cloudlibrary_client.CloudLibraryClient.request", # noqa + return_value=_TEST_LIBRARY_EVENTS_RESPONSE) response = test_instance.get_library_events(start) - assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + mock_request.assert_called_once_with( + path=f"data/cloudevents?startdate={start}&enddate={end}", + method_type="GET") + assert response == _TEST_LIBRARY_EVENTS_RESPONSE assert (f"Fetching all library events in time frame " f"{start} to {end}...") in caplog.text @@ -88,9 +97,13 @@ def test_get_library_events_exception_when_start_date_greater_than_end( assert (f"Start date {start} greater than end date " f"{end}, cannot retrieve library events") in caplog.text - def test_get_library_events_failure(self, test_instance, requests_mock): + def test_get_library_events_exception_when_connection_timeout( + self, test_instance, requests_mock): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" + + # We're making sure that a separate error during a sub-method will + # still result in CloudLibraryClientError requests_mock.get( f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa exc=ConnectTimeout) @@ -98,6 +111,36 @@ def test_get_library_events_failure(self, test_instance, requests_mock): with pytest.raises(CloudLibraryClientError): test_instance.get_library_events() + def test_request_success(self, test_instance, mocker): + start = "2024-11-10T10:00:00" + end = "2024-11-11T10:00:00" + expected_headers = {'3mcl-Datetime': 'Mon, 11 Nov 2024 10:00:00 GMT', + '3mcl-Authorization': '3MCLAUTH account_id:KipNmbVsmsT2xPjP4oHAaR3n00JgcszfF6mQRffBoRk=', # noqa + '3mcl-APIVersion': '3.0.2', + 'Accept': 'application/xml'} + mock_get = mocker.patch("requests.sessions.Session.get") + test_instance.request( + path=f"data/cloudevents?startdate={start}&enddate={end}", + method_type="GET") + + mock_get.assert_called_once_with( + url=f"{_API_URL}library_id/data/cloudevents?startdate={start}&enddate={end}", # noqa + data=None, + headers=expected_headers, + timeout=60) + + def test_request_failure(self, test_instance, requests_mock, caplog): + start = "2024-11-10T10:00:00" + end = "2024-11-11T10:00:00" + requests_mock.get( + f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa + exc=ConnectTimeout) + + with pytest.raises(CloudLibraryClientError): + test_instance.request( + path=f"data/cloudevents?startdate={start}&enddate={end}", + method_type="GET") + def test_create_request_body_success(self, test_instance): request_type = "CheckoutRequest" item_id = "df45qw" From 4c82ae1843560102144465908c7a247869c43d17 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Tue, 19 Nov 2024 12:37:59 -0500 Subject: [PATCH 07/11] Minor syntax updates --- README.md | 4 ++-- pyproject.toml | 2 +- .../classes/cloudlibrary_client.py | 3 +++ tests/test_cloudlibrary_client.py | 18 +++++------------- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index a355df4..70446ab 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ kinesis_client = KinesisClient(...) # Do not use any version below 1.0.0 # All available optional dependencies can be found in pyproject.toml. # See the "Managing dependencies" section below for more details. -nypl-py-utils[kinesis-client,config-helper]==1.4.0 +nypl-py-utils[kinesis-client,config-helper]==1.5.0 ``` ## Developing locally @@ -64,7 +64,7 @@ The optional dependency sets also give the developer the option to manually list ### Using PostgreSQLClient in an AWS Lambda Because `psycopg` requires a statically linked version of the `libpq` library, the `PostgreSQLClient` cannot be installed as-is in an AWS Lambda function. Instead, it must be packaged as follows: ```bash -pip install --target ./package nypl-py-utils[postgresql-client]==1.4.0 +pip install --target ./package nypl-py-utils[postgresql-client]==1.5.0 pip install \ --platform manylinux2014_x86_64 \ diff --git a/pyproject.toml b/pyproject.toml index 4c68601..6d6b5eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "nypl_py_utils" -version = "1.4.0" +version = "1.5.0" authors = [ { name="Aaron Friedman", email="aaronfriedman@nypl.org" }, ] diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py index f751b7e..af62dcb 100644 --- a/src/nypl_py_utils/classes/cloudlibrary_client.py +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -79,6 +79,9 @@ def request(self, path, method_type="GET", Use this method to call specific paths in the cloudLibrary API. This method is necessary for building headers/authorization. Example usage of this method is in the get_library_events function. + + Returns Response object by default -- you will need to parse the object + to retrieve text, status code, etc. """ extended_path = f"/cirrus/library/{self.library_id}/{path}" headers = self._build_headers(method_type, extended_path) diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py index 7d838c4..fdfcf92 100644 --- a/tests/test_cloudlibrary_client.py +++ b/tests/test_cloudlibrary_client.py @@ -40,7 +40,7 @@ def test_instance(self): "library_id", "account_id", "account_key") def test_get_library_events_success_no_args( - self, test_instance, mocker, caplog): + self, test_instance, mocker): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" mock_request = mocker.patch( @@ -52,11 +52,9 @@ def test_get_library_events_success_no_args( path=f"data/cloudevents?startdate={start}&enddate={end}", method_type="GET") assert response == _TEST_LIBRARY_EVENTS_RESPONSE - assert (f"Fetching all library events in time frame " - f"{start} to {end}...") in caplog.text def test_get_library_events_success_with_start_and_end_date( - self, test_instance, mocker, caplog): + self, test_instance, mocker): start = "2024-11-01T10:00:00" end = "2024-11-05T10:00:00" mock_request = mocker.patch( @@ -68,11 +66,9 @@ def test_get_library_events_success_with_start_and_end_date( path=f"data/cloudevents?startdate={start}&enddate={end}", method_type="GET") assert response == _TEST_LIBRARY_EVENTS_RESPONSE - assert (f"Fetching all library events in time frame " - f"{start} to {end}...") in caplog.text def test_get_library_events_success_with_no_end_date( - self, test_instance, mocker, caplog): + self, test_instance, mocker): start = "2024-11-01T09:00:00" end = "2024-11-11T10:00:00" mock_request = mocker.patch( @@ -84,18 +80,14 @@ def test_get_library_events_success_with_no_end_date( path=f"data/cloudevents?startdate={start}&enddate={end}", method_type="GET") assert response == _TEST_LIBRARY_EVENTS_RESPONSE - assert (f"Fetching all library events in time frame " - f"{start} to {end}...") in caplog.text def test_get_library_events_exception_when_start_date_greater_than_end( - self, test_instance, caplog): + self, test_instance): start = "2024-11-11T09:00:00" end = "2024-11-01T10:00:00" with pytest.raises(CloudLibraryClientError): test_instance.get_library_events(start, end) - assert (f"Start date {start} greater than end date " - f"{end}, cannot retrieve library events") in caplog.text def test_get_library_events_exception_when_connection_timeout( self, test_instance, requests_mock): @@ -129,7 +121,7 @@ def test_request_success(self, test_instance, mocker): headers=expected_headers, timeout=60) - def test_request_failure(self, test_instance, requests_mock, caplog): + def test_request_failure(self, test_instance, requests_mock): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" requests_mock.get( From 1f356b7549df7810fca4c8c49c65cf9109cb20f0 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Tue, 19 Nov 2024 13:40:42 -0500 Subject: [PATCH 08/11] Update tests and method descriptions --- .../classes/cloudlibrary_client.py | 6 +- tests/test_cloudlibrary_client.py | 73 ++++++++++++++++--- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py index af62dcb..615e284 100644 --- a/src/nypl_py_utils/classes/cloudlibrary_client.py +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -32,7 +32,7 @@ def get_library_events(self, start_date=None, end_date=None) -> requests.Response: """ Retrieves all the events related to library-owned items within the - optional timeframe. Pulls yesterday's events by default. + optional timeframe. Pulls past 24 hours of events by default. start_date and end_date are optional parameters, and must be formatted either YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS @@ -80,8 +80,8 @@ def request(self, path, method_type="GET", This method is necessary for building headers/authorization. Example usage of this method is in the get_library_events function. - Returns Response object by default -- you will need to parse the object - to retrieve text, status code, etc. + Returns Response object by default -- you will need to parse this + object to retrieve response text, status codes, etc. """ extended_path = f"/cirrus/library/{self.library_id}/{path}" headers = self._build_headers(method_type, extended_path) diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py index fdfcf92..f544863 100644 --- a/tests/test_cloudlibrary_client.py +++ b/tests/test_cloudlibrary_client.py @@ -103,23 +103,72 @@ def test_get_library_events_exception_when_connection_timeout( with pytest.raises(CloudLibraryClientError): test_instance.get_library_events() - def test_request_success(self, test_instance, mocker): + def test_get_request_success(self, test_instance, requests_mock): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" - expected_headers = {'3mcl-Datetime': 'Mon, 11 Nov 2024 10:00:00 GMT', - '3mcl-Authorization': '3MCLAUTH account_id:KipNmbVsmsT2xPjP4oHAaR3n00JgcszfF6mQRffBoRk=', # noqa - '3mcl-APIVersion': '3.0.2', - 'Accept': 'application/xml'} - mock_get = mocker.patch("requests.sessions.Session.get") - test_instance.request( + url = f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}" # noqa + expected_headers = {"3mcl-Datetime": "Mon, 11 Nov 2024 10:00:00 GMT", + "3mcl-Authorization": "3MCLAUTH account_id:KipNmbVsmsT2xPjP4oHAaR3n00JgcszfF6mQRffBoRk=", # noqa + "3mcl-APIVersion": "3.0.2", + "Accept": "application/xml"} + requests_mock.get( + url=url, text=_TEST_LIBRARY_EVENTS_RESPONSE) + + response = test_instance.request( path=f"data/cloudevents?startdate={start}&enddate={end}", method_type="GET") - mock_get.assert_called_once_with( - url=f"{_API_URL}library_id/data/cloudevents?startdate={start}&enddate={end}", # noqa - data=None, - headers=expected_headers, - timeout=60) + assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + assert requests_mock.request_history[0].method == "GET" + assert requests_mock.request_history[0].url == url + assert expected_headers.items() <= dict( + requests_mock.request_history[0].headers).items() + + def test_put_request_success(self, test_instance, requests_mock): + start = "2024-11-10T10:00:00" + end = "2024-11-11T10:00:00" + url = f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}" # noqa + expected_headers = {"3mcl-Datetime": "Mon, 11 Nov 2024 10:00:00 GMT", + "3mcl-Authorization": "3MCLAUTH account_id:3M773C6ZVWmB/ISoSjQy9iBp48T4tUWhoNOwXaseMtE=", # noqa + "3mcl-APIVersion": "3.0.2", + "Content-Type": "application/xml"} + requests_mock.put( + url=url, text=_TEST_LIBRARY_EVENTS_RESPONSE) + + response = test_instance.request( + path=f"data/cloudevents?startdate={start}&enddate={end}", + method_type="PUT", + body={"test": "test"}) + + assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + assert requests_mock.request_history[0].method == "PUT" + assert requests_mock.request_history[0].url == url + assert requests_mock.request_history[0].body == "test=test" + assert expected_headers.items() <= dict( + requests_mock.request_history[0].headers).items() + + def test_post_request_success(self, test_instance, requests_mock): + start = "2024-11-10T10:00:00" + end = "2024-11-11T10:00:00" + url = f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}" # noqa + expected_headers = {"3mcl-Datetime": "Mon, 11 Nov 2024 10:00:00 GMT", + "3mcl-Authorization": "3MCLAUTH account_id:vF0zI6ee1w1PbTLQ9EVvtxRly2vpCRxdBdAHb8DZQ4E=", # noqa + "3mcl-APIVersion": "3.0.2", + "Content-Type": "application/xml"} + requests_mock.post( + url=url, text=_TEST_LIBRARY_EVENTS_RESPONSE) + + response = test_instance.request( + path=f"data/cloudevents?startdate={start}&enddate={end}", + method_type="POST", + body={"test": "test"}) + + assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE + assert requests_mock.request_history[0].method == "POST" + assert requests_mock.request_history[0].url == url + assert requests_mock.request_history[0].body == "test=test" + assert expected_headers.items() <= dict( + requests_mock.request_history[0].headers).items() def test_request_failure(self, test_instance, requests_mock): start = "2024-11-10T10:00:00" From f246cb9fada634c02834a4cd1aea49055cb77316 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Tue, 19 Nov 2024 13:42:57 -0500 Subject: [PATCH 09/11] Check get request body is none --- tests/test_cloudlibrary_client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py index f544863..1a1000a 100644 --- a/tests/test_cloudlibrary_client.py +++ b/tests/test_cloudlibrary_client.py @@ -121,6 +121,7 @@ def test_get_request_success(self, test_instance, requests_mock): assert response.text == _TEST_LIBRARY_EVENTS_RESPONSE assert requests_mock.request_history[0].method == "GET" assert requests_mock.request_history[0].url == url + assert requests_mock.request_history[0].body is None assert expected_headers.items() <= dict( requests_mock.request_history[0].headers).items() From e2c88b360cc945283591c38edf801ef6c70760cc Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Tue, 19 Nov 2024 15:27:48 -0500 Subject: [PATCH 10/11] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba6640..2e08f9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Changelog -## v1.5.0 11/4/24 +## v1.5.0 11/19/24 - Added cloudLibrary client ## v1.4.0 9/23/24 From 5a882c64d67d4862c3e284454d109eec25b6b4e2 Mon Sep 17 00:00:00 2001 From: fatimarahman Date: Wed, 20 Nov 2024 09:18:30 -0500 Subject: [PATCH 11/11] Added caplog checks in tests --- .../classes/cloudlibrary_client.py | 3 ++- tests/test_cloudlibrary_client.py | 20 ++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/nypl_py_utils/classes/cloudlibrary_client.py b/src/nypl_py_utils/classes/cloudlibrary_client.py index 615e284..1c8c191 100644 --- a/src/nypl_py_utils/classes/cloudlibrary_client.py +++ b/src/nypl_py_utils/classes/cloudlibrary_client.py @@ -106,7 +106,8 @@ def request(self, path, method_type="GET", timeout=60) response.raise_for_status() except Exception as e: - error_message = f"Failed to retrieve response from {url}: {e}" + error_message = (f"Failed to retrieve response from {url}: " + f"{repr(e)}") self.logger.error(error_message) raise CloudLibraryClientError(error_message) diff --git a/tests/test_cloudlibrary_client.py b/tests/test_cloudlibrary_client.py index 1a1000a..84866f9 100644 --- a/tests/test_cloudlibrary_client.py +++ b/tests/test_cloudlibrary_client.py @@ -82,26 +82,29 @@ def test_get_library_events_success_with_no_end_date( assert response == _TEST_LIBRARY_EVENTS_RESPONSE def test_get_library_events_exception_when_start_date_greater_than_end( - self, test_instance): + self, test_instance, caplog): start = "2024-11-11T09:00:00" end = "2024-11-01T10:00:00" with pytest.raises(CloudLibraryClientError): test_instance.get_library_events(start, end) + assert (f"Start date {start} greater than end date {end}, " + f"cannot retrieve library events") in caplog.text def test_get_library_events_exception_when_connection_timeout( - self, test_instance, requests_mock): + self, test_instance, requests_mock, caplog): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" + url = f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}" # noqa # We're making sure that a separate error during a sub-method will # still result in CloudLibraryClientError requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa - exc=ConnectTimeout) + url, exc=ConnectTimeout) with pytest.raises(CloudLibraryClientError): test_instance.get_library_events() + assert (f"Failed to retrieve response from {url}") in caplog.text def test_get_request_success(self, test_instance, requests_mock): start = "2024-11-10T10:00:00" @@ -171,17 +174,20 @@ def test_post_request_success(self, test_instance, requests_mock): assert expected_headers.items() <= dict( requests_mock.request_history[0].headers).items() - def test_request_failure(self, test_instance, requests_mock): + def test_request_failure(self, test_instance, + requests_mock, caplog): start = "2024-11-10T10:00:00" end = "2024-11-11T10:00:00" + url = f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}" # noqa requests_mock.get( - f"{_API_URL}{test_instance.library_id}/data/cloudevents?startdate={start}&enddate={end}", # noqa - exc=ConnectTimeout) + url, exc=ConnectTimeout) with pytest.raises(CloudLibraryClientError): test_instance.request( path=f"data/cloudevents?startdate={start}&enddate={end}", method_type="GET") + assert (f"Failed to retrieve response from " + f"{url}: ConnectTimeout()") in caplog.text def test_create_request_body_success(self, test_instance): request_type = "CheckoutRequest"