Skip to content

Commit

Permalink
Merge pull request #127 from ral-facilities/feature/configurable-back…
Browse files Browse the repository at this point in the history
…end-#125

Enable backend to be configurable
  • Loading branch information
louise-davies authored Jun 12, 2020
2 parents 016ce7b + 108256f commit e4db23b
Show file tree
Hide file tree
Showing 16 changed files with 576 additions and 199 deletions.
177 changes: 177 additions & 0 deletions common/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
from abc import ABC, abstractmethod


class Backend(ABC):
"""
Abstact base class for implementations of a backend to inherit from
"""

@abstractmethod
def login(self, credentials):
"""
Attempt to log a user in using the provided credentials
:param credentials: The user's credentials
:returns: a session ID
"""
pass

@abstractmethod
def get_session_details(self, session_id):
"""
Returns the details of a user's session
:param session_id: The user's session ID
:returns: The user's session details
"""
pass

@abstractmethod
def refresh(self, session_id):
"""
Attempts to refresh a user's session
:param session_id: The user's session ID
:returns: the user's refreshed session ID
"""
pass

@abstractmethod
def logout(self, session_id):
"""
Logs a user out
:param session_id: The user's session ID
"""
pass

@abstractmethod
def get_with_filters(self, session_id, entity_type, filters):
"""
Given a list of filters supplied in json format, returns entities that match the filters for the given entity type
:param session_id: The session id of the requesting user
:param entity_type: The type of entity
:param filters: The list of filters to be applied
:return: A list of the matching entities in json format
"""
pass

@abstractmethod
def create(self, session_id, entity_type, data):
"""
Create one or more entities, from the given list containing json. Each entity must not contain its ID
:param session_id: The session id of the requesting user
:param entity_type: The type of entity
:param data: The entities to be created
:return: The created entities.
"""
pass

@abstractmethod
def update(self, session_id, entity_type, data):
"""
Update one or more entities, from the given list containing json. Each entity must contain its ID
:param session_id: The session id of the requesting user
:param entity_type: The type of entity
:param data: the list of updated values or a dictionary
:return: The list of updated entities.
"""
pass

@abstractmethod
def get_one_with_filters(self, session_id, entity_type, filters):
"""
returns the first entity that matches a given filter, for a given entity type
:param session_id: The session id of the requesting user
:param entity_type: The type of entity
:param filters: the filter to be applied to the query
:return: the first entity matching the filter
"""
pass

@abstractmethod
def count_with_filters(self, session_id, entity_type, filters):
"""
returns the count of the entities that match a given filter for a given entity type
:param session_id: The session id of the requesting user
:param entity_type: The type of entity
:param filters: the filters to be applied to the query
:return: int: the count of the entities
"""
pass

@abstractmethod
def get_with_id(self, session_id, entity_type, id):
"""
Gets the entity matching the given ID for the given entity type
:param session_id: The session id of the requesting user
:param entity_type: The type of entity
:param id: the id of the record to find
:return: the entity retrieved
"""
pass

@abstractmethod
def delete_with_id(self, session_id, entity_type, id):
"""
Deletes the row matching the given ID for the given entity type
:param session_id: The session id of the requesting user
:param table: the table to be searched
:param id: the id of the record to delete
"""
pass

@abstractmethod
def update_with_id(self, session_id, entity_type, id, data):
"""
Updates the row matching the given ID for the given entity type
:param session_id: The session id of the requesting user
:param entity_type: The type of entity
:param data: The dictionary that the entity should be updated with
:return: The updated entity.
"""
pass

@abstractmethod
def get_instrument_facilitycycles_with_filters(self, session_id, instrument_id, filters):
"""
Given an instrument_id get facility cycles where the instrument has investigations that occur within that cycle
:param session_id: The session id of the requesting user
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:return: A list of facility cycle entities
"""
pass

@abstractmethod
def count_instrument_facilitycycles_with_filters(self, session_id, instrument_id, filters):
"""
Given an instrument_id get the facility cycles count where the instrument has investigations that occur within
that cycle
:param session_id: The session id of the requesting user
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:return: The count of the facility cycles
"""
pass

@abstractmethod
def get_instrument_facilitycycle_investigations_with_filters(self, session_id, instrument_id, facilitycycle_id, filters):
"""
Given an instrument id and facility cycle id, get investigations that use the given instrument in the given cycle
:param session_id: The session id of the requesting user
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:param facility_cycle_id: the ID of the facility cycle
:return: The investigations
"""
pass

@abstractmethod
def count_instrument_facilitycycles_investigations_with_filters(self, session_id, instrument_id, facilitycycle_id, filters):
"""
Given an instrument id and facility cycle id, get the count of the investigations that use the given instrument in
the given cycle
:param session_id: The session id of the requesting user
:param filters: The filters to be applied to the query
:param instrument_id: The id of the instrument
:param facility_cycle_id: the ID of the facility cycle
:return: The investigations count
"""
pass
13 changes: 13 additions & 0 deletions common/backends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from common.database_backend import DatabaseBackend
from common.backend import Backend
from common.config import config
import sys

backend_type = config.get_backend_type()

if backend_type == "db":
backend = DatabaseBackend()
else:
sys.exit(
f"Invalid config value '{backend_type}' for config option backend")
backend = Backend()
6 changes: 6 additions & 0 deletions common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ def __init__(self):
self.config = json.load(target)
target.close()

def get_backend_type(self):
try:
return self.config["backend"]
except:
sys.exit("Missing config value, backend")

def get_db_url(self):
try:
return self.config["DB_URL"]
Expand Down
98 changes: 98 additions & 0 deletions common/database_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from common.backend import Backend
from common.database_helpers import get_facility_cycles_for_instrument, get_facility_cycles_for_instrument_count, \
get_investigations_for_instrument_in_facility_cycle, get_investigations_for_instrument_in_facility_cycle_count, \
get_rows_by_filter, create_rows_from_json, patch_entities, get_row_by_id, insert_row_into_table, \
delete_row_by_id, update_row_from_id, get_filtered_row_count, get_first_filtered_row
from common.helpers import requires_session_id, queries_records
from common.models.db_models import SESSION
import uuid
from common.exceptions import AuthenticationError
import datetime


class DatabaseBackend(Backend):
"""
Class that contains functions to access and modify data in an ICAT database directly
"""

def login(self, credentials):
if credentials["username"] == "user" and credentials["password"] == "password":
session_id = str(uuid.uuid1())
insert_row_into_table(SESSION, SESSION(ID=session_id, USERNAME="simple/root",
EXPIREDATETIME=datetime.datetime.now() + datetime.timedelta(days=1)))
return session_id
else:
raise AuthenticationError("Username and password are incorrect")

@requires_session_id
def get_session_details(self, session_id):
return get_row_by_id(SESSION, session_id)

@requires_session_id
def refresh(self, session_id):
return session_id

@requires_session_id
@queries_records
def logout(self, session_id):
return delete_row_by_id(SESSION, session_id)

@requires_session_id
@queries_records
def get_with_filters(self, session_id, table, filters):
return get_rows_by_filter(table, filters)

@requires_session_id
@queries_records
def create(self, session_id, table, data):
return create_rows_from_json(table, data)

@requires_session_id
@queries_records
def update(self, session_id, table, data):
return patch_entities(table, data)

@requires_session_id
@queries_records
def get_one_with_filters(self, session_id, table, filters):
return get_first_filtered_row(table, filters)

@requires_session_id
@queries_records
def count_with_filters(self, session_id, table, filters):
return get_filtered_row_count(table, filters)

@requires_session_id
@queries_records
def get_with_id(self, session_id, table, id):
return get_row_by_id(table, id).to_dict()

@requires_session_id
@queries_records
def delete_with_id(self, session_id, table, id):
return delete_row_by_id(table, id)

@requires_session_id
@queries_records
def update_with_id(self, session_id, table, id, data):
return update_row_from_id(table, id, data)

@requires_session_id
@queries_records
def get_instrument_facilitycycles_with_filters(self, session_id, instrument_id, filters):
return get_facility_cycles_for_instrument(instrument_id, filters)

@requires_session_id
@queries_records
def count_instrument_facilitycycles_with_filters(self, session_id, instrument_id, filters):
return get_facility_cycles_for_instrument_count(instrument_id, filters)

@requires_session_id
@queries_records
def get_instrument_facilitycycle_investigations_with_filters(self, session_id, instrument_id, facilitycycle_id, filters):
return get_investigations_for_instrument_in_facility_cycle(instrument_id, facilitycycle_id, filters)

@requires_session_id
@queries_records
def count_instrument_facilitycycles_investigations_with_filters(self, session_id, instrument_id, facilitycycle_id, filters):
return get_investigations_for_instrument_in_facility_cycle_count(instrument_id, facilitycycle_id, filters)
26 changes: 17 additions & 9 deletions common/database_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,10 @@ def apply_filter(self, query):
if self.included_included_field:
included_table = getattr(db_models, self.field)
included_included_table = getattr(db_models, self.included_field)
query.base_query = query.base_query.join(included_table).join(included_included_table)
field = getattr(included_included_table, self.included_included_field)
query.base_query = query.base_query.join(
included_table).join(included_included_table)
field = getattr(included_included_table,
self.included_included_field)

elif self.included_field:
included_table = getattr(db_models, self.field)
Expand All @@ -188,27 +190,31 @@ def apply_filter(self, query):
if self.operation == "eq":
query.base_query = query.base_query.filter(field == self.value)
elif self.operation == "like":
query.base_query = query.base_query.filter(field.like(f"%{self.value}%"))
query.base_query = query.base_query.filter(
field.like(f"%{self.value}%"))
elif self.operation == "lte":
query.base_query = query.base_query.filter(field <= self.value)
elif self.operation == "gte":
query.base_query = query.base_query.filter(field >= self.value)
elif self.operation == "in":
query.base_query = query.base_query.filter(field.in_(self.value))
else:
raise BadFilterError(f" Bad operation given to where filter. operation: {self.operation}")
raise BadFilterError(
f" Bad operation given to where filter. operation: {self.operation}")


class DistinctFieldFilter(QueryFilter):
precedence = 0

def __init__(self, fields):
self.fields = fields if type(fields) is list else [fields] # This allows single string distinct filters
# This allows single string distinct filters
self.fields = fields if type(fields) is list else [fields]

def apply_filter(self, query):
query.is_distinct_fields_query = True
try:
self.fields = [getattr(query.table, field) for field in self.fields]
self.fields = [getattr(query.table, field)
for field in self.fields]
except AttributeError:
raise BadFilterError("Bad field requested")
query.base_query = query.session.query(*self.fields).distinct()
Expand All @@ -223,9 +229,11 @@ def __init__(self, field, direction):

def apply_filter(self, query):
if self.direction.upper() == "ASC":
query.base_query = query.base_query.order_by(asc(self.field.upper()))
query.base_query = query.base_query.order_by(
asc(self.field.upper()))
elif self.direction.upper() == "DESC":
query.base_query = query.base_query.order_by(desc(self.field.upper()))
query.base_query = query.base_query.order_by(
desc(self.field.upper()))
else:
raise BadFilterError(f" Bad filter: {self.direction}")

Expand Down Expand Up @@ -260,7 +268,7 @@ def apply_filter(self, query):
if not query.include_related_entities:
query.include_related_entities = True
else:
raise MultipleIncludeError("Attempted multiple includes on a single query")
raise MultipleIncludeError()


class QueryFilterFactory(object):
Expand Down
Loading

0 comments on commit e4db23b

Please sign in to comment.