"""_____________________________________________________________________

:PROJECT: LARAsuite

*lara_django_data high_level_interface *

:details: lara_django_data high_level_interface.
         - generated with 'lara-django-dev generate_high_level_interface lara_django_data
:authors: mark doerr <mark.doerr@uni-greifswald.de>

.. note:: -
.. todo:: -
________________________________________________________________________
"""

import json
import asyncio
import logging
import grpc
from typing import Union

from google.protobuf.struct_pb2 import Struct
from google.protobuf.json_format import MessageToDict

import lara_django_base_grpc.v1.lara_django_base_pb2 as lara_django_base_pb2
import lara_django_base_grpc.v1.lara_django_base_pb2_grpc as lara_django_base_pb2_grpc

import lara_django_data_grpc.v1.lara_django_data_pb2 as lara_django_data_pb2
import lara_django_data_grpc.v1.lara_django_data_pb2_grpc as lara_django_data_pb2_grpc

import  lara_django_base_grpc.lara_django_base_data_model as base_dm
import  lara_django_base_grpc.lara_django_base_interface as base_if
from  lara_django_base_grpc.abstract_interface import LARAInterface, GRPCChannelSingleton, import_model_instance_from_file, export_model_instance_to_file, upload_file, download_file, update_file, upload_image, update_image, download_image, id_cache

import  lara_django_data_grpc.lara_django_data_data_model as data_dm

logger = logging.getLogger(__name__)



#_________________  ExtraData  ______________________


class ExtraDataInterface:
    def __init__(self, channel: grpc.Channel = None, 
                       default_namespace_uri : str = None,
                       default_namespace_id : str = None,
                       namespace_if = None ) -> None:
        self._default_namespace_uri = default_namespace_uri
        self._default_namespace_id = default_namespace_id

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.namespace_if = namespace_if if namespace_if is not None else base_if.NamespaceInterface(channel)

        # self.extradata_class_client = (
        #     lara_django_data_pb2_grpc.ExtraDataclassByIRIControllerStub(channel)
        # )

        self.extradata_client = lara_django_data_pb2_grpc.ExtraDataControllerStub(channel)
        
        self.item_id = None
        self.update_item_id = "extradata_id"
        self.stub = self.extradata_client

        self.file_download_info = lara_django_data_pb2.FileDownloadInfo
        self.file_upload_chunk = lara_django_data_pb2.FileUploadChunk

        self.image_upload_chunk = lara_django_data_pb2.ImageUploadChunk


        # self.extradata_full_client = lara_django_data_pb2_grpc.ExtraDataFullControllerStub(
        #     channel
        # )

    @property
    def default_namespace_uri(self):
        return self._default_namespace_uri

    @property
    def default_namespace_id(self):
        return self._default_namespace_id

    @classmethod
    async def build_if(cls, channel: grpc.Channel = None, default_namespace_uri: str = None):
    
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
    
        namespace_if = base_if.NamespaceInterface(channel)
        try:
            default_namespace_id = await namespace_if.retrieve_id(namespace_uri=default_namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
        
        return cls(channel=channel, default_namespace_uri=default_namespace_uri,  
                   default_namespace_id=default_namespace_id, namespace_if=namespace_if)
    
    async def set_default_namespace_uri(self, namespace_uri: str = None):
        if namespace_uri is not None:
            self._default_namespace_uri = namespace_uri.strip()
        try:
            self._default_namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
    
    @import_model_instance_from_file()
    @upload_file  # needed for file upload
    @upload_image  # needed for image upload
    async def create(self,  extradata:  list[data_dm.ExtraData] = None,  
                            ids_only: bool = False,
                            namespace_uri: str = None) -> Union[list[str], list[data_dm.ExtraData]]:
        """ create a list of extradata instances,
               returns a list of extradata data model objects or ids_only

        :param extradata: list of extradata data model objects
        :param ids_only: return only the ids of the created instances
        :return: list of extradata data model objects or ids_only
        """
        extradataclass_id = None

        namespace_id = None
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            namespace_id = self._default_namespace_id
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        if namespace_id is None:
            try:
                namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
            except Exception as e:
                logger.error(f"Error retrieving namespace_id: {e}")

        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if extradata_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.extradata_class_client.Retrieve(
        #         lara_django_data_pb2.ExtraDataclassRetrieveRequest(iri=extradata_class_iri)
        #     )
        #     extradataclass_id = res.extradataclass_id
        # except Exception as e:
        #     logger.error(f"Error retrieving extradataclass_id: {e}")
        for extradata in extradata:
            if extradata.namespace == "" or extradata.namespace == "default":
                    extradata.namespace = namespace_id
            # extradata.extradata_class = extradataclass_id # uncomment if a model class is used

        try:
            create_coroutines = ( self.extradata_client.Create(lara_django_data_pb2.ExtraDataRequest(**extradata.model_dump())) for extradata in extradata )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [ MessageToDict(item, preserving_proto_field_name=True).get("extradata_id") for item in res ]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating extradata: {e}")
        
    @export_model_instance_to_file
    async def retrieve(
        self,
        namespace_uri: str = None,
        extradata_id: str = None,
        name: str = None,
        # name_display: str = None,
        filter_dict: dict = {},
        raw : bool = False
        ) -> list[data_dm.ExtraData]:
        """ retrieve a list of extradata instances by extradata_id, name or filter_dict,
               returns a list of extradata data model objects,
               if raw is True, the raw protobuf objects are returned.
               If namespace_uri is "default", the default namespace_uri is used.
        """
        if namespace_uri == "default":
            if self._default_namespace_uri is not None:
                namespace_uri = self._default_namespace_uri
            else:
                raise ValueError("No default namespace URI is given.")
        
        results_list = []
        if  extradata_id is not None:
            try:
                res =  await self.extradata_client.Retrieve(
                    lara_django_data_pb2.ExtraDataRetrieveRequest(extradata_id=extradata_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.ExtraData(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving extradata_id: {e}")

        if name is not None:
            filter_dict["name"] = name
        if namespace_uri is not None:
            filter_dict["namespace__uri"] = namespace_uri
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.extradata_client.List(
                    lara_django_data_pb2.ExtraDataListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # :TODO: a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ data_dm.ExtraData(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving extradata name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str = None,
                          # name_display: str = None,
                          namespace_uri : str = None,
                          iri: str = None,) -> str:
        """ retrieve the extradata_id for a given name,
               name_display or iri
        """

        results_list = await self.retrieve(name=name, 
                        namespace_uri=namespace_uri, filter_dict={}, raw=True)

        if len(results_list) == 1 :
            return results_list[0].extradata_id
        else:
            raise ValueError(f"Error retrieving extradata_id - no or too many results found.")
    
    @download_file  # needed for file download
    @download_image  # needed for image download
    async def get_by_id(self, extradata_id: str = None, id_only: bool = False) -> data_dm.ExtraData:
        """ retrieve a extradata data model by extradata_id """
        try:
            res = await self.extradata_client.Retrieve(
                lara_django_data_pb2.ExtraDataRetrieveRequest(extradata_id=extradata_id)
            )
            item = data_dm.ExtraData(**MessageToDict(res, preserving_proto_field_name=True))
            self.item_id = item.extradata_id
            return item
        except Exception as e:
            logger.error(f"Error retrieving extradata_id: {e}")
    
    @download_file  # needed for file download
    @download_image  # needed for image download
    async def get_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                id_only: bool = False) -> data_dm.ExtraData | str:
        """ retrieve a extradata data model by name """
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            res = await self.extradata_client.RetrieveByNameNamespace(
                lara_django_data_pb2.ExtraDataRetrieveByNameNSRequest(name=name, namespace_uri=namespace_uri)
            )
            item = MessageToDict(res, preserving_proto_field_name=True)
            self.item_id = item["extradata_id"]
            if id_only:
                return self.item_id
            else:
                return data_dm.ExtraData(**item)
        except Exception as e:
            logger.error(f"Error retrieving extradata name: {e}")
   
    
    @download_file  # needed for file download
    async def get_file(self, extradata_id : str = None, id_only : bool = True ) -> bytes | None:
        """ retrieve a file by data_id """
        self.item_id = extradata_id
        
    
    async def get_file_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                filename_target : str = None,
                                as_byte_str : bool = None ) -> bytes | None:
        """ retrieve a file by name """
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            # logger.warning(f"no namespace_uri provided, using default namespace: {namespace_uri}")
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            extradata_id = await self.get_by_name(name=name, namespace_uri=namespace_uri, id_only=True)
            return await self.get_file(extradata_id=extradata_id, filename_target=filename_target, as_byte_str=as_byte_str, get_file=True)
        except Exception as e:
            logger.error(f"Error retrieving extradata name: {e}")
        
    
    @download_image  # needed for image download
    async def get_image(self, extradata_id : str = None, id_only : bool = True) -> bytes | None:
        """ retrieve an image by data_id """
        self.item_id = extradata_id
        
    
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        """ list all extradatas """
        if filter_dict is None:
            res = await self.extradata_client.List(lara_django_data_pb2.ExtraDataListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.extradata_client.List(
                lara_django_data_pb2.ExtraDataListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.extradata_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict = None) -> list:
        """ list all extradatas """
        if self.extradata_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.extradata_full_client.List(
                lara_django_data_pb2.ExtraDataFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.extradata_full_client.List(
                lara_django_data_pb2.ExtraDataFullListRequest(), metadata=metadata
            )
    
    @import_model_instance_from_file()
    @update_file  # needed for file update
    @update_image  # needed for image update
    async def update(self, extradata : data_dm.ExtraData = None) -> None:
        """ update a extradata model instance """
        try:
            await self.extradata_client.Update(
                lara_django_data_pb2.ExtraDataRequest(**extradata.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating extradata_id: {e}")
    
    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        """ delete a extradata by extradata_id or filter_dict """
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.extradata_client.Destroy(lara_django_data_pb2.ExtraDataDestroyRequest(extradata_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting extradata_id: {e}")

#_________________  BlockchainHashes  ______________________


class BlockchainHashesInterface:
    def __init__(self, channel: grpc.Channel = None) -> None:
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.blockchainhashes_client = lara_django_data_pb2_grpc.BlockchainHashesControllerStub(channel)
        
    
    @import_model_instance_from_file()
    async def create(self,  blockchainhashes:  list[data_dm.BlockchainHashes] = None,  ids_only: bool = False) -> Union[list[str], list[data_dm.BlockchainHashes]]:
        try:
            create_coroutines = ( self.blockchainhashes_client.Create(lara_django_data_pb2.BlockchainHashesRequest(**blockchainhashes.model_dump())) for blockchainhashes in blockchainhashes )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.blockchainhashes_id for item in res]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating blockchainhashes: {e}")
        
    @export_model_instance_to_file
    async def retrieve(
        self,
        blockchainhashes_id: str = None,
        name: str = None,
        # name_display: str = None,
        filter_dict: dict = {},
        raw : bool = False
        ) -> list[data_dm.BlockchainHashes]:
        """ retrieve a list of blockchainhashes instances by blockchainhashes_id, name or filter_dict,
               returns a list of blockchainhashes data model objects,
               if raw is True, the raw protobuf objects are returned
        """
        results_list = []
        if  blockchainhashes_id is not None:
            try:
                res =  await self.blockchainhashes_client.Retrieve(
                    lara_django_data_pb2.BlockchainHashesRetrieveRequest(blockchainhashes_id=blockchainhashes_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.BlockchainHashes(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving blockchainhashes_id: {e}")

        if name is not None:
            filter_dict["name"] = name
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.blockchainhashes_client.List(
                    lara_django_data_pb2.BlockchainHashesListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ data_dm.BlockchainHashes(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving blockchainhashes name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str = None,
                          # name_display: str = None,
                          iri: str = None,) -> str:
        """ retrieve the blockchainhashes_id for a given name,
               (name_display) or iri
        """
        
        results_list = await self.retrieve(name=name, filter_dict={}, raw=True)
        if len(results_list) == 1 :
            return results_list[0].blockchainhashes_id
        else:
            raise ValueError(f"Error retrieving blockchainhashes_id - no or too many results found.")
    
    async def get_by_id(self, blockchainhashes_id: str = None) -> data_dm.BlockchainHashes:
        """ retrieve a blockchainhashes data model by blockchainhashes_id """
        try:
            res = await self.blockchainhashes_client.Retrieve(
                lara_django_data_pb2.BlockchainHashesRetrieveRequest(blockchainhashes_id=blockchainhashes_id)
            )
            return data_dm.BlockchainHashes(**MessageToDict(res, preserving_proto_field_name=True))
        except Exception as e:
            logger.error(f"Error retrieving blockchainhashes_id: {e}")
    
    async def get_by_name(self, name: str = None, id_only: bool = False) -> data_dm.BlockchainHashes | str:
        """ retrieve a blockchainhashes data model by name """
        try:
            res = await self.blockchainhashes_client.RetrieveByNameNamespace(
                lara_django_data_pb2.BlockchainHashesRetrieveByNameNSRequest(name=name, namespace_uri="")
            )
            if id_only:
                return MessageToDict(res, preserving_proto_field_name=True)["blockchainhashes_id"]
            else:
                return data_dm.BlockchainHashes(**MessageToDict(res, preserving_proto_field_name=True))
        except Exception as e:
            logger.error(f"Error retrieving blockchainhashes name: {e}")
    
    
    
    
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        """ list all blockchainhashess """
        if filter_dict is None:
            res = await self.blockchainhashes_client.List(lara_django_data_pb2.BlockchainHashesListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.blockchainhashes_client.List(
                lara_django_data_pb2.BlockchainHashesListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.blockchainhashes_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict = None) -> list:
        """ list all blockchainhashess """
        if self.blockchainhashes_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.blockchainhashes_full_client.List(
                lara_django_data_pb2.BlockchainHashesFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.blockchainhashes_full_client.List(
                lara_django_data_pb2.BlockchainHashesFullListRequest(), metadata=metadata
            )
    
    @import_model_instance_from_file()
    async def update(self, blockchainhashes : data_dm.BlockchainHashes = None) -> None:
        """ update a blockchainhashes model instance """
        try:
            await self.blockchainhashes_client.Update(
                lara_django_data_pb2.BlockchainHashesRequest(**blockchainhashes.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating blockchainhashes_id: {e}")
    
    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        """ delete a blockchainhashes by blockchainhashes_id or filter_dict """
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.blockchainhashes_client.Destroy(lara_django_data_pb2.BlockchainHashesDestroyRequest(blockchainhashes_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting blockchainhashes_id: {e}")

#_________________  Visualisation  ______________________


class VisualisationInterface:
    def __init__(self, channel: grpc.Channel = None, 
                       default_namespace_uri : str = None,
                       default_namespace_id : str = None,
                       namespace_if = None ) -> None:
        self._default_namespace_uri = default_namespace_uri
        self._default_namespace_id = default_namespace_id

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.namespace_if = namespace_if if namespace_if is not None else base_if.NamespaceInterface(channel)

        # self.visualisation_class_client = (
        #     lara_django_data_pb2_grpc.VisualisationclassByIRIControllerStub(channel)
        # )

        self.visualisation_client = lara_django_data_pb2_grpc.VisualisationControllerStub(channel)
        
        self.item_id = None
        self.update_item_id = "visualisation_id"
        self.stub = self.visualisation_client

        self.image_upload_chunk = lara_django_data_pb2.ImageUploadChunk


        # self.visualisation_full_client = lara_django_data_pb2_grpc.VisualisationFullControllerStub(
        #     channel
        # )

    @property
    def default_namespace_uri(self):
        return self._default_namespace_uri

    @property
    def default_namespace_id(self):
        return self._default_namespace_id

    @classmethod
    async def build_if(cls, channel: grpc.Channel = None, default_namespace_uri: str = None):
    
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
    
        namespace_if = base_if.NamespaceInterface(channel)
        try:
            default_namespace_id = await namespace_if.retrieve_id(namespace_uri=default_namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
        
        return cls(channel=channel, default_namespace_uri=default_namespace_uri,  
                   default_namespace_id=default_namespace_id, namespace_if=namespace_if)
    
    async def set_default_namespace_uri(self, namespace_uri: str = None):
        if namespace_uri is not None:
            self._default_namespace_uri = namespace_uri.strip()
        try:
            self._default_namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
    
    @import_model_instance_from_file()
    @upload_image  # needed for image upload
    async def create(self,  visualisations:  list[data_dm.Visualisation] = None,  
                            ids_only: bool = False,
                            namespace_uri: str = None) -> Union[list[str], list[data_dm.Visualisation]]:
        """ create a list of visualisation instances,
               returns a list of visualisation data model objects or ids_only

        :param visualisations: list of visualisation data model objects
        :param ids_only: return only the ids of the created instances
        :return: list of visualisation data model objects or ids_only
        """
        visualisationclass_id = None

        namespace_id = None
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            namespace_id = self._default_namespace_id
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        if namespace_id is None:
            try:
                namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
            except Exception as e:
                logger.error(f"Error retrieving namespace_id: {e}")

        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if visualisation_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.visualisation_class_client.Retrieve(
        #         lara_django_data_pb2.VisualisationclassRetrieveRequest(iri=visualisation_class_iri)
        #     )
        #     visualisationclass_id = res.visualisationclass_id
        # except Exception as e:
        #     logger.error(f"Error retrieving visualisationclass_id: {e}")
        for visualisation in visualisations:
            if visualisation.namespace == "" or visualisation.namespace == "default":
                    visualisation.namespace = namespace_id
            # visualisation.visualisation_class = visualisationclass_id # uncomment if a model class is used

        try:
            create_coroutines = ( self.visualisation_client.Create(lara_django_data_pb2.VisualisationRequest(**visualisation.model_dump())) for visualisation in visualisations )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [ MessageToDict(item, preserving_proto_field_name=True).get("visualisation_id") for item in res ]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating visualisation: {e}")
        
    @export_model_instance_to_file
    async def retrieve(
        self,
        namespace_uri: str = None,
        visualisation_id: str = None,
        name: str = None,
        # name_display: str = None,
        filter_dict: dict = {},
        raw : bool = False
        ) -> list[data_dm.Visualisation]:
        """ retrieve a list of visualisation instances by visualisation_id, name or filter_dict,
               returns a list of visualisation data model objects,
               if raw is True, the raw protobuf objects are returned.
               If namespace_uri is "default", the default namespace_uri is used.
        """
        if namespace_uri == "default":
            if self._default_namespace_uri is not None:
                namespace_uri = self._default_namespace_uri
            else:
                raise ValueError("No default namespace URI is given.")
        
        results_list = []
        if  visualisation_id is not None:
            try:
                res =  await self.visualisation_client.Retrieve(
                    lara_django_data_pb2.VisualisationRetrieveRequest(visualisation_id=visualisation_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.Visualisation(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving visualisation_id: {e}")

        if name is not None:
            filter_dict["name"] = name
        if namespace_uri is not None:
            filter_dict["namespace__uri"] = namespace_uri
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.visualisation_client.List(
                    lara_django_data_pb2.VisualisationListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # :TODO: a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ data_dm.Visualisation(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving visualisation name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str = None,
                          # name_display: str = None,
                          namespace_uri : str = None,
                          iri: str = None,) -> str:
        """ retrieve the visualisation_id for a given name,
               name_display or iri
        """

        results_list = await self.retrieve(name=name, 
                        namespace_uri=namespace_uri, filter_dict={}, raw=True)

        if len(results_list) == 1 :
            return results_list[0].visualisation_id
        else:
            raise ValueError(f"Error retrieving visualisation_id - no or too many results found.")
    
    @download_image  # needed for image download
    async def get_by_id(self, visualisation_id: str = None, id_only: bool = False) -> data_dm.Visualisation:
        """ retrieve a visualisation data model by visualisation_id """
        try:
            res = await self.visualisation_client.Retrieve(
                lara_django_data_pb2.VisualisationRetrieveRequest(visualisation_id=visualisation_id)
            )
            item = data_dm.Visualisation(**MessageToDict(res, preserving_proto_field_name=True))
            self.item_id = item.visualisation_id
            return item
        except Exception as e:
            logger.error(f"Error retrieving visualisation_id: {e}")
    
    @download_image  # needed for image download
    async def get_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                id_only: bool = False) -> data_dm.Visualisation | str:
        """ retrieve a visualisation data model by name """
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            res = await self.visualisation_client.RetrieveByNameNamespace(
                lara_django_data_pb2.VisualisationRetrieveByNameNSRequest(name=name, namespace_uri=namespace_uri)
            )
            item = MessageToDict(res, preserving_proto_field_name=True)
            self.item_id = item["visualisation_id"]
            if id_only:
                return self.item_id
            else:
                return data_dm.Visualisation(**item)
        except Exception as e:
            logger.error(f"Error retrieving visualisation name: {e}")
   
    
    
    
    @download_image  # needed for image download
    async def get_image(self, visualisation_id : str = None, id_only : bool = True) -> bytes | None:
        """ retrieve an image by data_id """
        self.item_id = visualisation_id
        
    
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        """ list all visualisations """
        if filter_dict is None:
            res = await self.visualisation_client.List(lara_django_data_pb2.VisualisationListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.visualisation_client.List(
                lara_django_data_pb2.VisualisationListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.visualisation_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict = None) -> list:
        """ list all visualisations """
        if self.visualisation_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.visualisation_full_client.List(
                lara_django_data_pb2.VisualisationFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.visualisation_full_client.List(
                lara_django_data_pb2.VisualisationFullListRequest(), metadata=metadata
            )
    
    @import_model_instance_from_file()
    @update_image  # needed for image update
    async def update(self, visualisation : data_dm.Visualisation = None) -> None:
        """ update a visualisation model instance """
        try:
            await self.visualisation_client.Update(
                lara_django_data_pb2.VisualisationRequest(**visualisation.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating visualisation_id: {e}")
    
    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        """ delete a visualisation by visualisation_id or filter_dict """
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.visualisation_client.Destroy(lara_django_data_pb2.VisualisationDestroyRequest(visualisation_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting visualisation_id: {e}")

#_________________  Data  ______________________


class DataInterface:
    def __init__(self, channel: grpc.Channel = None, 
                       default_namespace_uri : str = None,
                       default_namespace_id : str = None,
                       namespace_if = None ) -> None:
        self._default_namespace_uri = default_namespace_uri
        self._default_namespace_id = default_namespace_id

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.namespace_if = namespace_if if namespace_if is not None else base_if.NamespaceInterface(channel)

        # self.data_class_client = (
        #     lara_django_data_pb2_grpc.DataclassByIRIControllerStub(channel)
        # )

        self.data_client = lara_django_data_pb2_grpc.DataControllerStub(channel)
        
        self.item_id = None
        self.update_item_id = "data_id"
        self.stub = self.data_client

        self.file_download_info = lara_django_data_pb2.FileDownloadInfo
        self.file_upload_chunk = lara_django_data_pb2.FileUploadChunk


        # self.data_full_client = lara_django_data_pb2_grpc.DataFullControllerStub(
        #     channel
        # )

    @property
    def default_namespace_uri(self):
        return self._default_namespace_uri

    @property
    def default_namespace_id(self):
        return self._default_namespace_id

    @classmethod
    async def build_if(cls, channel: grpc.Channel = None, default_namespace_uri: str = None):
    
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
    
        namespace_if = base_if.NamespaceInterface(channel)
        try:
            default_namespace_id = await namespace_if.retrieve_id(namespace_uri=default_namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
        
        return cls(channel=channel, default_namespace_uri=default_namespace_uri,  
                   default_namespace_id=default_namespace_id, namespace_if=namespace_if)
    
    async def set_default_namespace_uri(self, namespace_uri: str = None):
        if namespace_uri is not None:
            self._default_namespace_uri = namespace_uri.strip()
        try:
            self._default_namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
    
    @import_model_instance_from_file()
    @upload_file  # needed for file upload
    async def create(self,  data:  list[data_dm.Data] = None,  
                            ids_only: bool = False,
                            namespace_uri: str = None) -> Union[list[str], list[data_dm.Data]]:
        """ create a list of data instances,
               returns a list of data data model objects or ids_only

        :param data: list of data data model objects
        :param ids_only: return only the ids of the created instances
        :return: list of data data model objects or ids_only
        """
        dataclass_id = None

        namespace_id = None
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            namespace_id = self._default_namespace_id
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        if namespace_id is None:
            try:
                namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
            except Exception as e:
                logger.error(f"Error retrieving namespace_id: {e}")

        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if data_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.data_class_client.Retrieve(
        #         lara_django_data_pb2.DataclassRetrieveRequest(iri=data_class_iri)
        #     )
        #     dataclass_id = res.dataclass_id
        # except Exception as e:
        #     logger.error(f"Error retrieving dataclass_id: {e}")

        try:
            create_coroutines = []
            for datum in data:
                if datum.namespace == "" or datum.namespace == "default":
                    datum.namespace = namespace_id

                #  datum.data_class = dataclass_id
                datum_dict = datum.model_dump()

                if datum_dict.get("data_json") is not None:
                    try:
                        data_struct = Struct()

                        # string to json
                        data_json = datum_dict.get("data_json")
                        data_struct.update(data_json)
                        datum_dict["data_json"] = data_struct
                    except Exception as e:
                        logging.error(f"Error(DataInterface) - converting data_json to Struct: {e}")
                
                if datum_dict.get("metadata") is not None:
                    # convert metadata to struct
                    try:
                        metadata_struct = Struct()
                        metadata_json = datum_dict.get("metadata")
                        metadata_struct.update(metadata_json)
                        datum_dict["metadata"] = metadata_struct

                    except Exception as e:
                        logging.error(f"Error(DataInterface) - converting metadata to Struct: {e}")

                create_coroutines.append(self.data_client.Create(lara_django_data_pb2.DataRequest(**datum_dict)))

            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [ MessageToDict(item, preserving_proto_field_name=True).get("data_id") for item in res ]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating data: {e}")
        
    @export_model_instance_to_file
    async def retrieve(
        self,
        namespace_uri: str = None,
        data_id: str = None,
        name: str = None,
        name_display: str = None,
        filter_dict: dict = None,
        raw : bool = False
        ) -> list[data_dm.Data]:
        """ retrieve a list of data instances by data_id, name or filter_dict,
               returns a list of data data model objects,
               if raw is True, the raw protobuf objects are returned.
               If namespace_uri is "default", the default namespace_uri is used.
        """
        if namespace_uri == "default":
            if self._default_namespace_uri is not None:
                namespace_uri = self._default_namespace_uri
            else:
                raise ValueError("No default namespace URI is given.")
        
        results_list = []
        if  data_id is not None:
            try:
                res =  await self.data_client.Retrieve(
                    lara_django_data_pb2.DataRetrieveRequest(data_id=data_id) )
                if raw:
                    results_list.append(res)
                else:
                    res_dict = MessageToDict(res, preserving_proto_field_name=True)
                    if res_dict.get("data_json") is not None:
                        try:
                            data_struct = res_dict.get("data_json")
                            res_dict["data_json"] = data_struct.as_dict()
                        except Exception as e:
                            logging.error(f"Error converting data_json to dict: {e}")
                    if res_dict.get("metadata") is not None:
                        try:
                            metadata_struct = res_dict.get("metadata")
                            res_dict["metadata"] = metadata_struct.as_dict()
                        except Exception as e:
                            logging.error(f"Error converting metadata to dict: {e}")
                    results_list.append( data_dm.Data(**res_dict) )
            except Exception as e:
                logger.error(f"Error retrieving data_id: {e}")

        if name is not None:
            filter_dict["name"] = name
        if namespace_uri is not None:
            filter_dict["namespace__uri"] = namespace_uri
        if name_display is not None:
           filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        print("---- metaddata", metadata)
        try:
            res = await self.data_client.List(
                    lara_django_data_pb2.DataListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # :TODO: a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ data_dm.Data(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving data name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str = None,
                          name_display: str = None,
                          namespace_uri : str = None,
                          iri: str = None,) -> str:
        """ retrieve the data_id for a given name,
               name_display or iri
        """

        results_list = await self.retrieve(name=name, name_display=name_display,
                        namespace_uri=namespace_uri, filter_dict={}, raw=True)

        if len(results_list) == 1 :
            return results_list[0].data_id
        else:
            raise ValueError(f"Error retrieving data_id - no or too many results found.")
    
    @download_file  # needed for file download
    async def get_by_id(self, data_id: str = None, id_only: bool = False) -> data_dm.Data:
        """ retrieve a data data model by data_id """
        try:
            res = await self.data_client.Retrieve(
                lara_django_data_pb2.DataRetrieveRequest(data_id=data_id)
            )
            item = data_dm.Data(**MessageToDict(res, preserving_proto_field_name=True))
            self.item_id = item.data_id
            return item
        except Exception as e:
            logger.error(f"Error retrieving data_id: {e}")
    
    @download_file  # needed for file download
    async def get_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                id_only: bool = False) -> data_dm.Data | str:
        """ retrieve a data data model by name """
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            res = await self.data_client.RetrieveByNameNamespace(
                lara_django_data_pb2.DataRetrieveByNameNSRequest(name=name, namespace_uri=namespace_uri)
            )
            item = MessageToDict(res, preserving_proto_field_name=True)
            self.item_id = item["data_id"]
            if id_only:
                return self.item_id
            else:
                return data_dm.Data(**item)
        except Exception as e:
            logger.error(f"Error retrieving data name: {e}")
   
    
    @download_file  # needed for file download
    async def get_file(self, data_id : str = None, id_only : bool = True ) -> bytes | None:
        """ retrieve a file by data_id """
        self.item_id = data_id
        
    
    async def get_file_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                filename_target : str = None,
                                as_byte_str : bool = None ) -> bytes | None:
        """ retrieve a file by name """
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            # logger.warning(f"no namespace_uri provided, using default namespace: {namespace_uri}")
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            data_id = await self.get_by_name(name=name, namespace_uri=namespace_uri, id_only=True)
            return await self.get_file(data_id=data_id, filename_target=filename_target, as_byte_str=as_byte_str, get_file=True)
        except Exception as e:
            logger.error(f"Error retrieving data name: {e}")
        
    
    
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        """ list all datas """
        if filter_dict is None:
            res = await self.data_client.List(lara_django_data_pb2.DataListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.data_client.List(
                lara_django_data_pb2.DataListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.data_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict = None) -> list:
        """ list all datas """
        if self.data_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.data_full_client.List(
                lara_django_data_pb2.DataFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.data_full_client.List(
                lara_django_data_pb2.DataFullListRequest(), metadata=metadata
            )
    
    @import_model_instance_from_file()
    @update_file  # needed for file update
    async def update(self, data : data_dm.Data = None) -> None:
        """ update a data model instance """
        try:
            await self.data_client.Update(
                lara_django_data_pb2.DataRequest(**data.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating data_id: {e}")
    
    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        """ delete a data by data_id or filter_dict """
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.data_client.Destroy(lara_django_data_pb2.DataDestroyRequest(data_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting data_id: {e}")

#_________________  DataSubset  ______________________


class DataSubsetInterface:
    def __init__(self, channel: grpc.Channel = None, 
                       default_namespace_uri : str = None,
                       default_namespace_id : str = None,
                       namespace_if = None ) -> None:
        self._default_namespace_uri = default_namespace_uri
        self._default_namespace_id = default_namespace_id

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.namespace_if = namespace_if if namespace_if is not None else base_if.NamespaceInterface(channel)

        # self.datasubset_class_client = (
        #     lara_django_data_pb2_grpc.DataSubsetclassByIRIControllerStub(channel)
        # )

        self.datasubset_client = lara_django_data_pb2_grpc.DataSubsetControllerStub(channel)
        

        # self.datasubset_full_client = lara_django_data_pb2_grpc.DataSubsetFullControllerStub(
        #     channel
        # )

    @property
    def default_namespace_uri(self):
        return self._default_namespace_uri

    @property
    def default_namespace_id(self):
        return self._default_namespace_id

    @classmethod
    async def build_if(cls, channel: grpc.Channel = None, default_namespace_uri: str = None):
    
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
    
        namespace_if = base_if.NamespaceInterface(channel)
        try:
            default_namespace_id = await namespace_if.retrieve_id(namespace_uri=default_namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
        
        return cls(channel=channel, default_namespace_uri=default_namespace_uri,  
                   default_namespace_id=default_namespace_id, namespace_if=namespace_if)
    
    async def set_default_namespace_uri(self, namespace_uri: str = None):
        if namespace_uri is not None:
            self._default_namespace_uri = namespace_uri.strip()
        try:
            self._default_namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
    
    @import_model_instance_from_file()
    async def create(self,  datasubsets:  list[data_dm.DataSubset] = None,  
                            ids_only: bool = False,
                            namespace_uri: str = None) -> Union[list[str], list[data_dm.DataSubset]]:
        """ create a list of datasubset instances,
               returns a list of datasubset data model objects or ids_only

        :param datasubsets: list of datasubset data model objects
        :param ids_only: return only the ids of the created instances
        :return: list of datasubset data model objects or ids_only
        """
        datasubsetclass_id = None

        namespace_id = None
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            namespace_id = self._default_namespace_id
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        if namespace_id is None:
            try:
                namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
            except Exception as e:
                logger.error(f"Error retrieving namespace_id: {e}")

        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if datasubset_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.datasubset_class_client.Retrieve(
        #         lara_django_data_pb2.DataSubsetclassRetrieveRequest(iri=datasubset_class_iri)
        #     )
        #     datasubsetclass_id = res.datasubsetclass_id
        # except Exception as e:
        #     logger.error(f"Error retrieving datasubsetclass_id: {e}")
        for datasubset in datasubsets:
            if datasubset.namespace == "" or datasubset.namespace == "default":
                    datasubset.namespace = namespace_id
            # datasubset.datasubset_class = datasubsetclass_id # uncomment if a model class is used

        try:
            create_coroutines = ( self.datasubset_client.Create(lara_django_data_pb2.DataSubsetRequest(**datasubset.model_dump())) for datasubset in datasubsets )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [ MessageToDict(item, preserving_proto_field_name=True).get("datasubset_id") for item in res ]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating datasubset: {e}")
        
    @export_model_instance_to_file
    async def retrieve(
        self,
        namespace_uri: str = None,
        datasubset_id: str = None,
        name: str = None,
        # name_display: str = None,
        filter_dict: dict = {},
        raw : bool = False
        ) -> list[data_dm.DataSubset]:
        """ retrieve a list of datasubset instances by datasubset_id, name or filter_dict,
               returns a list of datasubset data model objects,
               if raw is True, the raw protobuf objects are returned.
               If namespace_uri is "default", the default namespace_uri is used.
        """
        if namespace_uri == "default":
            if self._default_namespace_uri is not None:
                namespace_uri = self._default_namespace_uri
            else:
                raise ValueError("No default namespace URI is given.")
        
        results_list = []
        if  datasubset_id is not None:
            try:
                res =  await self.datasubset_client.Retrieve(
                    lara_django_data_pb2.DataSubsetRetrieveRequest(datasubset_id=datasubset_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.DataSubset(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving datasubset_id: {e}")

        if name is not None:
            filter_dict["name"] = name
        if namespace_uri is not None:
            filter_dict["namespace__uri"] = namespace_uri
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.datasubset_client.List(
                    lara_django_data_pb2.DataSubsetListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # :TODO: a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ data_dm.DataSubset(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving datasubset name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str = None,
                          # name_display: str = None,
                          namespace_uri : str = None,
                          iri: str = None,) -> str:
        """ retrieve the datasubset_id for a given name,
               name_display or iri
        """

        results_list = await self.retrieve(name=name, 
                        namespace_uri=namespace_uri, filter_dict={}, raw=True)

        if len(results_list) == 1 :
            return results_list[0].datasubset_id
        else:
            raise ValueError(f"Error retrieving datasubset_id - no or too many results found.")
    
    async def get_by_id(self, datasubset_id: str = None, id_only: bool = False) -> data_dm.DataSubset:
        """ retrieve a datasubset data model by datasubset_id """
        try:
            res = await self.datasubset_client.Retrieve(
                lara_django_data_pb2.DataSubsetRetrieveRequest(datasubset_id=datasubset_id)
            )
            item = data_dm.DataSubset(**MessageToDict(res, preserving_proto_field_name=True))
            self.item_id = item.datasubset_id
            return item
        except Exception as e:
            logger.error(f"Error retrieving datasubset_id: {e}")
    
    async def get_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                id_only: bool = False) -> data_dm.DataSubset | str:
        """ retrieve a datasubset data model by name """
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            res = await self.datasubset_client.RetrieveByNameNamespace(
                lara_django_data_pb2.DataSubsetRetrieveByNameNSRequest(name=name, namespace_uri=namespace_uri)
            )
            item = MessageToDict(res, preserving_proto_field_name=True)
            self.item_id = item["datasubset_id"]
            if id_only:
                return self.item_id
            else:
                return data_dm.DataSubset(**item)
        except Exception as e:
            logger.error(f"Error retrieving datasubset name: {e}")
   
    
    
    
    
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        """ list all datasubsets """
        if filter_dict is None:
            res = await self.datasubset_client.List(lara_django_data_pb2.DataSubsetListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.datasubset_client.List(
                lara_django_data_pb2.DataSubsetListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.datasubset_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict = None) -> list:
        """ list all datasubsets """
        if self.datasubset_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.datasubset_full_client.List(
                lara_django_data_pb2.DataSubsetFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.datasubset_full_client.List(
                lara_django_data_pb2.DataSubsetFullListRequest(), metadata=metadata
            )
    
    @import_model_instance_from_file()
    async def update(self, datasubset : data_dm.DataSubset = None) -> None:
        """ update a datasubset model instance """
        try:
            await self.datasubset_client.Update(
                lara_django_data_pb2.DataSubsetRequest(**datasubset.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating datasubset_id: {e}")
    
    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        """ delete a datasubset by datasubset_id or filter_dict """
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.datasubset_client.Destroy(lara_django_data_pb2.DataSubsetDestroyRequest(datasubset_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting datasubset_id: {e}")

#_________________  OrderedDataDataSubset  ______________________


class OrderedDataDataSubsetInterface:
    def __init__(self, channel: grpc.Channel = None) -> None:
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.ordereddatadatasubset_client = lara_django_data_pb2_grpc.OrderedDataDataSubsetControllerStub(channel)
        
    
    @import_model_instance_from_file()
    async def create(self,  ordereddatadatasubsets:  list[data_dm.OrderedDataDataSubset] = None,  ids_only: bool = False) -> Union[list[str], list[data_dm.OrderedDataDataSubset]]:
        try:
            create_coroutines = ( self.ordereddatadatasubset_client.Create(lara_django_data_pb2.OrderedDataDataSubsetRequest(**ordereddatadatasubset.model_dump())) for ordereddatadatasubset in ordereddatadatasubsets )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [item.ordereddatadatasubset_id for item in res]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating ordereddatadatasubset: {e}")
        
    @export_model_instance_to_file
    async def retrieve(
        self,
        ordereddatadatasubset_id: str = None,
        name: str = None,
        # name_display: str = None,
        filter_dict: dict = {},
        raw : bool = False
        ) -> list[data_dm.OrderedDataDataSubset]:
        """ retrieve a list of ordereddatadatasubset instances by ordereddatadatasubset_id, name or filter_dict,
               returns a list of ordereddatadatasubset data model objects,
               if raw is True, the raw protobuf objects are returned
        """
        results_list = []
        if  ordereddatadatasubset_id is not None:
            try:
                res =  await self.ordereddatadatasubset_client.Retrieve(
                    lara_django_data_pb2.OrderedDataDataSubsetRetrieveRequest(ordereddatadatasubset_id=ordereddatadatasubset_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.OrderedDataDataSubset(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving ordereddatadatasubset_id: {e}")

        if name is not None:
            filter_dict["name"] = name
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.ordereddatadatasubset_client.List(
                    lara_django_data_pb2.OrderedDataDataSubsetListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ data_dm.OrderedDataDataSubset(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving ordereddatadatasubset name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str = None,
                          # name_display: str = None,
                          iri: str = None,) -> str:
        """ retrieve the ordereddatadatasubset_id for a given name,
               (name_display) or iri
        """
        
        results_list = await self.retrieve(name=name, filter_dict={}, raw=True)
        if len(results_list) == 1 :
            return results_list[0].ordereddatadatasubset_id
        else:
            raise ValueError(f"Error retrieving ordereddatadatasubset_id - no or too many results found.")
    
    async def get_by_id(self, ordereddatadatasubset_id: str = None) -> data_dm.OrderedDataDataSubset:
        """ retrieve a ordereddatadatasubset data model by ordereddatadatasubset_id """
        try:
            res = await self.ordereddatadatasubset_client.Retrieve(
                lara_django_data_pb2.OrderedDataDataSubsetRetrieveRequest(ordereddatadatasubset_id=ordereddatadatasubset_id)
            )
            return data_dm.OrderedDataDataSubset(**MessageToDict(res, preserving_proto_field_name=True))
        except Exception as e:
            logger.error(f"Error retrieving ordereddatadatasubset_id: {e}")
    
    async def get_by_name(self, name: str = None, id_only: bool = False) -> data_dm.OrderedDataDataSubset | str:
        """ retrieve a ordereddatadatasubset data model by name """
        try:
            res = await self.ordereddatadatasubset_client.RetrieveByNameNamespace(
                lara_django_data_pb2.OrderedDataDataSubsetRetrieveByNameNSRequest(name=name, namespace_uri="")
            )
            if id_only:
                return MessageToDict(res, preserving_proto_field_name=True)["ordereddatadatasubset_id"]
            else:
                return data_dm.OrderedDataDataSubset(**MessageToDict(res, preserving_proto_field_name=True))
        except Exception as e:
            logger.error(f"Error retrieving ordereddatadatasubset name: {e}")
    
    
    
    
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        """ list all ordereddatadatasubsets """
        if filter_dict is None:
            res = await self.ordereddatadatasubset_client.List(lara_django_data_pb2.OrderedDataDataSubsetListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.ordereddatadatasubset_client.List(
                lara_django_data_pb2.OrderedDataDataSubsetListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.ordereddatadatasubset_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict = None) -> list:
        """ list all ordereddatadatasubsets """
        if self.ordereddatadatasubset_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.ordereddatadatasubset_full_client.List(
                lara_django_data_pb2.OrderedDataDataSubsetFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.ordereddatadatasubset_full_client.List(
                lara_django_data_pb2.OrderedDataDataSubsetFullListRequest(), metadata=metadata
            )
    
    @import_model_instance_from_file()
    async def update(self, ordereddatadatasubset : data_dm.OrderedDataDataSubset = None) -> None:
        """ update a ordereddatadatasubset model instance """
        try:
            await self.ordereddatadatasubset_client.Update(
                lara_django_data_pb2.OrderedDataDataSubsetRequest(**ordereddatadatasubset.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating ordereddatadatasubset_id: {e}")
    
    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        """ delete a ordereddatadatasubset by ordereddatadatasubset_id or filter_dict """
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.ordereddatadatasubset_client.Destroy(lara_django_data_pb2.OrderedDataDataSubsetDestroyRequest(ordereddatadatasubset_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting ordereddatadatasubset_id: {e}")

#_________________  Evaluation  ______________________


class EvaluationInterface:
    def __init__(self, channel: grpc.Channel = None, 
                       default_namespace_uri : str = None,
                       default_namespace_id : str = None,
                       namespace_if = None ) -> None:
        self._default_namespace_uri = default_namespace_uri
        self._default_namespace_id = default_namespace_id

        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel

        self.namespace_if = namespace_if if namespace_if is not None else base_if.NamespaceInterface(channel)

        # self.evaluation_class_client = (
        #     lara_django_data_pb2_grpc.EvaluationclassByIRIControllerStub(channel)
        # )

        self.evaluation_client = lara_django_data_pb2_grpc.EvaluationControllerStub(channel)
        

        # self.evaluation_full_client = lara_django_data_pb2_grpc.EvaluationFullControllerStub(
        #     channel
        # )

    @property
    def default_namespace_uri(self):
        return self._default_namespace_uri

    @property
    def default_namespace_id(self):
        return self._default_namespace_id

    @classmethod
    async def build_if(cls, channel: grpc.Channel = None, default_namespace_uri: str = None):
    
        if channel is None:
            cs = GRPCChannelSingleton()
            channel = cs.channel
    
        namespace_if = base_if.NamespaceInterface(channel)
        try:
            default_namespace_id = await namespace_if.retrieve_id(namespace_uri=default_namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
        
        return cls(channel=channel, default_namespace_uri=default_namespace_uri,  
                   default_namespace_id=default_namespace_id, namespace_if=namespace_if)
    
    async def set_default_namespace_uri(self, namespace_uri: str = None):
        if namespace_uri is not None:
            self._default_namespace_uri = namespace_uri.strip()
        try:
            self._default_namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
        except Exception as e:
            logger.error(f"Error retrieving namespace_id: {e}")
    
    @import_model_instance_from_file()
    async def create(self,  evaluations:  list[data_dm.Evaluation] = None,  
                            ids_only: bool = False,
                            namespace_uri: str = None) -> Union[list[str], list[data_dm.Evaluation]]:
        """ create a list of evaluation instances,
               returns a list of evaluation data model objects or ids_only

        :param evaluations: list of evaluation data model objects
        :param ids_only: return only the ids of the created instances
        :return: list of evaluation data model objects or ids_only
        """
        evaluationclass_id = None

        namespace_id = None
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
            namespace_id = self._default_namespace_id
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        if namespace_id is None:
            try:
                namespace_id = await self.namespace_if.retrieve_id(namespace_uri=namespace_uri)
            except Exception as e:
                logger.error(f"Error retrieving namespace_id: {e}")

        # some sample code for retrieving a class IRI and adding namespace and class to the model
        # if evaluation_class_iri is None:
        #     raise ValueError("No sample class IRI is given.")
        # try:
        #     res = await self.evaluation_class_client.Retrieve(
        #         lara_django_data_pb2.EvaluationclassRetrieveRequest(iri=evaluation_class_iri)
        #     )
        #     evaluationclass_id = res.evaluationclass_id
        # except Exception as e:
        #     logger.error(f"Error retrieving evaluationclass_id: {e}")
        for evaluation in evaluations:
            if evaluation.namespace == "" or evaluation.namespace == "default":
                    evaluation.namespace = namespace_id
            # evaluation.evaluation_class = evaluationclass_id # uncomment if a model class is used

        try:
            create_coroutines = ( self.evaluation_client.Create(lara_django_data_pb2.EvaluationRequest(**evaluation.model_dump())) for evaluation in evaluations )
            res = await asyncio.gather(*create_coroutines)
            if ids_only:
                return [ MessageToDict(item, preserving_proto_field_name=True).get("evaluation_id") for item in res ]
            else:
                return res

        except Exception as e:
            logger.error(f"Error creating evaluation: {e}")
        
    @export_model_instance_to_file
    async def retrieve(
        self,
        namespace_uri: str = None,
        evaluation_id: str = None,
        name: str = None,
        # name_display: str = None,
        filter_dict: dict = {},
        raw : bool = False
        ) -> list[data_dm.Evaluation]:
        """ retrieve a list of evaluation instances by evaluation_id, name or filter_dict,
               returns a list of evaluation data model objects,
               if raw is True, the raw protobuf objects are returned.
               If namespace_uri is "default", the default namespace_uri is used.
        """
        if namespace_uri == "default":
            if self._default_namespace_uri is not None:
                namespace_uri = self._default_namespace_uri
            else:
                raise ValueError("No default namespace URI is given.")
        
        results_list = []
        if  evaluation_id is not None:
            try:
                res =  await self.evaluation_client.Retrieve(
                    lara_django_data_pb2.EvaluationRetrieveRequest(evaluation_id=evaluation_id) )
                if raw:
                    results_list.append(res)
                else:
                    results_list.append( data_dm.Evaluation(**MessageToDict(res, preserving_proto_field_name=True)) )
            except Exception as e:
                logger.error(f"Error retrieving evaluation_id: {e}")

        if name is not None:
            filter_dict["name"] = name
        if namespace_uri is not None:
            filter_dict["namespace__uri"] = namespace_uri
        # if name_display is not None:
        #    filter_dict["name_display"] = name_display

        metadata = (("filters", (json.dumps(filter_dict))),)
        try:
            res = await self.evaluation_client.List(
                    lara_django_data_pb2.EvaluationListRequest(), metadata=metadata
                    )
            if raw:
                    results_list += res.results
            else:
                # :TODO: a queue could be used here to speed up the retrieval
                results = res.results
                results_list += [ data_dm.Evaluation(**MessageToDict(res, preserving_proto_field_name=True)) for res in results ]
        except Exception as e:
            logger.error(f"Error retrieving evaluation name: {e}")

        return results_list

    #@id_cache
    async def retrieve_id(self,
                          name: str = None,
                          # name_display: str = None,
                          namespace_uri : str = None,
                          iri: str = None,) -> str:
        """ retrieve the evaluation_id for a given name,
               name_display or iri
        """

        results_list = await self.retrieve(name=name, 
                        namespace_uri=namespace_uri, filter_dict={}, raw=True)

        if len(results_list) == 1 :
            return results_list[0].evaluation_id
        else:
            raise ValueError(f"Error retrieving evaluation_id - no or too many results found.")
    
    async def get_by_id(self, evaluation_id: str = None, id_only: bool = False) -> data_dm.Evaluation:
        """ retrieve a evaluation data model by evaluation_id """
        try:
            res = await self.evaluation_client.Retrieve(
                lara_django_data_pb2.EvaluationRetrieveRequest(evaluation_id=evaluation_id)
            )
            item = data_dm.Evaluation(**MessageToDict(res, preserving_proto_field_name=True))
            self.item_id = item.evaluation_id
            return item
        except Exception as e:
            logger.error(f"Error retrieving evaluation_id: {e}")
    
    async def get_by_name(self, name: str = None,
                                namespace_uri : str = None,
                                id_only: bool = False) -> data_dm.Evaluation | str:
        """ retrieve a evaluation data model by name """
        if namespace_uri is None and self._default_namespace_uri is not None:
            namespace_uri = self._default_namespace_uri
        if namespace_uri is None:
            raise ValueError("No namespace URI is given.")
        try:
            res = await self.evaluation_client.RetrieveByNameNamespace(
                lara_django_data_pb2.EvaluationRetrieveByNameNSRequest(name=name, namespace_uri=namespace_uri)
            )
            item = MessageToDict(res, preserving_proto_field_name=True)
            self.item_id = item["evaluation_id"]
            if id_only:
                return self.item_id
            else:
                return data_dm.Evaluation(**item)
        except Exception as e:
            logger.error(f"Error retrieving evaluation name: {e}")
   
    
    
    
    
    async def list_items(self, filter_dict: dict = None, ids_only: bool = False) -> list[str]:
        """ list all evaluations """
        if filter_dict is None:
            res = await self.evaluation_client.List(lara_django_data_pb2.EvaluationListRequest())
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            res = await self.evaluation_client.List(
                lara_django_data_pb2.EvaluationListRequest(), metadata=metadata
            )
        if res is not None and ids_only:
            return [item.evaluation_id for item in res.results]
        else:
            return res.results

    async def list_full(self, filter_dict: dict = None) -> list:
        """ list all evaluations """
        if self.evaluation_full_client is None:
            raise ValueError("No full client available.")
        if filter_dict is None:
            return await self.evaluation_full_client.List(
                lara_django_data_pb2.EvaluationFullListRequest()
            )
        else:
            metadata = (("filters", (json.dumps(filter_dict))),)
            return await self.evaluation_full_client.List(
                lara_django_data_pb2.EvaluationFullListRequest(), metadata=metadata
            )
    
    @import_model_instance_from_file()
    async def update(self, evaluation : data_dm.Evaluation = None) -> None:
        """ update a evaluation model instance """
        try:
            await self.evaluation_client.Update(
                lara_django_data_pb2.EvaluationRequest(**evaluation.model_dump()
            ))
        except Exception as e:
            logger.error(f"Error updating evaluation_id: {e}")
    
    async def delete(self, id_list: list[str] = None, filter_dict: dict = None) -> None:
        """ delete a evaluation by evaluation_id or filter_dict """
        if filter_dict is not None:
            id_list = await self.list_items(filter_dict=filter_dict, ids_only=True)

        try:
            delete_coroutines = ( self.evaluation_client.Destroy(lara_django_data_pb2.EvaluationDestroyRequest(evaluation_id=id)) for id in id_list )
            await asyncio.gather(*delete_coroutines)

        except Exception as e:
            logger.error(f"Error deleting evaluation_id: {e}")

