"""
SPDX-License-Identifier: Apache-2.0
Copyright Contributors to the ODPi Egeria project.

    Maintain and explore the contents of nested collections.

"""

import asyncio
from datetime import datetime
from typing import Optional, Annotated, Literal, Union

from loguru import logger
from pydantic import Field, HttpUrl

from pyegeria.core._exceptions import PyegeriaInvalidParameterException
from pyegeria.core._globals import NO_ELEMENTS_FOUND, NO_GUID_RETURNED
from pyegeria.view.base_report_formats import select_report_spec, get_report_spec_match
from pyegeria.models import (SearchStringRequestBody, FilterRequestBody, GetRequestBody, NewElementRequestBody,
                             ReferenceableProperties, InitialClassifications, TemplateRequestBody,
                             UpdateElementRequestBody, NewRelationshipRequestBody,
                             DeleteElementRequestBody, DeleteRelationshipRequestBody, UpdateRelationshipRequestBody,
                             ResultsRequestBody,
                             DeploymentStatusSearchString, DeploymentStatusFilterRequestBody,
                             get_defined_field_values, PyegeriaModel)
from pyegeria.view.output_formatter import (generate_output,
                                            populate_common_columns)
from pyegeria.core.utils import body_slimmer, dynamic_catch

COLLECTION_PROPERTIES_LIST = ["CollectionProperties", "DataDictionaryProperties",
                              "DataSpecProperties", "DigitalProductProperties",
                              "AgreementProperties"]

AGREEMENT_PROPERTIES_LIST = ["AgreementProperties", "DigitalSubscriptionProperties",]


def query_seperator(current_string):
    if current_string == "":
        return "?"
    else:
        return "&"


# ("params are in the form of [(paramName, value), (param2Name, value)] if the value is not None, it will be added to "
# "the query string")


def query_string(params):
    result = ""
    for i in range(len(params)):
        if params[i][1] is not None:
            result = f"{result}{query_seperator(result)}{params[i][0]}={params[i][1]}"
    return result

from pyegeria.core._server_client import ServerClient

class CollectionProperties(ReferenceableProperties):
    class_: Annotated[Literal["CollectionProperties"], Field(alias="class")]


class RootCollectionProperties(CollectionProperties):
    class_: Annotated[Literal["RootCollectionProperties"], Field(alias="class")]


class HomeCollectionProperties(CollectionProperties):
    class_: Annotated[Literal["HomeCollectionProperties"], Field(alias="class")]


class CollectionFolderProperties(CollectionProperties):
    class_: Annotated[Literal["CollectionFolderProperties"], Field(alias="class")]


class NamespaceProperties(CollectionProperties):
    class_: Annotated[Literal["NamespaceProperties"], Field(alias="class")]


class ResultsSetProperties(CollectionProperties):
    class_: Annotated[Literal["ResultsSetProperties"], Field(alias="class")]


class RecentAccessProperties(CollectionProperties):
    class_: Annotated[Literal["RecentAccessProperties"], Field(alias="class")]


class WorkItemListProperties(CollectionProperties):
    class_: Annotated[Literal["WorkItemListProperties"], Field(alias="class")]


class FolioProperties(CollectionProperties):
    class_: Annotated[Literal["FolioProperties"], Field(alias="class")]


class GlossaryProperties(CollectionProperties):
    class_: Annotated[Literal["GlossaryProperties"], Field(alias="class")]
    language: str | None = "English"
    usage: str | None = None


class DataSpecProperties(CollectionProperties):
    class_: Annotated[Literal["DataSpecProperties"], Field(alias="class")]


class DataDictionaryProperties(CollectionProperties):
    class_: Annotated[Literal["DataDictionaryProperties"], Field(alias="class")]


class AgreementProperties(CollectionProperties):
    class_: Annotated[Literal["AgreementProperties"], Field(alias="class")]
    identifier: str | None = None
    user_defined_status: str | None = None


class DataSharingAgreementProperties(AgreementProperties):
    class_: Annotated[Literal["DataSharingAgreementProperties"], Field(alias="class")]

class DigitalSubscriptionProperties(AgreementProperties):
    class_: Annotated[Literal["DigitalSubscriptionProperties"], Field(alias="class")]
    support_level: str | None = None
    service_levels: dict | None = None


class DigitalProductProperties(CollectionProperties):
    class_: Annotated[Literal["DigitalProductProperties"], Field(alias="class")]
    user_defined_status: str | None = None
    product_name: str | None = None
    identifier: str | None = None
    introduction_date: datetime | None = None
    maturity: str | None = None
    service_life: str | None = None
    next_version_date: datetime | None = None
    withdrawal_date: datetime | None = None

class DigitalProductFamilyProperties(CollectionProperties):
    class_: Annotated[Literal["DigitalProductFamilyProperties"], Field(alias="class")]
    user_defined_status: str | None = None
    product_name: str | None = None
    identifier: str | None = None
    introduction_date: datetime | None = None
    maturity: str | None = None
    service_life: str | None = None
    next_version_date: datetime | None = None
    withdrawal_date: datetime | None = None


class DigitalProductCatalogProperties(CollectionProperties):
    class_: Annotated[Literal["DigitalProductCatalogProperties"], Field(alias="class")]


class Collections(PyegeriaModel):
    collection: Union[
        CollectionProperties,
        DigitalSubscriptionProperties,
        DigitalProductProperties,
        DigitalProductFamilyProperties,
        AgreementProperties,
        DataSpecProperties,
        DataDictionaryProperties,
        RootCollectionProperties,
        HomeCollectionProperties,
        CollectionFolderProperties,
        NamespaceProperties,
        ResultsSetProperties,
        RecentAccessProperties,
        WorkItemListProperties,
        FolioProperties,
        DigitalProductCatalogProperties,
        GlossaryProperties,
        DataSharingAgreementProperties,
    ] = Field(discriminator="class_")


class CollectionManager(ServerClient):
    """
    Maintain and explore the contents of nested collections. These collections can be used to represent digital
    products, or collections of resources for a particular project or team. They can be used to organize assets and
    other resources into logical groups.

    Attributes:

        server_name: str
            The name of the View Server to connect to.
        platform_url : str
            URL of the server platform to connect to
        user_id : str
            The identity of the user calling the method - this sets a default optionally used by the methods
            when the user doesn't pass the user_id on a method call.
        user_pwd: str
            The password associated with the user_id. Defaults to None
        token: str
            An optional bearer token

    """


    def __init__(self, view_server: str = None, platform_url: str = None, user_id: str = None, user_pwd: Optional[str] = None, token: Optional[str] = None, ):
        ServerClient.__init__(self, view_server, platform_url, user_id, user_pwd, token)
        self.view_server = self.server_name
        self.platform_url = self.platform_url
        self.user_id = self.user_id
        self.user_pwd = self.user_pwd


        self.collection_command_root: str = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections")
        #
        #       Retrieving Collections - https://egeria-project.org/concepts/collection
        #


    @dynamic_catch
    async def _async_get_attached_collections(self, parent_guid: str, start_from: int = 0, page_size: int = 0,
                                              category: Optional[str] = None, classification_names: list[str]= None,
                                              body: Optional[dict | FilterRequestBody] = None, output_format: str = "JSON",
                                              report_spec: str | dict = None) -> list | str:
        """Returns the list of collections that are linked off of the supplied element using the ResourceList
           relationship. Async version.

        Parameters
        ----------
        parent_guid: str
            The identity of the parent to find linked collections from.
        start_from: int, [default=0], optional
                    When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        body: dict, optional, default = None
            If supplied, adds addition request details - for instance, to filter the results on collectionType
        output_format: str, default = "JSON"
            - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
         report_spec: str | dict = None), optional, default = None
            The desired output columns/fields to include.

        Returns
        -------
        List

        A list of collections linked off of the supplied element.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Sample body:
        {
          "class": "FilterRequestBody",
          "asOfTime": "{{$isoTimestamp}}",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false,
          "limitResultsByStatus": ["ACTIVE"],
          "sequencingOrder": "PROPERTY_ASCENDING",
          "sequencingProperty": "qualifiedName",
          "filter": "Add collectionType value here"
        }

        """

        url = (f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/"
               f"metadata-elements/{parent_guid}/collections")

        response = await self._async_make_request("POST", url, body_slimmer(body))
        elements = response.json().get("elements", NO_ELEMENTS_FOUND)
        if type(elements) is str:
            logger.info(NO_ELEMENTS_FOUND)
            return NO_ELEMENTS_FOUND

        if output_format != 'JSON':  # return a simplified markdown representation
            logger.info(f"Found elements, output format: {output_format}, report_spec: {report_spec}")
            return self._generate_collection_output(elements, None, None, output_format,
                                                    report_spec=report_spec)
        return elements


    def get_attached_collections(self, parent_guid: str, start_from: int = 0, page_size: int = 0, body: dict = None,
                                 output_format: str = "JSON", report_spec: str | dict = None) -> list:
        """Returns the list of collections that are linked off of the supplied element using the ResourceList
           relationship. Async version.

        Parameters
        ----------
        parent_guid: str
            The identity of the parent to find linked collections from.
        start_from: int, [default=0], optional
                    When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        body: dict, optional, default = None
            If supplied, adds addition request details - for instance, to filter the results on collectionType
        output_format: str, default = "JSON"
            - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
         report_spec: str | dict = None), optional, default = None
                The desired output columns/fields to include.


        Returns
        -------
        List

        A list of collections linked off of the supplied element.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Sample body:
        {
          "class": "FilterRequestBody",
          "asOfTime": "{{$isoTimestamp}}",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false,
          "limitResultsByStatus": ["ACTIVE"],
          "sequencingOrder": "PROPERTY_ASCENDING",
          "sequencingProperty": "qualifiedName",
          "filter": "Add collectionType value here"
        }

        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_get_attached_collections(parent_guid, start_from, page_size,
                                                 body, output_format, report_spec))


    @dynamic_catch
    async def _async_find_collections(
            self,
            search_string: str = "*",
            body: Optional[dict | SearchStringRequestBody] = None,
            starts_with: bool = True,
            ends_with: bool = False,
            ignore_case: bool = False,
            start_from: int = 0,
            page_size: int = 100,
            output_format: str = "JSON",
            report_spec: str | dict = None,
            _type: str = "Collection",
            **kwargs
    ) -> list | str:
        """ Retrieve the list of collection metadata elements that contain the search string. Async Version.

        Parameters
        ----------
        search_string : str, default "*"
            Search string to match against - None or '*' indicate match against all collections.
        body : dict | SearchStringRequestBody, optional
            Request body. If provided, overrides other parameters.
        starts_with : bool, default True
            Starts with the supplied string.
        ends_with : bool, default False
            Ends with the supplied string.
        ignore_case : bool, default False
            Ignore case when searching.
        start_from : int, default 0
            Starting index for pagination.
        page_size : int, default 100
            Number of items to return in a single page.
        output_format : str, default "JSON"
            Output format: "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON".
        report_spec : str | dict, optional
            Report specification for output formatting.
        _type : str, default "Collection"
            The type of element to search for.
        **kwargs : dict, optional
            Additional parameters supported by the underlying find request:
            
            - anchor_domain : str - Domain to anchor the search
            - metadata_element_type : str - Specific metadata element type
            - metadata_element_subtypes : list[str] - List of metadata element subtypes
            - skip_relationships : list[str] - Relationship types to skip
            - include_only_relationships : list[str] - Only include these relationship types
            - skip_classified_elements : list[str] - Skip elements with these classifications
            - include_only_classified_elements : list[str] - Only include elements with these classifications
            - graph_query_depth : int - Depth of graph traversal (default 3)
            - governance_zone_filter : list[str] - Filter by governance zones
            - as_of_time : str - Historical query time (ISO 8601 format)
            - effective_time : str - Effective time for the query (ISO 8601 format)
            - relationship_page_size : int - Page size for relationships
            - limit_results_by_status : list[str] - Filter by element status
            - sequencing_order : str - Order of results
            - sequencing_property : str - Property to sequence by
            - property_names : list[str] - Specific properties to search

        Returns
        -------
        list | str
            List of collections in the requested format.

        Raises
        ------
        ValidationError
            If the client passes incorrect parameters on the request that don't conform to the data model.
        PyegeriaException
            Issues raised in communicating or server side processing.
        NotAuthorizedException
            The principle specified by the user_id does not have authorization for the requested action.
        """
        url = str(HttpUrl(f"{self.collection_command_root}/by-search-string"))
        
        # Merge explicit parameters with kwargs
        params = {
            'search_string': search_string,
            'body': body,
            'starts_with': starts_with,
            'ends_with': ends_with,
            'ignore_case': ignore_case,
            'start_from': start_from,
            'page_size': page_size,
            'output_format': output_format,
            'report_spec': report_spec
        }
        params.update(kwargs)
        
        # Filter out None values, but keep search_string even if None (it's required)
        params = {k: v for k, v in params.items() if v is not None or k == 'search_string'}
        
        response = await self._async_find_request(url, _type=_type, _gen_output=self._generate_collection_output,
                                                  **params)
        return response

    @dynamic_catch
    def find_collections(
            self,
            search_string: str = "*",
            body: Optional[dict | SearchStringRequestBody] = None,
            starts_with: bool = True,
            ends_with: bool = False,
            ignore_case: bool = False,
            start_from: int = 0,
            page_size: int = 100,
            output_format: str = "JSON",
            report_spec: str | dict = None,
            _type: str = "Collection",
            **kwargs
    ) -> list | str:
        """ Retrieve the list of collection metadata elements that contain the search string.

        Parameters
        ----------
        search_string : str, default "*"
            Search string to match against - None or '*' indicate match against all collections.
        body : dict | SearchStringRequestBody, optional
            Request body. If provided, overrides other parameters.
        starts_with : bool, default True
            Starts with the supplied string.
        ends_with : bool, default False
            Ends with the supplied string.
        ignore_case : bool, default False
            Ignore case when searching.
        start_from : int, default 0
            Starting index for pagination.
        page_size : int, default 100
            Number of items to return in a single page.
        output_format : str, default "JSON"
            Output format: "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON".
        report_spec : str | dict, optional
            Report specification for output formatting.
        _type : str, default "Collection"
            The type of element to search for.
        **kwargs : dict, optional
            Additional parameters supported by the underlying find request:
            
            - anchor_domain : str - Domain to anchor the search
            - metadata_element_type : str - Specific metadata element type
            - metadata_element_subtypes : list[str] - List of metadata element subtypes
            - skip_relationships : list[str] - Relationship types to skip
            - include_only_relationships : list[str] - Only include these relationship types
            - skip_classified_elements : list[str] - Skip elements with these classifications
            - include_only_classified_elements : list[str] - Only include elements with these classifications
            - graph_query_depth : int - Depth of graph traversal (default 3)
            - governance_zone_filter : list[str] - Filter by governance zones
            - as_of_time : str - Historical query time (ISO 8601 format)
            - effective_time : str - Effective time for the query (ISO 8601 format)
            - relationship_page_size : int - Page size for relationships
            - limit_results_by_status : list[str] - Filter by element status
            - sequencing_order : str - Order of results
            - sequencing_property : str - Property to sequence by
            - property_names : list[str] - Specific properties to search

        Returns
        -------
        list | str
            List of collections in the requested format.

        Raises
        ------
        ValidationError
            If the client passes incorrect parameters on the request that don't conform to the data model.
        PyegeriaException
            Issues raised in communicating or server side processing.
        NotAuthorizedException
            The principle specified by the user_id does not have authorization for the requested action.
        """
        loop = asyncio.get_event_loop()
        return loop.run_until_complete(
            self._async_find_collections(
                search_string, body, starts_with, ends_with, ignore_case,
                start_from, page_size, output_format, report_spec, _type, **kwargs
            )
        )


    @dynamic_catch
    async def _async_find_digital_products(
        self,
        search_string: str = "*",
        deployment_status_list: Optional[list[str]] = None,
        starts_with: bool = True,
        ends_with: bool = False,
        ignore_case: bool = False,
        start_from: int = 0,
        page_size: int = 100,
        output_format: str = "JSON",
        report_spec: str | dict = None,
        body: Optional[dict | DeploymentStatusSearchString] = None,
    ) -> list | str:
        """
        Returns the list of digital products matching the search string and optional deployment status.

        Parameters
        ----------
        search_string: str, default = "*"
            - the search string to use to find matching digital products
        deployment_status_list: list[str], optional
            - optional deployment status list to filter by
        starts_with: bool, default = True
            - if True, the search string must match the start of the property value
        ends_with: bool, default = False
            - if True, the search string must match the end of the property value
        ignore_case: bool, default = False
            - if True, the search is case-insensitive
        start_from: int, default = 0
            - the starting point in the results list
        page_size: int, default = 100
            - the maximum number of results to return
        output_format: str, default = "JSON"
            - the format of the output (JSON, DICT, etc.)
        report_spec: str | dict, optional
            - the report specification to use for the output
        body: dict | DeploymentStatusSearchString, optional
            - the request body to use for the request. If specified, this takes precedence over other parameters.

        Returns
        -------
        list | str
            - a list of digital products or a string message if no products are found

        Note:
        -----
        Sample body:
        {
          "class": "DeploymentStatusSearchString",
          "searchString" : "add search string here",
          "deploymentStatusList" : ["ACTIVE"],
          "startsWith" : false,
          "endsWith" : false,
          "ignoreCase" : true,
          "startFrom" : 0,
          "pageSize": 0
        }
        """
        url = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager"
            f"/digital-products/by-search-string"
        )

        if isinstance(body, DeploymentStatusSearchString):
            validated_body = body
        elif isinstance(body, dict):
            validated_body = self._deployment_status_search_request_adapter.validate_python(body)
        else:
            search_string = None if search_string == "*" else search_string
            body_dict = {
                "class": "DeploymentStatusSearchString",
                "searchString": search_string,
                "deploymentStatusList": deployment_status_list,
                "startsWith": starts_with,
                "endsWith": ends_with,
                "ignoreCase": ignore_case,
                "startFrom": start_from,
                "pageSize": page_size,
            }
            validated_body = DeploymentStatusSearchString.model_validate(body_dict)

        json_body = validated_body.model_dump_json(indent=2, exclude_none=True, by_alias=True)
        response = await self._async_make_request("POST", url, json_body)
        elements = response.json().get("elements", NO_ELEMENTS_FOUND)

        if type(elements) is str or len(elements) == 0:
            logger.info(NO_ELEMENTS_FOUND)
            return NO_ELEMENTS_FOUND

        if output_format.upper() != "JSON":
            return self._generate_collection_output(
                elements, search_string, "DigitalProduct", output_format, report_spec
            )
        return elements

    def find_digital_products(
        self,
        search_string: str = "*",
        deployment_status_list: Optional[list[str]] = None,
        starts_with: bool = True,
        ends_with: bool = False,
        ignore_case: bool = False,
        start_from: int = 0,
        page_size: int = 100,
        output_format: str = "JSON",
        report_spec: str | dict = None,
        body: Optional[dict | DeploymentStatusSearchString] = None,
    ) -> list | str:
        """
        Returns the list of digital products matching the search string and optional deployment status. Sync version.

        See _async_find_digital_products for more details.
        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_find_digital_products(
                search_string,
                deployment_status_list,
                starts_with,
                ends_with,
                ignore_case,
                start_from,
                page_size,
                output_format,
                report_spec,
                body,
            )
        )

    @dynamic_catch
    async def _async_get_digital_products_by_category(
        self,
        category: str,
        deployment_status_list: Optional[list[str]] = None,
        start_from: int = 0,
        page_size: int = 100,
        output_format: str = "JSON",
        report_spec: str | dict = None,
        body: Optional[dict | DeploymentStatusFilterRequestBody] = None,
    ) -> list | str:
        """
        Returns the list of digital products matching the category and optional deployment status.

        Parameters
        ----------
        category: str
            - the category to use to find matching digital products
        deployment_status_list: list[str], optional
            - optional deployment status list to filter by
        start_from: int, default = 0
            - the starting point in the results list
        page_size: int, default = 100
            - the maximum number of results to return
        output_format: str, default = "JSON"
            - the format of the output (JSON, DICT, etc.)
        report_spec: str | dict, optional
            - the report specification to use for the output
        body: dict | DeploymentStatusFilterRequestBody, optional
            - the request body to use for the request. If specified, this takes precedence over other parameters.

        Returns
        -------
        list | str
            - a list of digital products or a string message if no products are found

        Note:
        -----
        Sample body:
        {
          "class" : "DeploymentStatusFilterRequestBody",
          "filter" : "xxx",
          "deploymentStatusList" : ["ACTIVE"],
          "startFrom" : 0,
          "pageSize": 0
        }
        """
        url = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager"
            f"/digital-products/by-category"
        )

        if isinstance(body, DeploymentStatusFilterRequestBody):
            validated_body = body
        elif isinstance(body, dict):
            validated_body = self._deployment_status_filter_request_adapter.validate_python(body)
        else:
            body_dict = {
                "class": "DeploymentStatusFilterRequestBody",
                "filter": category,
                "deploymentStatusList": deployment_status_list,
                "startFrom": start_from,
                "pageSize": page_size,
            }
            validated_body = DeploymentStatusFilterRequestBody.model_validate(body_dict)

        json_body = validated_body.model_dump_json(indent=2, exclude_none=True, by_alias=True)
        response = await self._async_make_request("POST", url, json_body)
        elements = response.json().get("elements", NO_ELEMENTS_FOUND)

        if type(elements) is str or len(elements) == 0:
            logger.info(NO_ELEMENTS_FOUND)
            return NO_ELEMENTS_FOUND

        if output_format.upper() != "JSON":
            return self._generate_collection_output(
                elements, category, "DigitalProduct", output_format, report_spec
            )
        return elements

    def get_digital_products_by_category(
        self,
        category: str,
        deployment_status_list: Optional[list[str]] = None,
        start_from: int = 0,
        page_size: int = 100,
        output_format: str = "JSON",
        report_spec: str | dict = None,
        body: Optional[dict | DeploymentStatusFilterRequestBody] = None,
    ) -> list | str:
        """
        Returns the list of digital products matching the category and optional deployment status. Sync version.

        See _async_get_digital_products_by_category for more details.
        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_get_digital_products_by_category(
                category,
                deployment_status_list,
                start_from,
                page_size,
                output_format,
                report_spec,
                body,
            )
        )

    @dynamic_catch
    async def _async_get_collections_by_name(self, filter_string: Optional[str] = None, classification_names: Optional[list[str]] = None,
                                             body: Optional[dict | FilterRequestBody] = None,
                                             start_from: int = 0, page_size: int = 0,
                                             output_format: str = 'JSON',
                                             report_spec: str | dict = None) -> list | str:
        """ Returns the list of collections with a particular name.

            Parameters
            ----------
            name: str,
                name to use to find matching collections.
            classification_names: list[str], optional, default = None
                type of collection to filter by - e.g., DataDict, Folder, Root
            body: dict, optional, default = None
                Provides, a full request body. If specified, the body supercedes the name parameter.
            start_from: int, [default=0], optional
                        When multiple pages of results are available, the page number to start from.
            page_size: int, [default=None]
                The number of items to return in a single page. If not specified, the default will be taken from
                the class instance.
            output_format: str, default = "JSON"
                - one of "DICT", "MERMAID" or "JSON"
            report_spec: dict , optional, default = None
                The desired output columns/fields to include.

            Returns
            -------
            List | str

            A list of collections match matching the name. Returns a string if none found.

            Raises
            ------

            PyegeriaInvalidParameterException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            PyegeriaAPIException
              Raised by the server when an issue arises in processing a valid request
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action
        """
        url = str(HttpUrl(f"{self.collection_command_root}/by-name"))
        response = await self._async_get_name_request(url, _type="Collection",
                                                      _gen_output=self._generate_collection_output,
                                                      filter_string=filter_string,
                                                      classification_names=classification_names, start_from=start_from,
                                                      page_size=page_size, output_format=output_format,
                                                      report_spec=report_spec, body=body)

        return response


    def get_collections_by_name(self, filter_string: Optional[str] = None, classification_names: Optional[list[str]] = None,
                                body: Optional[dict | FilterRequestBody] = None,
                                start_from: int = 0, page_size: int = 0, output_format: str = 'JSON',
                                report_spec: str | dict = None) -> list | str:
        """Returns the list of collections matching the search string. Async version.
            The search string is located in the request body and is interpreted as a plain string.
            The request parameters, startsWith, endsWith and ignoreCase can be used to allow a fuzzy search.

        Parameters
        ----------
        filter_string: str,
            name to use to find matching collections.
        classification_names: list[str], optional, default = None
            type of collection to filter by - e.g., DataDict, Folder, Root
        body: dict, optional, default = None
            Provides, a full request body. If specified, the body supercedes the name parameter.
        start_from: int, [default=0], optional
                    When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        output_format: str, default = "JSON"
            - one of "DICT", "MERMAID" or "JSON"
         report_spec: str | dict, optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        List | str

        A list of collections match matching the search string. Returns a string if none found.

        Raises
        ------
        PyegeriaException

        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_get_collections_by_name(filter_string, classification_names, body, start_from, page_size,
                                                output_format, report_spec))


    @dynamic_catch
    async def _async_get_collections_by_category(self, category: str, classification_names: Optional[list[str]] = None,
                                             body: Optional[dict | FilterRequestBody] = None, start_from: int = 0, page_size: int = 0,
                                             output_format: str = 'JSON',
                                             report_spec: str | dict = None) -> list | str:
        """Returns the list of collections with a particular category. This is an optional text field in the
            collection element.

        Parameters
        ----------
        category: str
            category to use to find matching collections.
        classification_names: str, optional
            An optional filter on the search, e.g., DataSpec
        body: dict, optional, default = None
            Provides, a full request body. If specified, the body filter parameter supercedes the collection_type
            parameter.
        start_from: int, [default=0], optional
                    When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        output_format: str, default = "JSON"
            - one of "DICT", "MERMAID" or "JSON"
         report_spec: str | dict , optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        List | str

        A list of collections matching the collection type. Returns a string if none found.

        Raises
        ------
        PyegeriaException

        Notes
        -----
        Body sample:

        {
          "class": "FilterRequestBody",
          "asOfTime": "{{$isoTimestamp}}",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false,
          "limitResultsByStatus": ["ACTIVE"],
          "sequencingOrder": "PROPERTY_ASCENDING",
          "sequencingProperty": "qualifiedName",
          "filter": "Add category here"
        }

        """
        url = str(HttpUrl(f"{self.collection_command_root}/by-collection-category"))
        response = await self._async_get_name_request(url, _type="Collection",
                                                      _gen_output=self._generate_collection_output,
                                                      filter_string=category, classification_names=classification_names,
                                                      start_from=start_from, page_size=page_size,
                                                      output_format=output_format, report_spec=report_spec, body=body)

        return response



    def get_collections_by_category(self, category: str, classification_names: Optional[list[str]] = None,
                                body: Optional[dict | FilterRequestBody] = None,
                                start_from: int = 0, page_size: int = 0, output_format: str = 'JSON',
                                report_spec: str | dict = None) -> list | str:
        """Returns the list of collections with a particular collectionType. This is an optional text field in the
            collection element.

        Parameters
        ----------
        category: str
            category to use to find matching collections.
        classification_names: list[str], optional
            An optional filter on the search, e.g., DataSpec
        body: dict, optional, default = None
            Provides, a full request body. If specified, the body filter parameter supersedes the category
            parameter.
        start_from: int, [default=0], optional
                    When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        output_format: str, default = "JSON"
            - one of "DICT", "MERMAID" or "JSON"
        report_spec: str | dict, optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        List | str

        A list of collections filtered by the specified category. Output based on specified output format.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Body sample:

        {
          "class": "FilterRequestBody",
          "asOfTime": "{{$isoTimestamp}}",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false,
          "limitResultsByStatus": ["ACTIVE"],
          "sequencingOrder": "PROPERTY_ASCENDING",
          "sequencingProperty": "qualifiedName",
          "filter": "Add category here"
        }

        """

        return asyncio.get_event_loop().run_until_complete(
            self._async_get_collections_by_category(category, classification_names, body, start_from, page_size,
                                                output_format, report_spec))


    @dynamic_catch
    async def _async_get_collection_by_guid(self, collection_guid: str, element_type: Optional[str] = None,
                                            body: Optional[dict | GetRequestBody] = None,
                                            output_format: str = 'JSON',
                                            report_spec: str | dict = None) -> dict | str:
        """Return the properties of a specific collection. Async version.

        Parameters
        ----------
        collection_guid: str,
            unique identifier of the collection.
        element_type: str, default = None, optional
            type of collection - Collection, DataSpec, Agreement, etc.
        body: dict | GetRequestBody, optional, default = None
            full request body.
        output_format: str, default = "JSON"
            - one of "DICT", "MERMAID" or "JSON"
         report_spec: str | dict, optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        dict | str

        A JSON dict representing the specified collection. Returns a string if none found.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        ----
        Body sample:
        {
          "class": "GetRequestBody",
          "asOfTime": "{{$isoTimestamp}}",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """

        url = str(HttpUrl(f"{self.collection_command_root}/{collection_guid}/retrieve"))
        type = element_type if element_type else "Collection"

        response = await self._async_get_guid_request(url, _type=type,
                                                  _gen_output=self._generate_collection_output,
                                                  output_format=output_format, report_spec=report_spec,
                                                  body=body)

        return response




    def get_collection_by_guid(self, collection_guid: str, element_type: Optional[str] = None, body: dict | GetRequestBody= None,
                               output_format: str = 'JSON', report_spec: str | dict = None) -> dict | str:
        """ Return the properties of a specific collection. Async version.

            Parameters
            ----------
            collection_guid: str,
                unique identifier of the collection.
            element_type: str, default = None, optional
                type of element - Collection, DataSpec, Agreement, etc.
            body: dict | GetRequestBody, optional, default = None
                full request body.
            output_format: str, default = "JSON"
                - one of "DICT", "MERMAID" or "JSON"
            report_spec: dict , optional, default = None
                The desired output columns/fields to include.


            Returns
            -------
            dict | str

            A JSON dict representing the specified collection. Returns a string if none found.

            Raises
            ------

            PyegeriaInvalidParameterException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            PyegeriaAPIException
              Raised by the server when an issue arises in processing a valid request
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

            Notes
            ----
            Body sample:
            {
              "class": "AnyTimeRequestBody",
              "asOfTime": "{{$isoTimestamp}}",
              "effectiveTime": "{{$isoTimestamp}}",
              "forLineage": false,
              "forDuplicateProcessing": false
            }
        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_get_collection_by_guid(collection_guid, element_type, body,
                                               output_format, report_spec))


    @dynamic_catch
    async def _async_get_collection_members(self, collection_guid: Optional[str] = None, collection_name: Optional[str] = None,
                                            collection_qname: Optional[str] = None, body: Optional[dict | ResultsRequestBody] = None, start_from: int = 0,
                                            page_size: int = 0, output_format: str = "JSON",
                                            report_spec: str | dict = None) -> list | str:
        """Return a list of elements that are a member of a collection. Async version.

        Parameters
        ----------
        collection_guid: str,
            identity of the collection to return members for. If none, collection_name or
            collection_qname are used.
        collection_name: str,
            display the name of the collection to return members for. If none, collection_guid
            or collection_qname are used.
        collection_qname: str,
            qualified name of the collection to return members for. If none, collection_guid
            or collection_name are used.
        body: dict, optional, default = None
            Providing the body allows full control of the request and replaces filter parameters.
        start_from: int, [default=0], optional
                    When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        output_format: str, default = "JSON"
            - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
         report_spec: str | dict , optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        List | str

        A list of collection members in the collection.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        Body sample:
        {
          "class": "ResultsRequestBody",
          "effectiveTime": "{{$isoTimestamp}}",
          "limitResultsByStatus": ["ACTIVE"],
          "asOfTime": "{{$isoTimestamp}}",
          "sequencingOrder": "CREATION_DATE_RECENT",
          "sequencingProperty": ""
        }

        """

        if collection_guid is None:
            collection_guid = self.__get_guid__(collection_guid, collection_name, "displayName",
                                                collection_qname, None, )

        url = str(HttpUrl(f"{self.collection_command_root}/{collection_guid}/members"))


        response = await self._async_get_results_body_request(url, _type="Collection",
                                                  _gen_output=self._generate_collection_output,
                                                  output_format=output_format, report_spec=report_spec,
                                                  body=body)

        return response


    def get_collection_members(self, collection_guid: Optional[str] = None, collection_name: Optional[str] = None,
                               collection_qname: Optional[str] = None, body: dict = None, start_from: int = 0,
                               page_size: int = 0,
                               output_format: str = "JSON", report_spec: str | dict = None) -> list | str:
        """Return a list of elements that are a member of a collection.
        Parameters
        ----------
        collection_guid: str,
            identity of the collection to return members for. If none, collection_name or
            collection_qname are used.
        collection_name: str,
            display the name of the collection to return members for. If none, collection_guid
            or collection_qname are used.
        collection_qname: str,
            qualified name of the collection to return members for. If none, collection_guid
            or collection_name are used.
        body: dict, optional, default = None
            Providing the body allows full control of the request and replaces filter parameters.
        start_from: int, [default=0], optional
            When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        output_format: str, default = "JSON"
            - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
         report_spec: str | dict , optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        List | str

        A list of collection members in the collection.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        Body sample:
        {
          "class": "ResultsRequestBody",
          "effectiveTime": "{{$isoTimestamp}}",
          "limitResultsByStatus": ["ACTIVE"],
          "asOfTime": "{{$isoTimestamp}}",
          "sequencingOrder": "CREATION_DATE_RECENT",
          "sequencingProperty": ""
        }

    """
        loop = asyncio.get_event_loop()
        resp = loop.run_until_complete(
            self._async_get_collection_members(collection_guid, collection_name, collection_qname, body, start_from,
                                               page_size, output_format, report_spec))

        return resp


    @dynamic_catch
    async def _async_get_collection_hierarchy(self, collection_guid: str, body: Optional[dict | ResultsRequestBody] = None, start_from: int = 0,
                                          page_size: int = 0, output_format: str = "JSON",
                                          report_spec: str | dict = None) -> list | str:
        """ Return a hierarchy of nested collections. Request body is optional Async version.

            Parameters
            ----------
            collection_guid: str,
                identity of the collection to return members for. If none, collection_name or
                collection_qname are used.
            body: dict | ResultsRequestBody, optional, default = None
                Providing the body allows full control of the request and replaces filter parameters.
            start_from: int, [default=0], optional
                        When multiple pages of results are available, the page number to start from.
            page_size: int, [default=None]
                The number of items to return in a single page. If not specified, the default will be taken from
                the class instance.
            output_format: str, default = "JSON"
                - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
             report_spec: dict , optional, default = None
                The desired output columns/fields to include.

            Returns
            -------
            List | str

           Results based on the output format.

            Raises
            ------

            PyegeriaInvalidParameterException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            PyegeriaAPIException
              Raised by the server when an issue arises in processing a valid request
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

            Notes:
            -----
                Body sample:
            {
              "class": "ResultsRequestBody",
              "startFrom": 0,
              "pageSize": 0,
              "effectiveTime": "{{$isoTimestamp}}",
              "limitResultsByStatus": ["ACTIVE"],
              "asOfTime": "{{$isoTimestamp}}",
              "sequencingOrder": "CREATION_DATE_RECENT",
              "sequencingProperty": ""
            }
            """

        url = str(HttpUrl(f"{self.collection_command_root}/{collection_guid}/hierarchy"))
        response = await self._async_get_results_body_request(url, _type="Collection",
                                                              _gen_output=self._generate_collection_output,
                                                              start_from=start_from, page_size=page_size,
                                                              output_format=output_format,
                                                              report_spec=report_spec,
                                                              body=body)

        return response

    def get_collection_hierarchy(self, collection_guid: Optional[str] = None, body: dict| ResultsRequestBody = None, start_from: int = 0,
                             page_size: int = 0, output_format: str = "JSON",
                             report_spec: str | dict = None) -> list | str:
        """ Return a graph of elements that are the nested members of a collection along
            with elements immediately connected to the starting collection.  The result
            includes a mermaid graph of the returned elements.

        Parameters
        ----------
        collection_guid: str,
            identity of the collection to return members for. If none, collection_name or
            collection_qname are used.
        body: dict, optional, default = None
            Providing the body allows full control of the request and replaces filter parameters.
        start_from: int, [default=0], optional
                    When multiple pages of results are available, the page number to start from.
        page_size: int, [default=None]
            The number of items to return in a single page. If not specified, the default will be taken from
            the class instance.
        output_format: str, default = "JSON"
            - one of "MD", "LIST", "FORM", "REPORT", "DICT", "MERMAID" or "JSON"
         report_spec: str | dict , optional, default = None
                The desired output columns/fields to include.

        Returns
        -------
        List | str

        A graph anchored in the collection.

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
            Body sample:
            {
              "class": "ResultsRequestBody",
              "effectiveTime": "{{$isoTimestamp}}",
              "limitResultsByStatus": ["ACTIVE"],
              "asOfTime": "{{$isoTimestamp}}",
              "sequencingOrder": "CREATION_DATE_RECENT",
              "sequencingProperty": ""
            }
        """
        return asyncio.get_event_loop().run_until_complete(
            self._async_get_collection_hierarchy(collection_guid, body, start_from, page_size,
                                             output_format, report_spec))


    @dynamic_catch
    async def _async_create_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                       category: Optional[str] = None, initial_classifications: Optional[list[str]] = None, prop: list[str] = ["Collection"],
                                       body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new generic collection. If the body is not present, the display_name, description, category
            and classification will be used to create a simple, self-anchored collection.
            Collections: https://egeria-project.org/concepts/collection
            Async version.

        Parameters
        ----------
        display_name: str, optional
            The display name of the collection. The display name will be used to manufacture a qualified name. An
            exception will be raised if a unique qualified name can't be manufactured.
        description: str, optional
            The description of the collection.
        category: str, optional
            An optional user-assigned category for the collection.
        initial_classifications: list[str], optional
            An initial list of classifications for the collection. This can be used to distinguish, for instance, Folders
            from Root Collections.
        prop: list[str], optional
            The kind of collection property to use.
        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple:
        {
          "class": "NewElementRequestBody",
          "isOwnAnchor": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          }
        }

        anchored:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        a root collection:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "initialClassifications": {
            "RootCollection": {}
          },
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """
        if body:
            validated_body = self.validate_new_element_request(body, "CollectionProperties")
        elif (body is None) and (display_name is not None):
            # if initial_classifications:
            #     pre = initial_classifications[0]
            # else:
            pre = prop[0]

            qualified_name = self.__create_qualified_name__(pre, display_name)
            if initial_classifications:
                initial_classifications_dict = {}
                for c in initial_classifications:
                    initial_classifications_dict = initial_classifications_dict | {c: {"class": f"{c}Properties"}}

            else:
                initial_classifications_dict = None

            collection_properties = CollectionProperties(class_="CollectionProperties",
                                                         qualified_name=qualified_name,
                                                         display_name=display_name,
                                                         description=description,
                                                         category=category,
                                                         typeName=prop[0]
                                                         )
            body = {
                "class": "NewElementRequestBody",
                "isOwnAnchor": True,
                "initialClassifications": initial_classifications_dict,
                "properties": collection_properties.model_dump()
            }
            validated_body = NewElementRequestBody.model_validate(body)
        else:
            raise PyegeriaInvalidParameterException(additional_info={"reason": "Invalid input parameters"})

        url = f"{self.collection_command_root}"
        response = await self._async_create_element_body_request(url, prop, validated_body)
        return response


    def create_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                category: Optional[str] = None, initial_classifications: Optional[list[str]] = None, prop:list[str]=["Collection"],
                                body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new generic collection. If the body is not present, the display_name, description, category
            and classification will be used to create a simple, self-anchored collection.
            Collections: https://egeria-project.org/concepts/collection

        Parameters
        ----------
        display_name: str, optional
            The display name of the collection. The display name will be used to manufacture a qualified name. An
            exception will be raised if a unique qualified name can't be manufactured.
        description: str, optional
            The description of the collection.
        category: str, optional
            An optional user-assigned category for the collection.
        initial_classifications: str, optional
            An initial classification for the collection. This can be used to distinguish, for instance, Folders
            from Root Collections.
        prop: str, optional
            The types of the collection.
        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple:
        {
          "class": "NewElementRequestBody",
          "isOwnAnchor": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          }
        }

        anchored:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        a root collection:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "initialClassifications": {
            "RootCollection": {}
          },
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }


             """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_collection(display_name, description,category,
                                                 initial_classifications,  prop,  body))


    @dynamic_catch
    def create_root_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                      category: Optional[str] = None,  body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new collection with the RootCollection classification.  Used to identify the top of a
            collection hierarchy.
            Create Collections: https://egeria-project.org/concepts/collection
            Async version.

            Parameters
            ----------
            display_name: str
                The display name of the element. Will also be used as the basis of the qualified_name.
            description: str
                A description of the collection.
            category: str
                Adds an user supplied valid value for the collection type.
            body: dict | NewElementRequestBody, Optional = None
                The body of the collection request. Supersedes other parameters.

            Returns
            -------
            str - the guid of the created collection

            Raises
            ------
            PyegeriaException
              Error in the request or response
            ValueError
              Pydantic validation error
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

        """
        prop = "RootCollection"

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_collection(display_name=display_name, description=description, category=category,
                                         initial_classifications = [], prop = [prop], body=body))

    @dynamic_catch
    def create_folder_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                      category: Optional[str] = None, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new collection with the RootCollection classification.  Used to identify the top of a
            collection hierarchy.
            Create Collections: https://egeria-project.org/concepts/collection
            Async version.

            Parameters
            ----------
            display_name: str
                The display name of the element. Will also be used as the basis of the qualified_name.
            description: str
                A description of the collection.
            category: str
                Adds an user supplied valid value for the collection type.
            body: dict | NewElementRequestBody, Optional = None
                The body of the collection request. Supersedes other parameters.

            Returns
            -------
            str - the guid of the created collection

            Raises
            ------
            PyegeriaException
              Error in the request or response
            ValueError
              Pydantic validation error
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

        """
        prop = "CollectionFolder"

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_collection(display_name=display_name, description=description, category=category,
                                          initial_classifications=[], prop=[prop], body=body))

    @dynamic_catch
    def create_reference_list_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                      category: Optional[str] = None, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new collection with the RootCollection classification.  Used to identify the top of a
            collection hierarchy.
            Create Collections: https://egeria-project.org/concepts/collection
            Async version.

            Parameters
            ----------
            display_name: str
                The display name of the element. Will also be used as the basis of the qualified_name.
            description: str
                A description of the collection.
            category: str
                Adds an user supplied valid value for the collection type.
            body: dict | NewElementRequestBody, Optional = None
                The body of the collection request. Supersedes other parameters.

            Returns
            -------
            str - the guid of the created collection

            Raises
            ------
            PyegeriaException
              Error in the request or response
            ValueError
              Pydantic validation error
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

        """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_collection(display_name, description, category,
                                          ["ReferenceList"], body))

    @dynamic_catch
    def create_context_event_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                      category: Optional[str] = None, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new collection with the RootCollection classification.  Used to identify the top of a
            collection hierarchy.
            Create Collections: https://egeria-project.org/concepts/collection
            Async version.

            Parameters
            ----------
            display_name: str
                The display name of the element. Will also be used as the basis of the qualified_name.
            description: str
                A description of the collection.
            category: str
                Adds an user supplied valid value for the collection type.
            body: dict | NewElementRequestBody, Optional = None
                The body of the collection request. Supersedes other parameters.

            Returns
            -------
            str - the guid of the created collection

            Raises
            ------
            PyegeriaException
              Error in the request or response
            ValueError
              Pydantic validation error
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

        """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_collection(display_name, description, category,
                                          ["ContextEvent"], body))

    # @dynamic_catch
    # def create_glossary(self, display_name: str, description: Optional[str] = None, language: str = "English", usage: Optional[str] = None,
    #                     category: Optional[str] = None, body: Optional[dict | NewElementRequestBody] = None) -> str:
    #     """Create a new glossary with optional classification. """
    #     if body is None:
    #
    #         qualified_name = self.__create_qualified_name__("Glossary", display_name, EGERIA_LOCAL_QUALIFIER)
    #         body = {
    #             "class": "NewElementRequestBody",
    #             "is_own_anchor": True,
    #             "properties": {
    #                 "class": "GlossaryProperties",
    #                 "displayName": display_name,
    #                 "qualifiedName": qualified_name,
    #                 "description": description,
    #                 "language": language,
    #                 "usage": usage,
    #                 "category": category
    #                 },
    #             }
    #     response = self.create_collection(body=body)
    #     return response

    @dynamic_catch
    async def _async_create_data_spec_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                                category: Optional[str] = None, classification_name: Optional[str] = None,
                                                 body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new generic collection. If the body is not present, the display_name, description, category
            and classification will be used to create a simple, self-anchored collection.
            Collections: https://egeria-project.org/concepts/collection
            Async version.

        Parameters
        ----------
        display_name: str, optional
            The display name of the collection. The display name will be used to manufacture a qualified name. An
            exception will be raised if a unique qualified name can't be manufactured.
        description: str, optional
            The description of the collection.
        category: str, optional
            An optional user-assigned category for the collection.
        classification: str, optional
            An initial classification for the collection. This can be used to distinguish, for instance, Folders
            from Root Collections.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple:
        {
          "class": "NewElementRequestBody",
          "isOwnAnchor": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          }
        }

        anchored:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        a root collection:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "initialClassifications": {
            "RootCollection": {}
          },
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """
        if body:
            validated_body = self.validate_new_element_request(body,"DataSpecProperties")
        elif display_name is not None:
            qualified_name = self.__create_qualified_name__("DataSpec", display_name)
            logger.info(f"\n\tDisplayName was {display_name}, classification {classification_name}\n")
            if classification_name:
                initial_classification_data = {
                    classification_name: {
                    "class" : "ClassificationProperties"
                    }
                }
            else:
                initial_classification_dict = None
            collection_properties = DataSpecProperties( class_ = "DataSpecProperties",
                                                             qualified_name = qualified_name,
                                                             display_name = display_name,
                                                             description = description,
                                                             category = category
                                                             )
            body = {
                "class" :"NewElementRequestBody",
                "isOwnAnchor": True,
                "initialClassifications": initial_classification_dict,
                "properties": collection_properties.model_dump()
                }
            validated_body = NewElementRequestBody.model_validate(body)
        else:
            raise PyegeriaInvalidParameterException(additional_info={"reason": "Invalid input parameters"})




        url = f"{self.collection_command_root}"
        json_body = validated_body.model_dump_json(indent=2, exclude_none=True, by_alias=True)
        logger.info(json_body)
        resp = await self._async_make_request("POST", url, json_body, is_json=True)
        logger.info(f"Create collection with GUID: {resp.json().get('guid')}")
        return resp.json().get("guid", NO_GUID_RETURNED)

    @dynamic_catch
    def create_data_spec_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                category: Optional[str] = None, classification_name: Optional[str] = None,
                                body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new generic collection. If the body is not present, the display_name, description, category
            and classification will be used to create a simple, self-anchored collection.
            Collections: https://egeria-project.org/concepts/collection

        Parameters
        ----------
        display_name: str, optional
            The display name of the collection. The display name will be used to manufacture a qualified name. An
            exception will be raised if a unique qualified name can't be manufactured.
        description: str, optional
            The description of the collection.
        category: str, optional
            An optional user-assigned category for the collection.
        classification_name: str, optional
            An initial classification for the collection. This can be used to distinguish, for instance, Folders
            from Root Collections.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple:
        {
          "class": "NewElementRequestBody",
          "isOwnAnchor": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          }
        }

        anchored:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        a root collection:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "initialClassifications": {
            "RootCollection": {}
          },
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }


             """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_data_spec_collection(display_name, description,category,
                                                 classification_name, body))


    @dynamic_catch
    async def _async_create_data_dictionary_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                                category: Optional[str] = None, classification_name: Optional[str] = None,
                                                 body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new generic collection. If the body is not present, the display_name, description, category
            and classification will be used to create a simple, self-anchored collection.
            Collections: https://egeria-project.org/concepts/collection
            Async version.

        Parameters
        ----------
        display_name: str, optional
            The display name of the collection. The display name will be used to manufacture a qualified name. An
            exception will be raised if a unique qualified name can't be manufactured.
        description: str, optional
            The description of the collection.
        category: str, optional
            An optional user-assigned category for the collection.
        classification: str, optional
            An initial classification for the collection. This can be used to distinguish, for instance, Folders
            from Root Collections.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple:
        {
          "class": "NewElementRequestBody",
          "isOwnAnchor": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          }
        }

        anchored:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        a root collection:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "initialClassifications": {
            "RootCollection": {}
          },
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """

        validated_body = self.validate_new_element_request(body,"DataDictionaryProperties")

        if validated_body is None and display_name is not None:
            qualified_name = self.__create_qualified_name__("DataDictionary", display_name)
            initial_classifications_data = {"class" : "ClassificationProperties"}
            if classification_name:
                initial_classification_dict = {
                    classification_name: InitialClassifications.model_validate(initial_classifications_data)
                }
            else:
                initial_classification_dict = None
            collection_properties = DataDictionaryProperties( class_ = "DataDictionaryProperties",
                                                             qualified_name = qualified_name,
                                                             display_name = display_name,
                                                             description = description,
                                                             category = category
                                                             )
            body = {
                "class" :"NewElementRequestBody",
                "isOwnAnchor": True,
                "initialClassifications": initial_classification_dict,
                "properties": collection_properties.model_dump()
                }
            validated_body = NewElementRequestBody.model_validate(body)

        if validated_body is None:
            raise PyegeriaInvalidParameterException(additional_info={"reason": "Invalid input parameters"})


        url = f"{self.collection_command_root}"
        json_body = validated_body.model_dump_json(indent=2, exclude_none=True, by_alias=True)
        logger.info(json_body)
        resp = await self._async_make_request("POST", url, json_body, is_json=True)
        logger.info(f"Create collection with GUID: {resp.json().get('guid')}")
        return resp.json().get("guid", NO_GUID_RETURNED)

    @dynamic_catch
    def create_data_dictionary_collection(self, display_name: Optional[str] = None, description: Optional[str] = None,
                                category: Optional[str] = None, classification_name: Optional[str] = None,
                                body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new generic collection. If the body is not present, the display_name, description, category
            and classification will be used to create a simple, self-anchored collection.
            Collections: https://egeria-project.org/concepts/collection

        Parameters
        ----------
        display_name: str, optional
            The display name of the collection. The display name will be used to manufacture a qualified name. An
            exception will be raised if a unique qualified name can't be manufactured.
        description: str, optional
            The description of the collection.
        category: str, optional
            An optional user-assigned category for the collection.
        classification: str, optional
            An initial classification for the collection. This can be used to distinguish, for instance, Folders
            from Root Collections.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple:
        {
          "class": "NewElementRequestBody",
          "isOwnAnchor": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          }
        }

        anchored:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        a root collection:
        {
          "class": "NewElementRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "anchorScopeGUID": "optional GUID of search scope",
          "initialClassifications": {
            "RootCollection": {}
          },
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "properties": {
            "class": "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }


             """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_data_dictionary_collection(display_name, description,category,
                                                 classification_name, body))






#######


    @dynamic_catch
    async def _async_create_collection_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str:
        """Create a new metadata element to represent a collection using an existing metadata element as a template.
        The template defines additional classifications and relationships that are added to the new collection.
        Async version.

        Parameters
        ----------

        body: dict
            A dict representing the details of the collection to create.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format, or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the DeleteElementRequestBody.
        PyegeriaNotAuthorizedException
            The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:

        {
          "class": "TemplateRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "templateGUID": "template GUID",
          "replacementProperties": {
            "class": "ElementProperties",
            "propertyValueMap" : {
              "propertyName" : {
                "class": "PrimitiveTypePropertyValue",
                "typeName": "string",
                "primitiveTypeCategory" : "OM_PRIMITIVE_TYPE_STRING",
                "primitiveValue" : "value of property"
              }
            }
          },
          "placeholderPropertyValues" : {
            "placeholderProperty1Name" : "property1Value",
            "placeholderProperty2Name" : "property2Value"
          }
        }

        """


        if isinstance(body, TemplateRequestBody):
            validated_body = body

        elif isinstance(body, dict):
            validated_body = self._template_request_adapter.validate_python(body)




        url = f"{self.collection_command_root}/from-template"
        json_body = validated_body.model_dump_json(indent=2, exclude_none=True, by_alias=True)
        logger.info(json_body)
        resp = await self._async_make_request("POST", url, json_body, is_json=True)
        logger.info(f"Create collection from template with GUID: {resp.json().get('guid')}")
        return resp.json().get("guid", NO_GUID_RETURNED)


    def create_collection_from_template(self, body: Optional[dict | TemplateRequestBody] = None) -> str:
        """Create a new metadata element to represent a collection using an existing metadata element as a template.
        The template defines additional classifications and relationships that are added to the new collection.

        Parameters
        ----------
        body: dict
            A dict representing the details of the collection to create.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:

        {
          "class": "TemplateRequestBody",
          "anchorGUID": "anchor GUID, if set then isOwnAnchor=false",
          "isOwnAnchor": false,
          "parentGUID": "parent GUID, if set, set all parameters beginning 'parent'",
          "parentRelationshipTypeName": "open metadata type name",
          "parentAtEnd1": true,
          "templateGUID": "template GUID",
          "replacementProperties": {
            "class": "ElementProperties",
            "propertyValueMap" : {
              "propertyName" : {
                "class": "PrimitiveTypePropertyValue",
                "typeName": "string",
                "primitiveTypeCategory" : "OM_PRIMITIVE_TYPE_STRING",
                "primitiveValue" : "value of property"
              }
            }
          },
          "placeholderPropertyValues" : {
            "placeholderProperty1Name" : "property1Value",
            "placeholderProperty2Name" : "property2Value"
          }
        }
        """
        loop = asyncio.get_event_loop()
        resp = loop.run_until_complete(self._async_create_collection_from_template(body))
        return resp

        #
        # Manage collections
        #



    @dynamic_catch
    async def _async_update_collection(self, collection_guid: str,  body: dict | UpdateElementRequestBody) -> None:
        """ Update the properties of a collection. Use the correct properties object (CollectionProperties,
            DigitalProductProperties, AgreementProperties, etc), that is appropriate for your element.
            Collections: https://egeria-project.org/concepts/collection

            Async version.
        Parameters
        ----------
        collection_guid: str
            The guid of the collection to update.

        body: dict | UpdateElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.


        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple example:
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "CollectionProperties",
            "contentStatus": "Add appropriate valid value for type",
            "qualifiedName": "Must provide a unique name here",
            "displayName" : "Add display name here",
            "description" : "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """

        # try:

        url = (f"{self.collection_command_root}/{collection_guid}/update")
        await self._async_update_element_body_request(url, COLLECTION_PROPERTIES_LIST,body)


    @dynamic_catch
    def update_collection(self, collection_guid: str,  body: dict | NewElementRequestBody) -> None:
        """ Update the properties of a collection. Use the correct properties object (CollectionProperties,
            DigitalProductProperties, AgreementProperties, etc), that is appropriate for your element.
            Collections: https://egeria-project.org/concepts/collection

        Parameters
        ----------
        collection_guid: str
            The guid of the collection to update.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.


        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        simple example:
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "CollectionProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName" : "Add display name here",
            "description" : "Add description of the collection here",
            "contentStatus": "Add appropriate valid value for type",
            "category": "Add appropriate valid value for type"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """

        return asyncio.get_event_loop().run_until_complete(
            self._async_update_collection(collection_guid, body))


    @dynamic_catch
    async def _async_create_digital_product(self, body: dict | NewElementRequestBody) -> str:
        """ Create a new collection that represents a digital product.
            Async version.

        Parameters
        ----------
        body: dict | NewElementRequestBody
            A structure representing the details of the digital product to create.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        ValidationError
          Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
        be valid dates if specified, otherwise you will get a 400 error response.

        JSON Structure looks like:
        {
          "class" : "NewElementRequestBody",
          "isOwnAnchor" : true,
          "anchorScopeGUID" : "optional GUID of search scope",
          "parentGUID" : "xxx",
          "parentRelationshipTypeName" : "CollectionMembership",
          "parentAtEnd1": true,
          "properties": {
            "class" : "DigitalProductProperties",
            "qualifiedName": "DigitalProduct::Add product name here",
            "displayName" : "Product contents",
            "description" : "Add description of product and its expected usage here",
            "identifier" : "Add product identifier here",
            "productName" : "Add product name here",
            "category" : "Periodic Delta",
            "maturity" : "Add valid value here",
            "serviceLife" : "Add the estimated lifetime of the product",
            "introductionDate" : "date",
            "nextVersionDate": "date",
            "withdrawDate": "date",
            "currentVersion": "V0.1",
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "initialStatus" : "ACTIVE"
        }

        The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
        UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
        OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
        default to ACTIVE.
        """
        url = f"{self.collection_command_root}"
        return await self._async_create_element_body_request(url, ["DigitalProductProperties"], body)


    @dynamic_catch
    def create_digital_product(self, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new collection that represents a digital product.

            Parameters
            ----------
            body: dict | NewElementRequestBody
                A structure representing the details of the digital product to create.

            Returns
            -------
            str - the guid of the created collection

            Raises
            ------
            PyegeriaException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            ValidationError
              Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

            Notes
            -----
            Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
            be valid dates if specified, otherwise you will get a 400 error response.

            JSON Structure looks like:
            {
              "class" : "NewElementRequestBody",
              "isOwnAnchor" : true,
              "anchorScopeGUID" : "optional GUID of search scope",
              "parentGUID" : "xxx",
              "parentRelationshipTypeName" : "CollectionMembership",
              "parentAtEnd1": true,
              "properties": {
                "class" : "DigitalProductProperties",
                "qualifiedName": "DigitalProduct::Add product name here",
                "displayName" : "Product contents",
                "description" : "Add description of product and its expected usage here",
                "identifier" : "Add product identifier here",
                "productName" : "Add product name here",
                "category" : "Periodic Delta",
                "maturity" : "Add valid value here",
                "serviceLife" : "Add the estimated lifetime of the product",
                "introductionDate" : "date",
                "nextVersionDate": "date",
                "withdrawDate": "date",
                "currentVersion": "V0.1",
                "additionalProperties": {
                  "property1Name" : "property1Value",
                  "property2Name" : "property2Value"
                }
              },
              "externalSourceGUID": "add guid here",
              "externalSourceName": "add qualified name here",
              "effectiveTime" : "{{$isoTimestamp}}",
              "forLineage" : false,
              "forDuplicateProcessing" : false,
            }

            The valid values for Status are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
            UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
            OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
            default to ACTIVE.
            """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_digital_product(body))


    @dynamic_catch
    async def _async_create_digital_product_catalog(self, body: dict | NewElementRequestBody) -> str:
        """ Create a new collection that represents a digital product.
            Async version.

        Parameters
        ----------
        body: dict | NewElementRequestBody
            A structure representing the details of the digital product to create.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        ValidationError
          Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
        be valid dates if specified, otherwise you will get a 400 error response.

        simple body version:
        {
          "class": "NewElementRequestBody",
          "isOwnAnchor": true,
          "properties": {
            "class": "DigitalProductCatalogProperties",
            "qualifiedName": "Must provide a unique name here",
            "displayName": "Add display name here",
            "description": "Add description of the collection here",
            "category": "Add appropriate valid value for type"
          }
        }

        The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
        UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
        OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
        default to ACTIVE.
        """
        url = f"{self.collection_command_root}"
        return await self._async_create_element_body_request(url, "DigitalProductCatalogProperties", body)


    @dynamic_catch
    def create_digital_product_catalog(self, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new collection that represents a digital product.

            Parameters
            ----------
            body: dict | NewElementRequestBody
                A structure representing the details of the digital product to create.

            Returns
            -------
            str - the guid of the created collection

            Raises
            ------
            PyegeriaException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            ValidationError
              Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

            Notes
            -----
            Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
            be valid dates if specified, otherwise you will get a 400 error response.

             simple body version:
            {
              "class": "NewElementRequestBody",
              "isOwnAnchor": true,
              "properties": {
                "class": "DigitalProductCatalogProperties",
                "qualifiedName": "Must provide a unique name here",
                "displayName": "Add display name here",
                "description": "Add description of the collection here",
                "category": "Add appropriate valid value for type"
              }
            }

            The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
            UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
            OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
            default to ACTIVE.
            """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_digital_product_catalog(body))




    @dynamic_catch
    async def _async_update_digital_product(self, collection_guid: str,  body: dict | UpdateElementRequestBody) -> None:
        """ Update the properties of a digital product.
            Collections: https://egeria-project.org/concepts/collection

            Async version.
        Parameters
        ----------
        collection_guid: str
            The guid of the collection to update.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.


        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "DigitalProductProperties",
            "qualifiedName": "DigitalProduct::Add product name here",
            "displayName" : "Product contents",
            "description" : "Add description of product and its expected usage here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add product identifier here",
            "productName" : "Add product name here",
            "category" : "Periodic Delta",
            "maturity" : "Add valid value here",
            "serviceLife" : "Add the estimated lifetime of the product",
            "introductionDate" : "date",
            "nextVersionDate": "date",
            "withdrawDate": "date",
            "currentVersion": "V0.1",
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """

        url = (f"{self.collection_command_root}/{collection_guid}/update")
        await self._async_update_element_body_request(url, "DigitalProductProperties", body )


    @dynamic_catch
    def update_digital_product(self, collection_guid: str,  body: dict | UpdateElementRequestBody,) -> None:
        """ Update the properties of a digital product..
            Collections: https://egeria-project.org/concepts/collection

        Parameters
        ----------
        collection_guid: str
            The guid of the collection to update.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the collection to create. If supplied, this
            information will be used to create the collection and the other attributes will be ignored. The body is
            validated before being used.


        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "DigitalProductProperties",
            "qualifiedName": "DigitalProduct::Add product name here",
            "displayName" : "Product contents",
            "description" : "Add description of product and its expected usage here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add product identifier here",
            "productName" : "Add product name here",
            "category" : "Periodic Delta",
            "maturity" : "Add valid value here",
            "serviceLife" : "Add the estimated lifetime of the product",
            "introductionDate" : "date",
            "nextVersionDate": "date",
            "withdrawDate": "date",
            "currentVersion": "V0.1",
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """

        return asyncio.get_event_loop().run_until_complete(
            self._async_update_digital_product(collection_guid, body))


    # @dynamic_catch
    # async def _async_link_digital_product_dependency(self, upstream_digital_prod_guid: str,
    #                                                  downstream_digital_prod_guid: str,
    #                                                  body: Optional[dict | NewRelationshipRequestBody] = None):
    #     """ Link two dependent digital products.  The linked elements are of type DigitalProduct.
    #         Request body is optional. Async version.
    #
    #     Parameters
    #     ----------
    #     upstream_digital_prod_guid: str
    #         The guid of the first digital product
    #     downstream_digital_prod_guid: str
    #         The guid of the downstream digital product
    #     body: dict | NewRelationshipRequestBody, optional, default = None
    #         A dict representing the details of the relationship.
    #
    #     Returns
    #     -------
    #     Nothing
    #
    #     Raises
    #     ------
    #     PyegeriaInvalidParameterException
    #       If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #     PyegeriaAPIException
    #       Raised by the server when an issue arises in processing a valid request
    #     NotAuthorizedException
    #       The principle specified by the user_id does not have authorization for the requested action
    #
    #     Notes
    #     -----
    #     JSON Structure looks like:
    #     {
    #       "class" : "NewRelationshipRequestBody",
    #       "externalSourceGUID": "add guid here",
    #       "externalSourceName": "add qualified name here",
    #       "effectiveTime" : "{{$isoTimestamp}}",
    #       "forLineage" : false,
    #       "forDuplicateProcessing" : false,
    #       "properties": {
    #         "class": "DigitalProductDependencyProperties",
    #         "label": "add label here",
    #         "description": "add description here",
    #         "effectiveFrom": "{{$isoTimestamp}}",
    #         "effectiveTo": "{{$isoTimestamp}}"
    #       }
    #     }
    #     """
    #     url = (
    #         f"{self.platform_url}/servers/"
    #         f"{self.view_server}/api/open-metadata/collection-manager/collections/digital-products/"
    #         f"{upstream_digital_prod_guid}/product-dependencies/{downstream_digital_prod_guid}/attach")
    #     await self._async_new_relationship_request(url, ["InformationSupplyChainLinkProperties"], body)
    #     logger.info(f"Linked {upstream_digital_prod_guid} -> {downstream_digital_prod_guid}")
    #
    #
    # def link_digital_product_dependency(self, upstream_digital_prod_guid: str, downstream_digital_prod_guid: str,
    #                                     body: dict | NewRelationshipRequestBody= None):
    #     """ Link two dependent digital products.  The linked elements are of type DigitalProduct.
    #         Request body is optional.
    #
    #         Parameters
    #         ----------
    #         upstream_digital_prod_guid: str
    #             The guid of the first digital product
    #         downstream_digital_prod_guid: str
    #             The guid of the downstream digital product
    #         body: dict | NewRelationshipRequestBody, optional, default = None
    #             A structure representing the details of the relationship.
    #
    #         Returns
    #         -------
    #         Nothing
    #
    #         Raises
    #         ------
    #         PyegeriaInvalidParameterException
    #           If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #         PyegeriaAPIException
    #           Raised by the server when an issue arises in processing a valid request
    #         NotAuthorizedException
    #           The principle specified by the user_id does not have authorization for the requested action
    #
    #         Notes
    #         -----
    #         JSON Structure looks like:
    #         JSON Structure looks like:
    #         {
    #           "class" : "NewRelationshipRequestBody",
    #           "externalSourceGUID": "add guid here",
    #           "externalSourceName": "add qualified name here",
    #           "effectiveTime" : "{{$isoTimestamp}}",
    #           "forLineage" : false,
    #           "forDuplicateProcessing" : false,
    #           "properties": {
    #             "class": "InformationSupplyChainLinkProperties",
    #             "label": "add label here",
    #             "description": "add description here",
    #             "effectiveFrom": "{{$isoTimestamp}}",
    #             "effectiveTo": "{{$isoTimestamp}}"
    #           }
    #         }
    #     """
    #     loop = asyncio.get_event_loop()
    #     loop.run_until_complete(
    #         self._async_link_digital_product_dependency(upstream_digital_prod_guid, downstream_digital_prod_guid,
    #                                                     body))
    #
    #
    # @dynamic_catch
    # async def _async_detach_digital_product_dependency(self, upstream_digital_prod_guid: str,
    #                                                    downstream_digital_prod_guid: str,
    #                                                    body: Optional[dict | DeleteRelationshipRequestBody] = None)-> None:
    #     """ Unlink two dependent digital products.  The linked elements are of type DigitalProduct.
    #         Request body is optional. Async version.
    #
    #     Parameters
    #     ----------
    #     upstream_digital_prod_guid: str
    #         The guid of the first digital product
    #     downstream_digital_prod_guid: str
    #         The guid of the downstream digital product
    #     body: dict | DeleteRelationshipRequestBody, optional, default = None
    #         A structure representing the details of the relationship.
    #
    #     Returns
    #     -------
    #     Nothing
    #
    #     Raises
    #     ------
    #     PyegeriaInvalidParameterException
    #       If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #     PyegeriaAPIException
    #       Raised by the server when an issue arises in processing a valid request
    #     NotAuthorizedException
    #       The principle specified by the user_id does not have authorization for the requested action
    #
    #     Notes
    #     -----
    #     JSON Structure looks like:
    #     {
    #       "class": "DeleteRelationshipRequestBody",
    #       "externalSourceGUID": "add guid here",
    #       "externalSourceName": "add qualified name here",
    #       "effectiveTime": "{{$isoTimestamp}}",
    #       "forLineage": false,
    #       "forDuplicateProcessing": false
    #     }
    #
    #     """
    #
    #     url = (
    #         f"{self.platform_url}/servers/"
    #         f"{self.view_server}/api/open-metadata/collection-manager/collections/digital-products/"
    #         f"{upstream_digital_prod_guid}/product-dependencies/{downstream_digital_prod_guid}/detach")
    #     await self._async_delete_relationship_request(url, body)
    #     logger.info(f"Detached digital product dependency {upstream_digital_prod_guid} -> {downstream_digital_prod_guid}")
    #
    #
    # def detach_digital_product_dependency(self, upstream_digital_prod_guid: str, downstream_digital_prod_guid: str,
    #                                     body: dict | DeleteRelationshipRequestBody= None):
    #     """ Unlink two dependent digital products.  The linked elements are of type DigitalProduct.
    #         Request body is optional.
    #
    #     Parameters
    #     ----------
    #     upstream_digital_prod_guid: str
    #         The guid of the first digital product
    #     downstream_digital_prod_guid: str
    #         The guid of the downstream digital product
    #     body: dict | DeleteRelationshipRequestBody, optional, default = None
    #         A structure representing the details of the relationship.
    #
    #     Returns
    #     -------
    #     Nothing
    #
    #     Raises
    #     ------
    #     PyegeriaInvalidParameterException
    #       If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #     PyegeriaAPIException
    #       Raised by the server when an issue arises in processing a valid request
    #     NotAuthorizedException
    #       The principle specified by the user_id does not have authorization for the requested action
    #
    #     Notes
    #     -----
    #     JSON Structure looks like:
    #     {
    #       "class": "DeleteRelationshipRequestBody",
    #       "externalSourceGUID": "add guid here",
    #       "externalSourceName": "add qualified name here",
    #       "effectiveTime": "{{$isoTimestamp}}",
    #       "forLineage": false,
    #       "forDuplicateProcessing": false
    #     }
    #     """
    #     loop = asyncio.get_event_loop()
    #     loop.run_until_complete(
    #         self._async_detach_digital_product_dependency(upstream_digital_prod_guid, downstream_digital_prod_guid,
    #                                                     body))
    #
    #



    # @dynamic_catch
    # async def _async_link_product_manager(self, digital_prod_guid: str, digital_prod_manager_guid: str,
    #                                       body: Optional[dict | NewRelationshipRequestBody] = None) -> None:
    #     """ Attach a product manager to a digital product. Request body is optional.
    #         Request body is optional. Async version.
    #
    #     Parameters
    #     ----------
    #     digital_prod_guid: str
    #         The guid of the digital product
    #     digital_prod_manager_guid: str
    #         The guid of the digital_product_manager
    #     body: dict | NewRelationshipRequestBody, optional, default = None
    #         A structure representing the details of the relationship.
    #
    #     Returns
    #     -------
    #     Nothing
    #
    #     Raises
    #     ------
    #     PyegeriaInvalidParameterException
    #       If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #     PyegeriaAPIException
    #       Raised by the server when an issue arises in processing a valid request
    #     NotAuthorizedException
    #       The principle specified by the user_id does not have authorization for the requested action
    #
    #     Notes
    #     -----
    #     JSON Structure looks like:
    #     {
    #       "class": "NewRelationshipRequestBody",
    #       "properties": {
    #           "assignmentType": "Add type here",
    #           "description": "Add assignment description here"
    #       },
    #       "externalSourceGUID": "add guid here",
    #       "externalSourceName": "add qualified name here",
    #       "effectiveTime": "{{$isoTimestamp}}",
    #       "forLineage": false,
    #       "forDuplicateProcessing": false
    #     }
    #
    #     """
    #     url = (
    #         f"{self.platform_url}/servers/"
    #         f"{self.view_server}/api/open-metadata/collection-manager/collections/digital"
    #         f"-products/"
    #         f"{digital_prod_guid}/product-managers/{digital_prod_manager_guid}/attach")
    #     await self._async_new_relationship_request(url, "AssignmentScopeProperties",body)
    #     logger.info(f"Attached digital product manager {digital_prod_guid} -> {digital_prod_manager_guid}")
    #
    #
    # def link_product_manager(self, digital_prod_guid: str, digital_prod_manager_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None):
    #     """ Attach a product manager to a digital product. Request body is optional.
    #             Request body is optional.
    #
    #         Parameters
    #         ----------
    #         digital_prod_guid: str
    #             The guid of the digital product
    #         digital_prod_manager_guid: str
    #             The guid of the digital_product_manager
    #         body: dict | NewRelationshipRequestBody, optional, default = None
    #             A structure representing the details of the relationship.
    #
    #         Returns
    #         -------
    #         Nothing
    #
    #         Raises
    #         ------
    #         PyegeriaInvalidParameterException
    #           If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #         PyegeriaAPIException
    #           Raised by the server when an issue arises in processing a valid request
    #         NotAuthorizedException
    #           The principle specified by the user_id does not have authorization for the requested action
    #
    #         Notes
    #         -----
    #         JSON Structure looks like:
    #         {
    #           "class": "NewRelationshipRequestBody",
    #           "properties": {
    #               "assignmentType": "Add type here",
    #               "description": "Add assignment description here"
    #           },
    #           "externalSourceGUID": "add guid here",
    #           "externalSourceName": "add qualified name here",
    #           "effectiveTime": "{{$isoTimestamp}}",
    #           "forLineage": false,
    #           "forDuplicateProcessing": false
    #         }
    #
    #         """
    #     loop = asyncio.get_event_loop()
    #     loop.run_until_complete(
    #         self._async_link_product_manager(digital_prod_guid, digital_prod_manager_guid, body))
    #
    #
    # @dynamic_catch
    # async def _async_detach_product_manager(self, digital_prod_guid: str,
    #                                          digital_prod_manager_guid: str,
    #                                          body: Optional[dict | DeleteRelationshipRequestBody] = None)-> None:
    #     """ Detach a digital product manager from a digital product.
    #         Request body is optional. Async version.
    #
    #     Parameters
    #     ----------
    #     digital_prod_guid: str
    #         The guid of the digital product.
    #     prod_manager: str
    #         The guid of the digital product manager.
    #     body: dict | DeleteRelationshipRequestBody, optional, default = None
    #         A structure representing the details of the relationship.
    #
    #     Returns
    #     -------
    #     Nothing
    #
    #     Raises
    #     ------
    #     PyegeriaInvalidParameterException
    #       If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #     PyegeriaAPIException
    #       Raised by the server when an issue arises in processing a valid request
    #     NotAuthorizedException
    #       The principle specified by the user_id does not have authorization for the requested action
    #
    #     Notes
    #     -----
    #     JSON Structure looks like:
    #     {
    #       "class": "DeleteRelationshipRequestBody",
    #       "externalSourceGUID": "add guid here",
    #       "externalSourceName": "add qualified name here",
    #       "effectiveTime": "{{$isoTimestamp}}",
    #       "forLineage": false,
    #       "forDuplicateProcessing": false
    #     }
    #
    #     """
    #     url = (
    #         f"{self.platform_url}/servers/"
    #         f"{self.view_server}/api/open-metadata/collection-manager/collections/digital-products/"
    #         f"{digital_prod_guid}/product-dependencies/{digital_prod_manager_guid}/detach")
    #     await self._async_delete_relationship_request(url, body)
    #     logger.info(f"Detached digital product manager {digital_prod_guid} -> {digital_prod_manager_guid}")
    #
    # @dynamic_catch
    # def detach_product_manager(self, digital_prod_guid: str, digital_prod_manager_guid: str,
    #                             body: dict | DeleteRelationshipRequestBody= None):
    #     """ Detach a digital product manager from a digital product.
    #         Request body is optional. Async version.
    #
    #     Parameters
    #     ----------
    #     digital_prod_guid: str
    #         The guid of the digital product.
    #     digital_prod_manager_guid: str
    #         The guid of the digital product manager.
    #     body: dict | DeleteRelationshipRequestBody, optional, default = None
    #         A structure representing the details of the relationship.
    #
    #     Returns
    #     -------
    #     Nothing
    #
    #     Raises
    #     ------
    #     PyegeriaInvalidParameterException
    #       If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #     PyegeriaAPIException
    #       Raised by the server when an issue arises in processing a valid request
    #     NotAuthorizedException
    #       The principle specified by the user_id does not have authorization for the requested action
    #
    #     Notes
    #     -----
    #     JSON Structure looks like:
    #     {
    #       "class": "DeleteRelationshipRequestBody",
    #       "externalSourceGUID": "add guid here",
    #       "externalSourceName": "add qualified name here",
    #       "effectiveTime": "{{$isoTimestamp}}",
    #       "forLineage": false,
    #       "forDuplicateProcessing": false
    #     }
    #
    #     """
    #     loop = asyncio.get_event_loop()
    #     loop.run_until_complete(
    #         self._async_detach_product_manager(digital_prod_guid, digital_prod_manager_guid,
    #                                                     body))


#

    @dynamic_catch
    async def _async_create_agreement(self, body: dict | NewElementRequestBody) -> str:
        """ Create a new collection that represents an agreement.
            Async version.

        Parameters
        ----------
        body: dict | NewElementRequestBody
            A structure representing the details of the agreement to create.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        ValidationError
          Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
        be valid dates if specified, otherwise you will get a 400 error response.

        JSON Structure looks like:
        {
          "class" : "NewElementRequestBody",
          "isOwnAnchor" : true,
          "anchorScopeGUID" : "optional GUID of search scope",
          "parentGUID" : "xxx",
          "parentRelationshipTypeName" : "CollectionMembership",
          "parentAtEnd1": true,
          "properties": {
            "class" : "AgreementProperties",
            "qualifiedName": "Agreement::Add agreement name here",
            "displayName" : "display name",
            "description" : "Add description of the agreement here",
            "identifier" : "Add agreement identifier here",
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "initialStatus" : "ACTIVE"
        }

        The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
        UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
        OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
        default to ACTIVE.
        """
        url = f"{self.collection_command_root}"
        return await self._async_create_element_body_request(url, "AgreementProperties", body)

    @dynamic_catch
    def create_agreement(self, body: Optional[dict | NewElementRequestBody] = None) -> str:
        """ Create a new collection that represents an agreement..

            Parameters
            ----------
            body: dict | NewElementRequestBody
                A structure representing the details of the agreement to create.

            Returns
            -------
            str - the guid of the created collection

            Raises
            ------
            PyegeriaException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            ValidationError
              Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

            Notes
            -----
            Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
            be valid dates if specified, otherwise you will get a 400 error response.

            JSON Structure looks like:
            {
              "class" : "NewElementRequestBody",
              "isOwnAnchor" : true,
              "anchorScopeGUID" : "optional GUID of search scope",
              "parentGUID" : "xxx",
              "parentRelationshipTypeName" : "CollectionMembership",
              "parentAtEnd1": true,
              "properties": {
                "class" : "AgreementProperties",
                "qualifiedName": "Agreement::Add agreement name here",
                "displayName" : "display name",
                "description" : "Add description of the agreement here",
                "identifier" : "Add agreement identifier here",
                "additionalProperties": {
                  "property1Name" : "property1Value",
                  "property2Name" : "property2Value"
                }
              },
              "externalSourceGUID": "add guid here",
              "externalSourceName": "add qualified name here",
              "effectiveTime" : "{{$isoTimestamp}}",
              "forLineage" : false,
              "forDuplicateProcessing" : false,
              "initialStatus" : "ACTIVE"
            }

            The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
            UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
            OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
            default to ACTIVE.
            """

        return asyncio.get_event_loop().run_until_complete(
            self._async_create_agreement(body))

    #
    # @dynamic_catch
    # async def _async_create_data_sharing_agreement(self, body: dict | NewElementRequestBody) -> str:
    #     """ Create a new collection that represents a data sharing agreement.
    #         Async version.
    #
    #     Parameters
    #     ----------
    #     body: dict | NewElementRequestBody
    #         A structure representing the details of the data sharing agreement to create.
    #
    #     Returns
    #     -------
    #     str - the guid of the created collection
    #
    #     Raises
    #     ------
    #     PyegeriaException
    #       If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #     ValidationError
    #       Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
    #     NotAuthorizedException
    #       The principle specified by the user_id does not have authorization for the requested action
    #
    #     Notes
    #     -----
    #     Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
    #     be valid dates if specified, otherwise you will get a 400 error response.
    #
    #     JSON Structure looks like:
    #     {
    #       "class" : "NewElementRequestBody",
    #       "isOwnAnchor" : true,
    #       "anchorScopeGUID" : "optional GUID of search scope",
    #       "parentGUID" : "xxx",
    #       "parentRelationshipTypeName" : "CollectionMembership",
    #       "parentAtEnd1": true,
    #       "properties": {
    #         "class" : "AgreementProperties",
    #         "qualifiedName": "Agreement::Add agreement name here",
    #         "displayName" : "display name",
    #         "description" : "Add description of the agreement here",
    #         "userDefinedStatus" : "NEW",
    #         "identifier" : "Add agreement identifier here",
    #         "additionalProperties": {
    #           "property1Name" : "property1Value",
    #           "property2Name" : "property2Value"
    #         }
    #       },
    #       "initialStatus" : "ACTIVE",
    #       "externalSourceGUID": "add guid here",
    #       "externalSourceName": "add qualified name here",
    #       "effectiveTime" : "{{$isoTimestamp}}",
    #       "forLineage" : false,
    #       "forDuplicateProcessing" : false
    #     }
    #
    #     The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
    #     UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
    #     OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
    #     default to ACTIVE.
    #     """
    #
    #     url = f"{self.collection_command_root}/data-sharing-agreement"
    #     return await self._async_create_element_body_request(url, "AgreementProperties", body)
    #
    # @dynamic_catch
    # def create_data_sharing_agreement(self, body: Optional[dict | NewElementRequestBody] = None) -> str:
    #     """ Create a new collection that represents a digital product.
    #
    #         Parameters
    #         ----------
    #         body: dict | NewElementRequestBody
    #             A structure representing the details of the digital product to create.
    #
    #         Returns
    #         -------
    #         str - the guid of the created collection
    #
    #         Raises
    #         ------
    #         PyegeriaException
    #           If the client passes incorrect parameters on the request - such as bad URLs or invalid values
    #         ValidationError
    #           Raised by the pydantic validator if the body does not conform to the NewElementRequestBody.
    #         NotAuthorizedException
    #           The principle specified by the user_id does not have authorization for the requested action
    #
    #         Notes
    #         -----
    #         Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
    #         be valid dates if specified, otherwise you will get a 400 error response.
    #
    #         JSON Structure looks like:
    #         {
    #           "class" : "NewElementRequestBody",
    #           "isOwnAnchor" : true,
    #           "anchorScopeGUID" : "optional GUID of search scope",
    #           "parentGUID" : "xxx",
    #           "parentRelationshipTypeName" : "CollectionMembership",
    #           "parentAtEnd1": true,
    #           "properties": {
    #             "class" : "DigitalProductProperties",
    #             "qualifiedName": "DigitalProduct::Add product name here",
    #             "displayName" : "Product contents",
    #             "description" : "Add description of product and its expected usage here",
    #             "identifier" : "Add product identifier here",
    #             "productName" : "Add product name here",
    #             "category" : "Periodic Delta",
    #             "maturity" : "Add valid value here",
    #             "serviceLife" : "Add the estimated lifetime of the product",
    #             "introductionDate" : "date",
    #             "nextVersionDate": "date",
    #             "withdrawDate": "date",
    #             "currentVersion": "V0.1",
    #             "additionalProperties": {
    #               "property1Name" : "property1Value",
    #               "property2Name" : "property2Value"
    #             }
    #           },
    #           "externalSourceGUID": "add guid here",
    #           "externalSourceName": "add qualified name here",
    #           "effectiveTime" : "{{$isoTimestamp}}",
    #           "forLineage" : false,
    #           "forDuplicateProcessing" : false,
    #           "initialStatus" : "ACTIVE"
    #         }
    #
    #         The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED, APPROVED_CONCEPT,
    #         UNDER_DEVELOPMENT, DEVELOPMENT_COMPLETE, APPROVED_FOR_DEPLOYMENT, ACTIVE, DISABLED, DEPRECATED,
    #         OTHER.  If using OTHER, set the userDefinedStatus with the status value you want. If not specified, will
    #         default to ACTIVE.
    #         """
    #
    #     return asyncio.get_event_loop().run_until_complete(
    #         self._async_create_data_sharing_agreement(body))
    #

    @dynamic_catch
    async def _async_update_agreement(self, agreement_guid: str,  body: dict | UpdateElementRequestBody) -> None:
        """ Update the properties of an agreement.
            Collections: https://egeria-project.org/concepts/collection

            Async version.
        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to update.

        body: dict | NewElementRequestBody, optional
            A dict or NewElementRequestBody representing the details of the agreement to create.


        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes:
        -----
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "AgreementProperties",
            "qualifiedName": "Agreement::Add agreement name here",
            "displayName" : "display name",
            "description" : "Add description of the agreement here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add agreement identifier here",
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """
        url = (f"{self.collection_command_root}/{agreement_guid}/update")
        await self._async_update_element_body_request(url, AGREEMENT_PROPERTIES_LIST,body)

    @dynamic_catch
    def update_agreement(self, agreement_guid: str,  body: dict | UpdateElementRequestBody) -> None:
        """ Update the properties of an agreement.
                Collections: https://egeria-project.org/concepts/collection

            Parameters
            ----------
            agreement_guid: str
                The guid of the agreement to update.

            body: dict | NewElementRequestBody, optional
                A dict or NewElementRequestBody representing the details of the agreement to create.


            Returns
            -------
            None

            Raises
            ------
            PyegeriaException
                One of the pyegeria exceptions will be raised if there are issues in communications, message format or
                Egeria errors.
            ValidationError
                Pydantic validation errors are raised if the body does not conform to the NewElementRequestBody.
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

            Notes:
            -----
            {
              "class" : "UpdateElementRequestBody",
              "properties": {
                "class" : "AgreementProperties",
                "qualifiedName": "Agreement::Add agreement name here",
                "displayName" : "display name",
                "description" : "Add description of the agreement here",
                "userDefinedStatus" : "OBSOLETE",
                "identifier" : "Add agreement identifier here",
                "additionalProperties": {
                  "property1Name" : "property1Value",
                  "property2Name" : "property2Value"
                }
              },
              "externalSourceGUID": "add guid here",
              "externalSourceName": "add qualified name here",
              "effectiveTime" : "{{$isoTimestamp}}",
              "forLineage" : false,
              "forDuplicateProcessing" : false
            }
            """

        return asyncio.get_event_loop().run_until_complete(
            self._async_update_agreement(agreement_guid, body))


    @dynamic_catch
    async def _async_link_agreement_actor(self, agreement_guid: str, actor_guid: str,
                                          body: Optional[dict | NewRelationshipRequestBody] = None) -> None:
        """ Attach an actor to an agreement.  The actor element may be an actor profile (person, team or IT profile);
            actor role (person role, team role or IT profile role); or user identity. Request body is optional.
            Request body is optional. Async version.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement
        actor_guid: str
            The guid of the actor
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "NewRelationshipRequestBody",
          "properties": {
         "properties": {
            "class": "AgreementActorProperties",
            "actorName": "add name of actor used in agreement text",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """
        url = (
            f"{self.platform_url}/servers/"
            f"{self.view_server}/api/open-metadata/collection-manager/collections/agreements/"
            f"{agreement_guid}/agreement-actors/{actor_guid}/attach")
        await self._async_new_relationship_request(url, "AgreementActorProperties",body)
        logger.info(f"Attached digital product manager {agreement_guid} -> {actor_guid}")


    def link_agreement_actor(self, agreement_guid: str, actor_guid: str, body: Optional[dict | NewRelationshipRequestBody] = None):
        """ Attach an actor to an agreement.  The actor element may be an actor profile (person, team or IT profile);
            actor role (person role, team role or IT profile role); or user identity. Request body is optional.
            Async version.

          Parameters
          ----------
          agreement_guid: str
              The guid of the agreement
          actor_guid: str
              The guid of the actor
          body: dict | NewRelationshipRequestBody, optional, default = None
              A structure representing the details of the relationship.

          Returns
          -------
          Nothing

          Raises
          ------
          PyegeriaInvalidParameterException
            If the client passes incorrect parameters on the request - such as bad URLs or invalid values
          PyegeriaAPIException
            Raised by the server when an issue arises in processing a valid request
          NotAuthorizedException
            The principle specified by the user_id does not have authorization for the requested action

          Notes
          -----
          JSON Structure looks like:
          {
            "class": "NewRelationshipRequestBody",
            "properties": {
           "properties": {
              "class": "AgreementActorProperties",
              "actorName": "add name of actor used in agreement text",
              "effectiveFrom": "{{$isoTimestamp}}",
              "effectiveTo": "{{$isoTimestamp}}"
            },
            "externalSourceGUID": "add guid here",
            "externalSourceName": "add qualified name here",
            "effectiveTime": "{{$isoTimestamp}}",
            "forLineage": false,
            "forDuplicateProcessing": false
          }

          """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(
            self._async_link_agreement_actor(agreement_guid, actor_guid, body))


    @dynamic_catch
    async def _async_detach_agreement_actor(self, agreement_guid: str,
                                             actor_guid: str,
                                             body: Optional[dict | DeleteRelationshipRequestBody] = None)-> None:
        """ Detach an actor from an agreement.
            Request body is optional. Async Version.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement.
        actor_guid: str
            The guid of the actor.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """

        url = (
            f"{self.platform_url}/servers/"
            f"{self.view_server}/api/open-metadata/collection-manager/collections/agreements/"
            f"{agreement_guid}/agreement-actors/{actor_guid}/detach")
        self._async_delete_relationship_request(url, body)
        logger.info(f"Detached digital product manager {agreement_guid} -> {actor_guid}")


    def detach_agreement_actor(self, agreement_guid: str, actor_guid: str,
                                body: dict | DeleteRelationshipRequestBody= None):
        """ Detach an actor from an agreement.
            Request body is optional.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement.
        actor_guid: str
            The guid of the actor.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(
            self._async_detach_agreement_actor(agreement_guid, actor_guid,
                                                        body))



###

    @dynamic_catch
    async def _async_link_agreement_item(self, agreement_guid: str, agreement_item_guid: str,
                                         body: dict| NewRelationshipRequestBody = None) -> None:
        """ Attach an agreement to an element referenced in its definition. The agreement item element is of type
           'Referenceable' to allow the agreement to refer to many things. Request body is optional. Async version.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to update.
        agreement_item_guid: str
            The guid of the element to attach.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____
        {
          "class" : "NewRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "AgreementItemProperties",
            "agreementItemId": "add label here",
            "agreementStart": "{{$isoTimestamp}}",
            "agreementEnd": "{{$isoTimestamp}}",
            "restrictions": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "obligations" : {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "entitlements" : {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "usageMeasurements" : {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }

            """
        url = (f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections/"
               f"agreements/{agreement_guid}/agreement-items/{agreement_item_guid}/attach")

        await self._async_new_relationship_request(url, "AgreementItemProperties", body)
        logger.info(f"Attached agreement item {agreement_item_guid} to {agreement_guid}")


    def link_agreement_item(self, agreement_guid: str, agreement_item_guid: str, body: dict = None) -> None:
        """ Attach an agreement to an element referenced in its definition. The agreement item element is of type
                  'Referenceable' to allow the agreement to refer to many things. Request body is optional.

               Parameters
               ----------
               agreement_guid: str
                   The guid of the agreement to update.
               agreement_item_guid: str
                   The guid of the element to attach.
               body: dict, optional, default = None
                   A dict representing the details of the relationship.

               Returns
               -------
               Nothing

               Raises
               ------
               PyegeriaInvalidParameterException
                 If the client passes incorrect parameters on the request - such as bad URLs or invalid values
               PyegeriaAPIException
                 Raised by the server when an issue arises in processing a valid request
               NotAuthorizedException
                 The principle specified by the user_id does not have authorization for the requested action

               Notes
               _____

               {
                 "class" : "RelationshipRequestBody",
                 "externalSourceGUID": "add guid here",
                 "externalSourceName": "add qualified name here",
                 "effectiveTime" : "{{$isoTimestamp}}",
                 "forLineage" : false,
                 "forDuplicateProcessing" : false,
                 "properties": {
                   "class": "AgreementItemProperties",
                   "agreementItemId": "add label here",
                   "agreementStart": "{{$isoTimestamp}}",
                   "agreementEnd": "{{$isoTimestamp}}",
                   "restrictions": {
                     "property1Name" : "property1Value",
                     "property2Name" : "property2Value"
                   },
                   "obligations" : {
                     "property1Name" : "property1Value",
                     "property2Name" : "property2Value"
                   },
                   "usageMeasurements" : {
                     "property1Name" : "property1Value",
                     "property2Name" : "property2Value"
                   },
                   "effectiveFrom": "{{$isoTimestamp}}",
                   "effectiveTo": "{{$isoTimestamp}}"
                 }
               }

               """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_link_agreement_item(agreement_guid, agreement_item_guid, body))


    @dynamic_catch
    async def _async_detach_agreement_item(self, agreement_guid: str, agreement_item_guid: str,
                                           body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None:
        """Detach an agreement item from an agreement. Request body is optional. Async version.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to link.
        agreement_item_guid: str
            The guid of the element to attach.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____
        {
          "class": "DeleteRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """
        url = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections"
            f"/agreements"
            f"{agreement_guid}/agreement-items/{agreement_item_guid}/detach")
        await self._async_delete_relationship_request(url, body)
        logger.info(f"Detached agreement item {agreement_item_guid} from {agreement_guid}")


    def detach_agreement_item(self, agreement_guid: str, agreement_item_guid: str, body: dict = None) -> None:
        """Detach an agreement item from an agreement. Request body is optional. Async version.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to link.
        agreement_item_guid: str
            The guid of the element to attach.
        body: dict, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____
        {
          "class" : "MetadataSourceRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_detach_agreement_item(agreement_guid, agreement_item_guid, body))


    @dynamic_catch
    async def _async_link_contract(self, agreement_guid: str, external_ref_guid: str,
                                   body: Optional[dict | NewRelationshipRequestBody] = None) -> None:
        """ Attach an agreement to an external reference element that describes the location of the contract
        documents.
            Request body is optional. Async version.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to update.
        external_ref_guid: str
            The guid of the external reference to attach.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____

        {
          "class" : "RelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "ContractLinkProperties",
            "contractId": "add id here",
            "contractLiaison": "add identifier of actor here",
            "contractLiaisonTypeName": "add type of actor here",
            "contractLiaisonPropertyName": "add property of actor's identifier here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }

        """
        url = (f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections/"
               f"agreements/{agreement_guid}/contract-links/{external_ref_guid}/attach")
        await self._async_new_relationship_request(url, "ContractLinkProperties", body)
        logger.info(f"Attached agreement {agreement_guid} to contract {external_ref_guid}")

    @dynamic_catch
    def link_contract(self, agreement_guid: str, external_ref_guid: str, body: dict = None) -> None:
        """ Attach an agreement to an external reference element that describes the location of the contract
        documents.
            Request body is optional.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to update.
        external_ref_guid: str
            The guid of the external reference to attach.
        body: dict, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____

        {
          "class" : "RelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "ContractLinkProperties",
            "contractId": "add id here",
            "contractLiaison": "add identifier of actor here",
            "contractLiaisonTypeName": "add type of actor here",
            "contractLiaisonPropertyName": "add property of actor's identifier here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_link_contract(agreement_guid, external_ref_guid, body))


    @dynamic_catch
    async def _async_detach_contract(self, agreement_guid: str, external_ref_guid: str, body: dict = None) -> None:
        """Detach an external reference to a contract, from an agreement. Request body is optional. Async version.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to link.
        external_ref_guid: str
            The guid of the element to attach.
        body: dict, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____
        {
          "class" : "MetadataSourceRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """

        url = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections"
            f"/agreements/{agreement_guid}/contract-links/{external_ref_guid}/detach")
        await self._async_delete_relationship_request(url, body)
        logger.info(f"Detached contract: {external_ref_guid} from {agreement_guid}")

    @dynamic_catch
    def detach_contract(self, agreement_guid: str, external_ref_guid: str, body: dict = None) -> None:
        """Detach an external reference to a contract, from an agreement. Request body is optional.

        Parameters
        ----------
        agreement_guid: str
            The guid of the agreement to link.
        external_ref_guid: str
            The guid of the element to attach.
        body: dict, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____
        {
          "class" : "MetadataSourceRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_detach_contract(agreement_guid, external_ref_guid, body))

        #
        # Digital Subscriptions
        #


    @dynamic_catch
    async def _async_create_digital_subscription(self, body: dict | NewElementRequestBody) -> str:
        """Create a new collection that represents a type of agreement called a digital_subscription. Async version.

        Parameters
        ----------
        body | NewElementRequewstBody: dict
            A structure representing the details of the collection to create.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
        be valid dates if specified, otherwise you will get a 400 error response.

        JSON Structure looks like:
        {
          "class" : "NewElementRequestBody",
          "isOwnAnchor" : true,
          "anchorScopeGUID" : "optional GUID of search scope",
          "parentGUID" : "xxx",
          "parentRelationshipTypeName" : "CollectionMembership",
          "parentAtEnd1": true,
          "properties": {
            "class" : "DigitalSubscriptionProperties",
            "qualifiedName": "DigitalSubscription::Add subscription name here",
            "displayName" : "display name",
            "description" : "Add description of the subscription here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add subscription identifier here",
            "supportLevel" : "Add the level of support agreed/requested",
            "serviceLevels" : {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
             }
           },
           "externalSourceGUID": "add guid here",
           "externalSourceName": "add qualified name here",
           "effectiveTime" : "{{$isoTimestamp}}",
           "forLineage" : false,
           "forDuplicateProcessing" : false
        }

        With a lifecycle, the body is:

        The DigitalSubscription is a type of Agreement and so can have lifecycle states.
        Note: the three dates: introductionDate, nextVersionDate and withdrawDate must be valid dates if specified,
        otherwise you will get a 400 error response.
        The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED
        ACTIVE, DEPRECATED, OTHER.  If using OTHER, set the userDefinedStatus with the statu value you want.

        {
          "class" : "NewAgreementRequestBody",
          "isOwnAnchor" : true,
          "anchorScopeGUID" : "optional GUID of search scope",
          "parentGUID" : "xxx",
          "parentRelationshipTypeName" : "CollectionMembership",
          "parentAtEnd1": true,
          "properties": {
            "class" : "DigitalSubscriptionProperties",
            "qualifiedName": "DigitalSubscription::Add subscription name here",
            "displayName" : "display name",
            "description" : "Add description of the subscription here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add subscription identifier here",
            "supportLevel" : "Add the level of support agreed/requested",
            "serviceLevels" : {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
             }
           },
           "initialStatus" : "OTHER",
           "externalSourceGUID": "add guid here",
           "externalSourceName": "add qualified name here",
           "effectiveTime" : "{{$isoTimestamp}}",
           "forLineage" : false,
           "forDuplicateProcessing" : false
        }
        """
        url = f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections"
        return await self._async_create_element_body_request(url, "DigitalSubscriptionProperties", body)


    def create_digital_subscription(self, body: dict) -> str:
        """Create a new collection that represents a type of agreement called a digital_subscription.

        Parameters
        ----------
        body: dict
            A dict representing the details of the collection to create.

        Returns
        -------
        str - the guid of the created collection

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Note: the three dates: introductionDate, nextVersionDate and withdrawDate must
        be valid dates if specified, otherwise you will get a 400 error response.

        JSON Structure looks like:
        With a lifecycle, the body is:

        The DigitalSubscription is a type of Agreement and so can have lifecycle states.
        Note: the three dates: introductionDate, nextVersionDate and withdrawDate must be valid dates if specified,
        otherwise you will get a 400 error response.
        The valid values for initialStatus are: DRAFT, PREPARED, PROPOSED, APPROVED, REJECTED
        ACTIVE, DEPRECATED, OTHER.  If using OTHER, set the userDefinedStatus with the statu value you want.

        {
          "class" : "NewAgreementRequestBody",
          "isOwnAnchor" : true,
          "anchorScopeGUID" : "optional GUID of search scope",
          "parentGUID" : "xxx",
          "parentRelationshipTypeName" : "CollectionMembership",
          "parentAtEnd1": true,
          "properties": {
            "class" : "DigitalSubscriptionProperties",
            "qualifiedName": "DigitalSubscription::Add subscription name here",
            "displayName" : "display name",
            "description" : "Add description of the subscription here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add subscription identifier here",
            "supportLevel" : "Add the level of support agreed/requested",
            "serviceLevels" : {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
             }
           },
           "initialStatus" : "OTHER",
           "externalSourceGUID": "add guid here",
           "externalSourceName": "add qualified name here",
           "effectiveTime" : "{{$isoTimestamp}}",
           "forLineage" : false,
           "forDuplicateProcessing" : false
        }
        """
        loop = asyncio.get_event_loop()
        resp = loop.run_until_complete(self._async_create_digital_subscription(body))
        return resp


    @dynamic_catch
    async def _async_update_digital_subscription(self, digital_subscription_guid: str,
                                                 body: dict | UpdateElementRequestBody) -> None:
        """Update the properties of the digital_subscription collection. Async version.

        Parameters
        ----------
        digital_subscription_guid: str
            The guid of the digital_subscription to update.
        body: dict | UpdateElementRequestBody
            A structure representing the details of the collection to create.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "DigitalSubscriptionProperties",
            "qualifiedName": "DigitalSubscription::Add subscription name here",
            "displayName" : "display name",
            "description" : "Add description of the subscription here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add subscription identifier here",
            "supportLevel" : "Add the level of support agreed/requested",
            "serviceLevels" : {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            },
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """
        url = (f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections/"
               f"{digital_subscription_guid}/update")
        await self._async_update_element_body_request(url,["DigitalSubscriptionProperties"], body )
        logger.info(f"Updated digital subscription {digital_subscription_guid}")

    @dynamic_catch
    def update_digital_subscription(self, digital_subscription_guid: str, body: dict,):
        """Update the properties of the DigitalProduct classification attached to a collection.

        Parameters
        ----------
        digital_subscription_guid: str
            The guid of the digital_subscription to update.
        body: dict
            A dict representing the details of the collection to create.


        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
         {
          "class" : "UpdateElementRequestBody",
          "properties": {
            "class" : "AgreementProperties",
            "qualifiedName": "Agreement::Add digital_subscription name here",
            "displayName" : "display name",
            "description" : "Add description of the digital_subscription here",
            "userDefinedStatus" : "OBSOLETE",
            "identifier" : "Add digital_subscription identifier here",
            "additionalProperties": {
              "property1Name" : "property1Value",
              "property2Name" : "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(
            self._async_update_digital_subscription(digital_subscription_guid, body))


    @dynamic_catch
    async def _async_link_subscriber(self, subscriber_guid: str, subscription_guid: str,
                                     body: Optional[dict | NewRelationshipRequestBody] = None)-> None:
        """ Attach a subscriber to a subscription.  The subscriber is of type 'Referenceable' to allow digital
            products, team or business capabilities to be the subscriber. The subscription is an element of type
            DigitalSubscription.
            Async version.

        Parameters
        ----------
        subscriber_guid: str
            The unique identifier of the subscriber.
        subscription_guid: str
            The identifier of the subscription.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class" : "NewRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "DigitalSubscriberProperties",
            "subscriberId": "add id here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }
        """
        url = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections"
            f"/subscribers/"
            f"{subscriber_guid}/subscriptions/{subscription_guid}/attach")
        await self._async_new_relationship_request(url, "DigitalSubscriberProperties", body)
        logger.info(f"Linking subscriber {subscriber_guid} to subscription {subscription_guid}")

    @dynamic_catch
    def link_subscriber(self, subscriber_guid: str, subscription_guid: str,
                        body: Optional[dict | NewRelationshipRequestBody] = None):
        """ Attach a subscriber to a subscription.  The subscriber is of type 'Referenceable' to allow digital
            products, team or business capabilities to be the subscriber. The subscription is an element of type
            DigitalSubscription.
            Async version.

        Parameters
        ----------
        subscriber_guid: str
            The unique identifier of the subscriber.
        subscription_guid: str
            The identifier of the subscription.
        body: dict, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class" : "RelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false,
          "properties": {
            "class": "DigitalSubscriberProperties",
            "subscriberId": "add id here",
            "effectiveFrom": "{{$isoTimestamp}}",
            "effectiveTo": "{{$isoTimestamp}}"
          }
        }
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_link_subscriber(subscriber_guid, subscription_guid, body))


    @dynamic_catch
    async def _async_detach_subscriber(self, subscriber_guid: str, subscription_guid: str,
                                       body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None:
        """ Detach a subscriber from a subscription Request body is optional. Async version.

        Parameters
        ----------
        subscriber_guid: str
            The unique identifier of the subscriber.
        subscription_guid: str
            The unique identifier of the subscription.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "MetadataSourceRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        url = (
            f"{self.platform_url}/servers/{self.view_server}/api/open-metadata/collection-manager/collections"
            f"/agreements/"
            f"{subscriber_guid}/agreement-actors/{subscription_guid}/detach")
        await self._async_delete_relationship_request(url, body)
        logger.info(f"Detached subscriber {subscriber_guid} from subscription {subscription_guid}")


    def detach_subscriber(self, subscriber_guid: str, subscription_guid: str, body: dict | DeleteRelationshipRequestBody= None):
        """ Detach a subscriber from a subscription. Request body is optional.

        Parameters
        ----------
        subscriber_guid: str
            The unique identifier of the subscriber.
        subscription_guid: str
            The unique identifier of the subscription.
        body: dict, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "MetadataSourceRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_detach_subscriber(subscriber_guid, subscription_guid, body))

        #
        #
        #


    @dynamic_catch
    async def _async_attach_collection(self, parent_guid: str, collection_guid: str,
                                       body: dict | NewRelationshipRequestBody= None):
        """ Connect an existing collection to an element using the ResourceList relationship (0019).
            Async version.

        Parameters
        ----------
        parent_guid: str
            The unique identifier of the parent to attach to.
        collection_guid: str
            The identifier of the collection being attached.
        body: dict | NewRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:

        {
          "class" : "NewRelationshipRequestBody",
          "properties": {
            "class": "ResourceListProperties",
            "resourceUse": "Add valid value here",
            "resourceUseDescription": "Add description here",
            "watchResource": false,
            "resourceUseProperties": {
              "property1Name": "property1Value",
              "property2Name": "property2Value"
            }
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """

        url = (
            f"{self.platform_url}/servers/"
            f"{self.view_server}/api/open-metadata/collection-manager/metadata-elements/"
            f"{parent_guid}/collections/{collection_guid}/attach")
        await self._async_new_relationship_request(url, "ResourceListProperties", body)
        logger.info(f"Attached {collection_guid} to {parent_guid}")

    @dynamic_catch
    def attach_collection(self, parent_guid: str, collection_guid: str,
                          body: Optional[dict | NewRelationshipRequestBody] = None):
        """ Connect an existing collection to an element using the ResourceList relationship (0019).

            Parameters
            ----------
            parent_guid: str
                The unique identifier of the parent to attach to.
            collection_guid: str
                The identifier of the collection being attached.
            body: dict, optional, default = None
                A dict representing the details of the relationship.
            make_anchor: bool, optional, default = False
                Indicates if the collection should be anchored to the element.

            Returns
            -------
            Nothing

            Raises
            ------
            PyegeriaInvalidParameterException
              If the client passes incorrect parameters on the request - such as bad URLs or invalid values
            PyegeriaAPIException
              Raised by the server when an issue arises in processing a valid request
            NotAuthorizedException
              The principle specified by the user_id does not have authorization for the requested action

            Notes
            -----
            JSON Structure looks like:

            {
              "class" : "RelationshipRequestBody",
              "properties": {
                "class": "ResourceListProperties",
                "resourceUse": "Add valid value here",
                "resourceUseDescription": "Add description here",
                "watchResource": false,
                "resourceUseProperties": {
                  "property1Name": "property1Value",
                  "property2Name": "property2Value"
                }
              },
              "externalSourceGUID": "add guid here",
              "externalSourceName": "add qualified name here",
              "effectiveTime" : "{{$isoTimestamp}}",
              "forLineage" : false,
              "forDuplicateProcessing" : false
            }
            """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_attach_collection(parent_guid, collection_guid, body))


    @dynamic_catch
    async def _async_detach_collection(self, parent_guid: str, collection_guid: str,
                                       body: Optional[dict | DeleteRelationshipRequestBody] = None):
        """ Detach an existing collection from an element. If the collection is anchored to the element,
        it is delete.
            Async version.

        Parameters
        ----------
        parent_guid: str
                The unique identifier of the parent to detach from.
        collection_guid: str
                The identifier of the collection being detached.
        body: dict | DeleteRelationshipRequestBody, optional, default = None
            A structure representing the details of the relationship.


        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        JSON Structure looks like:
        {
          "class": "DeleteRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }

        """
        url = (
            f"{self.platform_url}/servers/"
            f"{self.view_server}/api/open-metadata/collection-manager/metadata-elements/"
            f"{parent_guid}/collections/{collection_guid}/detach")
        await self._async_delete_relationship_request(url, body)
        logger.info(f"Detached collection {collection_guid} from {parent_guid}")


    def detach_collection(self, parent_guid: str, collection_guid: str,
                          body: Optional[dict | DeleteRelationshipRequestBody] = None):
        """ Detach an existing collection from an element. If the collection is anchored to the element,
        it is delete.

          Parameters
          ----------
          parent_guid: str
                  The unique identifier of the parent to detach from.
          collection_guid: str
                 The identifier of the collection being detached.
          body: dict, optional, default = None
              A dict representing the details of the relationship.

          Returns
          -------
          Nothing

          Raises
          ------
          PyegeriaInvalidParameterException
            If the client passes incorrect parameters on the request - such as bad URLs or invalid values
          PyegeriaAPIException
            Raised by the server when an issue arises in processing a valid request
          NotAuthorizedException
            The principle specified by the user_id does not have authorization for the requested action

          Notes
          -----
          JSON Structure looks like:
          {
            "class": "MetadataSourceRequestBody",
            "externalSourceGUID": "add guid here",
            "externalSourceName": "add qualified name here",
            "effectiveTime": "{{$isoTimestamp}}",
            "forLineage": false,
            "forDuplicateProcessing": false
          }
          """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_detach_collection(parent_guid, collection_guid, body))


    @dynamic_catch
    async def _async_delete_collection(self, collection_guid: str, body: dict | DeleteElementRequestBody= None,
                                       cascade: bool = False) -> None:
        """Delete a collection.  It is detected from all parent elements.  If members are anchored to the collection
        then they are also deleted. Async version


        Parameters
        ----------
        collection_guid: str
            The guid of the collection to delete.

        cascade: bool, optional, defaults to True
            If true, a cascade delete is performed.

        body: dict | DeleteElementRequestBody, optional, default = None
            A structure representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____
        JSON Structure looks like:
        {
          "class": "DeleteElementRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime": "{{$isoTimestamp}}",
          "forLineage": false,
          "forDuplicateProcessing": false
        }


        """
        if body is None:
            body = {"class": "DeleteElementRequestBody"}
        url = f"{self.collection_command_root}/{collection_guid}/delete"
        await self._async_delete_element_request(url, body, cascade)
        logger.info(f"Deleted collection {collection_guid} with cascade {cascade}")


    def delete_collection(self, collection_guid: str, body: Optional[dict | DeleteElementRequestBody] = None,
                          cascade: bool = False) -> None:
        """Delete a collection.  It is deleted from all parent elements.  If members are anchored to the collection
        then they are also deleted.

        Parameters
        ----------
        collection_guid: str
            The guid of the collection to delete.

        cascade: bool, optional, defaults to True
            If true, a cascade delete is performed.

        body: dict | DeleteElementRequestBody, optional, default = None
            A dict representing the details of the relationship.

        Returns
        -------
        Nothing

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format, or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the DeleteElementRequestBody.
        PyegeriaNotAuthorizedException
            The principle specified by the user_id does not have authorization for the requested action

        Notes
        _____
        JSON Structure looks like:



        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_delete_collection(collection_guid, body, cascade))


    @dynamic_catch
    async def _async_add_to_collection(self, collection_guid: str, element_guid: str,
                                       body: Optional[dict | NewRelationshipRequestBody] = None, ) -> None:
        """Add an element to a collection.  The request body is optional. Async version.

        Parameters
        ----------
        collection_guid: str
            identity of the collection to return members for.
        element_guid: str
            Effective time of the query. If not specified will default to any time.
        body: dict NewRelationshipRequestBody, optional, defaults to None
            The body of the request to add to the collection. See notes.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
            One of the pyegeria exceptions will be raised if there are issues in communications, message format, or
            Egeria errors.
        ValidationError
            Pydantic validation errors are raised if the body does not conform to the NewRelationshipRequestBody.
        PyegeriaNotAuthorizedException
            The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Example body:
        { "class": "NewRelationshipRequestBody",
           "properties" : {
              "class" : "CollectionMembershipProperties",
              "membershipRationale": "xxx",
              "createdBy": "user id here",
              "expression": "expression that described why the element is a part of this collection",
              "confidence": 100,
              "status": "PROPOSED",
              "userDefinedStatus": "Add valid value here",
              "steward": "identifier of steward that validated this member",
              "stewardTypeName": "type name of element identifying the steward",
              "stewardPropertyName": "property name if the steward's identifier",
              "source": "source of the member",
              "notes": "Add notes here"
              },
              },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """

        url = (f"{self.collection_command_root}/{collection_guid}/members/"
               f"{element_guid}/attach")
        await self._async_new_relationship_request(url, "CollectionMembershipProperties", body)
        logger.info(f"Added {element_guid} to {collection_guid}")


    def add_to_collection(self, collection_guid: str, element_guid: str,
                          body: dict | NewRelationshipRequestBody= None, ) -> None:
        """Add an element to a collection.  The request body is optional.

        Parameters
        ----------
        collection_guid: str
            identity of the collection to return members for.
        element_guid: str
            Effective time of the query. If not specified will default to any time.
        body: dict, optional, defaults to None
            The body of the request to add to the collection. See notes.

            The name of the server to use.


        Returns
        -------
        None

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Example body:
         { "class": "RelationshipRequestBody",
           "properties" : {
              "class" : "CollectionMembershipProperties",
              "membershipRationale": "xxx",
              "createdBy": "user id here",
              "expression": "expression that described why the element is a part of this collection",
              "confidence": 100,
              "status": "PROPOSED",
              "userDefinedStatus": "Add valid value here",
              "steward": "identifier of steward that validated this member",
              "stewardTypeName": "type name of element identifying the steward",
              "stewardPropertyName": "property name if the steward's identifier",
              "source": "source of the member",
              "notes": "Add notes here"
              },
              },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_add_to_collection( collection_guid, element_guid, body))

    def add_term_to_folder(self, folder_guid: str, term_guid: str,
                             body: Optional[dict | NewRelationshipRequestBody] = None) -> None:
        """Add a term to a category.  The request body is optional."""
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_add_to_collection(folder_guid, term_guid, body))


    @dynamic_catch
    async def _async_update_collection_membership_prop(self, collection_guid: str, element_guid: str, body: dict = None,
                                                   ) -> None:
        """Update an element's membership to a collection. Async version.

        Parameters
        ----------
        collection_guid: str
            identity of the collection to return members for.
        element_guid: str
            Effective time of the query. If not specified will default to any time.
        body: dict, optional, defaults to None
            The body of the request to add to the collection. See notes.



        Returns
        -------
        None

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Example body:
        {
          "class" : "RelationshipRequestBody",
          "properties" : {
            "class": "CollectionMembershipProperties",
            "membershipRationale": "xxx",
            "createdBy": "user id here",
            "expression": "expression that described why the element is a part of this collection",
            "confidence": 100,
            "membershipStatus": "PROPOSED",
            "userDefinedStatus": "Add valid value here",
            "steward": "identifier of steward that validated this member",
            "stewardTypeName": "type name of element identifying the steward",
            "stewardPropertyName": "property name if the steward's identifier",
            "source": "source of the member",
            "notes": "Add notes here"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """

        url = (f"{self.collection_command_root}/{collection_guid}/members/"
               f"{element_guid}/update")
        await self._async_update_relationship_request(url, "CollectionMembershipProperties", body )
        logger.info(f"Updated membership for collection {collection_guid}")


    def update_collection_membership_prop(self, collection_guid: str, element_guid: str,
                                     body: dict | UpdateRelationshipRequestBody= None,
                                      ) -> None:
        """Update an element's membership to a collection.

        Parameters
        ----------
        collection_guid: str
            identity of the collection to return members for.
        element_guid: str
            Effective time of the query. If not specified will default to any time.
        body: dict, optional, defaults to None
            The body of the request to add to the collection. See notes.



        Returns
        -------
        None

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        Example body:
        {
          "class" : "RelationshipRequestBody",
          "properties" : {
            "class": "CollectionMembershipProperties",
            "membershipRationale": "xxx",
            "createdBy": "user id here",
            "expression": "expression that described why the element is a part of this collection",
            "confidence": 100,
            "membershipStatus": "PROPOSED",
            "userDefinedStatus": "Add valid value here",
            "steward": "identifier of steward that validated this member",
            "stewardTypeName": "type name of element identifying the steward",
            "stewardPropertyName": "property name if the steward's identifier",
            "source": "source of the member",
            "notes": "Add notes here"
          },
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }
        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(
            self._async_update_collection_membership(collection_guid, element_guid, body))


    @dynamic_catch
    async def _async_remove_from_collection(self, collection_guid: str, element_guid: str,
                                            body: Optional[dict | DeleteRelationshipRequestBody] = None) -> None:
        """Remove an element from a collection. Async version.

        Parameters
        ----------
        collection_guid: str
            identity of the collection to return members for.
        element_guid: str
            Effective time of the query. If not specified will default to any time.
        body: dict | DeleteRelationshipRequestBody, optional, defaults to None
            The body of the request to add to the collection. See notes.

        Returns
        -------
        None

        Raises
        ------
        PyegeriaException
        Notes
        -----
        {
          "class" : "DeleteRelationshipRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """

        url = f"{self.collection_command_root}/{collection_guid}/members/{element_guid}/detach"
        await self._async_delete_relationship_request(url, body)
        logger.info(f"Removed member {element_guid} from collection {collection_guid}")


    def remove_from_collection(self, collection_guid: str, element_guid: str,
                               body: dict | DeleteRelationshipRequestBody= None) -> None:
        """Remove an element from a collection. Async version.

        Parameters
        ----------
        collection_guid: str
            identity of the collection to return members for.
        element_guid: str
            Effective time of the query. If not specified will default to any time.
        body: dict, optional, defaults to None
            The body of the request to add to the collection. See notes.

        Returns
        -------
        None

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----
        {
          "class" : "MetadataSourceRequestBody",
          "externalSourceGUID": "add guid here",
          "externalSourceName": "add qualified name here",
          "effectiveTime" : "{{$isoTimestamp}}",
          "forLineage" : false,
          "forDuplicateProcessing" : false
        }

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_remove_from_collection(collection_guid, element_guid, body))

        #
        #
        #


    def remove_term_from_category(self, folder_guid: str, term_guid: str,
                               body: dict | DeleteRelationshipRequestBody= None) -> None:
        """Remove a term from a category.

        Parameters
        ----------
        category_guid: str
            identity of the collection to return members for.
        term_guid: str
            Effective time of the query. If not specified will default to any time.
        body: dict, optional, defaults to None
            The body of the request to add to the collection. See notes.

        Returns
        -------
        None

        Raises
        ------

        PyegeriaInvalidParameterException
          If the client passes incorrect parameters on the request - such as bad URLs or invalid values
        PyegeriaAPIException
          Raised by the server when an issue arises in processing a valid request
        NotAuthorizedException
          The principle specified by the user_id does not have authorization for the requested action

        Notes
        -----

        """
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self._async_remove_from_collection(folder_guid, term_guid, body))

        #
        #
        #


    @dynamic_catch
    async def _async_get_member_list(self, collection_guid: Optional[str] = None, collection_name: Optional[str] = None,
                                     collection_qname: Optional[str] = None, ) -> list | str:
        """Get the member list for the collection - async version.
        Parameters
        ----------
        collection_guid: str,
           identity of the collection to return members for. If none, collection_name or
           collection_qname are used.
        collection_name: str,
           display name of the collection to return members for. If none, collection_guid
           or collection_qname are used.
        collection_qname: str,
           qualified name of the collection to return members for. If none, collection_guid
           or collection_name are used.

        Returns
        -------
        list | str
            The list of member information if successful, otherwise the string "No members found"

        Raises
        ------
        PyegeriaInvalidParameterException
            If the root_collection_name does not have exactly one root collection.

        """

        # first find the guid for the collection we are using as root

        # now find the members of the collection
        member_list = []
        members = await self._async_get_collection_members(collection_guid, collection_name, collection_qname)
        if (type(members) is str) or (len(members) == 0):
            logger.trace(f"No members found for collection {collection_guid}")
            return "No members found"
        # finally, construct a list of  member information
        for member_rel in members:
            member_guid = member_rel["elementHeader"]["guid"]
            # member = await self._async_get_element_by_guid_(member_guid)
            member = member_rel
            if isinstance(member, dict):
                member_instance = {
                    "displayName": member["properties"].get('displayName', ''),
                    "qualifiedName": member["properties"]["qualifiedName"], "guid": member["elementHeader"]["guid"],
                    "description": member["properties"].get("description", ''),
                    "type": member["elementHeader"]["type"]['typeName'],
                    }
                member_list.append(member_instance)
        logger.debug(f"Member list for collection {collection_guid}: {member_list}")
        return member_list if len(member_list) > 0 else "No members found"


    def get_member_list(self, collection_guid: Optional[str] = None, collection_name: Optional[str] = None,
                        collection_qname: Optional[str] = None, ) -> list | bool:
        """Get the member list for a collection - async version.
        Parameters
        ----------
        collection_guid: str,
           identity of the collection to return members for. If none, collection_name or
           collection_qname are used.
        collection_name: str,
           display name of the collection to return members for. If none, collection_guid
           or collection_qname are used.
        collection_qname: str,
           qualified name of the collection to return members for. If none, collection_guid
           or collection_name are used.
        Returns
        -------
        list | bool
            The list of member information if successful, otherwise False.

        Raises
        ------
        PyegeriaInvalidParameterException
            If the root_collection_name does not have exactly one root collection.

        """
        loop = asyncio.get_event_loop()
        resp = loop.run_until_complete(
            self._async_get_member_list(collection_guid, collection_name, collection_qname))
        return resp

    def _extract_digital_product_properties(self, element: dict, guid: str, output_format: str) -> dict:
        props = element["properties"]
        prop_class = props.get("class", "DigitalProductProperties")
        if prop_class == "DigitalProductFamilyProperties":
            digital_prod = DigitalProductFamilyProperties.model_validate(props)
        else:
            digital_prod = DigitalProductProperties.model_validate(props)

        added_props = get_defined_field_values(digital_prod)

        used_by_digital_products = element.get("usedByDigitalProducts", "")
        if isinstance(used_by_digital_products, (list | dict)):
            used_by_digital_products_list = ""
            for prod in used_by_digital_products:
                used_by_digital_products_list += f"{prod["relatedElement"]["properties"]["qualifiedName"]}, "
            added_props["used_by_digital_products"] = used_by_digital_products_list[:-2]

        uses_digital_products = element.get("usesDigitalProducts", "")
        if isinstance(uses_digital_products, (list | dict)):
            uses_digital_products_list = ""
            for prod in uses_digital_products:
                uses_digital_products_list += f"{prod["relatedElement"]["properties"]["qualifiedName"]}, "
            added_props["uses_digital_products"] = uses_digital_products_list[:-2]

        resources = element.get("resourceList", "")
        if isinstance(resources, (list | dict)):
            resource_list = ""
            for resource in resources:
                resource_list += f"{resource.get('properties',{}).get('qualifiedName', None)}, "
            added_props["resource_list"] = resource_list[:-2] if resource_list else ""

        actors = element.get("assignedActors", "")
        if isinstance(actors, (list | dict)):
            actor_list = ""
            for actor in actors:
                actor_type = actor['relatedElement']['elementHeader']['type']['typeName']
                actor_qname = actor['relatedElement']['properties']['qualifiedName']
                actor_display_name = actor['relatedElement']['properties']['displayName']
                actor_list += actor_display_name
            added_props["actor_list"] = actor_list[:-2] if actor_list else ""

        governed_by = element.get("governedBy", "")
        if isinstance(governed_by, (list | dict)):
            governed_by_list = ""
            for gov in governed_by:
                license_type = gov['relatedElement']['properties'].get('typeName',"")
                license_type_qname = gov['relatedElement']['properties'].get('qualifiedName','')
                governed_by_list += license_type_qname
            added_props["governed_list"] = governed_by_list[:-2] if governed_by_list else ""

        solution_designs = element.get("solutionDesigns", "")
        if isinstance(solution_designs, (list | dict)):
            solution_design_list = ""
            for design in solution_designs:
                solution_design_qname = design['relatedElement']['properties'].get('qualifiedName', "")
                solution_design_list += solution_design_qname
            added_props["solution_design_list"] = solution_design_list[:-2] if solution_design_list else ""

        return added_props

    def _extract_agreement_properties(self, element: dict, guid: str, output_format: str) -> dict:
        props = element["properties"]
        # agreement = Collections.model_validate(props)
        # added_props = get_defined_field_values(agreement)

        added_props = {}
        agreement_items = element.get("agreementItems", "")
        if isinstance(agreement_items, (list | dict)):
            agreement_items_list = ""
            for item in agreement_items:
                agreement_items_list += f"{item["relatedElement"]["properties"]["qualifiedName"]}, "
            added_props["agreementItems"] = agreement_items_list[:-2]

        return added_props

    def _extract_collection_properties(self, element: dict, columns_struct: dict) -> dict:
        """Populate collection columns for output.

        Uses `populate_common_columns` to fill standard fields and derive requested relationships.

        Parameters
        ----------
        element : dict
            Raw element from the OMVS.
        columns_struct : dict
            Format-set structure whose columns' `value` fields will be populated.

        Returns
        -------
        dict
            The same columns_struct with populated values.
        """
        # Use centralized common population
        col_data = populate_common_columns(element, columns_struct)
        logger.trace(f"Extracted/Populated columns: {col_data}")
        return col_data


    def _generate_collection_output(self, elements: dict | list[dict], filter_string: Optional[str] = None,
                                    element_type_name: Optional[str] = None, output_format: str = "DICT",
                                    report_spec: dict | str = None, **kwargs) -> str | list[dict]:
        """ Generate output for collections in the specified format.

            Args:
                elements (Union[Dict, List[Dict]]): Dictionary or list of dictionaries containing data field elements
                filter_string (Optional[str]): The search string used to find the elements
                element_type_name (Optional[str]): The type of collection
                output_format (str): The desired output format (MD, FORM, REPORT, LIST, DICT, MERMAID, HTML)
                report_spec (Optional[dict], optional): List of dictionaries containing column data. Defaults
                to None.
                **kwargs: Additional arguments.

            Returns:
                Union[str, List[Dict]]: Formatted output as a string or list of dictionaries
        """
        return self._generate_formatted_output(
            elements=elements,
            query_string=filter_string,
            entity_type=element_type_name or "Collections",
            output_format=output_format,
            extract_properties_func=self._extract_collection_properties,
            report_spec=report_spec,
            **kwargs
        )


if __name__ == "__main__":
    print("Main-Collection Manager")
