diff --git a/README.md b/README.md index 911272a..49c406c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ Python access to the DINA API - Storage Unit - Form Template - Split Configuration + - Metagenomics Batch + - Metagenomics Batch Item + - Molecular Analysis Result + - Molecular Analysis Run + - Molecular Analysis Run Item + - Project #### DINA APIs - Currently supported APIs: - Agent API @@ -26,12 +32,18 @@ Python access to the DINA API - SplitConfiguration - MaterialSample - Organism + - Project - Object Store API - Any Object Store API endpoint using ObjectStoreApi's CRUD methods - SeqDB API - PCR Batch - PCR Batch Item - SEQ Reaction + - Metagenomics Batch + - Metagenomics Batch Item + - Molecular Analysis Result + - Molecular Analysis Run + - Molecular Analysis Run Item - Export API - Any Export API endpoint using DinaExportApi's CRUD methods @@ -84,6 +96,12 @@ Python access to the DINA API - Person - Split Configuration - Storage Unit Usage + - Metagenomics Batch + - Metagenomics Batch Item + - Molecular Analysis Result + - Molecular Analysis Run + - Molecular Analysis Run Item + - Project ## Installation Instructions ### Local Install diff --git a/dinapy/apis/collectionapi/project_api.py b/dinapy/apis/collectionapi/project_api.py new file mode 100644 index 0000000..ab5ebd4 --- /dev/null +++ b/dinapy/apis/collectionapi/project_api.py @@ -0,0 +1,9 @@ +"""Class that extracts common functionality for Project entity""" + +from .collectionapi import CollectionModuleApi + +class ProjectAPI(CollectionModuleApi): + + def __init__(self, config_path: str = None, base_url: str = None) -> None: + super().__init__(config_path, base_url) + self.base_url += "collecting-event" diff --git a/dinapy/entities/Project.py b/dinapy/entities/Project.py new file mode 100644 index 0000000..5d40b15 --- /dev/null +++ b/dinapy/entities/Project.py @@ -0,0 +1,103 @@ +class ProjectDTO: + def __init__(self, id = None, type = 'project', attributes = None, relationships = 'undefined'): + self.id = id + self.type = type + self.attributes = attributes + self.relationships = relationships + + def get_id(self): + return self.id + + def get_type(self): + return self.type + +class ProjectDTOBuilder: + def __init__(self): + self._id = None + self._type = 'project' + self._attributes = None + self._relationships = None + + def attributes(self, attributes): + self._attributes = attributes + return self + + def relationships(self, relationships): + self._relationships = relationships + return self + + def build(self): + return ProjectDTO(self._id, self._type, self._attributes, self._relationships) + +class ProjectAttributesDTO: + def __init__(self, createdOn = 'undefined', createdBy = 'undefined', group = 'undefined', name = 'undefined', startDate = 'undefined', endDate = 'undefined', status = 'undefined', multilingualDescription = 'undefined', extensionValues = 'undefined'): + self.createdOn = createdOn + self.createdBy = createdBy + self.group = group + self.name = name + self.startDate = startDate + self.endDate = endDate + self.status = status + self.multilingualDescription = multilingualDescription + self.extensionValues = extensionValues + +class ProjectAttributesDTOBuilder: + def __init__(self): + self._createdOn = 'undefined' + self._createdBy = 'undefined' + self._group = 'undefined' + self._name = 'undefined' + self._startDate = 'undefined' + self._endDate = 'undefined' + self._status = 'undefined' + self._multilingualDescription = 'undefined' + self._extensionValues = 'undefined' + + def createdOn(self, createdOn): + self._createdOn = createdOn + return self + + def createdBy(self, createdBy): + self._createdBy = createdBy + return self + + def group(self, group): + self._group = group + return self + + def name(self, name): + self._name = name + return self + + def startDate(self, startDate): + self._startDate = startDate + return self + + def endDate(self, endDate): + self._endDate = endDate + return self + + def status(self, status): + self._status = status + return self + + def multilingualDescription(self, multilingualDescription): + self._multilingualDescription = multilingualDescription + return self + + def extensionValues(self, extensionValues): + self._extensionValues = extensionValues + return self + + def build(self): + return ProjectAttributesDTO( + self._createdOn, + self._createdBy, + self._group, + self._name, + self._startDate, + self._endDate, + self._status, + self._multilingualDescription, + self._extensionValues + ) \ No newline at end of file diff --git a/dinapy/schemas/molecular_analysis_result_schema.py b/dinapy/schemas/molecular_analysis_result_schema.py index 2f79272..63761f8 100644 --- a/dinapy/schemas/molecular_analysis_result_schema.py +++ b/dinapy/schemas/molecular_analysis_result_schema.py @@ -9,7 +9,7 @@ class Attachment(BaseSchema): class Meta: - type_ = 'attacment' + type_ = 'attachment' class MolecularAnalysisResultSchema(Schema): id = fields.Str(load_only=True) diff --git a/dinapy/schemas/project_schema.py b/dinapy/schemas/project_schema.py new file mode 100644 index 0000000..9759e8e --- /dev/null +++ b/dinapy/schemas/project_schema.py @@ -0,0 +1,61 @@ +# This file holds schemas for serializing and deserializing Project entities +# using the JSON API format. It utilizes the marshmallow_jsonapi library. +from marshmallow import post_load +from marshmallow_jsonapi import Schema, fields +from dinapy.schemas.materialsampleschema import * +from dinapy.entities.Project import ProjectDTO +from .customFields import SkipUndefinedField +from .BaseSchema import * + +class Attachment(BaseSchema): + class Meta: + type_ = 'attachment' + +class ProjectSchema(Schema): + id = fields.Str(load_only=True) + createdBy = SkipUndefinedField(fields.Str,attribute="attributes.createdBy") + createdOn = SkipUndefinedField(fields.DateTime,load_only=True,attribute="attributes.createdOn") + group = SkipUndefinedField(fields.Str,attribute="attributes.group") + name = SkipUndefinedField(fields.Str,required=True,attribute="attributes.name") + startDate = SkipUndefinedField(fields.Str,allow_none=True,attribute="attributes.startDate") + endDate = SkipUndefinedField(fields.Str,allow_none=True,attribute="attributes.endDate") + status = SkipUndefinedField(fields.Str,allow_none=True,attribute="attributes.status") + multilingualDescription = SkipUndefinedField(fields.Dict,allow_none=True,attribute="attributes.multilingualDescription") + extensionValues = SkipUndefinedField(fields.Dict,allow_none=True,attribute="attributes.extensionValues") + + attachment = create_relationship("project","attachment") + + meta = fields.DocumentMeta() + + class Meta: + type_ = "project" + + @post_dump + def remove_skipped_fields(self, data, many, **kwargs): + # Remove fields with the special marker value + return {key: value for key, value in data.items() if value is not SkipUndefinedField(fields.Field).SKIP_MARKER} + + @post_dump + def remove_meta(self, data, many, **kwargs): + if 'meta' in data: + del(data['meta']) + return data + + @post_load + def set_none_to_undefined(self, data, **kwargs): + for attr in data.attributes: + if data.attributes[attr] is None: + data.attributes[attr] = 'undefined' + return data + + @post_load + def object_deserialization(self, data, **kwargs): + if 'meta' in data: + del data['meta'] + return ProjectDTO(**data) + + def dump(self, obj, many=None, *args, **kwargs): + data = super().dump(obj, many=many, *args, **kwargs) + if 'meta' in data: + del data['meta'] + return data diff --git a/examples/external-resource-import-demo/README.md b/examples/external-resource-import-demo/README.md index b96e46e..74441ca 100644 --- a/examples/external-resource-import-demo/README.md +++ b/examples/external-resource-import-demo/README.md @@ -18,4 +18,6 @@ Copy the .txt file containing the External URLs to be uploaded into the current Then modify (as needed) and run the **external_resource_importer.py** script. An output file called **external_url_uuids.txt** should have been created containing the generated UUIDs for each External Resource. -A similar file called material_sample_uuids.txt should also appear for the created Material Samples. +A similar file called **material_sample_uuids.txt** should also appear for the created Material Samples. + +Optionally, you can also link all creted Material Samples using **link_samples_to_project.py** which reads **material_sample_uuids.txt** and links them to a Project in DINA through a given Project UUID. diff --git a/tests/project_schema_test.py b/tests/project_schema_test.py new file mode 100644 index 0000000..4999b50 --- /dev/null +++ b/tests/project_schema_test.py @@ -0,0 +1,79 @@ +# This file contains tests relating to ProjectAPI. +# Currently only contains tests for the ProjectSchema (serialization and deserialization tests). +# API mock call tests should be added. + +import unittest +from marshmallow.exceptions import ValidationError + +import sys +import os + +# Add the root directory of the project to the Python path +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, project_root) + +# Import modules from the dinaapi package +from dinapy.schemas.project_schema import ProjectSchema +from dinapy.entities.Project import * + +VALID_PROJECT_DATA = { + "data": { + "id": "01939377-b15d-75f6-b405-54d9e719a509", + "type": "project", + "attributes": { + "createdOn": "2024-12-04T20:58:35.237491Z", + "createdBy": "dina-admin", + "group": "aafc", + "name": "test project", + "startDate": "2024-05-01", + "endDate": "2025-12-04", + "status": "Open", + "multilingualDescription": { + "descriptions": [ + { + "lang": "en", + "desc": "test decription" + } + ] + }, + "extensionValues": {} + }, + "relationships": {} + } +} + +class ProjectSchemaTest(unittest.TestCase): + def test_deserialize_project(self): + schema = ProjectSchema() + try: + result = schema.load(VALID_PROJECT_DATA) + print(result.__dict__) + self.assertIsInstance(result, ProjectDTO) + except ValidationError as e: + self.fail(f"Validation failed with error: {e.messages}") + + def test_serialize_project(self): + schema = ProjectSchema() + project_attributes = ProjectAttributesDTOBuilder( + ).name("test project").group("aafc").build() + project = ProjectDTOBuilder().attributes(project_attributes).build() + + try: + serialized_project = schema.dump(project) + expected = { + "data": { + "type": "project", + "attributes": { + "name": "test project", + "group": "aafc" + } + } + } + print(serialized_project) + self.assertIsInstance(serialized_project, dict) + self.assertDictEqual(serialized_project, expected) + except ValidationError as e: + self.fail(f"Validation failed with error: {e.messages}") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file