From 5ceda1e89922eec7a126b158895a9abef8d44258 Mon Sep 17 00:00:00 2001 From: andrey-canon Date: Tue, 1 Aug 2023 19:27:29 -0500 Subject: [PATCH] refactor: implementing abstract api class This move the general logic from the FuturexAPIClient to a new Abstract class. --- eox_nelp/api_clients/__init__.py | 123 ++++++++++++++++++++ eox_nelp/api_clients/futurex.py | 105 +---------------- eox_nelp/api_clients/tests/tests_futurex.py | 20 ++-- 3 files changed, 139 insertions(+), 109 deletions(-) diff --git a/eox_nelp/api_clients/__init__.py b/eox_nelp/api_clients/__init__.py index e69de29b..e845964a 100644 --- a/eox_nelp/api_clients/__init__.py +++ b/eox_nelp/api_clients/__init__.py @@ -0,0 +1,123 @@ +"""This file contains the common functions and classes for the api_clients module. + +Classes: + AbstractApiClient: Base API class. +""" +import logging +from abc import ABC, abstractmethod + +import requests +from django.core.cache import cache +from oauthlib.oauth2 import BackendApplicationClient +from requests_oauthlib import OAuth2Session +from rest_framework import status + +LOGGER = logging.getLogger(__name__) + + +class AbstractApiClient(ABC): + """Abstract api client class, this implement a basic authentication method and defines methods POST and GET""" + + @property + @abstractmethod + def base_url(self): + """Abstract base_url property method.""" + raise NotImplementedError + + def __init__(self, client_id, client_secret): + """ + Abstract ApiClient creator, this will set the session based on the authenticate result. + + Args: + client_id: Public application identifier. + client_secret: Confidential identifier. + """ + key = f"{client_id}-{client_secret}" + headers = cache.get(key) + + if not headers: + response = self._authenticate(client_id, client_secret) + headers = { + "Authorization": f"{response.get('token_type')} {response.get('access_token')}" + } + + cache.set(key, headers, response.get("expires_in", 300)) + + self.session = requests.Session() + self.session.headers.update(headers) + + def _authenticate(self, client_id, client_secret): + """This method uses the session attribute to perform a POS request based on the + base_url attribute and the given path, if the response has a status code 200 + this will return the json from that response otherwise this will return an empty dictionary. + + Args: + client_id: Public application identifier. + client_secret: Confidential identifier. + + Return: + Dictionary: Response from authentication request. + """ + client = BackendApplicationClient(client_id=client_id) + oauth = OAuth2Session(client_id=client_id, client=client) + authenticate_url = f"{self.base_url}/oauth/token" + + return oauth.fetch_token( + token_url=authenticate_url, + client_secret=client_secret, + include_client_id=True, + ) + + def make_post(self, path, data): + """This method uses the session attribute to perform a POST request based on the + base_url attribute and the given path, if the response has a status code 200 + this will return the json from that response otherwise this will return an empty dictionary. + + Args: + path: makes reference to the url path. + data: request body as dictionary. + + Return: + Dictionary: Empty dictionary or json response. + """ + url = f"{self.base_url}/{path}" + + response = self.session.post(url=url, json=data) + + if response.status_code == status.HTTP_200_OK: + return response.json() + + LOGGER.error( + "An error has occurred trying to make post request to %s with status code %s", + url, + response.status_code, + ) + + return {} + + def make_get(self, path, payload): + """This method uses the session attribute to perform a GET request based on the + base_url attribute and the given path, if the response has a status code 200 + this will return the json from that response otherwise this will return an empty dictionary. + + Args: + path: makes reference to the url path. + payload: queryparams as dictionary. + + Return: + Dictionary: Empty dictionary or json response. + """ + url = f"{self.base_url}/{path}" + + response = self.session.get(url=url, params=payload) + + if response.status_code == status.HTTP_200_OK: + return response.json() + + LOGGER.error( + "An error has occurred trying to make a get request to %s with status code %s", + url, + response.status_code, + ) + + return {} diff --git a/eox_nelp/api_clients/futurex.py b/eox_nelp/api_clients/futurex.py index a0aac9fb..a1cb6a1c 100644 --- a/eox_nelp/api_clients/futurex.py +++ b/eox_nelp/api_clients/futurex.py @@ -4,19 +4,12 @@ FuturexApiClient: Base class to interact with Futurex services. FuturexMissingArguments: Exception used for indicate that some required arguments are missing. """ -import logging - -import requests from django.conf import settings -from django.core.cache import cache -from oauthlib.oauth2 import BackendApplicationClient -from requests_oauthlib import OAuth2Session -from rest_framework import status -LOGGER = logging.getLogger(__name__) +from eox_nelp.api_clients import AbstractApiClient -class FuturexApiClient: +class FuturexApiClient(AbstractApiClient): """Allow to perform multiple Futurex API operations based on an authenticated session. Attributes: @@ -25,100 +18,14 @@ class FuturexApiClient: """ def __init__(self): - """FuturexApiClient creator, this will set the session based on the authenticate result""" - self.base_url = getattr(settings, "FUTUREX_API_URL") client_id = getattr(settings, "FUTUREX_API_CLIENT_ID") client_secret = getattr(settings, "FUTUREX_API_CLIENT_SECRET") - key = f"{client_id}-{client_secret}" - headers = cache.get(key) - - if not headers: - response = self._authenticate(client_id, client_secret) - headers = { - "Authorization": f"{response.get('token_type')} {response.get('access_token')}" - } - - cache.set(key, headers, response.get("expires_in", 300)) - - self.session = requests.Session() - self.session.headers.update(headers) - - def _authenticate(self, client_id, client_secret): - """This method uses the session attribute to perform a POS request based on the - base_url attribute and the given path, if the response has a status code 200 - this will return the json from that response otherwise this will return an empty dictionary. - - Args: - client_id: Public identifier of futurex application. - client_secret: Confidential identifier used to authenticate against Futurex. - - Return: - Dictionary: Response from authentication request. - """ - client = BackendApplicationClient(client_id=client_id) - oauth = OAuth2Session(client_id=client_id, client=client) - authenticate_url = f"{self.base_url}/oauth/token" - - return oauth.fetch_token( - token_url=authenticate_url, - client_secret=client_secret, - include_client_id=True, - ) - - def make_post(self, path, data): - """This method uses the session attribute to perform a POS request based on the - base_url attribute and the given path, if the response has a status code 200 - this will return the json from that response otherwise this will return an empty dictionary. - - Args: - path: makes reference to the url path. - data: request body as dictionary. - - Return: - Dictionary: Empty dictionary or json response. - """ - url = f"{self.base_url}/{path}" - - response = self.session.post(url=url, json=data) - - if response.status_code == status.HTTP_200_OK: - return response.json() - - LOGGER.error( - "An error has occurred trying to make post request to %s with status code %s", - url, - response.status_code, - ) - - return {} - - def make_get(self, path, payload): - """This method uses the session attribute to perform a GET request based on the - base_url attribute and the given path, if the response has a status code 200 - this will return the json from that response otherwise this will return an empty dictionary. - - Args: - path: makes reference to the url path. - payload: queryparams as dictionary. - - Return: - Dictionary: Empty dictionary or json response. - """ - url = f"{self.base_url}/{path}" - - response = self.session.get(url=url, params=payload) - - if response.status_code == status.HTTP_200_OK: - return response.json() - - LOGGER.error( - "An error has occurred trying to make a get request to %s with status code %s", - url, - response.status_code, - ) + super().__init__(client_id, client_secret) - return {} + @property + def base_url(self): + return getattr(settings, "FUTUREX_API_URL") def enrollment_progress(self, enrollment_data): """Push the user progress across for a course. This data will affect the learner profile diff --git a/eox_nelp/api_clients/tests/tests_futurex.py b/eox_nelp/api_clients/tests/tests_futurex.py index 7d56c0ab..dedeaaa5 100644 --- a/eox_nelp/api_clients/tests/tests_futurex.py +++ b/eox_nelp/api_clients/tests/tests_futurex.py @@ -9,7 +9,7 @@ from mock import Mock, patch from oauthlib.oauth2 import MissingTokenError -from eox_nelp.api_clients import futurex +from eox_nelp import api_clients from eox_nelp.api_clients.futurex import FuturexApiClient, FuturexMissingArguments @@ -27,7 +27,7 @@ def test_failed_authentication(self): """ self.assertRaises(MissingTokenError, FuturexApiClient) - @patch("eox_nelp.api_clients.futurex.OAuth2Session") + @patch("eox_nelp.api_clients.OAuth2Session") def test_successful_authentication(self, oauth2_session_mock): """Test case when the authentication response is valid. @@ -56,7 +56,7 @@ def test_successful_authentication(self, oauth2_session_mock): include_client_id=True, ) - @patch("eox_nelp.api_clients.futurex.requests") + @patch("eox_nelp.api_clients.requests") @patch.object(FuturexApiClient, "_authenticate") def test_successful_post(self, auth_mock, requests_mock): """Test case when a POST request success. @@ -84,7 +84,7 @@ def test_successful_post(self, auth_mock, requests_mock): json=data, ) - @patch("eox_nelp.api_clients.futurex.requests") + @patch("eox_nelp.api_clients.requests") @patch.object(FuturexApiClient, "_authenticate") def test_failed_post(self, auth_mock, requests_mock): """Test case when a POST request fails. @@ -105,7 +105,7 @@ def test_failed_post(self, auth_mock, requests_mock): ) api_client = FuturexApiClient() - with self.assertLogs(futurex.__name__, level="ERROR") as logs: + with self.assertLogs(api_clients.__name__, level="ERROR") as logs: response = api_client.make_post("fake/path", data) self.assertDictEqual(response, {}) @@ -114,10 +114,10 @@ def test_failed_post(self, auth_mock, requests_mock): json=data, ) self.assertEqual(logs.output, [ - f"ERROR:{futurex.__name__}:{log_error}" + f"ERROR:{api_clients.__name__}:{log_error}" ]) - @patch("eox_nelp.api_clients.futurex.requests") + @patch("eox_nelp.api_clients.requests") @patch.object(FuturexApiClient, "_authenticate") def test_successful_get(self, auth_mock, requests_mock): """Test case when a GET request success. @@ -145,7 +145,7 @@ def test_successful_get(self, auth_mock, requests_mock): params=params, ) - @patch("eox_nelp.api_clients.futurex.requests") + @patch("eox_nelp.api_clients.requests") @patch.object(FuturexApiClient, "_authenticate") def test_failed_get(self, auth_mock, requests_mock): """Test case when a GET request fails. @@ -166,7 +166,7 @@ def test_failed_get(self, auth_mock, requests_mock): ) api_client = FuturexApiClient() - with self.assertLogs(futurex.__name__, level="ERROR") as logs: + with self.assertLogs(api_clients.__name__, level="ERROR") as logs: response = api_client.make_get("fake/path", params) self.assertDictEqual(response, {}) @@ -175,7 +175,7 @@ def test_failed_get(self, auth_mock, requests_mock): params=params, ) self.assertEqual(logs.output, [ - f"ERROR:{futurex.__name__}:{log_error}" + f"ERROR:{api_clients.__name__}:{log_error}" ]) @patch.object(FuturexApiClient, "make_post")