diff --git a/src/sempy_labs/_helper_functions.py b/src/sempy_labs/_helper_functions.py index a1bd6a0..2eb39d1 100644 --- a/src/sempy_labs/_helper_functions.py +++ b/src/sempy_labs/_helper_functions.py @@ -15,8 +15,8 @@ import urllib.parse import numpy as np from IPython.display import display, HTML -import sempy_labs._authentication as auth import requests +import sempy_labs._authentication as auth def _build_url(url: str, params: dict) -> str: @@ -1485,12 +1485,12 @@ def _base_api( c = fabric.FabricRestClient() elif client == "fabric_sp": c = fabric.FabricRestClient(token_provider=auth.token_provider.get()) - elif client == "azure": + elif client in ["azure", "graph"]: pass else: raise ValueError(f"{icons.red_dot} The '{client}' client is not supported.") - if client != "azure": + if client not in ["azure", "graph"]: if method == "get": response = c.get(request) elif method == "delete": @@ -1504,9 +1504,12 @@ def _base_api( else: raise NotImplementedError else: - headers = _get_headers(auth.token_provider.get(), audience="azure") + headers = _get_headers(auth.token_provider.get(), audience=client) response = requests.request( - method.upper(), request, headers=headers, json=payload + method.upper(), + f"https://graph.microsoft.com/v1.0/{request}", + headers=headers, + json=payload, ) if lro_return_json: diff --git a/src/sempy_labs/graph/__init__.py b/src/sempy_labs/graph/__init__.py new file mode 100644 index 0000000..4ff62a3 --- /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 0000000..7a91eae --- /dev/null +++ b/src/sempy_labs/graph/_groups.py @@ -0,0 +1,386 @@ +import pandas as pd +from uuid import UUID +from sempy_labs._helper_functions import ( + _is_valid_uuid, + _base_api, + _create_dataframe, + _update_dataframe_datatypes, +) +import sempy_labs._icons as icons +from typing import List, Literal + + +def resolve_group_id(group: str | UUID) -> UUID: + """ + Resolves the group ID from the group name or ID. + + Parameters + ---------- + group : str | uuid.UUID + The group name. + + Returns + ------- + uuid.UUID + The group ID. + """ + if _is_valid_uuid(group): + group_id = group + else: + dfG = list_groups() + 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() -> pd.DataFrame: + """ + Shows a list of groups and their properties. + + This is a wrapper function for the following API: `List groups `_. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of groups and their properties. + """ + + result = _base_api(request="groups", client="graph").json() + + columns = { + "Group Id": "string", + "Group Name": "string", + "Mail": "string", + "Description": "string", + "Classification": "string", + "Mail Enabled": "bool", + "Security Enabled": "bool", + "Created Date Time": "datetime", + "Expiration Date Time": "string", + "Deleted Date Time": "string", + "Renewed Date Time": "string", + "Visibility": "string", + "Security Identifier": "string", + } + + df = _create_dataframe(columns=columns) + + 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) + + _update_dataframe_datatypes(dataframe=df, column_map=columns) + + return df + + +def _get_group(group_id: UUID) -> 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. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of groups and their properties. + """ + + result = _base_api(request=f"groups/{group_id}", client="graph").json() + + columns = { + "Group Id": "string", + "Group Name": "string", + "Mail": "string", + "Description": "string", + "Classification": "string", + "Mail Enabled": "bool", + "Security Enabled": "bool", + "Created Date Time": "datetime", + "Expiration Date Time": "string", + "Deleted Date Time": "string", + "Renewed Date Time": "string", + "Visibility": "string", + "Security Identifier": "string", + } + df = _create_dataframe(columns=columns) + + 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) + + _update_dataframe_datatypes(dataframe=df, column_map=columns) + + return df + + +def list_group_members(group: str | UUID) -> 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. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of the members of a group. + """ + + group_id = resolve_group_id(group) + + result = _base_api(request=f"groups/{group_id}/members", client="graph").json() + + columns = { + "Member Id": "string", + "Member Name": "string", + "User Principal Name": "string", + "Mail": "string", + "Job Title": "string", + "Office Location": "string", + "Mobile Phone": "string", + "Business Phones": "string", + "Preferred Language": "string", + "Given Name": "string", + "Surname": "string", + } + + df = _create_dataframe(columns=columns) + + 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) -> 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. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of the owners of a group. + """ + + group_id = resolve_group_id(group) + + result = _base_api(request=f"groups/{group_id}/owners", client="graph").json() + + columns = { + "Owner Id": "string", + "Owner Name": "string", + "User Principal Name": "string", + "Mail": "string", + "Job Title": "string", + "Office Location": "string", + "Mobile Phone": "string", + "Business Phones": "string", + "Preferred Language": "string", + "Given Name": "string", + "Surname": "string", + } + + df = _create_dataframe(columns=columns) + + 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, + 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) + 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) + 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} + + _base_api( + request=url, + client="graph", + payload=payload, + method="post", + status_codes=204, + ) + + else: + for o in object_list: + payload = {"odata.id": o} + _base_api( + request=url, + client="graph", + payload=payload, + method="post", + status_codes=204, + ) + + 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], +): + """ + 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. + """ + + _base_add_to_group(group=group, object=user, object_type="members") + + +def add_group_owners( + group: str | UUID, + user: str | UUID | List[str | UUID], +): + """ + 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. + """ + + _base_add_to_group(group=group, object=user, object_type="owners") + + +def renew_group(group: str | UUID): + """ + Renews the group. + + This is a wrapper function for the following API: `Renew group `_. + + Parameters + ---------- + group : str | uuid.UUID + The group name or ID. + """ + + group_id = resolve_group_id(group) + + _base_api( + request=f"groups/{group_id}/renew", + client="graph", + method="post", + status_codes=204, + ) + + 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 0000000..455f7f2 --- /dev/null +++ b/src/sempy_labs/graph/_teams.py @@ -0,0 +1,111 @@ +import pandas as pd +from uuid import UUID +from sempy_labs._helper_functions import ( + _base_api, + _create_dataframe, + _update_dataframe_datatypes, +) + + +def list_teams() -> pd.DataFrame: + """ + Shows a list of teams and their properties. + + This is a wrapper function for the following API: `List teams `_. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of teams and their properties. + """ + + result = _base_api(request="teams", client="graph").json() + + columns = { + "Team Id": "str", + "Team Name": "str", + "Description": "str", + "Creation Date Time": "datetime", + "Classification": "str", + "Specialization": "str", + "Visibility": "str", + "Web Url": "str", + "Archived": "bool", + "Favorite By Me": "bool", + "Discoverable By Me": "bool", + "Member Count": "int", + } + + df = _create_dataframe(columns=columns) + + 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) + + _update_dataframe_datatypes(dataframe=df, column_map=columns) + + return df + + +def list_chats(user: str | UUID) -> pd.DataFrame: + """ + In progress... + """ + + from sempy_labs.graph._users import resolve_user_id + + user_id = resolve_user_id(user=user) + result = _base_api(request=f"users/{user_id}/chats", client="graph").json() + + columns = { + "Chat Id": "str", + "Type": "str", + "Members": "str", + } + + df = _create_dataframe(columns=columns) + + 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): + """ + In progress... + """ + + payload = { + "body": { + "content": message, + } + } + + _base_api( + request=f"chats/{chat_id}/messages", + client="graph", + method="post", + payload=payload, + status_codes=201, + ) diff --git a/src/sempy_labs/graph/_users.py b/src/sempy_labs/graph/_users.py new file mode 100644 index 0000000..e8db846 --- /dev/null +++ b/src/sempy_labs/graph/_users.py @@ -0,0 +1,183 @@ +import pandas as pd +from uuid import UUID +import sempy_labs._icons as icons +from typing import List +from sempy_labs._helper_functions import ( + _is_valid_uuid, + _base_api, + _create_dataframe, +) + + +def resolve_user_id(user: str | UUID) -> UUID: + """ + Resolves the user ID from the user principal name or ID. + + Parameters + ---------- + user : str | uuid.UUID + The user ID or user principal name. + + Returns + ------- + uuid.UUID + The user ID. + """ + + if _is_valid_uuid(user): + return user + else: + result = _base_api(request=f"users/{user}", client="graph").json() + return result.get("id") + + +def get_user(user: str | UUID) -> 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. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing properties of a given user. + """ + + result = _base_api(request=f"users/{user}", client="graph").json() + + 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() -> pd.DataFrame: + """ + Shows a list of users and their properties. + + This is a wrapper function for the following API: `List users `_. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of users and their properties. + """ + + result = _base_api(request="users", client="graph").json() + + columns = { + "User Id": "string", + "User Principal Name": "string", + "User Name": "string", + "Mail": "string", + "Job Title": "string", + "Office Location": "string", + "Mobile Phone": "string", + "Business Phones": "string", + "Preferred Language": "string", + "Surname": "string", + } + + df = _create_dataframe(columns=columns) + + 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, + 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. + cc_recipients : str | List[str], default=None + The email address of the CC recipients. + """ + + user_id = resolve_user_id(user=user) + + 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 + + _base_api( + request=f"users/{user_id}/sendMail", + client="graph", + status_codes=202, + payload=payload, + method="post", + ) + + print(f"{icons.green_dot} The email has been sent to {to_recipients}.")