Skip to content

Commit

Permalink
Merge branch 'feature/WP3_first_integration' into 'master'
Browse files Browse the repository at this point in the history
Developed server side API connection to Service Data (WP3)

See merge request caimira/caimira!453
  • Loading branch information
lrdossan committed Jul 18, 2023
2 parents a59463c + 4d00f17 commit ad9001f
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 4 deletions.
3 changes: 3 additions & 0 deletions app-config/calculator-app/app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ if [[ "$APP_NAME" == "calculator-app" ]]; then

export "EXTRA_PAGES"="$EXTRA_PAGES"

export "DATA_SERVICE_CLIENT_EMAIL"="$DATA_SERVICE_CLIENT_EMAIL"
export "DATA_SERVICE_CLIENT_PASSWORD"="$DATA_SERVICE_CLIENT_PASSWORD"

echo "Starting the caimira webservice with: python -m caimira.apps.calculator ${args[@]}"
python -m caimira.apps.calculator "${args[@]}"
elif [[ "$APP_NAME" == "caimira-voila" ]]; then
Expand Down
27 changes: 23 additions & 4 deletions caimira/apps/calculator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import uuid
import zlib


import jinja2
import loky
from tornado.web import Application, RequestHandler, StaticFileHandler
Expand All @@ -29,6 +28,7 @@
from . import markdown_tools
from . import model_generator
from .report_generator import ReportGenerator, calculate_report_data
from .data_service import DataService
from .user import AuthenticatedUser, AnonymousUser

# The calculator version is based on a combination of the model version and the
Expand All @@ -38,12 +38,13 @@
# calculator version. If the calculator needs to make breaking changes (e.g. change
# form attributes) then it can also increase its MAJOR version without needing to
# increase the overall CAiMIRA version (found at ``caimira.__version__``).
__version__ = "4.11"
__version__ = "4.12"

LOG = logging.getLogger(__name__)


class BaseRequestHandler(RequestHandler):

async def prepare(self):
"""Called at the beginning of a request before `get`/`post`/etc."""

Expand Down Expand Up @@ -104,7 +105,17 @@ async def post(self) -> None:
from pprint import pprint
pprint(requested_model_config)
start = datetime.datetime.now()


# Data Service API Integration
data_service: DataService = self.settings["data_service"]
try:
access_token = await data_service.login()
service_data = await data_service.fetch(access_token)
except Exception as err:
error_message = f"Something went wrong with the data service: {str(err)}"
LOG.error(error_message, exc_info=True)
self.send_error(500, reason=error_message)

try:
form = model_generator.FormData.from_dict(requested_model_config)
except Exception as err:
Expand Down Expand Up @@ -417,6 +428,11 @@ def make_app(
)
template_environment.globals['get_url']=get_root_url
template_environment.globals['get_calculator_url']=get_root_calculator_url

data_service_credentials = {
'data_service_client_email': os.environ.get('DATA_SERVICE_CLIENT_EMAIL', None),
'data_service_client_password': os.environ.get('DATA_SERVICE_CLIENT_PASSWORD', None),
}

if debug:
tornado.log.enable_pretty_logging()
Expand All @@ -435,6 +451,9 @@ def make_app(
arve_client_secret=os.environ.get('ARVE_CLIENT_SECRET', None),
arve_api_key=os.environ.get('ARVE_API_KEY', None),

# Data Service Integration
data_service = DataService(data_service_credentials),

# Process parallelism controls. There is a balance between serving a single report
# requests quickly or serving multiple requests concurrently.
# The defaults are: handle one report at a time, and allow parallelism
Expand Down
58 changes: 58 additions & 0 deletions caimira/apps/calculator/data_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import dataclasses
import json
import logging

from tornado.httpclient import AsyncHTTPClient, HTTPRequest

LOG = logging.getLogger(__name__)


@dataclasses.dataclass
class DataService():
'''
Responsible for establishing a connection to a
database through a REST API by handling authentication
and fetching data. It utilizes the Tornado web framework
for asynchronous HTTP requests.
'''
# Credentials used for authentication
credentials: dict

# Host URL for the CAiMIRA Data Service API
host: str = 'https://caimira-data-api.app.cern.ch'

async def login(self):
client_email = self.credentials["data_service_client_email"]
client_password = self.credentials['data_service_client_password']

if (client_email == None or client_password == None):
# If the credentials are not defined, an exception is raised.
raise Exception("DataService credentials not set")

http_client = AsyncHTTPClient()
headers = {'Content-type': 'application/json'}
json_body = { "email": f"{client_email}", "password": f"{client_password}"}

response = await http_client.fetch(HTTPRequest(
url=self.host + '/login',
method='POST',
headers=headers,
body=json.dumps(json_body),
),
raise_error=True)

return json.loads(response.body)['access_token']

async def fetch(self, access_token: str):
http_client = AsyncHTTPClient()
headers = {'Authorization': f'Bearer {access_token}'}

response = await http_client.fetch(HTTPRequest(
url=self.host + '/data',
method='GET',
headers=headers,
),
raise_error=True)

return json.loads(response.body)

87 changes: 87 additions & 0 deletions caimira/tests/test_data_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from dataclasses import dataclass

import unittest
from unittest.mock import patch, MagicMock
from tornado.httpclient import HTTPError

from caimira.apps.calculator.data_service import DataService

@dataclass
class MockResponse:
body: str

class DataServiceTests(unittest.TestCase):
def setUp(self):
# Set up any necessary test data or configurations
self.credentials = {
"data_service_client_email": "[email protected]",
"data_service_client_password": "password123"
}
self.data_service = DataService(self.credentials)

@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_login_successful(self, mock_http_client):
# Mock successful login response
mock_response = MockResponse('{"access_token": "dummy_token"}')
mock_fetch = MagicMock(return_value=mock_response)
mock_http_client.return_value.fetch = mock_fetch

# Call the login method
access_token = await self.data_service.login()

# Assert that the access token is returned correctly
self.assertEqual(access_token, "dummy_token")

# Verify that the fetch method was called with the expected arguments
mock_fetch.assert_called_once_with(
url='https://caimira-data-api.app.cern.ch/login',
method='POST',
headers={'Content-type': 'application/json'},
body='{"email": "[email protected]", "password": "password123"}'
)

@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_login_error(self, mock_http_client):
# Mock login error response
mock_fetch = MagicMock(side_effect=HTTPError(500))
mock_http_client.return_value.fetch = mock_fetch

# Call the login method
access_token = await self.data_service.login()

# Assert that the login method returns None in case of an error
self.assertIsNone(access_token)

@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_fetch_successful(self, mock_http_client):
# Mock successful fetch response
mock_response = MockResponse('{"data": "dummy_data"}')
mock_fetch = MagicMock(return_value=mock_response)
mock_http_client.return_value.fetch = mock_fetch

# Call the fetch method with a mock access token
access_token = "dummy_token"
data = await self.data_service.fetch(access_token)

# Assert that the data is returned correctly
self.assertEqual(data, {"data": "dummy_data"})

# Verify that the fetch method was called with the expected arguments
mock_fetch.assert_called_once_with(
url='https://caimira-data-api.app.cern.ch/data',
method='GET',
headers={'Authorization': 'Bearer dummy_token'}
)

@patch('caimira.apps.calculator.data_service.AsyncHTTPClient')
async def test_fetch_error(self, mock_http_client):
# Mock fetch error response
mock_fetch = MagicMock(side_effect=HTTPError(404))
mock_http_client.return_value.fetch = mock_fetch

# Call the fetch method with a mock access token
access_token = "dummy_token"
data = await self.data_service.fetch(access_token)

# Assert that the fetch method returns None in case of an error
self.assertIsNone(data)

0 comments on commit ad9001f

Please sign in to comment.