Skip to content

Commit 14da041

Browse files
committed
Merge changes from main
2 parents 48297b0 + ce54407 commit 14da041

File tree

7 files changed

+741
-42
lines changed

7 files changed

+741
-42
lines changed

pyatlan/cache/user_cache.py

Lines changed: 59 additions & 0 deletions
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 UserCache:
9+
map_id_to_name: dict[str, str] = dict()
10+
map_name_to_id: dict[str, str] = dict()
11+
map_email_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+
users = client.get_all_users()
19+
if users is not None:
20+
cls.map_id_to_name = {}
21+
cls.map_name_to_id = {}
22+
cls.map_email_to_id = {}
23+
for user in users:
24+
user_id = str(user.id)
25+
username = str(user.username)
26+
user_email = str(user.email)
27+
cls.map_id_to_name[user_id] = username
28+
cls.map_name_to_id[username] = user_id
29+
cls.map_email_to_id[user_email] = user_id
30+
31+
@classmethod
32+
def get_id_for_name(cls, name: str) -> Optional[str]:
33+
"""
34+
Translate the provided human-readable username to its GUID.
35+
"""
36+
if user_id := cls.map_name_to_id.get(name):
37+
return user_id
38+
cls._refresh_cache()
39+
return cls.map_name_to_id.get(name)
40+
41+
@classmethod
42+
def get_id_for_email(cls, email: str) -> Optional[str]:
43+
"""
44+
Translate the provided email to its GUID.
45+
"""
46+
if user_id := cls.map_email_to_id.get(email):
47+
return user_id
48+
cls._refresh_cache()
49+
return cls.map_email_to_id.get(email)
50+
51+
@classmethod
52+
def get_name_for_id(cls, idstr: str) -> Optional[str]:
53+
"""
54+
Translate the provided user GUID to the human-readable username.
55+
"""
56+
if username := cls.map_id_to_name.get(idstr):
57+
return username
58+
cls._refresh_cache()
59+
return cls.map_id_to_name.get(idstr)

pyatlan/client/atlan.py

Lines changed: 185 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,34 @@
2121

2222
from pyatlan.client.constants import (
2323
ADD_BUSINESS_ATTRIBUTE_BY_ID,
24+
ADD_USER_TO_GROUPS,
2425
BULK_UPDATE,
26+
CHANGE_USER_ROLE,
2527
CREATE_GROUP,
2628
CREATE_TYPE_DEFS,
29+
CREATE_USERS,
2730
DELETE_ENTITY_BY_ATTRIBUTE,
2831
DELETE_ENTITY_BY_GUID,
2932
DELETE_GROUP,
3033
DELETE_TYPE_DEF_BY_NAME,
34+
DELETE_USER,
3135
GET_ALL_TYPE_DEFS,
36+
GET_CURRENT_USER,
3237
GET_ENTITY_BY_GUID,
3338
GET_ENTITY_BY_UNIQUE_ATTRIBUTE,
39+
GET_GROUP_MEMBERS,
3440
GET_GROUPS,
3541
GET_LINEAGE,
3642
GET_ROLES,
43+
GET_USER_GROUPS,
44+
GET_USERS,
3745
INDEX_SEARCH,
3846
PARTIAL_UPDATE_ENTITY_BY_ATTRIBUTE,
47+
REMOVE_USERS_FROM_GROUP,
3948
UPDATE_ENTITY_BY_ATTRIBUTE,
4049
UPDATE_GROUP,
4150
UPDATE_TYPE_DEFS,
51+
UPDATE_USER,
4252
)
4353
from pyatlan.error import AtlanError, NotFoundError
4454
from pyatlan.exceptions import AtlanServiceException, InvalidRequestException
@@ -78,6 +88,7 @@
7888
CreateGroupRequest,
7989
CreateGroupResponse,
8090
GroupResponse,
91+
RemoveFromGroupRequest,
8192
)
8293
from pyatlan.model.lineage import LineageRequest, LineageResponse
8394
from pyatlan.model.response import AssetMutationResponse
@@ -90,6 +101,14 @@
90101
TypeDef,
91102
TypeDefResponse,
92103
)
104+
from pyatlan.model.user import (
105+
AddToGroupsRequest,
106+
AtlanUser,
107+
ChangeRoleRequest,
108+
CreateUserRequest,
109+
UserMinimalResponse,
110+
UserResponse,
111+
)
93112
from pyatlan.utils import HTTPStatus, get_logger
94113

95114
LOGGER = get_logger()
@@ -371,7 +390,9 @@ def create_group(
371390
group: AtlanGroup,
372391
user_ids: Optional[list[str]] = None,
373392
) -> CreateGroupResponse:
374-
payload = CreateGroupRequest(group=group, user_ids=user_ids)
393+
payload = CreateGroupRequest(group=group)
394+
if user_ids:
395+
payload.users = user_ids
375396
raw_json = self._call_api(CREATE_GROUP, request_obj=payload, exclude_unset=True)
376397
return CreateGroupResponse(**raw_json)
377398

@@ -448,6 +469,169 @@ def get_group_by_name(
448469
return response.records
449470
return None
450471

472+
def get_group_members(self, guid: str) -> UserResponse:
473+
"""
474+
Retrieves the members (users) of a group.
475+
"""
476+
raw_json = self._call_api(GET_GROUP_MEMBERS.format_path({"group_guid": guid}))
477+
return UserResponse(**raw_json)
478+
479+
def remove_users_from_group(self, guid: str, user_ids=list[str]) -> None:
480+
"""
481+
Remove one or more users from a group.
482+
"""
483+
rfgr = RemoveFromGroupRequest(users=user_ids)
484+
self._call_api(
485+
REMOVE_USERS_FROM_GROUP.format_path({"group_guid": guid}),
486+
request_obj=rfgr,
487+
exclude_unset=True,
488+
)
489+
490+
def create_users(
491+
self,
492+
users: list[AtlanUser],
493+
) -> None:
494+
from pyatlan.cache.role_cache import RoleCache
495+
496+
cur = CreateUserRequest(users=[])
497+
for user in users:
498+
role_name = str(user.workspace_role)
499+
if role_id := RoleCache.get_id_for_name(role_name):
500+
to_create = CreateUserRequest.CreateUser(
501+
email=user.email,
502+
role_name=role_name,
503+
role_id=role_id,
504+
)
505+
cur.users.append(to_create)
506+
self._call_api(CREATE_USERS, request_obj=cur, exclude_unset=True)
507+
508+
def update_user(
509+
self,
510+
guid: str,
511+
user: AtlanUser,
512+
) -> UserMinimalResponse:
513+
raw_json = self._call_api(
514+
UPDATE_USER.format_path_with_params(guid),
515+
request_obj=user,
516+
exclude_unset=True,
517+
)
518+
return UserMinimalResponse(**raw_json)
519+
520+
def purge_user(
521+
self,
522+
guid: str,
523+
) -> None:
524+
self._call_api(DELETE_USER.format_path({"user_guid": guid}))
525+
526+
def get_groups_for_user(
527+
self,
528+
guid: str,
529+
) -> GroupResponse:
530+
raw_json = self._call_api(GET_USER_GROUPS.format_path({"user_guid": guid}))
531+
return GroupResponse(**raw_json)
532+
533+
def add_user_to_groups(
534+
self,
535+
guid: str,
536+
group_ids: list[str],
537+
) -> None:
538+
atgr = AddToGroupsRequest(groups=group_ids)
539+
self._call_api(
540+
ADD_USER_TO_GROUPS.format_path({"user_guid": guid}),
541+
request_obj=atgr,
542+
exclude_unset=True,
543+
)
544+
545+
def change_user_role(
546+
self,
547+
guid: str,
548+
role_id: str,
549+
) -> None:
550+
crr = ChangeRoleRequest(role_id=role_id)
551+
self._call_api(
552+
CHANGE_USER_ROLE.format_path({"user_guid": guid}),
553+
request_obj=crr,
554+
exclude_unset=True,
555+
)
556+
557+
def get_current_user(
558+
self,
559+
) -> UserMinimalResponse:
560+
raw_json = self._call_api(GET_CURRENT_USER)
561+
return UserMinimalResponse(**raw_json)
562+
563+
def get_users(
564+
self,
565+
limit: Optional[int] = None,
566+
post_filter: Optional[str] = None,
567+
sort: Optional[str] = None,
568+
count: bool = True,
569+
offset: int = 0,
570+
) -> UserResponse:
571+
query_params: dict[str, str] = {
572+
"count": str(count),
573+
"offset": str(offset),
574+
}
575+
if limit is not None:
576+
query_params["limit"] = str(limit)
577+
if post_filter is not None:
578+
query_params["filter"] = post_filter
579+
if sort is not None:
580+
query_params["sort"] = sort
581+
raw_json = self._call_api(GET_USERS.format_path_with_params(), query_params)
582+
return UserResponse(**raw_json)
583+
584+
def get_all_users(self) -> list[AtlanUser]:
585+
"""
586+
Retrieve all users defined in Atlan.
587+
"""
588+
users: list[AtlanUser] = []
589+
offset = 0
590+
limit = 100
591+
response: Optional[UserResponse] = self.get_users(
592+
offset=offset, limit=limit, sort="username"
593+
)
594+
while response:
595+
if page := response.records:
596+
users.extend(page)
597+
offset += limit
598+
response = self.get_users(offset=offset, limit=limit, sort="username")
599+
else:
600+
response = None
601+
return users
602+
603+
def get_users_by_email(
604+
self, email: str, limit: int = 100
605+
) -> Optional[list[AtlanUser]]:
606+
"""
607+
Retrieves all users with email addresses that contain the provided email.
608+
(This could include a complete email address, in which case there should be at
609+
most a single item in the returned list, or could be a partial email address
610+
such as "@example.com" to retrieve all users with that domain in their email
611+
address.)
612+
"""
613+
if response := self.get_users(
614+
offset=0,
615+
limit=limit,
616+
post_filter='{"email":{"$ilike":"%' + email + '%"}}',
617+
):
618+
return response.records
619+
return None
620+
621+
def get_user_by_username(self, username: str) -> Optional[AtlanUser]:
622+
"""
623+
Retrieves a user based on the username. (This attempts an exact match on username
624+
rather than a contains search.)
625+
"""
626+
if response := self.get_users(
627+
offset=0,
628+
limit=5,
629+
post_filter='{"username":"' + username + '"}',
630+
):
631+
if response.records and len(response.records) >= 1:
632+
return response.records[0]
633+
return None
634+
451635
@validate_arguments()
452636
def get_asset_by_qualified_name(
453637
self,

pyatlan/client/constants.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
ROLE_API = f"{ADMIN_URI}roles"
1616
GROUP_API = f"{ADMIN_URI}groups"
17+
USER_API = f"{ADMIN_URI}users"
1718

1819
# Role APIs
1920
GET_ROLES = API(ROLE_API, HTTPMethod.GET, HTTPStatus.OK)
@@ -23,6 +24,26 @@
2324
CREATE_GROUP = API(GROUP_API, HTTPMethod.POST, HTTPStatus.OK)
2425
UPDATE_GROUP = API(GROUP_API, HTTPMethod.POST, HTTPStatus.OK)
2526
DELETE_GROUP = API(GROUP_API + "/{group_guid}/delete", HTTPMethod.POST, HTTPStatus.OK)
27+
GET_GROUP_MEMBERS = API(
28+
GROUP_API + "/{group_guid}/members", HTTPMethod.GET, HTTPStatus.OK
29+
)
30+
REMOVE_USERS_FROM_GROUP = API(
31+
GROUP_API + "/{group_guid}/members/remove", HTTPMethod.POST, HTTPStatus.OK
32+
)
33+
34+
# User APIs
35+
GET_USERS = API(USER_API, HTTPMethod.GET, HTTPStatus.OK)
36+
CREATE_USERS = API(USER_API, HTTPMethod.POST, HTTPStatus.OK)
37+
UPDATE_USER = API(USER_API, HTTPMethod.POST, HTTPStatus.OK)
38+
DELETE_USER = API(USER_API + "/{user_guid}/delete", HTTPMethod.POST, HTTPStatus.OK)
39+
GET_USER_GROUPS = API(USER_API + "/{user_guid}/groups", HTTPMethod.GET, HTTPStatus.OK)
40+
ADD_USER_TO_GROUPS = API(
41+
USER_API + "/{user_guid}/groups", HTTPMethod.POST, HTTPStatus.OK
42+
)
43+
CHANGE_USER_ROLE = API(
44+
USER_API + "/{user_guid}/roles/update", HTTPMethod.POST, HTTPStatus.OK
45+
)
46+
GET_CURRENT_USER = API(f"{USER_API}/current", HTTPMethod.GET, HTTPStatus.OK)
2647

2748
ENTITY_API = f"{BASE_URI}entity/"
2849
PREFIX_ATTR = "attr:"

pyatlan/model/group.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def is_default(self) -> bool:
5858
return (
5959
self.attributes is not None
6060
and self.attributes.is_default is not None
61-
and self.attributes.is_default == "true"
61+
and self.attributes.is_default == ["true"]
6262
)
6363

6464
@staticmethod

0 commit comments

Comments
 (0)