diff --git a/.gitignore b/.gitignore index fa7caef..f1b90ec 100644 --- a/.gitignore +++ b/.gitignore @@ -129,5 +129,4 @@ dmypy.json .pyre/ # Alex Folder. -.vscode/ config/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..871def3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Run Client", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/samples/use_client.py", + "console": "integratedTerminal" + } + ] +} diff --git a/README.md b/README.md index 5f3b5ab..e840adf 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,3 @@ pay monthly fees. **YouTube:** If you'd like to watch more of my content, feel free to visit my YouTube channel [Sigma Coding](https://www.youtube.com/c/SigmaCoding). - - diff --git a/ms_graph/client.py b/ms_graph/client.py index 1e8d1da..262fb17 100644 --- a/ms_graph/client.py +++ b/ms_graph/client.py @@ -21,7 +21,7 @@ from ms_graph.mail import Mail from ms_graph.workbooks_and_charts.workbook import Workbooks - +from ms_graph.workbooks_and_charts.range import Range class MicrosoftGraphClient: @@ -477,3 +477,17 @@ def workbooks(self) -> Workbooks: workbook_service: Workbooks = Workbooks(session=self.graph_session) return workbook_service + + def range(self) -> Range: + """Used to access the Range Services and metadata. + + ### Returns + --- + Range: + The `Range` services Object. + """ + + # Grab the `Range` Object for the session. + range_service: Range = Range(session=self.graph_session) + + return range_service diff --git a/ms_graph/utils/range.py b/ms_graph/utils/range.py new file mode 100644 index 0000000..997743d --- /dev/null +++ b/ms_graph/utils/range.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass + + +@dataclass +class RangeProperties: + + """ + ### Overview + ---- + A python dataclass which is used to represent Range Properties. + The Microsoft Graph API allows users to update Range objects and + this utility makes constructing those updates in a concise way that + is python friendly. + + ### Parameters + ---- + column_hidden : bool (optional, Default=None) + Represents if all columns of the current range are hidden. + + formulas : list (optional, Default=None) + Represents the formula in A1-style notation. + + formulas_local : list (optional, Default=None) + Represents the formula in A1-style notation, in the user's + language and number-formatting locale. For example, the + English "=SUM(A1, 1.5)" formula would become + "=SUMME(A1; 1,5)" in German. + + formulas_r1c1 : list (optional, Default=None) + Represents the formula in R1C1-style notation. + + number_format : str (optional, Default=None) + Represents Excel's number format code for the given cell. + + row_hidden : bool (optional, Default=None) + Represents if all rows of the current range are hidden. + + values : list (optional, Default=None) + Represents the raw values of the specified range. The + data returned could be of type string, number, or a + boolean. Cell that contain an error will return the + error string. + """ + + column_hidden: bool + row_hidden: bool + formulas: list + formulas_local: list + formulas_r1c1: list + number_format: str + values: list + + def to_dict(self) -> dict: + """Generates a dictionary containing all the field + names and values. + + ### Returns + ---- + dict + The Field Name and Values. + """ + + class_dict = { + "columnHidden": self.column_hidden, + "rowHidden": self.row_hidden, + "formulas": self.formulas, + "numberFormat": self.number_format, + "formulasR1C1": self.formulas_r1c1, + "formulasLocal": self.formulas_local, + "values": self.values, + } + + return class_dict diff --git a/ms_graph/workbooks_and_charts/enums.py b/ms_graph/workbooks_and_charts/enums.py index a0bbe8d..5f491c0 100644 --- a/ms_graph/workbooks_and_charts/enums.py +++ b/ms_graph/workbooks_and_charts/enums.py @@ -1,5 +1,6 @@ from enum import Enum + class CalculationTypes(Enum): """Specifies the calculation types used in the `WorkbookApplication` calculate method. @@ -10,9 +11,9 @@ class CalculationTypes(Enum): >>> CalculationTypes.RECALCULATE.value """ - RECALCULATE = 'Recaulcaute' - FULL = 'Full' - FULLREBUILD = 'FullRebuild' + RECALCULATE = "Recaulcaute" + FULL = "Full" + FULLREBUILD = "FullRebuild" class WorksheetVisibility(Enum): @@ -25,6 +26,20 @@ class WorksheetVisibility(Enum): >>> WorksheetVisibility.VISIBLE.value """ - VISIBLE = 'Visible' - HIDDEN = 'Hidden' - VERYHIDDEN = 'VeryHidden' + VISIBLE = "Visible" + HIDDEN = "Hidden" + VERYHIDDEN = "VeryHidden" + + +class RangeShift(Enum): + """Specifies the shift directions used in the + `Range` `insert_range` method. + + ### Usage: + ---- + >>> from ms_graph.workbooks_and_charts.enums import RangeShift + >>> RangeShift.DOWN.value + """ + + DOWN = "Down" + RIGHT = "Right" diff --git a/ms_graph/workbooks_and_charts/range.py b/ms_graph/workbooks_and_charts/range.py new file mode 100644 index 0000000..d6f26ac --- /dev/null +++ b/ms_graph/workbooks_and_charts/range.py @@ -0,0 +1,279 @@ +from enum import Enum +from typing import Union +from ms_graph.session import GraphSession +from ms_graph.utils.range import RangeProperties + + +class Range: + + """ + ## Overview: + ---- + Range represents a set of one or more contiguous cells + such as a cell, a row, a column, block of cells, etc. + """ + + def __init__(self, session: object) -> None: + """Initializes the `Range` object. + + ### Parameters + ---- + session : object + An authenticated session for our Microsoft Graph Client. + """ + + # Set the session. + self.graph_session: GraphSession = session + + def get_range( + self, + address: str = None, + name: str = None, + table_name_or_id: str = None, + worksheet_name_or_id: str = None, + column_name_or_id: str = None, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Retrieve the properties and relationships of range object. + + ### Parameters + ---- + address : str (optional, Default=None) + The range address. + + name : str (optional, Default=None) + + table_name_or_id : str (optional, Default=None) + The name of the table or the resource id. + + worksheet_name_or_id : str (optional, Default=None) + The name of the worksheet or the resource id. + + column_name_or_id : str (optional, Default=None) + The name of the table column or the resource id. + This must be specified if you are grabbing a table + range. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A Range object. + """ + + if item_id: + + if worksheet_name_or_id: + endpoint = ( + f"/me/drive/items/{item_id}/workbook/" + + f"worksheets/{worksheet_name_or_id}/range(address='{address}')" + ) + elif name: + endpoint = f"/me/drive/items/{item_id}/workbook/names/{name}/range" + else: + endpoint = ( + f"/me/drive/items/{item_id}/workbook/" + + f"tables/{table_name_or_id}/columns/{column_name_or_id}/range" + ) + + elif item_path: + + if worksheet_name_or_id: + endpoint = ( + f"/me/drive/root:/{item_path}:/workbook/" + + f"worksheets/{worksheet_name_or_id}/range(address='{address}')" + ) + elif name: + endpoint = f"/me/drive/root:/{item_path}:/workbook/names/{name}/range" + else: + endpoint = ( + f"/me/drive/root:/{item_path}:/workbook/" + + f"tables/{table_name_or_id}/columns/{column_name_or_id}/range" + ) + + content = self.graph_session.make_request(method="get", endpoint=endpoint) + + return content + + def update_range( + self, + range_properties: Union[dict, RangeProperties], + address: str = None, + name: str = None, + table_name_or_id: str = None, + worksheet_name_or_id: str = None, + column_name_or_id: str = None, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Retrieve the properties and relationships of range object. + + ### Parameters + ---- + address : str (optional, Default=None) + The range address. + + name : str (optional, Default=None) + + table_name_or_id : str (optional, Default=None) + The name of the table or the resource id. + + worksheet_name_or_id : str (optional, Default=None) + The name of the worksheet or the resource id. + + column_name_or_id : str (optional, Default=None) + The name of the table column or the resource id. + This must be specified if you are grabbing a table + range. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A Range object. + """ + + if item_id: + + if worksheet_name_or_id: + endpoint = ( + f"/me/drive/items/{item_id}/workbook/" + + f"worksheets/{worksheet_name_or_id}/range(address='{address}')" + ) + elif name: + endpoint = f"/me/drive/items/{item_id}/workbook/names/{name}/range" + else: + endpoint = ( + f"/me/drive/items/{item_id}/workbook/" + + f"tables/{table_name_or_id}/columns/{column_name_or_id}/range" + ) + + elif item_path: + + if worksheet_name_or_id: + endpoint = ( + f"/me/drive/root:/{item_path}:/workbook/" + + f"worksheets/{worksheet_name_or_id}/range(address='{address}')" + ) + elif name: + endpoint = f"/me/drive/root:/{item_path}:/workbook/names/{name}/range" + else: + endpoint = ( + f"/me/drive/root:/{item_path}:/workbook/" + + f"tables/{table_name_or_id}/columns/{column_name_or_id}/range" + ) + + if isinstance(range_properties, RangeProperties): + range_properties = range_properties.to_dict() + + content = self.graph_session.make_request( + method="patch", + json=range_properties, + additional_headers={"Content-type": "application/json"}, + endpoint=endpoint, + ) + + return content + + def insert_range( + self, + shift: Union[str, Enum], + address: str = None, + name: str = None, + table_name_or_id: str = None, + worksheet_name_or_id: str = None, + column_name_or_id: str = None, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Retrieve the properties and relationships of range object. + + ### Parameters + ---- + shift : Union[str, Enum] + Specifies which way to shift the cells. The + possible values are: Down, Right. + + address : str (optional, Default=None) + The range address. + + name : str (optional, Default=None) + + table_name_or_id : str (optional, Default=None) + The name of the table or the resource id. + + worksheet_name_or_id : str (optional, Default=None) + The name of the worksheet or the resource id. + + column_name_or_id : str (optional, Default=None) + The name of the table column or the resource id. + This must be specified if you are grabbing a table + range. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A Range object. + """ + + if item_id: + + if worksheet_name_or_id: + endpoint = ( + f"/me/drive/items/{item_id}/workbook/" + + f"worksheets/{worksheet_name_or_id}/range(address='{address}')/insert" + ) + elif name: + endpoint = f"/me/drive/items/{item_id}/workbook/names/{name}/range/insert" + else: + endpoint = ( + f"/me/drive/items/{item_id}/workbook/" + + f"tables/{table_name_or_id}/columns/{column_name_or_id}/range/insert" + ) + + elif item_path: + + if worksheet_name_or_id: + endpoint = ( + f"/me/drive/root:/{item_path}:/workbook/" + + f"worksheets/{worksheet_name_or_id}/range(address='{address}')/insert" + ) + elif name: + endpoint = f"/me/drive/root:/{item_path}:/workbook/names/{name}/range/insert" + else: + endpoint = ( + f"/me/drive/root:/{item_path}:/workbook/" + + f"tables/{table_name_or_id}/columns/{column_name_or_id}/range/insert" + ) + + if isinstance(shift, Enum): + shift = shift.value + + content = self.graph_session.make_request( + method="post", + json={"shift":shift}, + additional_headers={"Content-type": "application/json"}, + endpoint=endpoint, + ) + + return content diff --git a/ms_graph/workbooks_and_charts/worksheets.py b/ms_graph/workbooks_and_charts/worksheets.py index 878e0bd..56f42c6 100644 --- a/ms_graph/workbooks_and_charts/worksheets.py +++ b/ms_graph/workbooks_and_charts/worksheets.py @@ -364,3 +364,273 @@ def get_cell( ) return content + + def get_range( + self, + worksheet_id_or_name: str, + address: str = None, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Gets the range object specified by the address or name. + + ### Parameters + ---- + worksheet_id_or_name : str + The worksheet resource id or the worksheet name. + + address : str (optional, Default=None) + The address or the name of the range. If not specified, + the entire worksheet range is returned. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A Range object. + """ + + if address: + endpoint = r"/range(address={address})" + else: + endpoint = "/range" + + if item_id: + content = self.graph_session.make_request( + method="get", + endpoint=f"/me/drive/items/{item_id}/workbook/worksheets" + + f"/{worksheet_id_or_name}" + + endpoint, + ) + elif item_path: + content = self.graph_session.make_request( + method="get", + endpoint=f"/me/drive/root:/{item_path}:/workbook/worksheets/" + + f"{worksheet_id_or_name}" + + endpoint, + ) + + return content + + def list_tables( + self, + worksheet_id_or_name: str, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Retrieve a list of WorkbookTable objects. + + ### Parameters + ---- + worksheet_id_or_name : str + The worksheet resource id or the worksheet name. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A collection of WorkbookTable Objects. + """ + + if item_id: + content = self.graph_session.make_request( + method="get", + endpoint=f"/me/drive/items/{item_id}/workbook/worksheets" + + f"/{worksheet_id_or_name}/tables", + ) + elif item_path: + content = self.graph_session.make_request( + method="get", + endpoint=f"/me/drive/root:/{item_path}:/workbook/worksheets/" + + f"{worksheet_id_or_name}/tables", + ) + + return content + + def add_table( + self, + worksheet_id_or_name: str, + address: str, + has_headers: bool, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Creates a new WorkbookTable Object. + + ### Parameters + ---- + worksheet_id_or_name : str + The worksheet resource id or the worksheet name. + + address : str + The range address. + + has_headers : bool + Boolean value that indicates whether the range has + column labels. If the source does not contain headers + (i.e,. when this property set to false), Excel will + automatically generate header shifting the data down + by one row. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A WorkbookTable Object. + """ + + body = {"address": address, "hasHeaders": has_headers} + + if item_id: + content = self.graph_session.make_request( + method="post", + json=body, + additional_headers={"Content-type": "application/json"}, + endpoint=f"/me/drive/items/{item_id}/workbook/worksheets" + + f"/{worksheet_id_or_name}/tables/add", + ) + elif item_path: + content = self.graph_session.make_request( + method="post", + json=body, + additional_headers={"Content-type": "application/json"}, + endpoint=f"/me/drive/root:/{item_path}:/workbook/worksheets/" + + f"{worksheet_id_or_name}/tables/add", + ) + + return content + + def list_charts( + self, + worksheet_id_or_name: str, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Retrieve a list of WorkbookChart Objects. + + ### Parameters + ---- + worksheet_id_or_name : str + The worksheet resource id or the worksheet name. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A collection of WorkbookChart Objects. + """ + + if item_id: + content = self.graph_session.make_request( + method="get", + endpoint=f"/me/drive/items/{item_id}/workbook/worksheets" + + f"/{worksheet_id_or_name}/charts", + ) + elif item_path: + content = self.graph_session.make_request( + method="get", + endpoint=f"/me/drive/root:/{item_path}:/workbook/worksheets/" + + f"{worksheet_id_or_name}/charts", + ) + + return content + + def add_chart( + self, + worksheet_id_or_name: str, + name: str, + height: float, + top: float, + left: float, + width: float, + item_id: str = None, + item_path: str = None, + ) -> dict: + """Retrieve a list of table objects. + + ### Parameters + ---- + worksheet_id_or_name : str + The worksheet resource id or the worksheet name. + + height : float + Represents the height, in points, of the chart object. + + top : float + Represents the distance, in points, from the top edge + of the object to the top of row 1 (on a worksheet) or + the top of the chart area (on a chart). + + left : float + The distance, in points, from the left side of the chart + to the worksheet origin. + + width : float + Represents the width, in points, of the chart object. + + name : str + Represents the name of a chart object. + + item_id : str (optional, Default=None) + The Drive Item Resource ID. + + item_path : str (optional, Default=None) + The Item Path. An Example would be the following: + `/TestFolder/TestFile.txt` + + ### Returns + ---- + dict: + A WorkbookChart object. + """ + + body = { + "height": height, + "top": top, + "left": left, + "width": width, + "name": name, + } + + if item_id: + content = self.graph_session.make_request( + method="post", + json=body, + additional_headers={"Content-type": "application/json"}, + endpoint=f"/me/drive/items/{item_id}/workbook/worksheets" + + f"/{worksheet_id_or_name}/charts", + ) + elif item_path: + content = self.graph_session.make_request( + method="post", + json=body, + additional_headers={"Content-type": "application/json"}, + endpoint=f"/me/drive/root:/{item_path}:/workbook/worksheets/" + + f"{worksheet_id_or_name}/charts", + ) + + return content diff --git a/samples/use_range_service.py b/samples/use_range_service.py new file mode 100644 index 0000000..926671b --- /dev/null +++ b/samples/use_range_service.py @@ -0,0 +1,55 @@ +from pprint import pprint +from configparser import ConfigParser +from ms_graph.client import MicrosoftGraphClient + +# Needed Permissions +# Files.ReadWrite + +scopes = [ + "Calendars.ReadWrite", + "Files.ReadWrite.All", + "User.ReadWrite.All", + "Notes.ReadWrite.All", + "Directory.ReadWrite.All", + "User.Read.All", + "Directory.Read.All", + "Directory.ReadWrite.All", + "Mail.ReadWrite", + "Sites.ReadWrite.All", + "ExternalItem.Read.All", +] + +# Initialize the Parser. +config = ConfigParser() + +# Read the file. +config.read("config/config.ini") + +# Get the specified credentials. +client_id = config.get("graph_api", "client_id") +client_secret = config.get("graph_api", "client_secret") +redirect_uri = config.get("graph_api", "redirect_uri") + +# Initialize the Client. +graph_client = MicrosoftGraphClient( + client_id=client_id, + client_secret=client_secret, + redirect_uri=redirect_uri, + scope=scopes, + credentials="config/ms_graph_state.jsonc", +) + +# Login to the Client. +graph_client.login() + +# Grab the Range Service. +range_service = graph_client.range() + +# Grab a range. +range_object = range_service.get_range( + item_path="Desktop/Personal Code/Repo - YouTube Channel Management/" + + "youtube-channel-management/YouTube Video Description Database.xlsm", + worksheet_name_or_id="Video_Database", + address="A1:P374", +) +pprint(range_object)