Skip to content

Commit

Permalink
refactor: implementing abstract api class
Browse files Browse the repository at this point in the history
This move the general logic from the FuturexAPIClient to a new Abstract class.
  • Loading branch information
andrey-canon committed Aug 2, 2023
1 parent 0ed13e5 commit 5ceda1e
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 109 deletions.
123 changes: 123 additions & 0 deletions eox_nelp/api_clients/__init__.py
Original file line number Diff line number Diff line change
@@ -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<str>: Public application identifier.
client_secret<str>: 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<str>: Public application identifier.
client_secret<str>: 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<str>: makes reference to the url path.
data<Dict>: 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<str>: makes reference to the url path.
payload<Dict>: 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 {}
105 changes: 6 additions & 99 deletions eox_nelp/api_clients/futurex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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<str>: Public identifier of futurex application.
client_secret<str>: 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<str>: makes reference to the url path.
data<Dict>: 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<str>: makes reference to the url path.
payload<Dict>: 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
Expand Down
20 changes: 10 additions & 10 deletions eox_nelp/api_clients/tests/tests_futurex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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, {})
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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, {})
Expand All @@ -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")
Expand Down

0 comments on commit 5ceda1e

Please sign in to comment.