-
Notifications
You must be signed in to change notification settings - Fork 0
DE-175: Create general cloudLibrary client #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
613d060
Initial pass
fatimarahman e9a3fea
Added tests for cloudLibrary client
fatimarahman 747f3c2
Update linting
fatimarahman d636a52
Update pyproject.toml
fatimarahman 604ec0e
Update session initialization
fatimarahman e76724f
Updated tests
fatimarahman 4c82ae1
Minor syntax updates
fatimarahman 1f356b7
Update tests and method descriptions
fatimarahman f246cb9
Check get request body is none
fatimarahman e2c88b3
Update CHANGELOG
fatimarahman 5a882c6
Added caplog checks in tests
fatimarahman File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,7 @@ | ||
| # Changelog | ||
| ## v1.5.0 11/19/24 | ||
| - Added cloudLibrary client | ||
|
|
||
| ## v1.4.0 9/23/24 | ||
| - Added SFTP client | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| import base64 | ||
| import hashlib | ||
| import hmac | ||
| import requests | ||
|
|
||
| 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" | ||
| _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 | ||
|
|
||
| # authenticate & 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=None, | ||
fatimarahman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| end_date=None) -> requests.Response: | ||
| """ | ||
| Retrieves all the events related to library-owned items within the | ||
| optional timeframe. Pulls past 24 hours of events by default. | ||
|
|
||
| start_date and end_date are optional parameters, and must be | ||
fatimarahman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| formatted either YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS | ||
| """ | ||
| 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 (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) | ||
| raise CloudLibraryClientError(error_message) | ||
|
|
||
| self.logger.info( | ||
| (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: | ||
| """ | ||
| Helper function to generate request body when performing item | ||
| and/or patron-specific functions (ex. checking out a title). | ||
| """ | ||
| request_template = "<%(request_type)s><ItemId>%(item_id)s</ItemId><PatronId>%(patron_id)s</PatronId></%(request_type)s>" # noqa | ||
| return request_template % { | ||
| "request_type": request_type, | ||
| "item_id": item_id, | ||
| "patron_id": patron_id, | ||
| } | ||
|
|
||
| def request(self, path, method_type="GET", | ||
| 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. | ||
|
|
||
| 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) | ||
| url = f"{_API_URL}{extended_path}" | ||
| method_type = method_type.upper() | ||
|
|
||
| try: | ||
| if method_type == "PUT": | ||
| response = self.session.put(url=url, | ||
| data=body, | ||
| headers=headers, | ||
| timeout=60) | ||
| 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}: " | ||
| f"{repr(e)}") | ||
| self.logger.error(error_message) | ||
| raise CloudLibraryClientError(error_message) | ||
|
|
||
| return response | ||
|
|
||
| def _build_headers(self, method_type, path) -> dict: | ||
| time, authorization = self._build_authorization( | ||
| method_type, path) | ||
| headers = { | ||
| "3mcl-Datetime": time, | ||
| "3mcl-Authorization": authorization, | ||
| "3mcl-APIVersion": _VERSION, | ||
| } | ||
|
|
||
| if method_type == "GET": | ||
| headers["Accept"] = "application/xml" | ||
| else: | ||
| headers["Content-Type"] = "application/xml" | ||
|
|
||
| return headers | ||
|
|
||
| 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() | ||
fatimarahman marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| signature = base64.standard_b64encode(digest).decode() | ||
|
|
||
| return now, f"3MCLAUTH {self.account_id}:{signature}" | ||
|
|
||
|
|
||
| class CloudLibraryClientError(Exception): | ||
| def __init__(self, message=None): | ||
| self.message = message | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.