Merge branch 'feature/WP3_first_integration' into 'master'
Developed server side API connection to Service Data (WP3) See merge request caimira/caimira!453
This commit is contained in:
commit
3d2e7f57d7
4 changed files with 171 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import typing
|
|||
import uuid
|
||||
import zlib
|
||||
|
||||
|
||||
import jinja2
|
||||
import loky
|
||||
from tornado.web import Application, RequestHandler, StaticFileHandler
|
||||
|
|
@ -29,6 +28,7 @@ import tornado.log
|
|||
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
|
||||
|
|
@ -38,12 +38,13 @@ from .user import AuthenticatedUser, AnonymousUser
|
|||
# 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."""
|
||||
|
||||
|
|
@ -104,7 +105,17 @@ class ConcentrationModel(BaseRequestHandler):
|
|||
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:
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
58
caimira/apps/calculator/data_service.py
Normal file
58
caimira/apps/calculator/data_service.py
Normal file
|
|
@ -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
caimira/tests/test_data_service.py
Normal file
87
caimira/tests/test_data_service.py
Normal file
|
|
@ -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": "test@example.com",
|
||||
"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": "test@example.com", "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)
|
||||
Loading…
Reference in a new issue