Skip to content

Commit 529dcf0

Browse files
committed
Adds more efficient cache management for typedefs, and group CRUD
Signed-off-by: Christopher Grote <[email protected]>
1 parent a823121 commit 529dcf0

File tree

8 files changed

+453
-47
lines changed

8 files changed

+453
-47
lines changed

pyatlan/cache/group_cache.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright 2022 Atlan Pte. Ltd.
3+
from typing import Optional
4+
5+
from pyatlan.client.atlan import AtlanClient
6+
7+
8+
class GroupCache:
9+
map_id_to_name: dict[str, str] = dict()
10+
map_name_to_id: dict[str, str] = dict()
11+
map_alias_to_id: dict[str, str] = dict()
12+
13+
@classmethod
14+
def _refresh_cache(cls) -> None:
15+
client = AtlanClient.get_default_client()
16+
if client is None:
17+
client = AtlanClient()
18+
groups = client.get_all_groups()
19+
if groups is not None:
20+
cls.map_id_to_name = {}
21+
cls.map_name_to_id = {}
22+
cls.map_alias_to_id = {}
23+
for group in groups:
24+
group_id = str(group.id)
25+
group_name = str(group.name)
26+
group_alias = str(group.alias)
27+
cls.map_id_to_name[group_id] = group_name
28+
cls.map_name_to_id[group_name] = group_id
29+
cls.map_alias_to_id[group_alias] = group_id
30+
31+
@classmethod
32+
def get_id_for_name(cls, name: str) -> Optional[str]:
33+
"""
34+
Translate the provided human-readable group name to its GUID.
35+
"""
36+
if group_id := cls.map_name_to_id.get(name):
37+
return group_id
38+
cls._refresh_cache()
39+
return cls.map_name_to_id.get(name)
40+
41+
@classmethod
42+
def get_id_for_alias(cls, alias: str) -> Optional[str]:
43+
"""
44+
Translate the provided alias to its GUID.
45+
"""
46+
if group_id := cls.map_alias_to_id.get(alias):
47+
return group_id
48+
cls._refresh_cache()
49+
return cls.map_alias_to_id.get(alias)
50+
51+
@classmethod
52+
def get_name_for_id(cls, idstr: str) -> Optional[str]:
53+
"""
54+
Translate the provided group GUID to the human-readable group name.
55+
"""
56+
if group_name := cls.map_id_to_name.get(idstr):
57+
return group_name
58+
cls._refresh_cache()
59+
return cls.map_id_to_name.get(idstr)

pyatlan/client/atlan.py

+134-13
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,13 @@
3232
GET_ENTITY_BY_UNIQUE_ATTRIBUTE,
3333
GET_LINEAGE,
3434
GET_ROLES,
35+
GET_GROUPS,
3536
INDEX_SEARCH,
3637
PARTIAL_UPDATE_ENTITY_BY_ATTRIBUTE,
3738
UPDATE_ENTITY_BY_ATTRIBUTE,
39+
CREATE_GROUP,
40+
DELETE_GROUP,
41+
UPDATE_GROUP,
3842
)
3943
from pyatlan.error import AtlanError, NotFoundError
4044
from pyatlan.exceptions import AtlanServiceException, InvalidRequestException
@@ -69,6 +73,12 @@
6973
AtlanTypeCategory,
7074
CertificateStatus,
7175
)
76+
from pyatlan.model.group import (
77+
GroupResponse,
78+
AtlanGroup,
79+
CreateGroupResponse,
80+
CreateGroupRequest,
81+
)
7282
from pyatlan.model.lineage import LineageRequest, LineageResponse
7383
from pyatlan.model.response import AssetMutationResponse
7484
from pyatlan.model.role import RoleResponse
@@ -123,7 +133,7 @@ def get_session():
123133
return session
124134

125135

126-
def _build_typdef_request(typedef: TypeDef) -> TypeDefResponse:
136+
def _build_typedef_request(typedef: TypeDef) -> TypeDefResponse:
127137
if isinstance(typedef, ClassificationDef):
128138
# Set up the request payload...
129139
payload = TypeDefResponse(
@@ -263,6 +273,8 @@ def _call_api(
263273
try:
264274
if (
265275
response.content is None
276+
or response.content == "null"
277+
or len(response.content) == 0
266278
or response.status_code == HTTPStatus.NO_CONTENT
267279
):
268280
return None
@@ -354,6 +366,88 @@ def get_all_roles(self) -> RoleResponse:
354366
raw_json = self._call_api(GET_ROLES.format_path_with_params())
355367
return RoleResponse(**raw_json)
356368

369+
def create_group(
370+
self,
371+
group: AtlanGroup,
372+
user_ids: Optional[list[str]] = None,
373+
) -> CreateGroupResponse:
374+
payload = CreateGroupRequest(group=group, user_ids=user_ids)
375+
raw_json = self._call_api(CREATE_GROUP, request_obj=payload, exclude_unset=True)
376+
return CreateGroupResponse(**raw_json)
377+
378+
def update_group(
379+
self,
380+
group: AtlanGroup,
381+
) -> None:
382+
self._call_api(
383+
UPDATE_GROUP.format_path_with_params(group.id),
384+
request_obj=group,
385+
exclude_unset=True,
386+
)
387+
388+
def purge_group(
389+
self,
390+
guid: str,
391+
) -> None:
392+
self._call_api(DELETE_GROUP.format_path({"group_guid": guid}))
393+
394+
def get_groups(
395+
self,
396+
limit: Optional[int] = None,
397+
post_filter: Optional[str] = None,
398+
sort: Optional[str] = None,
399+
count: bool = True,
400+
offset: int = 0,
401+
) -> GroupResponse:
402+
query_params: dict[str, str] = {
403+
"count": str(count),
404+
"offset": str(offset),
405+
}
406+
if limit is not None:
407+
query_params["limit"] = str(limit)
408+
if post_filter is not None:
409+
query_params["filter"] = post_filter
410+
if sort is not None:
411+
query_params["sort"] = sort
412+
raw_json = self._call_api(GET_GROUPS.format_path_with_params(), query_params)
413+
return GroupResponse(**raw_json)
414+
415+
def get_all_groups(self) -> list[AtlanGroup]:
416+
"""
417+
Retrieve all groups defined in Atlan.
418+
"""
419+
groups: list[AtlanGroup] = []
420+
offset = 0
421+
limit = 100
422+
response: Optional[GroupResponse] = self.get_groups(
423+
offset=offset, limit=limit, sort="createdAt"
424+
)
425+
while response:
426+
if page := response.records:
427+
groups.extend(page)
428+
offset += limit
429+
response = self.get_groups(offset=offset, limit=limit, sort="createdAt")
430+
else:
431+
response = None
432+
return groups
433+
434+
def get_group_by_name(
435+
self, alias: str, limit: int = 100
436+
) -> Optional[list[AtlanGroup]]:
437+
"""
438+
Retrieve all groups with a name that contains the provided string.
439+
(This could include a complete group name, in which case there should be at most
440+
a single item in the returned list, or could be a partial group name to retrieve
441+
all groups with that naming convention.)
442+
"""
443+
if response := self.get_groups(
444+
offset=0,
445+
limit=limit,
446+
post_filter='{"$and":[{"alias":{"$ilike":"%' + alias + '%"}}]}',
447+
):
448+
return response.records
449+
return None
450+
357451
@validate_arguments()
358452
def get_asset_by_qualified_name(
359453
self,
@@ -549,32 +643,59 @@ def get_typedefs(self, type_category: AtlanTypeCategory) -> TypeDefResponse:
549643
return TypeDefResponse(**raw_json)
550644

551645
def create_typedef(self, typedef: TypeDef) -> TypeDefResponse:
552-
payload = _build_typdef_request(typedef)
646+
payload = _build_typedef_request(typedef)
553647
raw_json = self._call_api(
554648
CREATE_TYPE_DEFS, request_obj=payload, exclude_unset=True
555649
)
556650
_refresh_caches(typedef)
557651
return TypeDefResponse(**raw_json)
558652

559653
def update_typedef(self, typedef: TypeDef) -> TypeDefResponse:
560-
payload = _build_typdef_request(typedef)
654+
payload = _build_typedef_request(typedef)
561655
raw_json = self._call_api(
562656
UPDATE_TYPE_DEFS, request_obj=payload, exclude_unset=True
563657
)
564658
_refresh_caches(typedef)
565659
return TypeDefResponse(**raw_json)
566660

567-
def purge_typedef(self, internal_name: str) -> None:
568-
self._call_api(DELETE_TYPE_DEF_BY_NAME.format_path_with_params(internal_name))
569-
# TODO: if we know which kind of typedef is being purged, we only need
570-
# to refresh that particular cache
571-
from pyatlan.cache.classification_cache import ClassificationCache
572-
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
573-
from pyatlan.cache.enum_cache import EnumCache
661+
def purge_typedef(self, name: str, typedef_type: type) -> None:
662+
if typedef_type == CustomMetadataDef:
663+
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
574664

575-
ClassificationCache.refresh_cache()
576-
CustomMetadataCache.refresh_cache()
577-
EnumCache.refresh_cache()
665+
internal_name = CustomMetadataCache.get_id_for_name(name)
666+
elif typedef_type == EnumDef:
667+
internal_name = name
668+
elif typedef_type == ClassificationDef:
669+
from pyatlan.cache.classification_cache import ClassificationCache
670+
671+
internal_name = str(ClassificationCache.get_id_for_name(name))
672+
else:
673+
raise InvalidRequestException(
674+
message=f"Unable to purge type definitions of type: {typedef_type}",
675+
)
676+
# Throw an invalid request exception
677+
if internal_name:
678+
self._call_api(
679+
DELETE_TYPE_DEF_BY_NAME.format_path_with_params(internal_name)
680+
)
681+
else:
682+
raise NotFoundError(
683+
message=f"Unable to find {typedef_type} with name: {name}",
684+
code="ATLAN-PYTHON-404-000",
685+
)
686+
687+
if typedef_type == CustomMetadataDef:
688+
from pyatlan.cache.custom_metadata_cache import CustomMetadataCache
689+
690+
CustomMetadataCache.refresh_cache()
691+
elif typedef_type == EnumDef:
692+
from pyatlan.cache.enum_cache import EnumCache
693+
694+
EnumCache.refresh_cache()
695+
elif typedef_type == ClassificationDef:
696+
from pyatlan.cache.classification_cache import ClassificationCache
697+
698+
ClassificationCache.refresh_cache()
578699

579700
@validate_arguments()
580701
def add_classifications(

pyatlan/client/constants.py

+7
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@
1313
)
1414

1515
ROLE_API = f"{ADMIN_URI}roles"
16+
GROUP_API = f"{ADMIN_URI}groups"
1617

1718
# Role APIs
1819
GET_ROLES = API(ROLE_API, HTTPMethod.GET, HTTPStatus.OK)
1920

21+
# Group APIs
22+
GET_GROUPS = API(GROUP_API, HTTPMethod.GET, HTTPStatus.OK)
23+
CREATE_GROUP = API(GROUP_API, HTTPMethod.POST, HTTPStatus.OK)
24+
UPDATE_GROUP = API(GROUP_API, HTTPMethod.POST, HTTPStatus.OK)
25+
DELETE_GROUP = API(GROUP_API + "/{group_guid}/delete", HTTPMethod.POST, HTTPStatus.OK)
26+
2027
ENTITY_API = f"{BASE_URI}entity/"
2128
PREFIX_ATTR = "attr:"
2229
PREFIX_ATTR_ = "attr_"

0 commit comments

Comments
 (0)