diff --git a/src/sempy_labs/_copyjobs.py b/src/sempy_labs/_copyjobs.py new file mode 100644 index 00000000..2f4a63fe --- /dev/null +++ b/src/sempy_labs/_copyjobs.py @@ -0,0 +1,147 @@ +import pandas as pd +from typing import Optional +from uuid import UUID +from sempy_labs._helper_functions import ( + resolve_workspace_name_and_id, + _base_api, + _create_dataframe, + _update_dataframe_datatypes, + delete_item, + get_item_definition, + _conv_b64, + resolve_workspace_id, +) +from sempy._utils._log import log +import sempy_labs._icons as icons + + +@log +def list_copy_jobs(workspace: Optional[str | UUID] = None) -> pd.DataFrame: + """ + Shows a list of CopyJobs from the specified workspace. + + Parameters + ---------- + workspace : str | uuid.UUID, default=None + The Fabric workspace name or ID. + Defaults to None which resolves to the workspace of the attached lakehouse + or if no lakehouse attached, resolves to the workspace of the notebook. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of CopyJobs from the specified workspace. + """ + + workspace_id = resolve_workspace_id(workspace) + + columns = { + "Copy Job Name": "string", + "Copy Job Id": "string", + "Description": "string", + } + + df = _create_dataframe(columns=columns) + + responses = _base_api( + request=f"/v1/workspaces/{workspace_id}/copyJobs", + client="fabric_sp", + uses_pagination=True, + ) + + dfs = [] + for r in responses: + for v in r.get("value", []): + new_data = { + "Copy Job Name": v.get("displayName"), + "Copy Job Id": v.get("id"), + "Description": v.get("description"), + } + + dfs.append(pd.DataFrame(new_data, index=[0])) + + if dfs: + df = pd.concat(dfs, ignore_index=True) + _update_dataframe_datatypes(dataframe=df, column_map=columns) + + return df + + +@log +def delete_copy_job(copy_job: str | UUID, workspace: Optional[str | UUID] = None): + + delete_item(item=copy_job, type="CopyJob", workspace=workspace) + + +@log +def get_copy_job_definition( + copy_job: str | UUID, + workspace: Optional[str | UUID] = None, + return_dataframe: bool = False, +) -> pd.DataFrame | dict: + + if return_dataframe: + return get_item_definition( + item=copy_job, type="CopyJob", workspace=workspace, return_dataframe=True + ) + else: + return get_item_definition( + item=copy_job, type="CopyJob", workspace=workspace, return_dataframe=False + ) + + +def create_copy_job( + name: str, + workspace: Optional[str | UUID] = None, + definition: Optional[dict] = None, + description: Optional[str] = None, +) -> dict: + """ + Creates a new CopyJob in the specified workspace. + + This is a wrapper function for the following API: `CopyJobs - Create Copy Job `_. + + Parameters + ---------- + name : str + Name of the CopyJob. + workspace : str | uuid.UUID, default=None + The Fabric workspace name or ID. + Defaults to None which resolves to the workspace of the attached lakehouse + or if no lakehouse attached, resolves to the workspace of the notebook. + definition : dict, default=None + The "copyjob-content.json" definition of the CopyJob. If None, no definition is provided. + description : str, default=None + Description of the CopyJob. If None, no description is provided. + """ + + (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) + + payload = { + "displayName": name, + } + + if description: + payload["description"] = description + if definition: + payload["definition"] = { + "parts": [ + { + "path": "copyjob-content.json", + "payload": _conv_b64(file=definition), + "payloadType": "InlineBase64", + } + ] + } + + _base_api( + request=f"/v1/workspaces/{workspace_id}/copyJobs", + client="fabric_sp", + method="post", + payload=payload, + status_codes=[201, 202], + ) + + print( + f"{icons.green_dot} The '{name}' copy job has been successfully created within the workspace '{workspace_name}'." + ) diff --git a/src/sempy_labs/_folders.py b/src/sempy_labs/_folders.py new file mode 100644 index 00000000..a8531644 --- /dev/null +++ b/src/sempy_labs/_folders.py @@ -0,0 +1,268 @@ +import pandas as pd +from typing import Optional +from uuid import UUID +from sempy_labs._helper_functions import ( + resolve_workspace_name_and_id, + resolve_workspace_id, + _base_api, + _create_dataframe, + _update_dataframe_datatypes, + _is_valid_uuid, +) +import sempy_labs._icons as icons + + +def list_folders( + workspace: Optional[str | UUID] = None, + recursive: bool = True, + root_folder: Optional[str | UUID] = None, +) -> pd.DataFrame: + """ + Shows a list of folders from the specified workspace. + + Parameters + ---------- + workspace : str | uuid.UUID, default=None + The Fabric workspace name or ID. + Defaults to None which resolves to the workspace of the attached lakehouse + or if no lakehouse attached, resolves to the workspace of the notebook. + recursive : bool, default=True + Lists folders in a folder and its nested folders, or just a folder only. True - All folders in the folder and its nested folders are listed, False - Only folders in the folder are listed. + root_folder : str | uuid.UUID, default=None + This parameter allows users to filter folders based on a specific root folder. If not provided, the workspace is used as the root folder. + + Returns + ------- + pandas.DataFrame + A pandas dataframe showing a list of folders from the specified workspace. + """ + + workspace_id = resolve_workspace_id(workspace) + + columns = { + "Folder Name": "string", + "Folder Id": "string", + "Parent Folder Id": "string", + } + + df = _create_dataframe(columns=columns) + + url = f"/v1/workspaces/{workspace_id}/folders?recursive={recursive}" + + if _is_valid_uuid(root_folder): + url += f"&rootFolderId={root_folder}" + + responses = _base_api( + request=url, + client="fabric_sp", + uses_pagination=True, + ) + + for r in responses: + for v in r.get("value", []): + new_data = { + "Folder Name": v.get("displayName"), + "Folder Id": v.get("id"), + "Parent Folder Id": v.get("parentFolderId"), + } + + df = pd.concat([df, pd.DataFrame(new_data, index=[0])], ignore_index=True) + + _update_dataframe_datatypes(dataframe=df, column_map=columns) + + # Add folder path + folder_map = {row["Folder Id"]: row["Folder Name"] for _, row in df.iterrows()} + + def get_folder_path(folder_id): + if folder_id not in folder_map: + return "" + + row = df.loc[df["Folder Id"] == folder_id].iloc[0] + if "Parent Folder Id" in row: + return get_folder_path(row["Parent Folder Id"]) + "/" + row["Folder Name"] + return row["Folder Name"] + + # Apply function to create the path column + df["Folder Path"] = df["Folder Id"].apply(get_folder_path) + + # Filter the folders if specified + if root_folder is not None and not _is_valid_uuid(root_folder): + root = df[df["Folder Name"] == root_folder] + if root.empty: + raise ValueError(f"Folder name '{root_folder}' not found.") + root_folder_id = root["Folder Id"].iloc[0] + df = df[df["Parent Folder Id"] == root_folder_id] + + return df + + +def create_folder( + name, + workspace: Optional[str | UUID] = None, + parent_folder: Optional[str | UUID] = None, +): + """ + Creates a new folder in the specified workspace. + + Parameters + ---------- + name : str + The name of the folder to create. + workspace : str | uuid.UUID, default=None + The Fabric workspace name or ID. + Defaults to None which resolves to the workspace of the attached lakehouse + or if no lakehouse attached, resolves to the workspace of the notebook. + parent_folder : str | uuid.UUID, default=None + The ID of the parent folder. If not provided, the folder will be created in the root folder of the workspace. + """ + + (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) + + url = f"/v1/workspaces/{workspace_id}/folders" + + payload = { + "displayName": name, + } + + if parent_folder: + parent_folder_id = resolve_folder_id(folder=parent_folder, workspace=workspace) + payload["parentFolderId"] = parent_folder_id + + _base_api( + request=url, + client="fabric_sp", + method="post", + payload=payload, + status_codes=201, + ) + + print( + f"{icons.green_dot} The '{name}' folder has been successfully created within the '{workspace_name}' workspace." + ) + + +def resolve_folder_id( + folder: str | UUID, workspace: Optional[str | UUID] = None +) -> UUID: + + if _is_valid_uuid(folder): + return folder + else: + df = list_folders(workspace=workspace) + if not folder.startswith("/"): + folder_path = f"/{folder}" + else: + folder_path = folder + df_filt = df[df["Folder Path"] == folder_path] + if df_filt.empty: + (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) + raise ValueError( + f"{icons.red_dot} The '{folder}' folder does not exist within the '{workspace_name}' workspace." + ) + return df_filt["Folder Id"].iloc[0] + + +def delete_folder(folder: str | UUID, workspace: Optional[str | UUID] = None): + """ + Deletes a folder from the specified workspace. + + Parameters + ---------- + folder : str | uuid.UUID + The name or ID of the folder to move. If the folder is a subfolder, specify the path (i.e. "/folder/subfolder"). + workspace : str | uuid.UUID, default=None + The Fabric workspace name or ID. + Defaults to None which resolves to the workspace of the attached lakehouse + or if no lakehouse attached, resolves to the workspace of the notebook. + """ + + (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) + + folder_id = resolve_folder_id(folder=folder, workspace=workspace) + + _base_api( + request=f"/v1/workspaces/{workspace_id}/folders/{folder_id}", + client="fabric_sp", + method="delete", + ) + + print( + f"{icons.green_dot} The '{folder}' folder has been successfully deleted from the '{workspace_name}' workspace." + ) + + +def move_folder( + folder: str | UUID, + target_folder: str | UUID, + workspace: Optional[str | UUID] = None, +): + """ + Moves a folder to a new location in the workspace. + + Parameters + ---------- + folder : str | uuid.UUID + The name or ID of the folder to move. If the folder is a subfolder, specify the path (i.e. "/folder/subfolder"). + target_folder : str | uuid.UUID + The name or ID of the target folder where the folder will be moved. If the folder is a subfolder, specify the path (i.e. "/folder/subfolder"). + workspace : str | uuid.UUID, default=None + The Fabric workspace name or ID. + Defaults to None which resolves to the workspace of the attached lakehouse + or if no lakehouse attached, resolves to the workspace of the notebook. + """ + + (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) + target_folder_id = resolve_folder_id(folder=target_folder, workspace=workspace) + folder_id = resolve_folder_id(folder=folder, workspace=workspace) + + payload = { + "targetFolderId": target_folder_id, + } + + _base_api( + request=f"/v1/workspaces/{workspace_id}/folders/{folder_id}/move", + client="fabric_sp", + method="post", + payload=payload, + ) + + print( + f"{icons.green_dot} The '{folder}' folder has been successfully moved to the '{target_folder}' folder within the '{workspace_name}' workspace." + ) + + +def update_folder( + folder: str | UUID, name: str, workspace: Optional[str | UUID] = None +): + """ + Updates the name of a folder in the specified workspace. + + Parameters + ---------- + folder : str | uuid.UUID + The name/path or ID of the folder to update. If the folder is a subfolder, specify the path (i.e. "/folder/subfolder"). + name : str + The new name for the folder. Must meet the `folder name requirements `_. + workspace : str | uuid.UUID, default=None + The Fabric workspace name or ID. + Defaults to None which resolves to the workspace of the attached lakehouse + or if no lakehouse attached, resolves to the workspace of the notebook. + """ + + (workspace_name, workspace_id) = resolve_workspace_name_and_id(workspace) + folder_id = resolve_folder_id(folder=folder, workspace=workspace) + + payload = { + "displayName": name, + } + + _base_api( + request=f"/v1/workspaces/{workspace_id}/folders/{folder_id}", + client="fabric_sp", + method="patch", + payload=payload, + ) + + print( + f"{icons.green_dot} The '{folder}' folder has been successfully updated to '{name}' within the '{workspace_name}' workspace." + ) diff --git a/src/sempy_labs/_utils.py b/src/sempy_labs/_utils.py index b651a3dc..4b4fedef 100644 --- a/src/sempy_labs/_utils.py +++ b/src/sempy_labs/_utils.py @@ -1,4 +1,5 @@ item_types = { + "CopyJob": ["Copy Job", "copyJobs", "copyjob-content.json"], "Dashboard": ["Dashboard", "dashboards"], "DataPipeline": ["Data Pipeline", "dataPipelines", "pipeline-content.json"], "Datamart": ["Datamart", "datamarts"], diff --git a/src/sempy_labs/tom/_model.py b/src/sempy_labs/tom/_model.py index e38dae89..3569b4c7 100644 --- a/src/sempy_labs/tom/_model.py +++ b/src/sempy_labs/tom/_model.py @@ -5065,7 +5065,9 @@ def _generate_m_expression( # Remove the Direct Lake partition self.remove_object(object=p) - print(f"{icons.green_dot} The '{table_name}' table has been converted to Import mode.") + print( + f"{icons.green_dot} The '{table_name}' table has been converted to Import mode." + ) def close(self):