Skip to content

Commit

Permalink
Merge pull request #249 from ral-facilities/feature/ping-endpoint-#241
Browse files Browse the repository at this point in the history
Ping Endpoint
  • Loading branch information
MRichards99 authored Aug 13, 2021
2 parents a393483 + c902750 commit f48b471
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 5 deletions.
9 changes: 9 additions & 0 deletions datagateway_api/common/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ class Backend(ABC):
Abstact base class for implementations of a backend to inherit from
"""

@abstractmethod
def ping(self):
"""
Endpoint requiring no authentication to check the API is alive and does a basic
check to ensure the connection method to ICAT is working
:returns: String to tell user the API is OK
"""
pass

@abstractmethod
def login(self, credentials):
"""
Expand Down
1 change: 1 addition & 0 deletions datagateway_api/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
class Constants:
PYTHON_ICAT_DISTNCT_CONDITION = "!= null"
TEST_MOD_CREATE_DATETIME = datetime(2000, 1, 1, tzinfo=tzlocal())
PING_OK_RESPONSE = "DataGateway API OK"
19 changes: 18 additions & 1 deletion datagateway_api/common/database/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
import logging
import uuid

from sqlalchemy import inspect
from sqlalchemy.exc import SQLAlchemyError

from datagateway_api.common.backend import Backend
from datagateway_api.common.constants import Constants
from datagateway_api.common.database.helpers import (
create_rows_from_json,
db,
delete_row_by_id,
get_facility_cycles_for_instrument,
get_facility_cycles_for_instrument_count,
Expand All @@ -20,7 +25,7 @@
update_row_from_id,
)
from datagateway_api.common.database.models import SESSION
from datagateway_api.common.exceptions import AuthenticationError
from datagateway_api.common.exceptions import AuthenticationError, DatabaseError
from datagateway_api.common.helpers import get_entity_object_from_name, queries_records


Expand All @@ -32,6 +37,18 @@ class DatabaseBackend(Backend):
Class that contains functions to access and modify data in an ICAT database directly
"""

def ping(self, **kwargs):
log.info("Pinging DB connection to ensure API is alive and well")

try:
inspector = inspect(db.engine)
tables = inspector.get_table_names()
log.debug("Tables on ping: %s", tables)
except SQLAlchemyError as e:
raise DatabaseError(e)

return Constants.PING_OK_RESPONSE

def login(self, credentials, **kwargs):
if credentials["username"] == "user" and credentials["password"] == "password":
session_id = str(uuid.uuid1())
Expand Down
20 changes: 16 additions & 4 deletions datagateway_api/common/icat/backend.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging

from icat.exception import ICATSessionError
from icat.exception import ICATError, ICATSessionError

from datagateway_api.common.backend import Backend
from datagateway_api.common.exceptions import AuthenticationError
from datagateway_api.common.constants import Constants
from datagateway_api.common.exceptions import AuthenticationError, PythonICATError
from datagateway_api.common.helpers import queries_records
from datagateway_api.common.icat.helpers import (
create_entities,
Expand Down Expand Up @@ -34,8 +35,19 @@ class PythonICATBackend(Backend):
Class that contains functions to access and modify data in an ICAT database directly
"""

def __init__(self):
pass
def ping(self, **kwargs):
log.info("Pinging ICAT to ensure API is alive and well")

client_pool = kwargs.get("client_pool")
client = get_cached_client(None, client_pool)

try:
entity_names = client.getEntityNames()
log.debug("Entity names on ping: %s", entity_names)
except ICATError as e:
raise PythonICATError(e)

return Constants.PING_OK_RESPONSE

def login(self, credentials, **kwargs):
log.info("Logging in to get session ID")
Expand Down
6 changes: 6 additions & 0 deletions datagateway_api/src/api_start_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
get_id_endpoint,
)
from datagateway_api.src.resources.entities.entity_endpoint_dict import endpoints
from datagateway_api.src.resources.non_entities.ping_endpoint import ping_endpoint
from datagateway_api.src.resources.non_entities.sessions_endpoints import (
session_endpoints,
)
Expand Down Expand Up @@ -161,6 +162,11 @@ def create_api_endpoints(flask_app, api, spec):
)
spec.path(resource=count_instrument_investigation_resource, api=api)

# Ping endpoint
ping_resource = ping_endpoint(backend, client_pool=icat_client_pool)
api.add_resource(ping_resource, "/ping")
spec.path(resource=ping_resource, api=api)


def openapi_config(spec):
# Reorder paths (e.g. get, patch, post) so openapi.yaml only changes when there's a
Expand Down
39 changes: 39 additions & 0 deletions datagateway_api/src/resources/non_entities/ping_endpoint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from flask_restful import Resource


def ping_endpoint(backend, **kwargs):
"""
Generate a flask_restful Resource class using the configured backend. In main.py
these generated classes are registered with the api e.g.
`api.add_resource(get_endpoint("Datafiles", DATAFILE), "/datafiles")`
:param backend: The backend instance used for processing requests
:type backend: :class:`DatabaseBackend` or :class:`PythonICATBackend`
:return: The generated ping endpoint class
"""

class Ping(Resource):
def get(self):
"""
Pings the connection method to ensure the API is responsive
:return: String: A standard OK message, 200
---
summary: Ping API connection method
description: Pings the API's connection method to check responsiveness
tags:
- Ping
responses:
200:
description: Success - the API is responsive on the backend configured
content:
application/json:
schema:
type: string
description: OK message
example: DataGateway API OK
500:
description: Pinging the API's connection method has gone wrong
"""
return backend.ping(**kwargs), 200

return Ping
17 changes: 17 additions & 0 deletions datagateway_api/src/swagger/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10996,5 +10996,22 @@ paths:
summary: Count investigations for a given Facility Cycle & Instrument
tags:
- Investigations
/ping:
get:
description: Pings the API's connection method to check responsiveness
responses:
'200':
content:
application/json:
schema:
description: OK message
example: DataGateway API OK
type: string
description: Success - the API is responsive on the backend configured
'500':
description: Pinging the API's connection method has gone wrong
summary: Ping API connection method
tags:
- Ping
security:
- session_id: []
24 changes: 24 additions & 0 deletions test/db/endpoints/test_ping_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from unittest.mock import patch

import pytest
from sqlalchemy.exc import SQLAlchemyError

from datagateway_api.common.backends import create_backend
from datagateway_api.common.constants import Constants
from datagateway_api.common.exceptions import DatabaseError


class TestICATPing:
def test_valid_ping(self, flask_test_app_db):
test_response = flask_test_app_db.get("/ping")

assert test_response.json == Constants.PING_OK_RESPONSE

def test_invalid_ping(self):
with patch(
"sqlalchemy.engine.reflection.Inspector.get_table_names",
side_effect=SQLAlchemyError("Mocked Exception"),
):
with pytest.raises(DatabaseError):
backend = create_backend("db")
backend.ping()
26 changes: 26 additions & 0 deletions test/icat/endpoints/test_ping_icat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from unittest.mock import patch

from icat.exception import ICATError
import pytest

from datagateway_api.common.backends import create_backend
from datagateway_api.common.constants import Constants
from datagateway_api.common.exceptions import PythonICATError
from datagateway_api.common.icat.icat_client_pool import create_client_pool


class TestICATPing:
def test_valid_ping(self, flask_test_app_icat):
test_response = flask_test_app_icat.get("/ping")

assert test_response.json == Constants.PING_OK_RESPONSE

def test_invalid_ping(self):
with patch(
"icat.client.Client.getEntityNames",
side_effect=ICATError("Mocked Exception"),
):
with pytest.raises(PythonICATError):
backend = create_backend("python_icat")
client_pool = create_client_pool()
backend.ping(client_pool=client_pool)
1 change: 1 addition & 0 deletions test/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class DummyBackend(Backend):
facilitycycle_id = "facilitycycle_id"
id_ = "id_"

assert d.ping() is None
assert d.login(credentials) is None
assert d.get_session_details(session_id) is None
assert d.refresh(session_id) is None
Expand Down

0 comments on commit f48b471

Please sign in to comment.