diff --git a/src/sempy_labs/__init__.py b/src/sempy_labs/__init__.py index da57e489..0dee1046 100644 --- a/src/sempy_labs/__init__.py +++ b/src/sempy_labs/__init__.py @@ -11,7 +11,6 @@ update_on_premises_gateway, bind_semantic_model_to_gateway, ) - from sempy_labs._authentication import ( ServicePrincipalTokenProvider, ) diff --git a/src/sempy_labs/graph/__init__.py b/src/sempy_labs/graph/__init__.py new file mode 100644 index 00000000..4ff62a37 --- /dev/null +++ b/src/sempy_labs/graph/__init__.py @@ -0,0 +1,33 @@ +from sempy_labs.graph._groups import ( + list_groups, + list_group_owners, + list_group_members, + add_group_members, + add_group_owners, + resolve_group_id, + renew_group, +) +from sempy_labs.graph._users import ( + resolve_user_id, + get_user, + list_users, + send_mail, +) +from sempy_labs.graph._teams import ( + list_teams, +) + +__all__ = [ + "list_groups", + "list_group_owners", + "list_group_members", + "add_group_members", + "add_group_owners", + "renew_group", + "resolve_group_id", + "resolve_user_id", + "get_user", + "list_users", + "send_mail", + "list_teams", +] diff --git a/src/sempy_labs/graph/_groups.py b/src/sempy_labs/graph/_groups.py new file mode 100644 index 00000000..608c8a6c --- /dev/null +++ b/src/sempy_labs/graph/_groups.py @@ -0,0 +1,425 @@ +import pandas as pd +from uuid import UUID +from sempy.fabric._token_provider import TokenProvider +from sempy_labs._helper_functions import _is_valid_uuid +import sempy_labs._icons as icons +from typing import List, Literal +from sempy_labs.graph._util import _ms_graph_base + + +def resolve_group_id(group: str | UUID, token_provider: TokenProvider) -> UUID: + """ + Resolves the group ID from the group name or ID. + + Parameters + ---------- + group : str | uuid.UUID + The group name. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + uuid.UUID + The group ID. + """ + if _is_valid_uuid(group): + group_id = group + else: + dfG = list_groups(token_provider=token_provider) + dfG_filt = dfG[dfG["Group Name"] == group] + if dfG_filt.empty: + raise ValueError(f"{icons.red_dot} The '{group}' group does not exist.") + group_id = dfG_filt["Group Id"].iloc[0] + + return group_id + + +def list_groups(token_provider: TokenProvider) -> pd.DataFrame: + """ + Shows a list of groups and their properties. + + This is a wrapper function for the following API: `List groups `_. + + Parameters + ---------- + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of groups and their properties. + """ + + result = _ms_graph_base(api_name="groups", token_provider=token_provider) + + df = pd.DataFrame( + columns=[ + "Group Id", + "Group Name", + "Mail", + "Description", + "Classification", + "Mail Enabled", + "Security Enabled", + "Created Date Time", + "Expiration Date Time", + "Deleted Date Time", + "Renewed Date Time", + "Visibility", + "Security Identifier", + ] + ) + + for v in result.get("value"): + new_data = { + "Group Id": v.get("id"), + "Group Name": v.get("displayName"), + "Mail": v.get("mail"), + "Description": v.get("description"), + "Classification": v.get("classification"), + "Mail Enabled": v.get("mailEnabled"), + "Security Enabled": v.get("securityEnabled"), + "Created Date Time": v.get("createdDateTime"), + "Expiration Date Time": v.get("expirationDateTime"), + "Renewed Date Time": v.get("renewedDateTime"), + "Deleted Date Time": v.get("deletedDateTime"), + "Visibility": v.get("visibility"), + "Security Identifier": v.get("securityIdentifier"), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + bool_cols = ["Mail Enabled", "Security Enabled"] + df[bool_cols] = df[bool_cols].astype(bool) + df["Created Date Time"] = pd.to_datetime(df["Created Date Time"]) + + return df + + +def _get_group(group_id: UUID, token_provider: TokenProvider) -> pd.DataFrame: + """ + Shows a list of groups and their properties. + + This is a wrapper function for the following API: `Get group `_. + + Parameters + ---------- + group_id : uuid.UUID + The group ID. + token_provider : TokenProvider + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of groups and their properties. + """ + + result = _ms_graph_base( + api_name=f"groups/{group_id}", token_provider=token_provider + ) + + df = pd.DataFrame( + columns=[ + "Group Id", + "Group Name", + "Mail", + "Description", + "Classification", + "Mail Enabled", + "Security Enabled", + "Created Date Time", + "Expiration Date Time", + "Deleted Date Time", + "Renewed Date Time", + "Visibility", + "Security Identifier", + ] + ) + + for v in result.get("value"): + new_data = { + "Group Id": v.get("id"), + "Group Name": v.get("displayName"), + "Mail": v.get("mail"), + "Description": v.get("description"), + "Classification": v.get("classification"), + "Mail Enabled": v.get("mailEnabled"), + "Security Enabled": v.get("securityEnabled"), + "Created Date Time": v.get("createdDateTime"), + "Expiration Date Time": v.get("expirationDateTime"), + "Deleted Date Time": v.get("deletedDateTime"), + "Renewed Date Time": v.get("renewedDateTime"), + "Visibility": v.get("visibility"), + "Security Identifier": v.get("securityIdentifier"), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + bool_cols = ["Mail Enabled", "Security Enabled"] + df[bool_cols] = df[bool_cols].astype(bool) + df["Created Date Time"] = pd.to_datetime(df["Created Date Time"]) + + return df + + +def list_group_members( + group: str | UUID, token_provider: TokenProvider +) -> pd.DataFrame: + """ + Shows a list of the members of a group. + + This is a wrapper function for the following API: `List group members `_. + + Parameters + ---------- + group : str | uuid.UUID + The group name or ID. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of the members of a group. + """ + + group_id = resolve_group_id(group, token_provider) + + result = _ms_graph_base( + api_name=f"groups/{group_id}/members", token_provider=token_provider + ) + + df = pd.DataFrame( + columns=[ + "Member Id", + "Member Name", + "User Principal Name", + "Mail", + "Job Title", + "Office Location", + "Mobile Phone", + "Business Phones", + "Preferred Language", + "Given Name", + "Surname", + ] + ) + + for v in result.get("value"): + new_data = { + "Member Id": v.get("id"), + "Member Name": v.get("displayName"), + "User Principal Name": v.get("userPrincipalName"), + "Mail": v.get("mail"), + "Job Title": v.get("jobTitle"), + "Office Location": v.get("officeLocation"), + "Mobile Phone": v.get("mobilePhone"), + "Business Phones": str(v.get("businessPhones")), + "Preferred Language": v.get("preferredLanguage"), + "Given Name": v.get("givenName"), + "Surname": v.get("surname"), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + return df + + +def list_group_owners(group: str | UUID, token_provider: TokenProvider) -> pd.DataFrame: + """ + Shows a list of the owners of a group. + + This is a wrapper function for the following API: `List group owners `_. + + Parameters + ---------- + group : str | uuid.UUID + The group name or ID. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of the owners of a group. + """ + + if _is_valid_uuid(group): + group_id = group + else: + group_id = resolve_group_id(group, token_provider) + + result = _ms_graph_base( + api_name=f"groups/{group_id}/members", token_provider=token_provider + ) + + df = pd.DataFrame( + columns=[ + "Owner Id", + "Owner Name", + "User Principal Name", + "Mail", + "Job Title", + "Office Location", + "Mobile Phone", + "Business Phones", + "Preferred Language", + "Given Name", + "Surname", + ] + ) + + for v in result.get("value"): + new_data = { + "Owner Id": v.get("id"), + "Owner Name": v.get("displayName"), + "User Principal Name": v.get("userPrincipalName"), + "Mail": v.get("mail"), + "Job Title": v.get("jobTitle"), + "Office Location": v.get("officeLocation"), + "Mobile Phone": v.get("mobilePhone"), + "Business Phones": str(v.get("businessPhones")), + "Preferred Language": v.get("preferredLanguage"), + "Given Name": v.get("givenName"), + "Surname": v.get("surname"), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + return df + + +def _base_add_to_group( + group: str | UUID, + object: str | UUID, + token_provider: TokenProvider, + object_type: Literal["members", "owners"], +): + + from sempy_labs.graph._users import resolve_user_id + + object_list = [] + + if isinstance(object, str): + object = [object] + + group_id = resolve_group_id(group, token_provider) + url = f"groups/{group_id}/{object_type}/$ref" + + for m in object: + if _is_valid_uuid(m): + member_id = m + else: + member_id = resolve_user_id(m, token_provider) + if object_type == "members": + object_list.append( + f"https://graph.microsoft.com/v1.0/directoryObjects/{member_id}" + ) + else: + object_list.append(f"https://graph.microsoft.com/v1.0/users/{member_id}") + + # Must submit one request for each owner. Members can be sent in a single request. + if object_type == "members": + payload = {"members@odata.bind": object_list} + _ms_graph_base( + api_name=url, + token_provider=token_provider, + payload=payload, + status_success_code=204, + return_json=False, + call_type="post", + ) + else: + for o in object_list: + payload = {"odata.id": o} + _ms_graph_base( + api_name=url, + token_provider=token_provider, + payload=payload, + status_success_code=204, + return_json=False, + call_type="post", + ) + + print( + f"{icons.green_dot} The {object} {object_type[:-1]}(s) have been added to the '{group}' group." + ) + + +def add_group_members( + group: str | UUID, + user: str | UUID | List[str | UUID], + token_provider: TokenProvider, +): + """ + Adds a member to a group. + + This is a wrapper function for the following API: `Add members `_. + + Parameters + ---------- + group : str | uuid.UUID + The group name or ID. + user : str | uuid.UUID + The user ID or user principal name. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + """ + + _base_add_to_group( + group=group, object=user, token_provider=token_provider, object_type="members" + ) + + +def add_group_owners( + group: str | UUID, + user: str | UUID | List[str | UUID], + token_provider: TokenProvider, +): + """ + Adds an owner to a group. + + This is a wrapper function for the following API: `Add owners `_. + + Parameters + ---------- + group : str | uuid.UUID + The group name or ID. + user : str | uuid.UUID + The user ID or user principal name. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + """ + + _base_add_to_group( + group=group, object=user, token_provider=token_provider, object_type="owners" + ) + + +def renew_group(group: str | UUID, token_provider: TokenProvider): + """ + Renews the group. + + This is a wrapper function for the following API: `Renew group `_. + + Parameters + ---------- + group : str | uuid.UUID + The group name or ID. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + """ + + group_id = resolve_group_id(group, token_provider) + + _ms_graph_base( + api_name=f"groups/{group_id}/renew", + token_provider=token_provider, + status_success_code=204, + return_json=False, + call_type="post", + ) + + print(f"{icons.green_dot} The '{group}' group has been renewed.") diff --git a/src/sempy_labs/graph/_teams.py b/src/sempy_labs/graph/_teams.py new file mode 100644 index 00000000..05a97b00 --- /dev/null +++ b/src/sempy_labs/graph/_teams.py @@ -0,0 +1,110 @@ +import pandas as pd +from uuid import UUID +from sempy.fabric._token_provider import TokenProvider +from sempy_labs.graph._util import _ms_graph_base + + +def list_teams(token_provider: TokenProvider) -> pd.DataFrame: + """ + Shows a list of teams and their properties. + + This is a wrapper function for the following API: `List teams `_. + + Parameters + ---------- + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of teams and their properties. + """ + + result = _ms_graph_base(api_name="teams", token_provider=token_provider) + + df = pd.DataFrame( + columns=[ + "Team Id", + "Team Name", + "Description", + "Creation Date Time", + "Classification", + "Specialization", + "Visibility", + "Web Url", + "Archived", + "Favorite By Me", + "Discoverable By Me", + "Member Count", + ] + ) + + for v in result.get("value"): + new_data = { + "Team Id": v.get("id"), + "Team Name": v.get("displayName"), + "Description": v.get("description"), + "Creation Date Time": v.get("createdDateTime"), + "Classification": v.get("classification"), + "Specialization": v.get("specialization"), + "Visibility": v.get("visibility"), + "Web Url": v.get("webUrl"), + "Archived": v.get("isArchived"), + "Favorite By Me": v.get("isFavoriteByMe"), + "Discoverable By Me": v.get("isDiscoverableByMe"), + "Member Count": v.get("memberCount"), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + bool_cols = ["Archived", "Favorite By Me", "Discoverable By Me"] + df[bool_cols] = df[bool_cols].astype(bool) + df["Creation Date Time"] = pd.to_datetime(df["Creation Date Time"]) + + return df + + +def list_chats(user: str | UUID, token_provider: TokenProvider) -> pd.DataFrame: + """ + In progress... + """ + + from sempy_labs.graph._users import resolve_user_id + + user_id = resolve_user_id(user=user, token_provider=token_provider) + result = _ms_graph_base(api_name=f"users/{user_id}/chats") + + df = pd.DataFrame(columns=['Chat Id', 'Type', 'Members']) + + for v in result.get("value"): + new_data = { + "Chat Id": v.get("id"), + "Type": v.get('chatType'), + "Members": v.get('members'), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + return df + + +def send_teams_message(chat_id: str, message: str, token_provider: TokenProvider): + """ + In progress... + """ + + payload = { + "body": { + "content": message, + } + } + + _ms_graph_base( + api_name=f"chats/{chat_id}/messages", + token_provider=token_provider, + status_success_code=201, + return_json=False, + payload=payload, + call_type="post", + ) diff --git a/src/sempy_labs/graph/_users.py b/src/sempy_labs/graph/_users.py new file mode 100644 index 00000000..227d36e7 --- /dev/null +++ b/src/sempy_labs/graph/_users.py @@ -0,0 +1,196 @@ +import pandas as pd +from uuid import UUID +from sempy.fabric._token_provider import TokenProvider +import sempy_labs._icons as icons +from typing import List +from sempy_labs.graph._util import ( + _ms_graph_base, +) +from sempy_labs._helper_functions import _is_valid_uuid + + +def resolve_user_id(user: str | UUID, token_provider: TokenProvider) -> UUID: + """ + Resolves the user ID from the user principal name or ID. + + Parameters + ---------- + user : str | uuid.UUID + The user ID or user principal name. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + uuid.UUID + The user ID. + """ + + if _is_valid_uuid(user): + return user + else: + result = _ms_graph_base(api_name=f"users/{user}", token_provider=token_provider) + return result.get("id") + + +def get_user(user: str | UUID, token_provider: TokenProvider) -> pd.DataFrame: + """ + Shows properties of a given user. + + This is a wrapper function for the following API: `Get a user `_. + + Parameters + ---------- + user : str | uuid.UUID + The user ID or user principal name. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing properties of a given user. + """ + + result = _ms_graph_base(api_name=f"users/{user}", token_provider=token_provider) + + new_data = { + "User Id": result.get("id"), + "User Principal Name": result.get("userPrincipalName"), + "User Name": result.get("displayName"), + "Mail": result.get("mail"), + "Job Title": result.get("jobTitle"), + "Office Location": result.get("officeLocation"), + "Mobile Phone": result.get("mobilePhone"), + "Business Phones": str(result.get("businessPhones")), + "Preferred Language": result.get("preferredLanguage"), + "Surname": result.get("surname"), + } + + return pd.DataFrame([new_data]) + + +def list_users(token_provider: TokenProvider) -> pd.DataFrame: + """ + Shows a list of users and their properties. + + This is a wrapper function for the following API: `List users `_. + + Parameters + ---------- + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of users and their properties. + """ + + result = _ms_graph_base(api_name="users", token_provider=token_provider) + + df = pd.DataFrame( + columns=[ + "User Id", + "User Principal Name", + "User Name", + "Mail", + "Job Title", + "Office Location", + "Mobile Phone", + "Business Phones", + "Preferred Language", + "Surname", + ] + ) + + for v in result.get("value"): + new_data = { + "User Id": v.get("id"), + "User Principal Name": v.get("userPrincipalName"), + "User Name": v.get("displayName"), + "Mail": v.get("mail"), + "Job Title": v.get("jobTitle"), + "Office Location": v.get("officeLocation"), + "Mobile Phone": v.get("mobilePhone"), + "Business Phones": str(v.get("businessPhones")), + "Preferred Language": v.get("preferredLanguage"), + "Surname": v.get("surname"), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + return df + + +def send_mail( + user: UUID | str, + subject: str, + to_recipients: str | List[str], + content: str, + token_provider: TokenProvider, + cc_recipients: str | List[str] = None, +): + """ + Sends an email to the specified recipients. + + This is a wrapper function for the following API: `user: sendMail `_. + + Parameters + ---------- + user : uuid.UUID | str + The user ID or user principal name. + subject : str + The email subject. + to_recipients : str | List[str] + The email address of the recipients. + content : str + The email content. + token_provider : TokenProvider + The token provider for authentication, created by using the ServicePrincipalTokenProvider class. + cc_recipients : str | List[str], default=None + The email address of the CC recipients. + """ + + user_id = resolve_user_id(user=user, token_provider=token_provider) + + if isinstance(to_recipients, str): + to_recipients = [to_recipients] + + if isinstance(cc_recipients, str): + cc_recipients = [cc_recipients] + + to_email_addresses = [ + {"emailAddress": {"address": email}} for email in to_recipients + ] + + cc_email_addresses = ( + [{"emailAddress": {"address": email}} for email in cc_recipients] + if cc_recipients + else None + ) + + payload = { + "message": { + "subject": subject, + "body": { + "contentType": "Text", + "content": content, + }, + "toRecipients": to_email_addresses, + }, + } + + if cc_email_addresses: + payload["message"]["ccRecipients"] = cc_email_addresses + + _ms_graph_base( + api_name=f"users/{user_id}/sendMail", + token_provider=token_provider, + status_success_code=202, + return_json=False, + payload=payload, + call_type="post", + ) + + print(f"{icons.green_dot} The email has been sent to {to_recipients}.") diff --git a/src/sempy_labs/graph/_util.py b/src/sempy_labs/graph/_util.py new file mode 100644 index 00000000..c217e119 --- /dev/null +++ b/src/sempy_labs/graph/_util.py @@ -0,0 +1,44 @@ +import requests +from sempy_labs._authentication import _get_headers +from sempy.fabric._token_provider import TokenProvider +from sempy.fabric.exceptions import FabricHTTPException +from typing import Optional + + +def _ms_graph_base( + api_name, + token_provider: TokenProvider, + status_success_code: int = 200, + call_type: str = "get", + payload: Optional[str] = None, + return_json: bool = True, +): + + if isinstance(status_success_code, int): + status_success_codes = [status_success_code] + + headers = _get_headers(token_provider, audience="graph") + url = f"https://graph.microsoft.com/v1.0/{api_name}" + + if payload: + if call_type == "post": + response = requests.post(url, headers=headers, json=payload) + elif call_type == "get": + response = requests.get(url, headers=headers, json=payload) + elif call_type == "put": + response = requests.put(url, headers=headers, json=payload) + else: + if call_type == "post": + response = requests.post(url, headers=headers) + elif call_type == "get": + response = requests.get(url, headers=headers) + elif call_type == "put": + response = requests.put(url, headers=headers) + + if response.status_code not in status_success_codes: + raise FabricHTTPException(response) + + if return_json: + return response.json() + else: + return response.status_code