From 017bfaefc4fe31bb5d9d00718be5b21a0535c10f Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Sat, 16 Dec 2023 12:30:37 -0500 Subject: [PATCH 1/4] docs(auth): add some documentation --- src/nbiatoolkit/auth.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/nbiatoolkit/auth.py b/src/nbiatoolkit/auth.py index b133b38..4882a09 100644 --- a/src/nbiatoolkit/auth.py +++ b/src/nbiatoolkit/auth.py @@ -14,16 +14,12 @@ class OAuth2: to the collections tagged with "limited access" you can use those credentials to access those collections. - NOTE::This class is mainly for developers looking to add functionality + Notes + ----- + This class is mainly for developers looking to add functionality to the nbiatoolkit package. If you are a user looking to access the NBIA API, you can use the `NBIAClient` class without knowledge of this class. - TODO::implement better access token handling - TODO::implement better error handling - TODO::implement refresh token functionality - TODO::implement logout functionality - TODO::implement encryption for username and password - Attributes ---------- client_id : str @@ -43,12 +39,15 @@ class OAuth2: Example Usage ------------- - >>> from nbiatoolkit import OAuth2 + >>> from nbiatoolkit.auth import OAuth2 + To use the NBIA Guest account: + >>> oauth = OAuth2() + To use a custom account: - >>> oauth = OAuth2(username="my_username", password="my_password") + >>> oauth = OAuth2(username="my_username", password="my_password") """ def __init__(self, username: str = "nbia_guest", password: str = "", client_id: str = "NBIA"): From 43197fae10fccfdf6992f750398512313bbc7ada Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Sat, 16 Dec 2023 13:11:26 -0500 Subject: [PATCH 2/4] docs(auth):Add requests module and update test cases --- src/nbiatoolkit/auth.py | 96 ++++++++++++++++++++++++----------------- tests/oldtest_nbia.py | 61 -------------------------- tests/test_auth.py | 37 +++++++++------- 3 files changed, 78 insertions(+), 116 deletions(-) delete mode 100644 tests/oldtest_nbia.py diff --git a/src/nbiatoolkit/auth.py b/src/nbiatoolkit/auth.py index 4882a09..d172e49 100644 --- a/src/nbiatoolkit/auth.py +++ b/src/nbiatoolkit/auth.py @@ -1,6 +1,6 @@ import requests import time - +from typing import Union class OAuth2: """ @@ -14,12 +14,7 @@ class OAuth2: to the collections tagged with "limited access" you can use those credentials to access those collections. - Notes - ----- - This class is mainly for developers looking to add functionality - to the nbiatoolkit package. If you are a user looking to access the NBIA - API, you can use the `NBIAClient` class without knowledge of this class. - + Attributes ---------- client_id : str @@ -28,8 +23,18 @@ class OAuth2: The username for authentication. password : str The password for authentication. - access_token : str + access_token : str or None The access token retrieved from the API. + api_headers : dict or None + The authentication headers containing the access token. + expiry_time : str or None + The expiry time of the access token. + refresh_token : str or None + The refresh token for obtaining a new access token. + refresh_expiry : int or None + The expiry time of the refresh token. + scope : str or None + The scope of the access token. Methods ------- @@ -48,28 +53,44 @@ class OAuth2: To use a custom account: >>> oauth = OAuth2(username="my_username", password="my_password") + + Notes + ----- + This class is mainly for developers looking to add functionality + to the nbiatoolkit package. If you are a user looking to access the NBIA + API, you can use the `NBIAClient` class without knowledge of this class. + + As there are many packages for handling OAuth2 authentication, this class + was for myself to learn how OAuth2 works and to provide a simple way to + authenticate with the NBIA API. If you have any suggestions for improving + this class, please open an issue on the GitHub repository. """ def __init__(self, username: str = "nbia_guest", password: str = "", client_id: str = "NBIA"): """ Initialize the OAuth2 class. + Parameters ---------- - client_id : str, optional - The client ID for authentication. Default is "NBIA". username : str, optional The username for authentication. Default is "nbia_guest". password : str, optional The password for authentication. Default is an empty string. - + client_id : str, optional + The client ID for authentication. Default is "NBIA". """ self.client_id = client_id self.username = username self.password = password self.access_token = None + self.api_headers = None + self.expiry_time = None + self.refresh_token = None + self.refresh_expiry = None + self.scope = None - def getToken(self): + def getToken(self) -> Union[dict, int]: """ Retrieves the access token from the API. @@ -88,7 +109,7 @@ def getToken(self): """ # Check if the access token is valid and not expired if self.access_token is not None: - return 401 if self.access_token == 401 else self.access_token + return 401 if self.access_token == -1 else self.access_token # Prepare the request data data = { @@ -101,30 +122,27 @@ def getToken(self): response = requests.post(token_url, data=data) - try: - response.raise_for_status() - except requests.exceptions.HTTPError as e: - print(f"HTTP Error occurred: {e}") - print(f"Failed to get access token. Status code: {response.status_code}") - - self.access_token = response.status_code - return response.status_code - token_data = response.json() - self.access_token = token_data.get('access_token') - - self.api_headers = { - 'Authorization':f'Bearer {self.access_token}' - } - - self.expiry_time = time.ctime(time.time() + token_data.get('expires_in')) - self.refresh_token = token_data.get('refresh_token') - self.refresh_expiry = token_data.get('refresh_expires_in') - self.scope = token_data.get('scope') - return self.api_headers - - # def logout(self): - # # Request for logout - # # curl -X -v -d "Authorization:Bearer YOUR_ACCESS_TOKEN" -k "https://services.cancerimagingarchive.net/nbia-api/logout" - - \ No newline at end of file + try: + response = requests.post(token_url, data=data) + response.raise_for_status() # Raise an HTTPError for bad responses + except requests.exceptions.RequestException as e: + self.access_token = -1 + raise requests.exceptions.RequestException(\ + f'Failed to get access token. Status code:\ + {response.status_code}') from e + else: + # Code to execute if there is no exception + token_data = response.json() + self.access_token = token_data.get('access_token') + + self.api_headers = { + 'Authorization': f'Bearer {self.access_token}' + } + + self.expiry_time = time.ctime(time.time() + token_data.get('expires_in')) + self.refresh_token = token_data.get('refresh_token') + self.refresh_expiry = token_data.get('refresh_expires_in') + self.scope = token_data.get('scope') + + return self.api_headers diff --git a/tests/oldtest_nbia.py b/tests/oldtest_nbia.py deleted file mode 100644 index 4aa2799..0000000 --- a/tests/oldtest_nbia.py +++ /dev/null @@ -1,61 +0,0 @@ -## test_nbia.py - -import pytest -from ..nbia import NBIAClient -import pandas as pd - -@pytest.fixture(scope="session") -def nbia_client(): - nbia = NBIAClient() - return nbia - -@pytest.fixture(scope="session") -def nbia_client_bad_username(): - nbia = NBIAClient(username="bad_username", password="bad_password") - return nbia - -@pytest.fixture(scope="session") -def nbia_client_collections(nbia_client): - nbia_client.getCollections() - return nbia_client - -def test_nbiaclient_access_token(nbia_client): - assert nbia_client.access_token is not None - -def test_nbia_getCollections(nbia_client_collections): - collections = nbia_client_collections._collections - assert isinstance(collections, dict) - assert collections.shape[0] > 0 - assert collections.shape[1] == 1 - assert collections.columns[0] == 'Collection' - -def test_nbia_getCollections_again(nbia_client_collections): - collections = nbia_client_collections.getCollections() - assert isinstance(collections, pd.DataFrame) - assert collections.shape[0] > 0 - assert collections.shape[1] == 1 - assert collections.columns[0] == 'Collection' - -def test_nbia_getCollectionDescription(nbia_client_collections): - collectionName = '4D-Lung' - collectionDescription = nbia_client_collections.getCollectionDescription(collectionName) - # assert collectionDescriptions is a string and is not empty - assert isinstance(collectionDescription, str) - assert len(collectionDescription) > 0 - -def test_nbia_getCollectionDescription_bad_collection(nbia_client_collections, capsys): - collectionName = 'bad_collection_testcase' - collectionDescription = nbia_client_collections.getCollectionDescription(collectionName) - captured = capsys.readouterr() - # should be "Collection name {collectionName} does not exist." - assert captured.out == \ - f"Collection name {collectionName} is not in the list of collections available. Please check the spelling or if this collection is restricted to authenticated users.\n" - -def test_nbia_getBodyPartCounts(nbia_client): - bodyPartCounts = nbia_client.getBodyPartCounts() - assert isinstance(bodyPartCounts, pd.DataFrame) - assert bodyPartCounts.shape[0] > 0 - assert bodyPartCounts.shape[1] == 2 - assert bodyPartCounts.columns[0] == 'BodyPart' - assert bodyPartCounts.columns[1] == 'count' - diff --git a/tests/test_auth.py b/tests/test_auth.py index 8deb5dc..b99ed3d 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -6,7 +6,7 @@ import pytest from nbiatoolkit import OAuth2 import time - +import requests @pytest.fixture(scope="session") def oauth2(): @@ -17,7 +17,6 @@ def oauth2(): @pytest.fixture(scope="session") def failed_oauth2(): oauth = OAuth2(username="bad_username", password="bad_password") - oauth.getToken() return oauth def test_getToken(oauth2): @@ -29,23 +28,29 @@ def test_expiry(oauth2): print(oauth2.expiry_time) assert oauth2.expiry_time <= time.ctime(time.time() + 7200) -def test_failed_oauth(failed_oauth2,capsys): - # Answer should be Failed to get access token. Status code: 401 - # because the username and password are incorrect - # assert Status code 401 - captured = capsys.readouterr() - assert failed_oauth2.access_token == 401 +def test_failed_oauth(failed_oauth2): + # should raise requests.exceptions.RequestException + with pytest.raises(requests.exceptions.RequestException): + failed_oauth2.getToken() + assert failed_oauth2.getToken() == 401 + assert failed_oauth2.access_token == -1 + assert failed_oauth2.getToken() == 401 + # self.api_headers + assert failed_oauth2.api_headers is None + assert failed_oauth2.expiry_time is None + assert failed_oauth2.refresh_token is None + assert failed_oauth2.refresh_expiry is None + assert failed_oauth2.scope is None -def test_failed_oauth_retried(failed_oauth2,capsys): - failed_oauth2.getToken() - captured = capsys.readouterr() - assert failed_oauth2.access_token == 401 def test_getToken_valid_token(oauth2): # Test if the access token is valid and not expired assert oauth2.getToken() == oauth2.access_token + assert oauth2.getToken() != 401 + assert oauth2.api_headers is not None + assert oauth2.expiry_time is not None + assert oauth2.refresh_token is not None + assert oauth2.refresh_expiry is not None + assert oauth2.scope is not None + -def test_getToken_failed_token(failed_oauth2, capsys): - # Test if the access token retrieval fails with incorrect credentials - assert failed_oauth2.getToken() == 401 - captured = capsys.readouterr() From d4e914b6e151b666aac505159c766907f6f9e636 Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Sat, 16 Dec 2023 13:20:39 -0500 Subject: [PATCH 3/4] feat(auth): added property decorators for token and headers --- src/nbiatoolkit/auth.py | 24 ++++++++++++++++++++++++ tests/test_auth.py | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/nbiatoolkit/auth.py b/src/nbiatoolkit/auth.py index d172e49..b030a8d 100644 --- a/src/nbiatoolkit/auth.py +++ b/src/nbiatoolkit/auth.py @@ -146,3 +146,27 @@ def getToken(self) -> Union[dict, int]: self.scope = token_data.get('scope') return self.api_headers + + @property + def token(self): + """ + Returns the access token. + + Returns + ------- + access_token : str or None + The access token retrieved from the API. + """ + return self.access_token + + @property + def headers(self): + """ + Returns the API headers. + + Returns + ------- + api_headers : dict or None + The authentication headers containing the access token. + """ + return self.api_headers \ No newline at end of file diff --git a/tests/test_auth.py b/tests/test_auth.py index b99ed3d..ed1185f 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -21,6 +21,7 @@ def failed_oauth2(): def test_getToken(oauth2): assert oauth2.access_token is not None + assert oauth2.token is not None def test_expiry(oauth2): # expiry should be in the form of :'Tue Jun 29 13:58:57 2077' @@ -34,8 +35,9 @@ def test_failed_oauth(failed_oauth2): failed_oauth2.getToken() assert failed_oauth2.getToken() == 401 assert failed_oauth2.access_token == -1 + assert failed_oauth2.token == -1 assert failed_oauth2.getToken() == 401 - # self.api_headers + assert failed_oauth2.headers is None assert failed_oauth2.api_headers is None assert failed_oauth2.expiry_time is None assert failed_oauth2.refresh_token is None @@ -47,7 +49,10 @@ def test_getToken_valid_token(oauth2): # Test if the access token is valid and not expired assert oauth2.getToken() == oauth2.access_token assert oauth2.getToken() != 401 + assert oauth2.access_token != -1 + assert oauth2.token != -1 assert oauth2.api_headers is not None + assert oauth2.headers is not None assert oauth2.expiry_time is not None assert oauth2.refresh_token is not None assert oauth2.refresh_expiry is not None From 51410053189b43be20164b4a33f0a6e1bc13dcca Mon Sep 17 00:00:00 2001 From: Jermiah Joseph Date: Sat, 16 Dec 2023 13:29:54 -0500 Subject: [PATCH 4/4] fix(gha): fix CD to activate on pull too --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 05c4f76..0383990 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,7 +43,7 @@ jobs: Continuous-Development: needs: Continuous-Integration - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' # github.event_name == 'push' && runs-on: ubuntu-latest