Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ per-file-ignores =
datagateway_api/datagateway_api/icat/models.py: N815,A003
datagateway_api/datagateway_api/icat/filters.py: C901
datagateway_api/search_api/models.py: B950
datagateway_api/read_only_api/models/*: N815
enable-extensions=G
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.venv
venv/
.idea/
*.pyc
Expand Down
45 changes: 32 additions & 13 deletions datagateway_api/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pydantic import (
AfterValidator,
BaseModel,
PositiveInt,
computed_field,
Field,
model_validator,
Expand Down Expand Up @@ -45,11 +46,10 @@ def validate_extension(extension):
DataGatewayAPIExtension = Annotated[StrictStr, AfterValidator(validate_extension)]


class UseReaderForPerformance(BaseModel):
enabled: StrictBool
reader_mechanism: StrictStr
reader_username: StrictStr
reader_password: SecretStr
class ReaderConfig(BaseModel):
mechanism: StrictStr
username: StrictStr
password: SecretStr
maxsize: int = Field(
default=128,
description="Each cacheable function will store up to this many results in memory.",
Expand All @@ -60,24 +60,43 @@ class UseReaderForPerformance(BaseModel):
)


class IcatConfig(BaseModel):
url: StrictStr
check_cert: StrictBool
client_cache_size: StrictInt
client_pool_init_size: StrictInt
client_pool_max_size: StrictInt
reader: ReaderConfig | None = None


class DataGatewayAPI(BaseModel):
"""
Configuration model class that implements pydantic's BaseModel class to allow for
validation of the DataGatewayAPI config data using Python type annotations.
"""

client_cache_size: StrictInt
client_pool_init_size: StrictInt
client_pool_max_size: StrictInt
extension: DataGatewayAPIExtension
icat_check_cert: StrictBool
icat_url: StrictStr
use_reader_for_performance: Optional[UseReaderForPerformance] = None

def __getitem__(self, item):
return getattr(self, item)


class LimitConfig(BaseModel):
default: PositiveInt = 100
maximum: PositiveInt = 100

@model_validator(mode="after")
def _validate(self) -> Self:
if self.default > self.maximum:
raise ValueError("default limit cannot exceed maximum limit")

return self


class ReadOnlyAPI(DataGatewayAPI):
limit: LimitConfig = LimitConfig()


class SearchScoring(BaseModel):
enabled: StrictBool
api_url: StrictStr
Expand All @@ -93,8 +112,6 @@ class SearchAPI(BaseModel):
"""

extension: DataGatewayAPIExtension
icat_check_cert: StrictBool
icat_url: StrictStr
mechanism: StrictStr
username: StrictStr
password: StrictStr
Expand Down Expand Up @@ -130,7 +147,9 @@ class APIConfig(BaseModel):
API startup so any missing options will be caught quickly.
"""

icat: IcatConfig
datagateway_api: Optional[DataGatewayAPI] = None
read_only_api: ReadOnlyAPI | None = None
reload: Optional[StrictBool] = None
host: Optional[StrictStr] = None
port: Optional[StrictInt] = None
Expand Down
22 changes: 11 additions & 11 deletions datagateway_api/config.yaml.example
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
---
datagateway_api:
extension: "/datagateway-api"
icat:
url: "http://icat_payara_container:8080"
check_cert: false
client_cache_size: 5
client_pool_init_size: 2
client_pool_max_size: 5
icat_url: "http://icat_payara_container:8080"
icat_check_cert: false
use_reader_for_performance:
enabled: false
reader_mechanism: simple
reader_username: reader
reader_password: readerpw
# reader:
# mechanism: simple
# username: reader
# password: readerpw
datagateway_api:
extension: "/datagateway-api"
read_only_api:
extension: "/read-only-api"
search_api:
extension: "/search-api"
icat_url: "http://icat_payara_container:8080"
icat_check_cert: false
mechanism: "anon"
username: ""
password: ""
Expand Down
8 changes: 4 additions & 4 deletions datagateway_api/datagateway_api/icat/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,13 @@ def __init__(self, skip_value, filter_use="datagateway_api"):
def apply_filter(self, query):
if self.filter_use == "datagateway_api":
icat_properties = get_icat_properties(
Config.config.datagateway_api.icat_url,
Config.config.datagateway_api.icat_check_cert,
Config.config.icat.url,
Config.config.icat.check_cert,
)
else:
icat_properties = get_icat_properties(
Config.config.search_api.icat_url,
Config.config.search_api.icat_check_cert,
Config.config.icat.url,
Config.config.icat.check_cert,
)
icat_set_limit(query, self.skip_value, icat_properties["maxEntities"])

Expand Down
35 changes: 16 additions & 19 deletions datagateway_api/datagateway_api/icat/helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import datetime, timedelta
from functools import wraps
import logging
from typing import Literal

from cachetools import cached
from dateutil.tz import tzlocal
Expand Down Expand Up @@ -58,9 +59,13 @@ def requires_session_id(method):
def wrapper_requires_session(*args, **kwargs):
try:
client_pool = kwargs.get("client_pool")
if "session_id" in kwargs:
session_id = kwargs["session_id"]
else:
session_id = args[1]

client = get_cached_client(args[1], client_pool)
client.sessionId = args[1]
client = get_cached_client(session_id=session_id, client_pool=client_pool)
client.sessionId = session_id
# Client object put into kwargs so it can be accessed by
# python ICAT functions
kwargs["client"] = client
Expand Down Expand Up @@ -211,7 +216,7 @@ def get_entity_by_id(
entity_type,
id_,
return_json_formattable_data,
return_related_entities=False,
includes: Literal["1"] | list[str] | None = None,
):
"""
Gets a record of a given ID from the specified entity
Expand All @@ -227,26 +232,24 @@ def get_entity_by_id(
data will be used as a response for an API call) or whether to leave the data in
a Python ICAT format
:type return_json_formattable_data: :class:`bool`
:param return_related_entities: Flag to determine whether related entities should
automatically be returned or not. Returning related entities used as a bug fix
for an `IcatException` where ICAT attempts to set a field to null because said
field hasn't been included in the updated data
:param includes: List of include strings, "1" (shorthand for include 1:1 relationships) or None.
Returning related entities used as a bug fix for an `IcatException` where ICAT attempts to set a field to null
because said field hasn't been included in the updated data
:type return_related_entities: :class:`bool`
:return: The record of the specified ID from the given entity
:raises: MissingRecordError: If Python ICAT cannot find a record of the specified ID
"""
log.info("Getting %s of the ID %s", entity_type, id_)
log.debug("Return related entities set to: %s", return_related_entities)
log.debug("includes set to: %s", includes)

# Set query condition for the selected ID
id_condition = PythonICATWhereFilter.create_condition("id", "=", id_)

includes_value = "1" if return_related_entities else None
id_query = ICATQuery(
client,
entity_type,
conditions=id_condition,
includes=includes_value,
includes=includes,
)
entity_by_id_data = id_query.execute_query(client, return_json_formattable_data)

Expand Down Expand Up @@ -294,7 +297,7 @@ def update_entity_by_id(client, entity_type, id_, new_data):
entity_type,
id_,
False,
return_related_entities=True,
includes="1",
)
# There will only ever be one record associated with a single ID - if a record with
# the specified ID cannot be found, it'll be picked up by the MissingRecordError in
Expand Down Expand Up @@ -417,13 +420,7 @@ def is_use_reader_for_performance_enabled() -> bool:
Returns true is the 'use_reader_for_performance' section is present in the
config file and 'enabled' in that section is set to true
"""
reader_config = Config.config.datagateway_api.use_reader_for_performance
if not reader_config:
return False
if not reader_config.enabled:
return False

return True
return Config.config.icat.reader is not None


def get_first_result_with_filters(client, entity_type, filters):
Expand Down Expand Up @@ -494,7 +491,7 @@ def update_entities(client, entity_type, data_to_update):
entity_type,
entity_request["id"],
False,
return_related_entities=True,
includes="1",
)
icat_data_backup.append(entity_data.copy())

Expand Down
16 changes: 4 additions & 12 deletions datagateway_api/datagateway_api/icat/icat_client_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,8 @@
class ICATClient(Client):
"""Wrapper class to allow an object pool of client objects to be created"""

def __init__(self, client_use="datagateway_api"):
if client_use == "datagateway_api":
icat_url = Config.config.datagateway_api.icat_url
icat_check_cert = Config.config.datagateway_api.icat_check_cert
else:
# Search API use cases
icat_url = Config.config.search_api.icat_url
icat_check_cert = Config.config.search_api.icat_check_cert

super().__init__(icat_url, checkCert=icat_check_cert)
def __init__(self):
super().__init__(Config.config.icat.url, checkCert=Config.config.icat.check_cert)
# When clients are cleaned up, sessions won't be logged out
self.autoLogout = False

Expand All @@ -41,8 +33,8 @@ def create_client_pool():

return ObjectPool(
ICATClient,
min_init=Config.config.datagateway_api.client_pool_init_size,
max_capacity=Config.config.datagateway_api.client_pool_max_size,
min_init=Config.config.icat.client_pool_init_size,
max_capacity=Config.config.icat.client_pool_max_size,
max_reusable=0,
expires=0,
)
2 changes: 1 addition & 1 deletion datagateway_api/datagateway_api/icat/lru_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class ExtendedLRUCache(LRUCache):
"""

def __init__(self):
super().__init__(maxsize=Config.config.datagateway_api.client_cache_size)
super().__init__(maxsize=Config.config.icat.client_cache_size)

def popitem(self):
key, client = super().popitem()
Expand Down
6 changes: 4 additions & 2 deletions datagateway_api/datagateway_api/icat/python_icat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Literal

from icat.exception import ICATError, ICATSessionError

Expand Down Expand Up @@ -175,15 +176,16 @@ def count_with_filters(self, session_id, entity_type, filters, **kwargs):

@requires_session_id
@queries_records
def get_with_id(self, session_id, entity_type, id_, **kwargs):
def get_with_id(self, session_id, entity_type, id_, includes: Literal["1"] | list[str] | None = None, **kwargs):
"""
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.
:param includes: List of include strings, "1" (shorthand for include 1:1 relationships) or None.
:return: The entity retrieved.
"""
return get_entity_by_id(kwargs.get("client"), entity_type, id_, True)
return get_entity_by_id(kwargs.get("client"), entity_type, id_, True, includes=includes)

@requires_session_id
@queries_records
Expand Down
16 changes: 7 additions & 9 deletions datagateway_api/datagateway_api/icat/reader_query_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ class ReaderQueryHandler:
reader_client = None
maxsize = 128 # cachetools default value
ttl = 600 # seconds, cachetools default value
if Config.config.datagateway_api.use_reader_for_performance is not None:
maxsize = Config.config.datagateway_api.use_reader_for_performance.maxsize
ttl = Config.config.datagateway_api.use_reader_for_performance.ttl
if Config.config.icat.reader is not None:
maxsize = Config.config.icat.reader.maxsize
ttl = Config.config.icat.reader.ttl

def __init__(self, entity_type: str, filters: List[QueryFilter]) -> None:
self.entity_type = entity_type
Expand All @@ -70,15 +70,13 @@ def create_reader_client(cls) -> ICATClient:
raised (resulting in a 500). The client object is returned
"""
log.info("Creating reader_client")
cls.reader_client = ICATClient("datagateway_api")
cls.reader_client = ICATClient()
try:
cls.reader_client.login(
auth=Config.config.datagateway_api.use_reader_for_performance.reader_mechanism,
auth=Config.config.icat.reader.mechanism,
credentials={
"username": Config.config.datagateway_api.use_reader_for_performance.reader_username,
"password": (
Config.config.datagateway_api.use_reader_for_performance.reader_password.get_secret_value()
),
"username": Config.config.icat.reader.username,
"password": Config.config.icat.reader.password.get_secret_value(),
},
)
except ICATSessionError as e:
Expand Down
20 changes: 20 additions & 0 deletions datagateway_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
from datagateway_api.common.config import Config
from datagateway_api.common.exceptions import ApiError
from datagateway_api.common.logger_setup import LOGGING_CONFIG_FILE_PATH, setup_logger
from datagateway_api.read_only_api.routers.entities import my_data_endpoints

# Check which APIs are enabled
datagateway_api_enabled = Config.config.datagateway_api is not None
read_only_api_enabled = Config.config.read_only_api is not None
search_api_enabled = Config.config.search_api is not None

if datagateway_api_enabled:
Expand Down Expand Up @@ -107,6 +109,18 @@ def create_datagateway_app() -> FastAPI | None:
return None


def create_read_only_app() -> FastAPI | None:
read_only_app = FastAPI(title="Read Only API")
enable_cors(read_only_app)
register_common_handlers(read_only_app)
python_icat = PythonICAT()
icat_client_pool = create_client_pool()
read_only_app.include_router(my_data_endpoints(python_icat=python_icat, client_pool=icat_client_pool))
read_only_app.include_router(ping_endpoint(python_icat, client_pool=icat_client_pool))
read_only_app.include_router(sessions_endpoints(python_icat, client_pool=icat_client_pool))
return read_only_app


def create_search_api_app() -> FastAPI | None:
if search_api_enabled:
search_api_app = FastAPI(
Expand Down Expand Up @@ -137,13 +151,19 @@ def create_search_api_app() -> FastAPI | None:
)
if datagateway_api_enabled:
app.mount(path=Config.config.datagateway_api.extension, app=create_datagateway_app())
if read_only_api_enabled:
app.mount(path=Config.config.read_only_api.extension, app=create_read_only_app())
if search_api_enabled:
app.mount(path=Config.config.search_api.extension, app=create_search_api_app())

elif datagateway_api_enabled:
app = create_datagateway_app()
app.root_path = f"{Config.config.url_prefix}{Config.config.datagateway_api.extension}"

elif read_only_api_enabled:
app = create_read_only_app()
app.root_path = f"{Config.config.url_prefix}{Config.config.read_only_api.extension}"

elif search_api_enabled:
app = create_search_api_app()
app.root_path = f"{Config.config.url_prefix}{Config.config.search_api.extension}"
Expand Down
Loading
Loading