Source code for pyatlan.cache.custom_metadata_cache

# SPDX-License-Identifier: Apache-2.0
# Copyright 2025 Atlan Pte. Ltd.
from __future__ import annotations

from threading import Lock
from typing import TYPE_CHECKING, Dict, List, Optional, Set

from pyatlan.errors import ErrorCode
from pyatlan.model.enums import AtlanTypeCategory
from pyatlan.model.typedef import AttributeDef, CustomMetadataDef

if TYPE_CHECKING:
    from pyatlan.client.atlan import AtlanClient

lock: Lock = Lock()


[docs] class CustomMetadataCache: """ Lazily-loaded cache for translating between Atlan-internal ID strings and human-readable names for custom metadata (including attributes). """ def __init__(self, client: AtlanClient): self.client: AtlanClient = client self.cache_by_id: Dict[str, CustomMetadataDef] = {} self.attr_cache_by_id: Dict[str, AttributeDef] = {} self.map_id_to_name: Dict[str, str] = {} self.map_name_to_id: Dict[str, str] = {} self.map_attr_id_to_name: Dict[str, Dict[str, str]] = {} self.map_attr_name_to_id: Dict[str, Dict[str, str]] = {} self.archived_attr_ids: Dict[str, str] = {} self.types_by_asset: Dict[str, Set[type]] = {} self.lock: Lock = Lock()
[docs] def refresh_cache(self) -> None: """ Refreshes the cache of custom metadata structures by requesting the full set of custom metadata structures from Atlan. :raises LogicError: if duplicate custom attributes are detected """ self._refresh_cache()
[docs] def get_id_for_name(self, name: str) -> str: """ Translate the provided human-readable custom metadata set name to its Atlan-internal ID string. :param name: human-readable name of the custom metadata set :returns: Atlan-internal ID string of the custom metadata set :raises InvalidRequestError: if no name was provided :raises NotFoundError: if the custom metadata cannot be found """ return self._get_id_for_name(name=name)
[docs] def get_name_for_id(self, idstr: str) -> str: """ Translate the provided Atlan-internal custom metadata ID string to the human-readable custom metadata set name. :param idstr: Atlan-internal ID string of the custom metadata set :returns: human-readable name of the custom metadata set :raises InvalidRequestError: if no ID was provided :raises NotFoundError: if the custom metadata cannot be found """ return self._get_name_for_id(idstr=idstr)
[docs] def get_all_custom_attributes( self, include_deleted: bool = False, force_refresh: bool = False ) -> Dict[str, List[AttributeDef]]: """ Retrieve all the custom metadata attributes. The dict will be keyed by custom metadata set name, and the value will be a listing of all the attributes within that set (with all the details of each of those attributes). :param include_deleted: if True, include the archived (deleted) custom attributes; otherwise only include active custom attributes :param force_refresh: if True, will refresh the custom metadata cache; if False, will only refresh the cache if it is empty :returns: a dict from custom metadata set name to all details about its attributes :raises NotFoundError: if the custom metadata cannot be found """ return self._get_all_custom_attributes( include_deleted=include_deleted, force_refresh=force_refresh )
[docs] def get_attr_id_for_name(self, set_name: str, attr_name: str) -> str: """ Translate the provided human-readable custom metadata set and attribute names to the Atlan-internal ID string for the attribute. :param set_name: human-readable name of the custom metadata set :param attr_name: human-readable name of the attribute :returns: Atlan-internal ID string for the attribute :raises NotFoundError: if the custom metadata attribute cannot be found """ return self._get_attr_id_for_name(set_name=set_name, attr_name=attr_name)
[docs] def get_attr_name_for_id(self, set_id: str, attr_id: str) -> str: """ Given the Atlan-internal ID string for the set and the Atlan-internal ID for the attribute return the human-readable custom metadata name for the attribute. :param set_id: Atlan-internal ID string for the custom metadata set :param attr_id: Atlan-internal ID string for the attribute :returns: human-readable name of the attribute :raises NotFoundError: if the custom metadata attribute cannot be found """ return self._get_attr_name_for_id(set_id=set_id, attr_id=attr_id)
[docs] def is_attr_archived(self, attr_id: str) -> bool: """ Determine if an attribute is archived :param attr_id: Atlan-internal ID string for the attribute :returns: True if the attribute has been archived """ return self._is_attr_archived(attr_id=attr_id)
[docs] def get_attributes_for_search_results(self, set_name: str) -> Optional[List[str]]: """ Retrieve the full set of custom attributes to include on search results. :param set_name: human-readable name of the custom metadata set for which to retrieve attribute names :returns: a list of the attribute names, strictly useful for inclusion in search results """ return self._get_attributes_for_search_results(set_name=set_name)
[docs] def get_attribute_for_search_results( self, set_name: str, attr_name: str ) -> Optional[str]: """ Retrieve a single custom attribute name to include on search results. :param set_name: human-readable name of the custom metadata set for which to retrieve the custom metadata attribute name :param attr_name: human-readable name of the attribute :returns: the attribute name, strictly useful for inclusion in search results """ return self._get_attribute_for_search_results( set_name=set_name, attr_name=attr_name )
[docs] def get_custom_metadata_def(self, name: str) -> CustomMetadataDef: """ Retrieve the full custom metadata structure definition. :param name: human-readable name of the custom metadata set :returns: the full custom metadata structure definition for that set :raises InvalidRequestError: if no name was provided :raises NotFoundError: if the custom metadata cannot be found """ return self._get_custom_metadata_def(name=name)
[docs] def get_attribute_def(self, attr_id: str) -> AttributeDef: """ Retrieve a specific custom metadata attribute definition by its unique Atlan-internal ID string. :param attr_id: Atlan-internal ID string for the custom metadata attribute :returns: attribute definition for the custom metadata attribute :raises InvalidRequestError: if no attribute ID was provided :raises NotFoundError: if the custom metadata attribute cannot be found """ return self._get_attribute_def(attr_id=attr_id)
def _refresh_cache(self) -> None: """ Refreshes the cache of custom metadata structures by requesting the full set of custom metadata structures from Atlan. :raises LogicError: if duplicate custom attributes are detected """ with self.lock: response = self.client.typedef.get( type_category=[ AtlanTypeCategory.CUSTOM_METADATA, AtlanTypeCategory.STRUCT, ] ) if not response or not response.struct_defs: raise ErrorCode.EXPIRED_API_TOKEN.exception_with_parameters() if response is not None: self.map_id_to_name = {} self.map_name_to_id = {} self.map_attr_id_to_name = {} self.map_attr_name_to_id = {} self.archived_attr_ids = {} self.cache_by_id = {} self.attr_cache_by_id = {} for cm in response.custom_metadata_defs: type_id = cm.name type_name = cm.display_name self.cache_by_id[type_id] = cm self.map_id_to_name[type_id] = type_name self.map_name_to_id[type_name] = type_id self.map_attr_id_to_name[type_id] = {} self.map_attr_name_to_id[type_id] = {} if cm.attribute_defs: for attr in cm.attribute_defs: attr_id = str(attr.name) attr_name = str(attr.display_name) self.map_attr_id_to_name[type_id][attr_id] = attr_name self.attr_cache_by_id[attr_id] = attr if attr.options and attr.options.is_archived: self.archived_attr_ids[attr_id] = attr_name elif attr_name in self.map_attr_name_to_id[type_id]: raise ErrorCode.DUPLICATE_CUSTOM_ATTRIBUTES.exception_with_parameters( attr_name, type_name ) else: self.map_attr_name_to_id[type_id][attr_name] = attr_id def _get_id_for_name(self, name: str) -> str: """ Translate the provided human-readable custom metadata set name to its Atlan-internal ID string. :param name: human-readable name of the custom metadata set :returns: Atlan-internal ID string of the custom metadata set :raises InvalidRequestError: if no name was provided :raises NotFoundError: if the custom metadata cannot be found """ if name is None or not name.strip(): raise ErrorCode.MISSING_CM_NAME.exception_with_parameters() if cm_id := self.map_name_to_id.get(name): return cm_id # If not found, refresh the cache and look again (could be stale) self._refresh_cache() if cm_id := self.map_name_to_id.get(name): return cm_id raise ErrorCode.CM_NOT_FOUND_BY_NAME.exception_with_parameters(name) def _get_name_for_id(self, idstr: str) -> str: """ Translate the provided Atlan-internal custom metadata ID string to the human-readable custom metadata set name. :param idstr: Atlan-internal ID string of the custom metadata set :returns: human-readable name of the custom metadata set :raises InvalidRequestError: if no ID was provided :raises NotFoundError: if the custom metadata cannot be found """ if idstr is None or not idstr.strip(): raise ErrorCode.MISSING_CM_ID.exception_with_parameters() if cm_name := self.map_id_to_name.get(idstr): return cm_name # If not found, refresh the cache and look again (could be stale) self._refresh_cache() if cm_name := self.map_id_to_name.get(idstr): return cm_name raise ErrorCode.CM_NOT_FOUND_BY_ID.exception_with_parameters(idstr) def _get_all_custom_attributes( self, include_deleted: bool = False, force_refresh: bool = False ) -> Dict[str, List[AttributeDef]]: """ Retrieve all the custom metadata attributes. The dict will be keyed by custom metadata set name, and the value will be a listing of all the attributes within that set (with all the details of each of those attributes). :param include_deleted: if True, include the archived (deleted) custom attributes; otherwise only include active custom attributes :param force_refresh: if True, will refresh the custom metadata cache; if False, will only refresh the cache if it is empty :returns: a dict from custom metadata set name to all details about its attributes :raises NotFoundError: if the custom metadata cannot be found """ if len(self.cache_by_id) == 0 or force_refresh: self._refresh_cache() m = {} for type_id, cm in self.cache_by_id.items(): type_name = self._get_name_for_id(type_id) if not type_name: raise ErrorCode.CM_NOT_FOUND_BY_ID.exception_with_parameters(type_id) attribute_defs = cm.attribute_defs if include_deleted: to_include = attribute_defs else: to_include = [] if attribute_defs: to_include.extend( attr for attr in attribute_defs if not attr.options or not attr.options.is_archived ) m[type_name] = to_include return m def _get_attr_id_for_name(self, set_name: str, attr_name: str) -> str: """ Translate the provided human-readable custom metadata set and attribute names to the Atlan-internal ID string for the attribute. :param set_name: human-readable name of the custom metadata set :param attr_name: human-readable name of the attribute :returns: Atlan-internal ID string for the attribute :raises NotFoundError: if the custom metadata attribute cannot be found """ set_id = self._get_id_for_name(set_name) if sub_map := self.map_attr_name_to_id.get(set_id): if attr_id := sub_map.get(attr_name): # If found, return straight away return attr_id # Otherwise, refresh the cache and look again (could be stale) self._refresh_cache() if sub_map := self.map_attr_name_to_id.get(set_id): if attr_id := sub_map.get(attr_name): # If found, return straight away return attr_id raise ErrorCode.CM_ATTR_NOT_FOUND_BY_NAME.exception_with_parameters( attr_name, set_name ) raise ErrorCode.CM_ATTR_NOT_FOUND_BY_ID.exception_with_parameters(set_id) def _get_attr_name_for_id(self, set_id: str, attr_id: str) -> str: """ Given the Atlan-internal ID string for the set and the Atlan-internal ID for the attribute return the human-readable custom metadata name for the attribute. :param set_id: Atlan-internal ID string for the custom metadata set :param attr_id: Atlan-internal ID string for the attribute :returns: human-readable name of the attribute :raises NotFoundError: if the custom metadata attribute cannot be found """ if sub_map := self.map_attr_id_to_name.get(set_id): if attr_name := sub_map.get(attr_id): return attr_name self._refresh_cache() if sub_map := self.map_attr_id_to_name.get(set_id): if attr_name := sub_map.get(attr_id): return attr_name raise ErrorCode.CM_ATTR_NOT_FOUND_BY_ID.exception_with_parameters( attr_id, set_id ) def _is_attr_archived(self, attr_id: str) -> bool: """ Determine if an attribute id is archived :param attr_id: Atlan-internal ID string for the attribute :returns: True if the attribute has been archived """ return attr_id in self.archived_attr_ids def _get_attributes_for_search_results_(self, set_id: str) -> Optional[List[str]]: if sub_map := self.map_attr_name_to_id.get(set_id): attr_ids = sub_map.values() return [f"{set_id}.{idstr}" for idstr in attr_ids] return None def _get_attribute_for_search_results_( self, set_id: str, attr_name: str ) -> Optional[str]: if sub_map := self.map_attr_name_to_id.get(set_id): return sub_map.get(attr_name, None) return None def _get_attributes_for_search_results(self, set_name: str) -> Optional[List[str]]: """ Retrieve the full set of custom attributes to include on search results. :param set_name: human-readable name of the custom metadata set for which to retrieve attribute names :returns: a list of the attribute names, strictly useful for inclusion in search results """ if set_id := self._get_id_for_name(set_name): if dot_names := self._get_attributes_for_search_results_(set_id): return dot_names self._refresh_cache() return self._get_attributes_for_search_results_(set_id) return None def _get_attribute_for_search_results( self, set_name: str, attr_name: str ) -> Optional[str]: """ Retrieve a single custom attribute name to include on search results. :param set_name: human-readable name of the custom metadata set for which to retrieve the custom metadata attribute name :param attr_name: human-readable name of the attribute :returns: the attribute name, strictly useful for inclusion in search results """ if set_id := self._get_id_for_name(set_name): if attr_id := self._get_attribute_for_search_results_(set_id, attr_name): return attr_id self._refresh_cache() return self._get_attribute_for_search_results_(set_id, attr_name) return None def _get_custom_metadata_def(self, name: str) -> CustomMetadataDef: """ Retrieve the full custom metadata structure definition. :param name: human-readable name of the custom metadata set :returns: the full custom metadata structure definition for that set :raises InvalidRequestError: if no name was provided :raises NotFoundError: if the custom metadata cannot be found """ ba_id = self._get_id_for_name(name) if typedef := self.cache_by_id.get(ba_id): return typedef else: raise ErrorCode.CM_NOT_FOUND_BY_NAME.exception_with_parameters(name) def _get_attribute_def(self, attr_id: str) -> AttributeDef: """ Retrieve a specific custom metadata attribute definition by its unique Atlan-internal ID string. :param attr_id: Atlan-internal ID string for the custom metadata attribute :returns: attribute definition for the custom metadata attribute :raises InvalidRequestError: if no attribute ID was provided :raises NotFoundError: if the custom metadata attribute cannot be found """ if not attr_id: raise ErrorCode.MISSING_CM_ATTR_ID.exception_with_parameters() if self.attr_cache_by_id is None: self._refresh_cache() if attr_def := self.attr_cache_by_id.get(attr_id): return attr_def raise ErrorCode.CM_ATTR_NOT_FOUND_BY_ID.exception_with_parameters( attr_id, "(unknown)" )