Skip to content

Commit 670d1ce

Browse files
authored
Merge pull request #50 from atlanhq/ACTIV-557
Adds shortcuts to create custom metadata properties
2 parents f56ec3c + 3ab0782 commit 670d1ce

26 files changed

+2025
-552
lines changed

Diff for: .github/workflows/pytest.yaml

+4-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ jobs:
2929
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
3030
- name: Test with pytest
3131
env: # Or as an environment variable
32-
ATLAN_API_KEY: ${{ secrets.MARK_ATLAN_API_KEY }}
33-
ATLAN_BASE_URL: https://mark.atlan.com
32+
ATLAN_API_KEY: ${{ secrets.ATLAN_API_KEY }}
33+
ATLAN_BASE_URL: ${{ secrets.ATLAN_BASE_URL }}
34+
MARK_API_KEY: ${{ secrets.MARK_ATLAN_API_KEY }}
35+
MARK_BASE_URL: https://mark.atlan.com
3436
run: |
3537
pytest

Diff for: pyatlan/cache/classification_cache.py

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88

99
class ClassificationCache:
10-
1110
cache_by_id: dict[str, ClassificationDef] = dict()
1211
map_id_to_name: dict[str, str] = dict()
1312
map_name_to_id: dict[str, str] = dict()

Diff for: pyatlan/cache/custom_metadata_cache.py

+24-11
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ def __get__(self, instance, owner):
2424

2525

2626
class CustomMetadataCache:
27-
2827
cache_by_id: dict[str, CustomMetadataDef] = dict()
2928
map_id_to_name: dict[str, str] = dict()
3029
map_id_to_type: dict[str, type] = dict()
@@ -66,25 +65,26 @@ def refresh_cache(cls) -> None:
6665
applicable_types: set[str] = set()
6766
if cm.attribute_defs:
6867
for attr in cm.attribute_defs:
69-
if attr.options.custom_applicable_entity_types:
68+
if attr.options and attr.options.custom_applicable_entity_types:
7069
applicable_types.update(
7170
json.loads(attr.options.custom_applicable_entity_types)
7271
)
73-
attr_id = attr.name
74-
attr_name = attr.display_name
75-
cls.map_attr_id_to_name[type_id][attr_id] = attr_name
72+
attr_id = str(attr.name)
73+
attr_name = str(attr.display_name)
74+
# Use a renamed attribute everywhere
75+
attr_renamed = to_snake_case(attr_name.replace(" ", ""))
76+
cls.map_attr_id_to_name[type_id][attr_id] = attr_renamed
7677
if attr.options and attr.options.is_archived:
77-
cls.archived_attr_ids[attr_id] = attr_name
78-
elif attr_name in cls.map_attr_name_to_id[type_id]:
78+
cls.archived_attr_ids[attr_id] = attr_renamed
79+
elif attr_renamed in cls.map_attr_name_to_id[type_id]:
7980
raise LogicError(
80-
f"Multiple custom attributes with exactly the same name ({attr_name}) "
81+
f"Multiple custom attributes with exactly the same name ({attr_renamed}) "
8182
f"found for: {type_name}",
8283
code="ATLAN-PYTHON-500-100",
8384
)
8485
else:
85-
attr_name = to_snake_case(attr_name.replace(" ", ""))
86-
setattr(attrib_type, attr_name, Synonym(attr_id))
87-
cls.map_attr_name_to_id[type_id][attr_name] = attr_id
86+
setattr(attrib_type, attr_renamed, Synonym(attr_id))
87+
cls.map_attr_name_to_id[type_id][attr_renamed] = attr_id
8888
for asset_type in applicable_types:
8989
if asset_type not in cls.types_by_asset:
9090
cls.types_by_asset[asset_type] = set()
@@ -230,3 +230,16 @@ def get_custom_metadata(
230230
else ba_type()
231231
)
232232
raise ValueError(f"Custom metadata {name} is not applicable to {type_name}")
233+
234+
@classmethod
235+
def get_custom_metadata_def(cls, name: str) -> CustomMetadataDef:
236+
"""
237+
Retrieve the full custom metadata structure definition.
238+
"""
239+
ba_id = cls.get_id_for_name(name)
240+
if ba_id is None:
241+
raise ValueError(f"No custom metadata with the name: {name} exist")
242+
if typedef := cls.cache_by_id.get(ba_id):
243+
return typedef
244+
else:
245+
raise ValueError(f"No custom metadata with the name: {name} found")

Diff for: pyatlan/cache/enum_cache.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2023 Atlan Pte. Ltd.
3+
from typing import Optional
4+
5+
from pyatlan.model.enums import AtlanTypeCategory
6+
from pyatlan.model.typedef import EnumDef
7+
8+
9+
class EnumCache:
10+
cache_by_name: dict[str, EnumDef] = dict()
11+
12+
@classmethod
13+
def refresh_cache(cls) -> None:
14+
from pyatlan.client.atlan import AtlanClient
15+
16+
client = AtlanClient.get_default_client()
17+
if client is None:
18+
client = AtlanClient()
19+
response = client.get_typedefs(type_category=AtlanTypeCategory.ENUM)
20+
cls.cache_by_name = {}
21+
if response is not None:
22+
for enum in response.enum_defs:
23+
type_name = enum.name
24+
cls.cache_by_name[type_name] = enum
25+
26+
@classmethod
27+
def get_by_name(cls, name: str) -> Optional[EnumDef]:
28+
"""
29+
Retrieve the enumeration definition by its name.
30+
"""
31+
if name:
32+
if enum_def := cls.cache_by_name.get(name):
33+
return enum_def
34+
cls.refresh_cache()
35+
return cls.cache_by_name.get(name)
36+
return None

Diff for: pyatlan/cache/role_cache.py

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88

99
class RoleCache:
10-
1110
cache_by_id: dict[str, AtlanRole] = dict()
1211
map_id_to_name: dict[str, str] = dict()
1312
map_name_to_id: dict[str, str] = dict()

Diff for: pyatlan/client/atlan.py

+152-46
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ADD_BUSINESS_ATTRIBUTE_BY_ID,
2424
BULK_UPDATE,
2525
CREATE_TYPE_DEFS,
26+
UPDATE_TYPE_DEFS,
2627
DELETE_ENTITY_BY_ATTRIBUTE,
2728
DELETE_ENTITY_BY_GUID,
2829
DELETE_TYPE_DEF_BY_NAME,
@@ -122,6 +123,61 @@ def get_session():
122123
return session
123124

124125

126+
def _build_typdef_request(typedef: TypeDef) -> TypeDefResponse:
127+
if isinstance(typedef, ClassificationDef):
128+
# Set up the request payload...
129+
payload = TypeDefResponse(
130+
classification_defs=[typedef],
131+
enum_defs=[],
132+
struct_defs=[],
133+
entity_defs=[],
134+
relationship_defs=[],
135+
custom_metadata_defs=[],
136+
)
137+
elif isinstance(typedef, CustomMetadataDef):
138+
# Set up the request payload...
139+
payload = TypeDefResponse(
140+
classification_defs=[],
141+
enum_defs=[],
142+
struct_defs=[],
143+
entity_defs=[],
144+
relationship_defs=[],
145+
custom_metadata_defs=[typedef],
146+
)
147+
elif isinstance(typedef, EnumDef):
148+
# Set up the request payload...
149+
payload = TypeDefResponse(
150+
classification_defs=[],
151+
enum_defs=[typedef],
152+
struct_defs=[],
153+
entity_defs=[],
154+
relationship_defs=[],
155+
custom_metadata_defs=[],
156+
)
157+
else:
158+
raise InvalidRequestException(
159+
"Unable to update type definitions of category: " + typedef.category.value,
160+
param="category",
161+
)
162+
# Throw an invalid request exception
163+
return payload
164+
165+
166+
def _refresh_caches(typedef: TypeDef) -> None:
167+
if isinstance(typedef, ClassificationDef):
168+
from pyatlan.cache.classification_cache import ClassificationCache
169+
170+
ClassificationCache.refresh_cache()
171+
if isinstance(typedef, CustomMetadataDef):
172+
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
173+
174+
CustomMetadataCache.refresh_cache()
175+
if isinstance(typedef, EnumDef):
176+
from pyatlan.cache.enum_cache import EnumCache
177+
178+
EnumCache.refresh_cache()
179+
180+
125181
class AtlanClient(BaseSettings):
126182
_default_client: "ClassVar[Optional[AtlanClient]]" = None
127183
base_url: HttpUrl
@@ -409,6 +465,44 @@ def upsert(
409465
raw_json = self._call_api(BULK_UPDATE, query_params, request)
410466
return AssetMutationResponse(**raw_json)
411467

468+
def upsert_merging_cm(
469+
self, entity: Union[Asset, list[Asset]], replace_classifications: bool = False
470+
) -> AssetMutationResponse:
471+
query_params = {
472+
"replaceClassifications": replace_classifications,
473+
"replaceBusinessAttributes": True,
474+
"overwriteBusinessAttributes": False,
475+
}
476+
entities: list[Asset] = []
477+
if isinstance(entity, list):
478+
entities.extend(entity)
479+
else:
480+
entities.append(entity)
481+
for asset in entities:
482+
asset.validate_required()
483+
request = BulkRequest[Asset](entities=entities)
484+
raw_json = self._call_api(BULK_UPDATE, query_params, request)
485+
return AssetMutationResponse(**raw_json)
486+
487+
def upsert_replacing_cm(
488+
self, entity: Union[Asset, list[Asset]], replace_classifications: bool = False
489+
) -> AssetMutationResponse:
490+
query_params = {
491+
"replaceClassifications": replace_classifications,
492+
"replaceBusinessAttributes": True,
493+
"overwriteBusinessAttributes": True,
494+
}
495+
entities: list[Asset] = []
496+
if isinstance(entity, list):
497+
entities.extend(entity)
498+
else:
499+
entities.append(entity)
500+
for asset in entities:
501+
asset.validate_required()
502+
request = BulkRequest[Asset](entities=entities)
503+
raw_json = self._call_api(BULK_UPDATE, query_params, request)
504+
return AssetMutationResponse(**raw_json)
505+
412506
def purge_entity_by_guid(self, guid) -> AssetMutationResponse:
413507
raw_json = self._call_api(
414508
DELETE_ENTITY_BY_GUID.format_path_with_params(guid),
@@ -455,54 +549,19 @@ def get_typedefs(self, type_category: AtlanTypeCategory) -> TypeDefResponse:
455549
return TypeDefResponse(**raw_json)
456550

457551
def create_typedef(self, typedef: TypeDef) -> TypeDefResponse:
458-
if isinstance(typedef, ClassificationDef):
459-
# Set up the request payload...
460-
payload = TypeDefResponse(
461-
classification_defs=[typedef],
462-
enum_defs=[],
463-
struct_defs=[],
464-
entity_defs=[],
465-
relationship_defs=[],
466-
custom_metadata_defs=[],
467-
)
468-
elif isinstance(typedef, CustomMetadataDef):
469-
# Set up the request payload...
470-
payload = TypeDefResponse(
471-
classification_defs=[],
472-
enum_defs=[],
473-
struct_defs=[],
474-
entity_defs=[],
475-
relationship_defs=[],
476-
custom_metadata_defs=[typedef],
477-
)
478-
elif isinstance(typedef, EnumDef):
479-
# Set up the request payload...
480-
payload = TypeDefResponse(
481-
classification_defs=[],
482-
enum_defs=[typedef],
483-
struct_defs=[],
484-
entity_defs=[],
485-
relationship_defs=[],
486-
custom_metadata_defs=[],
487-
)
488-
else:
489-
raise InvalidRequestException(
490-
"Unable to create new type definitions of category: "
491-
+ typedef.category.value,
492-
param="category",
493-
)
494-
# Throw an invalid request exception
552+
payload = _build_typdef_request(typedef)
495553
raw_json = self._call_api(
496-
CREATE_TYPE_DEFS, request_obj=payload, exclude_unset=False
554+
CREATE_TYPE_DEFS, request_obj=payload, exclude_unset=True
497555
)
498-
if isinstance(typedef, ClassificationDef):
499-
from pyatlan.cache.classification_cache import ClassificationCache
500-
501-
ClassificationCache.refresh_cache()
502-
if isinstance(typedef, CustomMetadataDef):
503-
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
556+
_refresh_caches(typedef)
557+
return TypeDefResponse(**raw_json)
504558

505-
CustomMetadataCache.refresh_cache()
559+
def update_typedef(self, typedef: TypeDef) -> TypeDefResponse:
560+
payload = _build_typdef_request(typedef)
561+
raw_json = self._call_api(
562+
UPDATE_TYPE_DEFS, request_obj=payload, exclude_unset=True
563+
)
564+
_refresh_caches(typedef)
506565
return TypeDefResponse(**raw_json)
507566

508567
def purge_typedef(self, internal_name: str) -> None:
@@ -511,9 +570,11 @@ def purge_typedef(self, internal_name: str) -> None:
511570
# to refresh that particular cache
512571
from pyatlan.cache.classification_cache import ClassificationCache
513572
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
573+
from pyatlan.cache.enum_cache import EnumCache
514574

515575
ClassificationCache.refresh_cache()
516576
CustomMetadataCache.refresh_cache()
577+
EnumCache.refresh_cache()
517578

518579
@validate_arguments()
519580
def add_classifications(
@@ -628,8 +689,35 @@ def remove_announcement(
628689
asset.remove_announcement()
629690
return self._update_asset_by_attribute(asset, asset_type, qualified_name)
630691

692+
def update_custom_metadata_attributes(
693+
self, guid: str, custom_metadata: CustomMetadata
694+
):
695+
custom_metadata_request = CustomMetadataReqest(__root__=custom_metadata)
696+
self._call_api(
697+
ADD_BUSINESS_ATTRIBUTE_BY_ID.format_path(
698+
{"entity_guid": guid, "bm_id": custom_metadata._meta_data_type_id}
699+
),
700+
None,
701+
custom_metadata_request,
702+
)
703+
631704
def replace_custom_metadata(self, guid: str, custom_metadata: CustomMetadata):
632-
# TODO: This endpoint is not currently functioning correctly on the server
705+
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
706+
707+
# Iterate through the custom metadata provided and explicitly set every
708+
# single attribute, so that they are all serialized out (forcing removal
709+
# of any empty ones)
710+
for k, v in custom_metadata.items():
711+
# Need to translate the hashed-string key here back to an attribute name
712+
attr_name = str(
713+
CustomMetadataCache.get_attr_name_for_id(
714+
set_id=custom_metadata._meta_data_type_id, attr_id=k
715+
)
716+
)
717+
if not v:
718+
setattr(custom_metadata, attr_name, None)
719+
else:
720+
setattr(custom_metadata, attr_name, v)
633721
custom_metadata_request = CustomMetadataReqest(__root__=custom_metadata)
634722
self._call_api(
635723
ADD_BUSINESS_ATTRIBUTE_BY_ID.format_path(
@@ -639,6 +727,24 @@ def replace_custom_metadata(self, guid: str, custom_metadata: CustomMetadata):
639727
custom_metadata_request,
640728
)
641729

730+
def remove_custom_metadata(self, guid: str, cm_name: str):
731+
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
732+
733+
# Ensure the custom metadata exists first - let this throw an error if not
734+
if cm_id := CustomMetadataCache.get_id_for_name(cm_name):
735+
# Initialize a dict of empty attributes for the custom metadata, and then
736+
# send that so that they are removed accordingly
737+
if cm_type := CustomMetadataCache.get_type_for_id(cm_id):
738+
custom_metadata = cm_type()
739+
custom_metadata_request = CustomMetadataReqest(__root__=custom_metadata)
740+
self._call_api(
741+
ADD_BUSINESS_ATTRIBUTE_BY_ID.format_path(
742+
{"entity_guid": guid, "bm_id": cm_id}
743+
),
744+
None,
745+
custom_metadata_request,
746+
)
747+
642748
@validate_arguments()
643749
def append_terms(
644750
self,

Diff for: pyatlan/error.py

-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ def __init__(
6666
status_code: Optional[int] = None,
6767
should_retry: bool = False,
6868
):
69-
7069
super(APIConnectionError, self).__init__(message, code, status_code)
7170
self.should_retry = should_retry
7271

Diff for: pyatlan/generator/generate_from_typdefs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
def get_type(type_: str):
5454
ret_value = type_
5555

56-
for (field, replacement) in TYPE_REPLACEMENTS:
56+
for field, replacement in TYPE_REPLACEMENTS:
5757
ret_value = ret_value.replace(field, replacement)
5858
return ret_value
5959

0 commit comments

Comments
 (0)