data service: add toggle env var to make it optional

This commit is contained in:
Luis Aleixo 2023-07-19 11:35:47 +02:00
parent 3d2e7f57d7
commit e732fb48d7
7 changed files with 94 additions and 26 deletions

View file

@ -168,7 +168,6 @@ export COOKIE_SECRET=$(openssl rand -hex 50)
export OIDC_SERVER=https://auth.cern.ch/auth export OIDC_SERVER=https://auth.cern.ch/auth
export OIDC_REALM=CERN export OIDC_REALM=CERN
export CLIENT_ID=caimira-test export CLIENT_ID=caimira-test
export CLIENT_SECRET
``` ```
Run docker-compose: Run docker-compose:
@ -324,6 +323,38 @@ Please note that there is no need for keys on this API call. It is **free-of-cha
- **Humidity and Inside Temperature:** - **Humidity and Inside Temperature:**
There is the possibility of using one external API call to fetch information related to a location specified in the UI. The data is related to the inside temperature and humidity taken from an indoor measurement device. Note that the API currently used from ARVE is only available for the `CERN theme` as the authorised sensors are installed at CERN." There is the possibility of using one external API call to fetch information related to a location specified in the UI. The data is related to the inside temperature and humidity taken from an indoor measurement device. Note that the API currently used from ARVE is only available for the `CERN theme` as the authorised sensors are installed at CERN."
- **ARVE:**
The ARVE Swiss Air Quality System provides trusted air data for commercial buildings in real-time and analyzes it with the help of AI and machine learning algorithms to create actionable insights.
Create secret:
```console
$ read ARVE_CLIENT_ID
$ read ARVE_CLIENT_SECRET
$ read ARVE_API_KEY
$ oc create secret generic \
--from-literal="ARVE_CLIENT_ID=$ARVE_CLIENT_ID" \
--from-literal="ARVE_CLIENT_SECRET=$ARVE_CLIENT_SECRET" \
--from-literal="ARVE_API_KEY=$ARVE_API_KEY" \
arve-api
```
- **CERN Data Service:**
The CERN data service collects data from various sources and expose them via a REST API endpoint.
Create secret:
```console
$ read DATA_SERVICE_CLIENT_EMAIL
$ read DATA_SERVICE_CLIENT_PASSWORD
$ oc create secret generic \
--from-literal="DATA_SERVICE_CLIENT_EMAIL=$DATA_SERVICE_CLIENT_EMAIL" \
--from-literal="DATA_SERVICE_CLIENT_PASSWORD=$DATA_SERVICE_CLIENT_PASSWORD" \
data-service-api
```
## Update configuration ## Update configuration
If you need to **update** existing configuration, then modify this repository and after having logged in, run: If you need to **update** existing configuration, then modify this repository and after having logged in, run:

View file

@ -23,6 +23,7 @@ if [[ "$APP_NAME" == "calculator-app" ]]; then
export "EXTRA_PAGES"="$EXTRA_PAGES" export "EXTRA_PAGES"="$EXTRA_PAGES"
export "DATA_SERVICE_ENABLED"="${DATA_SERVICE_ENABLED:=False}"
export "DATA_SERVICE_CLIENT_EMAIL"="$DATA_SERVICE_CLIENT_EMAIL" export "DATA_SERVICE_CLIENT_EMAIL"="$DATA_SERVICE_CLIENT_EMAIL"
export "DATA_SERVICE_CLIENT_PASSWORD"="$DATA_SERVICE_CLIENT_PASSWORD" export "DATA_SERVICE_CLIENT_PASSWORD"="$DATA_SERVICE_CLIENT_PASSWORD"

View file

@ -20,6 +20,7 @@ services:
- APPLICATION_ROOT=/ - APPLICATION_ROOT=/
- CAIMIRA_CALCULATOR_PREFIX=/calculator-cern - CAIMIRA_CALCULATOR_PREFIX=/calculator-cern
- CAIMIRA_THEME=caimira/apps/templates/cern - CAIMIRA_THEME=caimira/apps/templates/cern
- DATA_SERVICE_ENABLED=False
user: ${CURRENT_UID} user: ${CURRENT_UID}
calculator-open-app: calculator-open-app:
@ -29,6 +30,7 @@ services:
- APP_NAME=calculator-app - APP_NAME=calculator-app
- APPLICATION_ROOT=/ - APPLICATION_ROOT=/
- CAIMIRA_CALCULATOR_PREFIX=/calculator-open - CAIMIRA_CALCULATOR_PREFIX=/calculator-open
- DATA_SERVICE_ENABLED=False
user: ${CURRENT_UID} user: ${CURRENT_UID}
auth-service: auth-service:

View file

@ -283,6 +283,18 @@
secretKeyRef: secretKeyRef:
key: ARVE_API_KEY key: ARVE_API_KEY
name: arve-api name: arve-api
- name: DATA_SERVICE_ENABLED
value: False
- name: DATA_SERVICE_CLIENT_EMAIL
valueFrom:
secretKeyRef:
key: DATA_SERVICE_CLIENT_EMAIL
name: data-service-api
- name: DATA_SERVICE_CLIENT_PASSWORD
valueFrom:
secretKeyRef:
key: DATA_SERVICE_CLIENT_PASSWORD
name: data-service-api
image: '${PROJECT_NAME}/calculator-app' image: '${PROJECT_NAME}/calculator-app'
ports: ports:
- containerPort: 8080 - containerPort: 8080
@ -360,6 +372,8 @@
value: / value: /
- name: CAIMIRA_CALCULATOR_PREFIX - name: CAIMIRA_CALCULATOR_PREFIX
value: /calculator-open value: /calculator-open
- name: DATA_SERVICE_ENABLED
value: False
image: '${PROJECT_NAME}/calculator-app' image: '${PROJECT_NAME}/calculator-app'
ports: ports:
- containerPort: 8080 - containerPort: 8080

View file

@ -107,15 +107,16 @@ class ConcentrationModel(BaseRequestHandler):
start = datetime.datetime.now() start = datetime.datetime.now()
# Data Service API Integration # Data Service API Integration
fetched_service_data = None
data_service: DataService = self.settings["data_service"] data_service: DataService = self.settings["data_service"]
try: if self.settings["data_service"]:
access_token = await data_service.login() try:
service_data = await data_service.fetch(access_token) fetched_service_data = await data_service.fetch()
except Exception as err: except Exception as err:
error_message = f"Something went wrong with the data service: {str(err)}" error_message = f"Something went wrong with the data service: {str(err)}"
LOG.error(error_message, exc_info=True) LOG.error(error_message, exc_info=True)
self.send_error(500, reason=error_message) self.send_error(500, reason=error_message)
try: try:
form = model_generator.FormData.from_dict(requested_model_config) form = model_generator.FormData.from_dict(requested_model_config)
except Exception as err: except Exception as err:
@ -134,7 +135,7 @@ class ConcentrationModel(BaseRequestHandler):
timeout=300, timeout=300,
) )
# Re-generate the report with the conditional probability of infection plot # Re-generate the report with the conditional probability of infection plot
if self.get_cookie('conditional_plot'): if self.get_cookie('conditional_plot'):
form.conditional_probability_plot = True if self.get_cookie('conditional_plot') == '1' else False form.conditional_probability_plot = True if self.get_cookie('conditional_plot') == '1' else False
self.clear_cookie('conditional_plot') # Clears cookie after changing the form value. self.clear_cookie('conditional_plot') # Clears cookie after changing the form value.
@ -260,7 +261,7 @@ class ArveData(BaseRequestHandler):
http_client = AsyncHTTPClient() http_client = AsyncHTTPClient()
URL = 'https://arveapi.auth.eu-central-1.amazoncognito.com/oauth2/token' URL = 'https://arveapi.auth.eu-central-1.amazoncognito.com/oauth2/token'
headers = { "Content-Type": "application/x-www-form-urlencoded", headers = { "Content-Type": "application/x-www-form-urlencoded",
"Authorization": b"Basic " + base64.b64encode(f'{client_id}:{client_secret}'.encode()) "Authorization": b"Basic " + base64.b64encode(f'{client_id}:{client_secret}'.encode())
} }
@ -390,7 +391,7 @@ def make_app(
'active_page': 'about', 'active_page': 'about',
'filename': 'about.html.j2'}), 'filename': 'about.html.j2'}),
(get_root_calculator_url(r'/user-guide'), GenericExtraPage, { (get_root_calculator_url(r'/user-guide'), GenericExtraPage, {
'active_page': 'calculator/user-guide', 'active_page': 'calculator/user-guide',
'filename': 'userguide.html.j2'}), 'filename': 'userguide.html.j2'}),
] ]
@ -407,7 +408,7 @@ def make_app(
pass pass
for extra in pages: for extra in pages:
urls.append((get_root_url(r'%s' % extra['url_path']), urls.append((get_root_url(r'%s' % extra['url_path']),
GenericExtraPage, { GenericExtraPage, {
'active_page': extra['url_path'].strip('/'), 'active_page': extra['url_path'].strip('/'),
'filename': extra['filename'], })) 'filename': extra['filename'], }))
@ -433,6 +434,9 @@ def make_app(
'data_service_client_email': os.environ.get('DATA_SERVICE_CLIENT_EMAIL', None), 'data_service_client_email': os.environ.get('DATA_SERVICE_CLIENT_EMAIL', None),
'data_service_client_password': os.environ.get('DATA_SERVICE_CLIENT_PASSWORD', None), 'data_service_client_password': os.environ.get('DATA_SERVICE_CLIENT_PASSWORD', None),
} }
data_service = None
if bool(os.environ.get('DATA_SERVICE_ENABLED', False)):
data_service = DataService(data_service_credentials)
if debug: if debug:
tornado.log.enable_pretty_logging() tornado.log.enable_pretty_logging()
@ -452,7 +456,7 @@ def make_app(
arve_api_key=os.environ.get('ARVE_API_KEY', None), arve_api_key=os.environ.get('ARVE_API_KEY', None),
# Data Service Integration # Data Service Integration
data_service = DataService(data_service_credentials), data_service=data_service,
# Process parallelism controls. There is a balance between serving a single report # Process parallelism controls. There is a balance between serving a single report
# requests quickly or serving multiple requests concurrently. # requests quickly or serving multiple requests concurrently.

View file

@ -1,6 +1,7 @@
import dataclasses import dataclasses
import json import json
import logging import logging
import typing
from tornado.httpclient import AsyncHTTPClient, HTTPRequest from tornado.httpclient import AsyncHTTPClient, HTTPRequest
@ -10,8 +11,8 @@ LOG = logging.getLogger(__name__)
@dataclasses.dataclass @dataclasses.dataclass
class DataService(): class DataService():
''' '''
Responsible for establishing a connection to a Responsible for establishing a connection to a
database through a REST API by handling authentication database through a REST API by handling authentication
and fetching data. It utilizes the Tornado web framework and fetching data. It utilizes the Tornado web framework
for asynchronous HTTP requests. for asynchronous HTTP requests.
''' '''
@ -20,8 +21,20 @@ class DataService():
# Host URL for the CAiMIRA Data Service API # Host URL for the CAiMIRA Data Service API
host: str = 'https://caimira-data-api.app.cern.ch' host: str = 'https://caimira-data-api.app.cern.ch'
async def login(self): # Cached access token
_access_token: typing.Optional[str] = None
def _is_valid(self, access_token):
# decode access_token
# check validity
return False
async def _login(self):
if self._is_valid(self._access_token):
return self._access_token
# invalid access_token, fetch it again
client_email = self.credentials["data_service_client_email"] client_email = self.credentials["data_service_client_email"]
client_password = self.credentials['data_service_client_password'] client_password = self.credentials['data_service_client_password']
@ -41,9 +54,12 @@ class DataService():
), ),
raise_error=True) raise_error=True)
return json.loads(response.body)['access_token'] self._access_token = json.loads(response.body)['access_token']
return self._access_token
async def fetch(self, access_token: str):
async def fetch(self):
access_token = await self._login()
http_client = AsyncHTTPClient() http_client = AsyncHTTPClient()
headers = {'Authorization': f'Bearer {access_token}'} headers = {'Authorization': f'Bearer {access_token}'}

View file

@ -27,7 +27,7 @@ class DataServiceTests(unittest.TestCase):
mock_http_client.return_value.fetch = mock_fetch mock_http_client.return_value.fetch = mock_fetch
# Call the login method # Call the login method
access_token = await self.data_service.login() access_token = await self.data_service._login()
# Assert that the access token is returned correctly # Assert that the access token is returned correctly
self.assertEqual(access_token, "dummy_token") self.assertEqual(access_token, "dummy_token")
@ -60,8 +60,8 @@ class DataServiceTests(unittest.TestCase):
mock_http_client.return_value.fetch = mock_fetch mock_http_client.return_value.fetch = mock_fetch
# Call the fetch method with a mock access token # Call the fetch method with a mock access token
access_token = "dummy_token" self.data_service._access_token = "dummy_token"
data = await self.data_service.fetch(access_token) data = await self.data_service.fetch()
# Assert that the data is returned correctly # Assert that the data is returned correctly
self.assertEqual(data, {"data": "dummy_data"}) self.assertEqual(data, {"data": "dummy_data"})
@ -80,8 +80,8 @@ class DataServiceTests(unittest.TestCase):
mock_http_client.return_value.fetch = mock_fetch mock_http_client.return_value.fetch = mock_fetch
# Call the fetch method with a mock access token # Call the fetch method with a mock access token
access_token = "dummy_token" self.data_service._access_token = "dummy_token"
data = await self.data_service.fetch(access_token) data = await self.data_service.fetch()
# Assert that the fetch method returns None in case of an error # Assert that the fetch method returns None in case of an error
self.assertIsNone(data) self.assertIsNone(data)