From 85c4bfc94cffc7c0c1664b78bb0824921aa286db Mon Sep 17 00:00:00 2001 From: Viktor Bozhinov Date: Fri, 7 Jan 2022 15:48:07 +0000 Subject: [PATCH] conversion proof of concept #265 --- datagateway_api/src/search_api/models.py | 113 ++++++++++++++++++----- test/search_api/test_models.py | 28 ++++++ 2 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 test/search_api/test_models.py diff --git a/datagateway_api/src/search_api/models.py b/datagateway_api/src/search_api/models.py index 4c3fdc56..411f874a 100644 --- a/datagateway_api/src/search_api/models.py +++ b/datagateway_api/src/search_api/models.py @@ -1,5 +1,7 @@ -from abc import ABC, abstractclassmethod +import abc +from abc import ABC from datetime import datetime +import sys from typing import ClassVar, List, Optional, Union from pydantic import ( @@ -12,11 +14,74 @@ StrictStr, ) +from datagateway_api.src.search_api.panosc_mappings import mappings + + +# TODO - Merge this with `get_icat_mapping` from src\search_api\filters.py +def _get_icat_mapping(panosc_entity_name, field_name): + icat_mapping = mappings.mappings[panosc_entity_name][field_name] + + if isinstance(icat_mapping, str): + # Field name + icat_field_name = icat_mapping + if isinstance(icat_mapping, dict): + # Relation - JSON format: {PaNOSC entity name: ICAT related field name} + panosc_entity_name = list(icat_mapping.keys())[0] + icat_field_name = icat_mapping[panosc_entity_name] + + return panosc_entity_name, icat_field_name + + +def _get_icat_field_value(icat_field_name, icat_data): + icat_field_name = icat_field_name.split(".") + value = icat_data + for f in icat_field_name: + value = value[f] + + return value + class PaNOSCAttribute(ABC, BaseModel): - @abstractclassmethod - def from_icat(self): - pass + @classmethod + @abc.abstractmethod + def from_icat(cls, icat_data): # noqa: B902, N805 + model_fields = cls.__fields__ + + model_data = {} + for field in model_fields: + # Some fields have aliases so we must use them when creating a model instance. + # If a field does not have an alias then the `alias` property holds the name + # of the field + field_alias = cls.__fields__[field].alias + + panosc_entity_name, icat_field_name = _get_icat_mapping( + cls.__name__, field_alias, + ) + try: + field_value = _get_icat_field_value(icat_field_name, icat_data) + except KeyError: + continue + + if panosc_entity_name != cls.__name__: + # If we are here, it means that the field references another model so we + # have to get hold of its class definition and call its `from_icat` method + # to create an instance of itself with the ICAT data provided. Doing this + # allows for recursion. + data = icat_data[icat_field_name] + if not isinstance(data, list): + data = [data] + + # Get the class of the referenced model + panosc_model_attr = getattr(sys.modules[__name__], panosc_entity_name) + field_value = [panosc_model_attr.from_icat(d) for d in data] + + field_type = cls.__fields__[field].outer_type_._name + if field_type != "List": + field_value = field_value[0] + + model_data[field_alias] = field_value + + return cls(**model_data) class Affiliation(PaNOSCAttribute): @@ -33,8 +98,8 @@ class Affiliation(PaNOSCAttribute): members: Optional[List["Member"]] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Affiliation, cls).from_icat(icat_query_data) class Dataset(PaNOSCAttribute): @@ -59,8 +124,8 @@ class Dataset(PaNOSCAttribute): samples: Optional[List["Sample"]] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Dataset, cls).from_icat(icat_query_data) class Document(PaNOSCAttribute): @@ -87,8 +152,8 @@ class Document(PaNOSCAttribute): parameters: Optional[List["Parameter"]] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Document, cls).from_icat(icat_query_data) class File(PaNOSCAttribute): @@ -104,8 +169,8 @@ class File(PaNOSCAttribute): dataset: Dataset @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(File, cls).from_icat(icat_query_data) class Instrument(PaNOSCAttribute): @@ -120,8 +185,8 @@ class Instrument(PaNOSCAttribute): datasets: Optional[List[Dataset]] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Instrument, cls).from_icat(icat_query_data) class Member(PaNOSCAttribute): @@ -137,8 +202,8 @@ class Member(PaNOSCAttribute): affiliation: Optional[Affiliation] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Member, cls).from_icat(icat_query_data) class Parameter(PaNOSCAttribute): @@ -169,8 +234,8 @@ def validate_dataset_and_document(cls, values): # noqa: B902, N805 return values @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Parameter, cls).from_icat(icat_query_data) class Person(PaNOSCAttribute): @@ -188,8 +253,8 @@ class Person(PaNOSCAttribute): members: Optional[List[Member]] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Person, cls).from_icat(icat_query_data) class Sample(PaNOSCAttribute): @@ -204,8 +269,8 @@ class Sample(PaNOSCAttribute): datasets: Optional[List[Dataset]] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Sample, cls).from_icat(icat_query_data) class Technique(PaNOSCAttribute): @@ -219,8 +284,8 @@ class Technique(PaNOSCAttribute): datasets: Optional[List[Dataset]] @classmethod - def from_icat(cls): - pass + def from_icat(cls, icat_query_data): + return super(Technique, cls).from_icat(icat_query_data) # The below models reference other models that may not be defined during their diff --git a/test/search_api/test_models.py b/test/search_api/test_models.py new file mode 100644 index 00000000..34d52590 --- /dev/null +++ b/test/search_api/test_models.py @@ -0,0 +1,28 @@ +import json + +import datagateway_api.src.search_api.models as models + + +class TestModels: + def test_from_icat_person_model(self): + expected_model_data = { + "id": "1", + "fullName": "Test fullname", + "orcid": "1111", + "researcherId": None, + "firstName": "Test given name", + "lastName": "Test family name", + "members": None, + } + # TODO: `id` is returned as `int` from ICAT whereas Person model expects `str` + icat_data = { + "id": "1", + "fullName": "Test fullname", + "orcidId": "1111", + "givenName": "Test given name", + "familyName": "Test family name", + } + + person_model = models.Person.from_icat(icat_data) + + assert person_model.json(by_alias=True) == json.dumps(expected_model_data)