diff --git a/README.md b/README.md index 93cbcca..4310b25 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,40 @@ -# microsoft-python +# microsoftgraph-python Microsoft graph API wrapper for Microsoft Graph written in Python. +## Before start +To use Microsoft Graph to read and write resources on behalf of a user, your app must get an access token from +the Microsoft identity platform and attach the token to requests that it sends to Microsoft Graph. The exact +authentication flow that you will use to get access tokens will depend on the kind of app you are developing and +whether you want to use OpenID Connect to sign the user in to your app. One common flow used by native and mobile +apps and also by some Web apps is the OAuth 2.0 authorization code grant flow. + +See [Get access on behalf of a user](https://docs.microsoft.com/en-us/graph/auth-v2-user) + +## Breaking changes if you're upgrading prior 1.0.0 +- Added structure to library to match API documentation for e.g. `client.get_me()` => `client.users.get_me()`. +- Renamed several methods to match API documentation for e.g. `client.get_me_events()` => `client.calendar.list_events()`. +- Result from calling a method is not longer a dictionary but a Response object. To access the dict response as before then call `.data` attribute for e.g `r = client.users.get_me()` then `r.data`. +- Previous API calls made through beta endpoints are now pointing to v1.0 by default. This can be changed to beta if needed with the parameter `api_version` in the client instantiation. +- Removed Office 365 endpoints as they were merged with the Microsoft Graph API. See [Office 365 APIs](https://docs.microsoft.com/en-us/previous-versions/office/office-365-api/). +## New in 1.0.0 +- You can access to [Requests library's Response Object](https://docs.python-requests.org/en/latest/user/advanced/#request-and-response-objects) for e.g. `r = client.users.get_me()` then `r.original` or the response handled by the library `r.data`. +- New Response properties `r.status_code` and `r.throttling`. +- You can pass [Requests library's Event Hooks](https://docs.python-requests.org/en/latest/user/advanced/#event-hooks) with the parameter `requests_hooks` in the client instantiation. If you are using Django and want to log in database every request made through this library, see [django-requests-logger](https://github.com/GearPlug/django-requests-logger). +- Library can auto paginate responses. Set `paginate` parameter in client initialization. Defaults to `True`. +- Better method docstrings and type hinting. +- Better library structure. ## Installing ``` pip install microsoftgraph-python ``` - ## Usage -If you need an office 365 token, send office365 attribute in True like this: +### Client instantiation ``` from microsoftgraph.client import Client -client = Client('CLIENT_ID', 'CLIENT_SECRET', account_type='common', office365=True) # by default common, thus account_type is optional parameter -``` - -If you don't, just instance the library like this: -``` -from microsoftgraph.client import Client -client = Client('CLIENT_ID', 'CLIENT_SECRET', account_type='common') # by default common, thus account_type is optional parameter +client = Client('CLIENT_ID', 'CLIENT_SECRET', account_type='common') # by default common, thus account_type is optional parameter. ``` +### OAuth 2.0 #### Get authorization url ``` url = client.authorization_url(redirect_uri, scope, state=None) @@ -26,238 +42,292 @@ url = client.authorization_url(redirect_uri, scope, state=None) #### Exchange the code for an access token ``` -token = client.exchange_code(redirect_uri, code) +response = client.exchange_code(redirect_uri, code) ``` #### Refresh token ``` -token = client.refresh_token(redirect_uri, refresh_token) +response = client.refresh_token(redirect_uri, refresh_token) ``` #### Set token ``` -token = client.set_token(token) +client.set_token(token) ``` +### Users #### Get me ``` -me = client.get_me() +response = client.users.get_me() ``` +### Mail + +#### List messages +``` +response = client.mail.list_messages() +``` #### Get message ``` -me = client.get_message(message_id="") +response = client.mail.get_message(message_id) ``` -### Webhook section, see the api documentation: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/webhooks - -#### Create subscription +#### Send mail ``` -subscription = client.create_subscription(change_type, notification_url, resource, expiration_datetime, client_state=None) +data = { + subject="Meet for lunch?", + content="The new cafeteria is open.", + content_type="text", + to_recipients=["fannyd@contoso.onmicrosoft.com"], + cc_recipients=None, + save_to_sent_items=True, +} +response = client.mail.send_mail(**data) ``` -#### Renew subscription +#### List mail folders ``` -renew = client.renew_subscription(subscription_id, expiration_datetime) +response = client.mail.list_mail_folders() ``` -#### Delete subscription +#### Create mail folder ``` -renew = client.delete_subscription(subscription_id) +response = client.mail.create_mail_folder(display_name) ``` -### Onenote section, see the api documentation: https://developer.microsoft.com/en-us/graph/docs/concepts/integrate_with_onenote - +### Notes #### List notebooks ``` -notebooks = client.list_notebooks() +response = client.notes.list_notebooks() ``` #### Get notebook ``` -notebook = client.get_notebook(notebook_id) +response = client.notes.get_notebook(notebook_id) ``` #### Get notebook sections ``` -section_notebook = client.get_notebook_sections(notebook_id) +response = client.notes.list_sections(notebook_id) ``` -#### Create page +#### List pages ``` -add_page = client.create_page(section_id, files) +response = client.notes.list_pages() ``` -#### List pages +#### Create page ``` -pages = client.list_pages() +response = client.notes.create_page(section_id, files) ``` -### Calendar section, see the api documentation: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/calendar - +### Calendar #### Get events ``` -events = client.get_me_events() +response = client.calendar.list_events(calendar_id) +``` + +#### Get event +``` +response = client.calendar.get_event(event_id) ``` #### Create calendar event ``` -events = client.create_calendar_event(subject, content, start_datetime, start_timezone, end_datetime, end_timezone, - recurrence_type, recurrence_interval, recurrence_days_of_week, recurrence_range_type, - recurrence_range_startdate, recurrence_range_enddate, location, attendees, calendar=None) +from datetime import datetime, timedelta + +start_datetime = datetime.now() + timedelta(days=1) # tomorrow +end_datetime = datetime.now() + timedelta(days=1, hours=1) # tomorrow + one hour +timezone = "America/Bogota" + +data = { + "calendar_id": "CALENDAR_ID", + "subject": "Let's go for lunch", + "content": "Does noon work for you?", + "content_type": "text", + "start_datetime": start_datetime, + "start_timezone": timezone, + "end_datetime": end_datetime, + "end_timezone": timezone, + "location": "Harry's Bar", +} +response = client.calendar.create_event(**data) ``` #### Get calendars ``` -events = client.get_me_calendars() +response = client.calendar.list_calendars() ``` #### Create calendar ``` -events = client.create_calendar(name) +response = client.calendar.create_calendar(name) ``` -### Contacts section, see the api documentation: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/contact - -#### Get contacts -If you need a specific contact send the contact id in data_id -``` -specific_contact = client.outlook_get_me_contacts(data_id="") +### Contacts +#### Get a contact ``` -If you want all the contacts -``` -specific_contact = client.outlook_get_me_contacts() +response = client.contacts.get_contact(contact_id) ``` -#### Create contact +#### Get contacts ``` -add_contact = client.outlook_create_me_contact() +response = client.contacts.list_contacts() ``` -#### Create contact in specific folder +#### Create contact ``` -add_contact_folder = client.outlook_create_contact_in_folder(folder_id) +data = { + "given_name": "Pavel", + "surname": "Bansky", + "email_addresses": [ + { + "address": "pavelb@fabrikam.onmicrosoft.com", + "name": "Pavel Bansky" + } + ], + "business_phones": [ + "+1 732 555 0102" + ], + "folder_id": None, +} +response = client.contacts.create_contact(**data) ``` #### Get contact folders ``` -folders = client.outlook_get_contact_folders() +response = client.contacts.list_contact_folders() ``` #### Create contact folders ``` -add_folders = client.outlook_create_contact_folder() +response = client.contacts.create_contact_folder() ``` -### Onedrive section, see the api documentation: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/onedrive - +### Files #### Get root items ``` -root_items = client.drive_root_items() +response = client.files.drive_root_items() ``` #### Get root children items ``` -root_children_items = client.drive_root_children_items() +response = client.files.drive_root_children_items() ``` #### Get specific folder items ``` -folder_items = client.drive_specific_folder(folder_id) +response = client.files.drive_specific_folder(folder_id) ``` -### Excel section, see the api documentation: https://developer.microsoft.com/en-us/graph/docs/api-reference/beta/resources/excel -For use excel, you should know the folder id where the file is -#### Create session for specific item +#### Get item ``` -create_session = client.drive_create_session(item_id) +response = client.files.drive_get_item(item_id) ``` -#### Refresh session for specific item +#### Download the contents of a specific item ``` -refresh_session = client.drive_refresh_session(item_id) +response = client.files.drive_download_contents(item_id) ``` -#### Close session for specific item +### Workbooks +#### Create session for specific item ``` -close_session = client.drive_close_session(item_id) +response = client.workbooks.create_session(workbook_id) ``` -#### Download the contents of a specific item +#### Refresh session for specific item ``` -contents_bytes = client.drive_download_contents(item_id) +response = client.workbooks.refresh_session(workbook_id) ``` -#### Get a Drive item resource +#### Close session for specific item ``` -drive_item_dict = client.drive_get_item(item_id) +response = client.workbooks.close_session(workbook_id) ``` #### Get worksheets ``` -get_worksheets = client.excel_get_worksheets(item_id) +response = client.workbooks.list_worksheets(workbook_id) ``` #### Get specific worksheet ``` -specific_worksheet = client.excel_get_specific_worksheet(item_id, worksheet_id) +response = client.workbooks.get_worksheet(workbook_id, worksheet_id) ``` #### Add worksheets ``` -add_worksheet = client.excel_add_worksheet(item_id) +response = client.workbooks.add_worksheet(workbook_id) ``` #### Update worksheet ``` -update_worksheet = client.excel_update_worksheet(item_id, worksheet_id) +response = client.workbooks.update_worksheet(workbook_id, worksheet_id) ``` #### Get charts ``` -get_charts = client.excel_get_charts(item_id, worksheet_id) +response = client.workbooks.list_charts(workbook_id, worksheet_id) ``` #### Add chart ``` -add_chart = client.excel_add_chart(item_id, worksheet_id) +response = client.workbooks.add_chart(workbook_id, worksheet_id) ``` #### Get tables ``` -get_tables = client.excel_get_tables(item_id) +response = client.workbooks.list_tables(workbook_id) ``` #### Add table ``` -add_table = client.excel_add_table(item_id) +response = client.workbooks.add_table(workbook_id) ``` #### Add column to table ``` -add_column = client.excel_add_column(item_id, worksheets_id, table_id) +response = client.workbooks.create_column(workbook_id, worksheet_id, table_id) ``` #### Add row to table ``` -add_row = client.excel_add_row(item_id, worksheets_id, table_id) +response = client.workbooks.create_row(workbook_id, worksheet_id, table_id) ``` #### Get table rows ``` -get_rows = client.excel_get_rows(item_id, table_id) +response = client.workbooks.list_rows(workbook_id, table_id) ``` #### Get range ``` -get_range = client.excel_get_range(item_id, worksheets_id) +response = client.workbooks.get_range(workbook_id, worksheet_id) ``` #### Update range ``` -update_range = client.excel_update_range(item_id, worksheets_id) +response = client.workbooks.update_range(workbook_id, worksheet_id) ``` +### Webhooks +#### Create subscription +``` +response = client.webhooks.create_subscription(change_type, notification_url, resource, expiration_datetime, client_state=None) +``` + +#### Renew subscription +``` +response = client.webhooks.renew_subscription(subscription_id, expiration_datetime) +``` + +#### Delete subscription +``` +response = client.webhooks.delete_subscription(subscription_id) +``` + + ## Requirements - requests diff --git a/microsoftgraph/calendar.py b/microsoftgraph/calendar.py new file mode 100644 index 0000000..c6c16b7 --- /dev/null +++ b/microsoftgraph/calendar.py @@ -0,0 +1,138 @@ +from datetime import datetime + +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response +from microsoftgraph.utils import format_time + + +class Calendar(object): + def __init__(self, client) -> None: + """Working with Outlook Calendar. + + https://docs.microsoft.com/en-us/graph/api/resources/calendar?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def list_events(self, calendar_id: str = None, params: dict = None) -> Response: + """Get a list of event objects in the user's mailbox. The list contains single instance meetings and series + masters. + + https://docs.microsoft.com/en-us/graph/api/user-list-events?view=graph-rest-1.0&tabs=http + + Args: + calendar_id (str): Calendar ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = "me/calendars/{}/events".format(calendar_id) if calendar_id else "me/events" + return self._client._get(self._client.base_url + url, params=params) + + @token_required + def get_event(self, event_id: str, params: dict = None) -> Response: + """Get the properties and relationships of the specified event object. + + https://docs.microsoft.com/en-us/graph/api/event-get?view=graph-rest-1.0&tabs=http + + Args: + event_id (str): Event ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/events/{}".format(event_id), params=params) + + @token_required + def create_event( + self, + subject: str, + content: str, + start_datetime: datetime, + start_timezone: str, + end_datetime: datetime, + end_timezone: str, + location: str, + calendar_id: str = None, + content_type: str = "HTML", + **kwargs, + ) -> Response: + """Create an event in the user's default calendar or specified calendar. + + https://docs.microsoft.com/en-us/graph/api/user-post-events?view=graph-rest-1.0&tabs=http + + Additional time zones: https://docs.microsoft.com/en-us/graph/api/resources/datetimetimezone?view=graph-rest-1.0 + + Args: + subject (str): The text of the event's subject line. + content (str): The body of the message associated with the event. + start_datetime (datetime): A single point of time in a combined date and time representation ({date}T{time}; + start_timezone (str): Represents a time zone, for example, "Pacific Standard Time". + end_datetime (datetime): A single point of time in a combined date and time representation ({date}T{time}; for + end_timezone (str): Represents a time zone, for example, "Pacific Standard Time". + location (str): The location of the event. + calendar_id (str, optional): Calendar ID. Defaults to None. + content_type (str, optional): It can be in HTML or text format. Defaults to HTML. + + Returns: + Response: Microsoft Graph Response. + """ + if isinstance(start_datetime, datetime): + start_datetime = format_time(start_datetime) + if isinstance(end_datetime, datetime): + end_datetime = format_time(end_datetime) + + body = { + "subject": subject, + "body": { + "contentType": content_type, + "content": content, + }, + "start": { + "dateTime": start_datetime, + "timeZone": start_timezone, + }, + "end": { + "dateTime": end_datetime, + "timeZone": end_timezone, + }, + "location": {"displayName": location}, + } + body.update(kwargs) + url = "me/calendars/{}/events".format(calendar_id) if calendar_id is not None else "me/events" + return self._client._post(self._client.base_url + url, json=body) + + @token_required + def list_calendars(self, params: dict = None) -> Response: + """Get all the user's calendars (/calendars navigation property), get the calendars from the default calendar + group or from a specific calendar group. + + https://docs.microsoft.com/en-us/graph/api/user-list-calendars?view=graph-rest-1.0&tabs=http + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/calendars", params=params) + + @token_required + def create_calendar(self, name: str) -> Response: + """Create a new calendar for a user. + + https://docs.microsoft.com/en-us/graph/api/user-post-calendars?view=graph-rest-1.0&tabs=http + + Args: + name (str): The calendar name. + + Returns: + Response: Microsoft Graph Response. + """ + body = {"name": "{}".format(name)} + return self._client._post(self._client.base_url + "me/calendars", json=body) diff --git a/microsoftgraph/client.py b/microsoftgraph/client.py index 20cc0ea..9344d9a 100644 --- a/microsoftgraph/client.py +++ b/microsoftgraph/client.py @@ -1,644 +1,228 @@ -import base64 -import mimetypes +from urllib.parse import urlencode + import requests -import json + from microsoftgraph import exceptions -from microsoftgraph.decorators import token_required -from urllib.parse import urlencode, urlparse, quote_plus +from microsoftgraph.calendar import Calendar +from microsoftgraph.contacts import Contacts +from microsoftgraph.files import Files +from microsoftgraph.mail import Mail +from microsoftgraph.notes import Notes +from microsoftgraph.response import Response +from microsoftgraph.users import Users +from microsoftgraph.webhooks import Webhooks +from microsoftgraph.workbooks import Workbooks class Client(object): - AUTHORITY_URL = 'https://login.microsoftonline.com/' - AUTH_ENDPOINT = '/oauth2/v2.0/authorize?' - TOKEN_ENDPOINT = '/oauth2/v2.0/token' - RESOURCE = 'https://graph.microsoft.com/' - - OFFICE365_AUTHORITY_URL = 'https://login.live.com' - OFFICE365_AUTH_ENDPOINT = '/oauth20_authorize.srf?' - OFFICE365_TOKEN_ENDPOINT = '/oauth20_token.srf' + AUTHORITY_URL = "https://login.microsoftonline.com/" + AUTH_ENDPOINT = "/oauth2/v2.0/authorize?" + TOKEN_ENDPOINT = "/oauth2/v2.0/token" + RESOURCE = "https://graph.microsoft.com/" + + def __init__( + self, + client_id: str, + client_secret: str, + api_version: str = "v1.0", + account_type: str = "common", + requests_hooks: dict = None, + paginate: bool = True, + ) -> None: + """Instantiates library. - def __init__(self, client_id, client_secret, api_version='v1.0', account_type='common', office365=False): + Args: + client_id (str): Application client id. + client_secret (str): Application client secret. + api_version (str, optional): v1.0 or beta. Defaults to "v1.0". + account_type (str, optional): common, organizations or consumers. Defaults to "common". + requests_hooks (dict, optional): Requests library event hooks. Defaults to None. + + Raises: + Exception: requests_hooks is not a dict. + """ self.client_id = client_id self.client_secret = client_secret self.api_version = api_version self.account_type = account_type - self.base_url = self.RESOURCE + self.api_version + '/' + self.base_url = self.RESOURCE + self.api_version + "/" self.token = None - self.office365 = office365 - self.office365_token = None - - def authorization_url(self, redirect_uri, scope, state=None): - """ + self.paginate = paginate + + self.calendar = Calendar(self) + self.contacts = Contacts(self) + self.files = Files(self) + self.mail = Mail(self) + self.notes = Notes(self) + self.users = Users(self) + self.webhooks = Webhooks(self) + self.workbooks = Workbooks(self) + + if requests_hooks and not isinstance(requests_hooks, dict): + raise Exception( + 'requests_hooks must be a dict. e.g. {"response": func}. http://docs.python-requests.org/en/master/user/advanced/#event-hooks' + ) + self.requests_hooks = requests_hooks + + def authorization_url(self, redirect_uri: str, scope: list, state: str = None) -> str: + """Generates an Authorization URL. + + The first step to getting an access token for many OpenID Connect (OIDC) and OAuth 2.0 flows is to redirect the + user to the Microsoft identity platform /authorize endpoint. Azure AD will sign the user in and ensure their + consent for the permissions your app requests. In the authorization code grant flow, after consent is obtained, + Azure AD will return an authorization_code to your app that it can redeem at the Microsoft identity platform + /token endpoint for an access token. + + https://docs.microsoft.com/en-us/graph/auth-v2-user#2-get-authorization Args: - redirect_uri: The redirect_uri of your app, where authentication responses can be sent and received by - your app. It must exactly match one of the redirect_uris you registered in the app registration portal - - scope: A list of the Microsoft Graph permissions that you want the user to consent to. This may also + redirect_uri (str): The redirect_uri of your app, where authentication responses can be sent and received by + your app. It must exactly match one of the redirect_uris you registered in the app registration portal. + scope (list): A list of the Microsoft Graph permissions that you want the user to consent to. This may also include OpenID scopes. - - state: A value included in the request that will also be returned in the token response. + state (str, optional): A value included in the request that will also be returned in the token response. It can be a string of any content that you wish. A randomly generated unique value is typically used for preventing cross-site request forgery attacks. The state is also used to encode information about the user's state in the app before the authentication request occurred, such as the page or view - they were on. + they were on. Defaults to None. Returns: - A string. - + str: Url for OAuth 2.0. """ params = { - 'client_id': self.client_id, - 'redirect_uri': redirect_uri, - 'scope': ' '.join(scope), - 'response_type': 'code', - 'response_mode': 'query' + "client_id": self.client_id, + "redirect_uri": redirect_uri, + "scope": " ".join(scope), + "response_type": "code", + "response_mode": "query", } if state: - params['state'] = state - if self.office365: - response = self.OFFICE365_AUTHORITY_URL + self.OFFICE365_AUTH_ENDPOINT + urlencode(params) - else: - response = self.AUTHORITY_URL + self.account_type + self.AUTH_ENDPOINT + urlencode(params) + params["state"] = state + response = self.AUTHORITY_URL + self.account_type + self.AUTH_ENDPOINT + urlencode(params) return response - def exchange_code(self, redirect_uri, code): - """Exchanges a code for a Token. - - Args: - redirect_uri: The redirect_uri of your app, where authentication responses can be sent and received by - your app. It must exactly match one of the redirect_uris you registered in the app registration portal + def exchange_code(self, redirect_uri: str, code: str) -> Response: + """Exchanges an oauth code for an user token. - code: The authorization_code that you acquired in the first leg of the flow. + Your app uses the authorization code received in the previous step to request an access token by sending a POST + request to the /token endpoint. - Returns: - A dict. - - """ - data = { - 'client_id': self.client_id, - 'redirect_uri': redirect_uri, - 'client_secret': self.client_secret, - 'code': code, - 'grant_type': 'authorization_code', - } - if self.office365: - response = requests.post(self.OFFICE365_AUTHORITY_URL + self.OFFICE365_TOKEN_ENDPOINT, data=data) - else: - response = requests.post(self.AUTHORITY_URL + self.account_type + self.TOKEN_ENDPOINT, data=data) - return self._parse(response) - - def refresh_token(self, redirect_uri, refresh_token): - """ + https://docs.microsoft.com/en-us/graph/auth-v2-user#3-get-a-token Args: - redirect_uri: The redirect_uri of your app, where authentication responses can be sent and received by - your app. It must exactly match one of the redirect_uris you registered in the app registration portal - - refresh_token: An OAuth 2.0 refresh token. Your app can use this token acquire additional access tokens - after the current access token expires. Refresh tokens are long-lived, and can be used to retain access - to resources for extended periods of time. + redirect_uri (str): The redirect_uri of your app, where authentication responses can be sent and received by + your app. It must exactly match one of the redirect_uris you registered in the app registration portal. + code (str): The authorization_code that you acquired in the first leg of the flow. Returns: - A dict. - + Response: Microsoft Graph Response. """ data = { - 'client_id': self.client_id, - 'redirect_uri': redirect_uri, - 'client_secret': self.client_secret, - 'refresh_token': refresh_token, - 'grant_type': 'refresh_token', + "client_id": self.client_id, + "redirect_uri": redirect_uri, + "client_secret": self.client_secret, + "code": code, + "grant_type": "authorization_code", } - if self.office365: - response = requests.post(self.OFFICE365_AUTHORITY_URL + self.OFFICE365_TOKEN_ENDPOINT, data=data) - else: - response = requests.post(self.AUTHORITY_URL + self.account_type + self.TOKEN_ENDPOINT, data=data) + response = requests.post(self.AUTHORITY_URL + self.account_type + self.TOKEN_ENDPOINT, data=data) return self._parse(response) - def set_token(self, token): - """Sets the Token for its use in this library. - - Args: - token: A string with the Token. - - """ - if self.office365: - self.office365_token = token - else: - self.token = token + def refresh_token(self, redirect_uri: str, refresh_token: str) -> Response: + """Exchanges a refresh token for an user token. - @token_required - def get_me(self, params=None): - """Retrieve the properties and relationships of user object. + Access tokens are short lived, and you must refresh them after they expire to continue accessing resources. + You can do so by submitting another POST request to the /token endpoint, this time providing the refresh_token + instead of the code. - Note: Getting a user returns a default set of properties only (businessPhones, displayName, givenName, id, - jobTitle, mail, mobilePhone, officeLocation, preferredLanguage, surname, userPrincipalName). - Use $select to get the other properties and relationships for the user object. + https://docs.microsoft.com/en-us/graph/auth-v2-user#5-use-the-refresh-token-to-get-a-new-access-token Args: - params: A dict. - - Returns: - A dict. - - """ - return self._get(self.base_url + 'me', params=params) - - @token_required - def get_message(self, message_id, params=None): - """Retrieve the properties and relationships of a message object. - - Args: - message_id: A dict. - params: - - Returns: - A dict. - - """ - return self._get(self.base_url + 'me/messages/' + message_id, params=params) - - @token_required - def create_subscription(self, change_type, notification_url, resource, expiration_datetime, client_state=None): - """Creating a subscription is the first step to start receiving notifications for a resource. - - Args: - change_type: The event type that caused the notification. For example, created on mail receive, or updated - on marking a message read. - notification_url: - resource: The URI of the resource relative to https://graph.microsoft.com. - expiration_datetime: The expiration time for the subscription. - client_state: The clientState property specified in the subscription request. - - Returns: - A dict. - - """ - data = { - 'changeType': change_type, - 'notificationUrl': notification_url, - 'resource': resource, - 'expirationDateTime': expiration_datetime, - 'clientState': client_state - } - return self._post(self.base_url + 'subscriptions', json=data) - - @token_required - def renew_subscription(self, subscription_id, expiration_datetime): - """The client can renew a subscription with a specific expiration date of up to three days from the time - of request. The expirationDateTime property is required. - - - Args: - subscription_id: - expiration_datetime: + redirect_uri (str): The redirect_uri of your app, where authentication responses can be sent and received by + your app. It must exactly match one of the redirect_uris you registered in the app registration portal. + refresh_token (str): An OAuth 2.0 refresh token. Your app can use this token acquire additional access tokens + after the current access token expires. Refresh tokens are long-lived, and can be used to retain access + to resources for extended periods of time. Returns: - A dict. - + Response: Microsoft Graph Response. """ data = { - 'expirationDateTime': expiration_datetime - } - return self._patch(self.base_url + 'subscriptions/{}'.format(subscription_id), json=data) - - @token_required - def delete_subscription(self, subscription_id): - """The client can stop receiving notifications by deleting the subscription using its ID. - - Args: - subscription_id: - - Returns: - None. - - """ - return self._delete(self.base_url + 'subscriptions/{}'.format(subscription_id)) - - # Onenote - @token_required - def list_notebooks(self): - """Retrieve a list of notebook objects. - - Returns: - A dict. - - """ - return self._get(self.base_url + 'me/onenote/notebooks') - - @token_required - def get_notebook(self, notebook_id): - """Retrieve the properties and relationships of a notebook object. - - Args: - notebook_id: - - Returns: - A dict. - - """ - return self._get(self.base_url + 'me/onenote/notebooks/' + notebook_id) - - @token_required - def get_notebook_sections(self, notebook_id): - """Retrieve the properties and relationships of a notebook object. - - Args: - notebook_id: - - Returns: - A dict. - - """ - return self._get(self.base_url + 'me/onenote/notebooks/{}/sections'.format(notebook_id)) - - @token_required - def create_page(self, section_id, files): - """Create a new page in the specified section. - - Args: - section_id: - files: - - Returns: - A dict. - - """ - return self._post(self.base_url + '/me/onenote/sections/{}/pages'.format(section_id), files=files) - - @token_required - def list_pages(self, params=None): - """Create a new page in the specified section. - - Args: - params: - - Returns: - A dict. - - """ - return self._get(self.base_url + '/me/onenote/pages', params=params) - - # Calendar - @token_required - def get_me_events(self): - """Get a list of event objects in the user's mailbox. The list contains single instance meetings and - series masters. - - Currently, this operation returns event bodies in only HTML format. - - Returns: - A dict. - - """ - return self._get(self.base_url + 'me/events') - - @token_required - def create_calendar_event(self, subject, content, start_datetime, start_timezone, end_datetime, end_timezone, - location, calendar=None, **kwargs): - """ - Create a new calendar event. - - Args: - subject: subject of event, string - content: content of event, string - start_datetime: in the format of 2017-09-04T11:00:00, dateTimeTimeZone string - start_timezone: in the format of Pacific Standard Time, string - end_datetime: in the format of 2017-09-04T11:00:00, dateTimeTimeZone string - end_timezone: in the format of Pacific Standard Time, string - location: string - attendees: list of dicts of the form: - {"emailAddress": {"address": a['attendees_email'],"name": a['attendees_name']} - calendar: - - Returns: - A dict. - - """ - # TODO: attendees - # attendees_list = [{ - # "emailAddress": { - # "address": a['attendees_email'], - # "name": a['attendees_name'] - # }, - # "type": a['attendees_type'] - # } for a in kwargs['attendees']] - body = { - "subject": subject, - "body": { - "contentType": "HTML", - "content": content - }, - "start": { - "dateTime": start_datetime, - "timeZone": start_timezone - }, - "end": { - "dateTime": end_datetime, - "timeZone": end_timezone - }, - "location": { - "displayName": location - }, - # "attendees": attendees_list + "client_id": self.client_id, + "redirect_uri": redirect_uri, + "client_secret": self.client_secret, + "refresh_token": refresh_token, + "grant_type": "refresh_token", } - url = 'me/calendars/{}/events'.format(calendar) if calendar is not None else 'me/events' - return self._post(self.base_url + url, json=body) - - @token_required - def create_calendar(self, name): - """Create an event in the user's default calendar or specified calendar. - - You can specify the time zone for each of the start and end times of the event as part of these values, - as the start and end properties are of dateTimeTimeZone type. + response = requests.post(self.AUTHORITY_URL + self.account_type + self.TOKEN_ENDPOINT, data=data) + return self._parse(response) - When an event is sent, the server sends invitations to all the attendees. + def set_token(self, token: dict) -> None: + """Sets the User token for its use in this library. Args: - name: - - Returns: - A dict. - + token (dict): User token data. """ - body = { - 'name': '{}'.format(name) - } - return self._post(self.base_url + 'me/calendars', json=body) - - @token_required - def get_me_calendars(self): - """Get all the user's calendars (/calendars navigation property), get the calendars from the default - calendar group or from a specific calendar group. + self.token = token - Returns: - A dict. - - """ - return self._get(self.base_url + 'me/calendars') + def _paginate_response(self, response: dict, **kwargs) -> dict: + """Some queries against Microsoft Graph return multiple pages of data either due to server-side paging or due to + the use of the $top query parameter to specifically limit the page size in a request. When a result set spans + multiple pages, Microsoft Graph returns an @odata.nextLink property in the response that contains a URL to the + next page of results. - # Mail - @token_required - def send_mail(self, subject=None, recipients=None, body='', content_type='HTML', attachments=None): - """Helper to send email from current user. + https://docs.microsoft.com/en-us/graph/paging?context=graph%2Fapi%2F1.0&view=graph-rest-1.0 Args: - subject: email subject (required) - recipients: list of recipient email addresses (required) - body: body of the message - content_type: content type (default is 'HTML') - attachments: list of file attachments (local filenames) + response (dict): Graph API Response. Returns: - Returns the response from the POST to the sendmail API. + dict: Graph API Response. """ - - # Verify that required arguments have been passed. - if not all([subject, recipients]): - raise ValueError('sendmail(): required arguments missing') - - # Create recipient list in required format. - recipient_list = [{'EmailAddress': {'Address': address}} for address in recipients] - - # Create list of attachments in required format. - attached_files = [] - if attachments: - for filename in attachments: - b64_content = base64.b64encode(open(filename, 'rb').read()) - mime_type = mimetypes.guess_type(filename)[0] - mime_type = mime_type if mime_type else '' - attached_files.append( - {'@odata.type': '#microsoft.graph.fileAttachment', 'ContentBytes': b64_content.decode('utf-8'), - 'ContentType': mime_type, 'Name': filename}) - - # Create email message in required format. - email_msg = {'Message': {'Subject': subject, - 'Body': {'ContentType': content_type, 'Content': body}, - 'ToRecipients': recipient_list, - 'Attachments': attached_files}, - 'SaveToSentItems': 'true'} - - # Do a POST to Graph's sendMail API and return the response. - return self._post(self.base_url + 'me/microsoft.graph.sendMail', json=email_msg) - - # Outlook - @token_required - def outlook_get_me_contacts(self, data_id=None, params=None): - if data_id is None: - url = "{0}me/contacts".format(self.base_url) - else: - url = "{0}me/contacts/{1}".format(self.base_url, data_id) - return self._get(url, params=params) - - @token_required - def outlook_create_me_contact(self, **kwargs): - url = "{0}me/contacts".format(self.base_url) - return self._post(url, **kwargs) - - @token_required - def outlook_create_contact_in_folder(self, folder_id, **kwargs): - url = "{0}/me/contactFolders/{1}/contacts".format(self.base_url, folder_id) - return self._post(url, **kwargs) - - @token_required - def outlook_get_contact_folders(self, params=None): - url = "{0}me/contactFolders".format(self.base_url) - return self._get(url, params=params) - - @token_required - def outlook_create_contact_folder(self, **kwargs): - url = "{0}me/contactFolders".format(self.base_url) - return self._post(url, **kwargs) - - # Onedrive - @token_required - def drive_root_items(self, params=None): - return self._get('https://graph.microsoft.com/beta/me/drive/root', params=params) - - @token_required - def drive_root_children_items(self, params=None): - return self._get('https://graph.microsoft.com/beta/me/drive/root/children', params=params) - - @token_required - def drive_specific_folder(self, folder_id, params=None): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/children".format(folder_id) - return self._get(url, params=params) - - @token_required - def drive_create_session(self, item_id, **kwargs): - url = "https://graph.microsoft.com/v1.0/me/drive/items/{0}/workbook/createSession".format(item_id) - # url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/createSession".format(item_id) - return self._post(url, **kwargs) - - @token_required - def drive_refresh_session(self, item_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/refreshSession".format(item_id) - return self._post(url, **kwargs) - - @token_required - def drive_close_session(self, item_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/closeSession".format(item_id) - return self._post(url, **kwargs) - - @token_required - def drive_download_contents(self, item_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/content".format(item_id) - return self._get(url, params=params, **kwargs) - - def drive_download_shared_contents(self, share_id, params=None, **kwargs): - base64_value = base64.b64encode(share_id.encode()).decode() - encoded_share_url = ( - 'u!' + - base64_value.rstrip('=').replace('/', '_').replace('+', '-') - ) - url = "https://graph.microsoft.com/beta/shares/{0}/driveItem".format( - encoded_share_url - ) - drive_item = self._get(url) - file_download_url = drive_item['@microsoft.graph.downloadUrl'] - return drive_item['name'], requests.get(file_download_url).content - - @token_required - def drive_download_large_contents(self, downloadUrl, offset, size): - headers = {"Range": f'bytes={offset}-{size + offset - 1}'} - return self._get(downloadUrl, headers = headers) - - @token_required - def drive_get_item(self, item_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}".format(item_id) - return self._get(url, params=params, **kwargs) - - @token_required - def drive_upload_item(self, item_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/content".format(item_id) - kwargs['headers'] = {'Content-Type': 'text/plain'} - return self._put(url, params=params, **kwargs) - - # Excel - @token_required - def excel_get_worksheets(self, item_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets".format(item_id) - return self._get(url, params=params, **kwargs) - - @token_required - def excel_get_names(self, item_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/names".format(item_id) - return self._get(url, params=params, **kwargs) - - @token_required - def excel_add_worksheet(self, item_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/add".format(item_id) - return self._post(url, **kwargs) - - @token_required - def excel_get_specific_worksheet(self, item_id, worksheet_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}".format(item_id, quote_plus(worksheet_id)) - return self._get(url, **kwargs) - - @token_required - def excel_update_worksheet(self, item_id, worksheet_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}".format(item_id, quote_plus(worksheet_id)) - return self._patch(url, **kwargs) - - @token_required - def excel_get_charts(self, item_id, worksheet_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/charts".format(item_id, quote_plus(worksheet_id)) - return self._get(url, params=params, **kwargs) - - @token_required - def excel_add_chart(self, item_id, worksheet_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/charts/add".format(item_id, quote_plus(worksheet_id)) - return self._post(url, **kwargs) - - @token_required - def excel_get_tables(self, item_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/tables".format(item_id) - return self._get(url, params=params, **kwargs) - - @token_required - def excel_add_table(self, item_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/tables/add".format(item_id) - return self._post(url, **kwargs) - - @token_required - def excel_add_column(self, item_id, worksheets_id, table_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/tables/{2}/columns".format(item_id, quote_plus(worksheets_id), table_id) - return self._post(url, **kwargs) - - @token_required - def excel_add_row(self, item_id, worksheets_id, table_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/tables/{2}/rows".format(item_id, quote_plus(worksheets_id), table_id) - return self._post(url, **kwargs) - - @token_required - def excel_get_rows(self, item_id, table_id, params=None, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/tables/{1}/rows".format(item_id, table_id) - return self._get(url, params=params, **kwargs) - - # @token_required - # def excel_get_cell(self, item_id, worksheets_id, params=None, **kwargs): - # url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/Cell(row='1', column='A')".format(item_id, quote_plus(worksheets_id)) - # return self._get(url, params=params, **kwargs) - - # @token_required - # def excel_add_cell(self, item_id, worksheets_id, **kwargs): - # url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/rows".format(item_id, worksheets_id) - # return self._patch(url, **kwargs) - - @token_required - def excel_get_range(self, item_id, worksheets_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/range(address='A1:B2')".format(item_id, quote_plus(worksheets_id)) - return self._get(url, **kwargs) - - @token_required - def excel_update_range(self, item_id, worksheets_id, **kwargs): - url = "https://graph.microsoft.com/beta/me/drive/items/{0}/workbook/worksheets/{1}/range(address='A1:B2')".format(item_id, quote_plus(worksheets_id)) - return self._patch(url, **kwargs) + if not self.paginate: + return response + while "@odata.nextLink" in response.data: + data = response.data["value"] + response = self._get(response.data["@odata.nextLink"], **kwargs) + response.data["value"] += data + return response def _get(self, url, **kwargs): - return self._request('GET', url, **kwargs) + return self._paginate_response(self._request("GET", url, **kwargs), **kwargs) def _post(self, url, **kwargs): - return self._request('POST', url, **kwargs) + return self._request("POST", url, **kwargs) def _put(self, url, **kwargs): - return self._request('PUT', url, **kwargs) + return self._request("PUT", url, **kwargs) def _patch(self, url, **kwargs): - return self._request('PATCH', url, **kwargs) + return self._request("PATCH", url, **kwargs) def _delete(self, url, **kwargs): - return self._request('DELETE', url, **kwargs) + return self._request("DELETE", url, **kwargs) def _request(self, method, url, headers=None, **kwargs): _headers = { - 'Accept': 'application/json', + "Accept": "application/json", } - if self.office365: - _headers['Authorization'] = 'Bearer ' + self.office365_token['access_token'] - else: - _headers['Authorization'] = 'Bearer ' + self.token['access_token'] + _headers["Authorization"] = "Bearer " + self.token["access_token"] if headers: _headers.update(headers) - if 'files' not in kwargs: + if self.requests_hooks: + kwargs.update({"hooks": self.requests_hooks}) + if "files" not in kwargs: # If you use the 'files' keyword, the library will set the Content-Type to multipart/form-data # and will generate a boundary. - _headers['Content-Type'] = 'application/json' + _headers["Content-Type"] = "application/json" return self._parse(requests.request(method, url, headers=_headers, **kwargs)) def _parse(self, response): status_code = response.status_code - if 'application/json' in response.headers.get('Content-Type', ''): - r = response.json() - else: - r = response.content - if status_code in (200, 201, 202, 206): + r = Response(original=response) + if status_code in (200, 201, 202, 204, 206): return r - elif status_code == 204: - return None elif status_code == 400: raise exceptions.BadRequest(r) elif status_code == 401: @@ -682,7 +266,7 @@ def _parse(self, response): elif status_code == 509: raise exceptions.BandwidthLimitExceeded(r) else: - if r['error']['innerError']['code'] == 'lockMismatch': + if r["error"]["innerError"]["code"] == "lockMismatch": # File is currently locked due to being open in the web browser # while attempting to reupload a new version to the drive. # Thus temporarily unavailable. diff --git a/microsoftgraph/contacts.py b/microsoftgraph/contacts.py new file mode 100644 index 0000000..f817481 --- /dev/null +++ b/microsoftgraph/contacts.py @@ -0,0 +1,116 @@ +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response + + +class Contacts(object): + def __init__(self, client) -> None: + """Working with Outlook Contacts. + + https://docs.microsoft.com/en-us/graph/api/resources/contact?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def get_contact(self, contact_id: str, params: dict = None) -> Response: + """Retrieve the properties and relationships of a contact object. + + https://docs.microsoft.com/en-us/graph/api/contact-get?view=graph-rest-1.0&tabs=http + + Args: + contact_id (str): The contact's unique identifier. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/contacts/{}".format(contact_id), params=params) + + @token_required + def list_contacts(self, folder_id: str = None, params: dict = None) -> Response: + """Get a contact collection from the default contacts folder of the signed-in user. + + https://docs.microsoft.com/en-us/graph/api/user-list-contacts?view=graph-rest-1.0&tabs=http + + Args: + folder_id (str): Folder ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = "me/contactfolders/{}/contacts".format(folder_id) if folder_id else "me/contacts" + return self._client._get(self._client.base_url + url, params=params) + + @token_required + def create_contact( + self, + given_name: str, + surname: str, + email_addresses: list, + business_phones: list, + folder_id: str = None, + **kwargs + ) -> Response: + """Add a contact to the root Contacts folder or to the contacts endpoint of another contact folder. + + https://docs.microsoft.com/en-us/graph/api/user-post-contacts?view=graph-rest-1.0&tabs=http + + Args: + given_name (str): The contact's given name. + surname (str): The contact's surname. + email_addresses (list): The contact's email addresses. + business_phones (list): The contact's business phone numbers. + folder_id (str, optional): Unique identifier of the contact folder. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + if isinstance(email_addresses, str): + email_addresses = [{"address": email_addresses, "name": "{} {}".format(given_name, surname)}] + + if isinstance(business_phones, str): + business_phones = [business_phones] + + body = { + "givenName": given_name, + "surname": surname, + "emailAddresses": email_addresses, + "businessPhones": business_phones, + } + body.update(kwargs) + url = "me/contactFolders/{}/contacts".format(folder_id) if folder_id else "me/contacts" + return self._client._post(self._client.base_url + url, json=body) + + @token_required + def list_contact_folders(self, params: dict = None) -> Response: + """Get the contact folder collection in the default Contacts folder of the signed-in user. + + https://docs.microsoft.com/en-us/graph/api/user-list-contactfolders?view=graph-rest-1.0&tabs=http + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/contactFolders", params=params) + + @token_required + def create_contact_folder(self, display_name: str, parent_folder_id: str, **kwargs) -> Response: + """Create a new contactFolder under the user's default contacts folder. + + https://docs.microsoft.com/en-us/graph/api/user-post-contactfolders?view=graph-rest-1.0&tabs=http + + Args: + display_name (str): The folder's display name. + parent_folder_id (str): The ID of the folder's parent folder. + + Returns: + Response: Microsoft Graph Response. + """ + data = {"displayName": display_name, "parentFolderId": parent_folder_id} + data.update(kwargs) + return self._client._post(self._client.base_url + "me/contactFolders", json=data) diff --git a/microsoftgraph/decorators.py b/microsoftgraph/decorators.py index 9bca31e..e2c2469 100644 --- a/microsoftgraph/decorators.py +++ b/microsoftgraph/decorators.py @@ -5,9 +5,9 @@ def token_required(func): @wraps(func) def helper(*args, **kwargs): - client = args[0] - if not client.token: - raise TokenRequired('You must set the Token.') + module = args[0] + if not module._client.token: + raise TokenRequired("You must set the Token.") return func(*args, **kwargs) return helper diff --git a/microsoftgraph/files.py b/microsoftgraph/files.py new file mode 100644 index 0000000..8ffbd9b --- /dev/null +++ b/microsoftgraph/files.py @@ -0,0 +1,153 @@ +import base64 + +import requests + +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response + + +class Files(object): + def __init__(self, client) -> None: + """Working with files in Microsoft Graph + + https://docs.microsoft.com/en-us/graph/api/resources/onedrive?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def drive_root_items(self, params: dict = None) -> Response: + """Return a collection of DriveItems in the children relationship of a DriveItem. + + https://docs.microsoft.com/en-us/graph/api/driveitem-list-children?view=graph-rest-1.0&tabs=http + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/drive/root", params=params) + + @token_required + def drive_root_children_items(self, params: dict = None) -> Response: + """Return a collection of DriveItems in the children relationship of a DriveItem. + + https://docs.microsoft.com/en-us/graph/api/driveitem-list-children?view=graph-rest-1.0&tabs=http + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/drive/root/children", params=params) + + @token_required + def drive_specific_folder(self, folder_id: str, params: dict = None) -> Response: + """Return a collection of DriveItems in the children relationship of a DriveItem. + + https://docs.microsoft.com/en-us/graph/api/driveitem-list-children?view=graph-rest-1.0&tabs=http + + Args: + folder_id (str): Unique identifier of the folder. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/children".format(folder_id) + return self._client._get(url, params=params) + + @token_required + def drive_get_item(self, item_id: str, params: dict = None, **kwargs) -> Response: + """Retrieve the metadata for a driveItem in a drive by file system path or ID. It may also be the unique ID of a + SharePoint list item. + + https://docs.microsoft.com/en-us/graph/api/driveitem-get?view=graph-rest-1.0&tabs=http + + Args: + item_id (str): ID of a driveItem. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}".format(item_id) + return self._client._get(url, params=params, **kwargs) + + @token_required + def drive_download_contents(self, item_id: str, params: dict = None, **kwargs) -> Response: + """Download the contents of the primary stream (file) of a DriveItem. Only driveItems with the file property can + be downloaded. + + https://docs.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0&tabs=http + + Args: + item_id (str): ID of a driveItem. + params (dict, optional): Extra params. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/content".format(item_id) + return self._client._get(url, params=params, **kwargs) + + @token_required + def drive_download_shared_contents(self, share_id: str, params: dict = None, **kwargs) -> Response: + """Download the contents of the primary stream (file) of a DriveItem. Only driveItems with the file property can + be downloaded. + + https://docs.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0&tabs=http + + Args: + share_id (str): ID of a driveItem. + params (dict, optional): Extra params. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + base64_value = base64.b64encode(share_id.encode()).decode() + encoded_share_url = "u!" + base64_value.rstrip("=").replace("/", "_").replace("+", "-") + url = self._client.base_url + "shares/{0}/driveItem".format(encoded_share_url) + drive_item = self._client._get(url) + file_download_url = drive_item["@microsoft.graph.downloadUrl"] + return drive_item["name"], requests.get(file_download_url).content + + @token_required + def drive_download_large_contents(self, downloadUrl: str, offset: int, size: int) -> Response: + """Download the contents of the primary stream (file) of a DriveItem. Only driveItems with the file property can + be downloaded. + + https://docs.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0&tabs=http + + Args: + downloadUrl (str): Url of the driveItem. + offset (int): offset. + size (int): size. + + Returns: + Response: Microsoft Graph Response. + """ + headers = {"Range": f"bytes={offset}-{size + offset - 1}"} + return self._client._get(downloadUrl, headers=headers) + + @token_required + def drive_upload_item(self, item_id: str, params: dict = None, **kwargs) -> Response: + """The simple upload API allows you to provide the contents of a new file or update the contents of an existing + file in a single API call. This method only supports files up to 4MB in size. + + https://docs.microsoft.com/en-us/graph/api/driveitem-put-content?view=graph-rest-1.0&tabs=http + + Args: + item_id (str): Id of a driveItem. + params (dict, optional): Extra params. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/content".format(item_id) + kwargs["headers"] = {"Content-Type": "text/plain"} + return self._client._put(url, params=params, **kwargs) diff --git a/microsoftgraph/mail.py b/microsoftgraph/mail.py new file mode 100644 index 0000000..c8a43b6 --- /dev/null +++ b/microsoftgraph/mail.py @@ -0,0 +1,162 @@ +import base64 +import mimetypes + +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response + + +class Mail(object): + def __init__(self, client) -> None: + """Use the Outlook mail REST API + + https://docs.microsoft.com/en-us/graph/api/resources/mail-api-overview?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def list_messages(self, folder_id: str = None, params: dict = None) -> Response: + """Get the messages in the signed-in user's mailbox (including the Deleted Items and Clutter folders). + + https://docs.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0&tabs=http + + Args: + folder_id (str, optional): Mail Folder ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = "me/mailFolders/{id}/messages".format(folder_id) if folder_id else "me/messages" + return self._client._get(self._client.base_url + url, params=params) + + @token_required + def get_message(self, message_id: str, params: dict = None) -> Response: + """Retrieve the properties and relationships of a message object. + + https://docs.microsoft.com/en-us/graph/api/message-get?view=graph-rest-1.0&tabs=http + + Args: + message_id (str): Unique identifier for the message. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/messages/" + message_id, params=params) + + @token_required + def send_mail( + self, + subject: str, + content: str, + to_recipients: list, + cc_recipients: list = None, + content_type: str = "HTML", + attachments: list = None, + save_to_sent_items: bool = True, + **kwargs, + ) -> Response: + """Send the message specified in the request body using either JSON or MIME format. + + https://docs.microsoft.com/en-us/graph/api/user-sendmail?view=graph-rest-1.0&tabs=http + + Args: + subject (str): The subject of the message. + content (str): The body of the message. + to_recipients (list, optional): The To: recipients for the message. + cc_recipients (list, optional): The Cc: recipients for the message. Defaults to None. + content_type (str, optional): It can be in HTML or text format. Defaults to "HTML". + attachments (list, optional): The fileAttachment and itemAttachment attachments for the message. Defaults to None. + save_to_sent_items (bool, optional): Indicates whether to save the message in Sent Items. Defaults to True. + + Returns: + Response: Microsoft Graph Response. + """ + # Create recipient list in required format. + if isinstance(to_recipients, list): + if all([isinstance(e, str) for e in to_recipients]): + to_recipients = [{"EmailAddress": {"Address": address}} for address in to_recipients] + elif isinstance(to_recipients, str): + to_recipients = [{"EmailAddress": {"Address": to_recipients}}] + else: + raise Exception("to_recipients value is invalid.") + + if cc_recipients and isinstance(cc_recipients, list): + if all([isinstance(e, str) for e in cc_recipients]): + cc_recipients = [{"EmailAddress": {"Address": address}} for address in cc_recipients] + elif cc_recipients and isinstance(cc_recipients, str): + cc_recipients = [{"EmailAddress": {"Address": cc_recipients}}] + else: + cc_recipients = [] + + # Create list of attachments in required format. + attached_files = [] + if attachments: + for filename in attachments: + b64_content = base64.b64encode(open(filename, "rb").read()) + mime_type = mimetypes.guess_type(filename)[0] + mime_type = mime_type if mime_type else "" + attached_files.append( + { + "@odata.type": "#microsoft.graph.fileAttachment", + "ContentBytes": b64_content.decode("utf-8"), + "ContentType": mime_type, + "Name": filename, + } + ) + + # Create email message in required format. + email_msg = { + "Message": { + "Subject": subject, + "Body": {"ContentType": content_type, "Content": content}, + "ToRecipients": to_recipients, + "ccRecipients": cc_recipients, + "Attachments": attached_files, + }, + "SaveToSentItems": save_to_sent_items, + } + email_msg.update(kwargs) + + # Do a POST to Graph's sendMail API and return the response. + return self._client._post(self._client.base_url + "me/sendMail", json=email_msg) + + @token_required + def list_mail_folders(self, params: dict = None) -> Response: + """Get the mail folder collection directly under the root folder of the signed-in user. The returned collection + includes any mail search folders directly under the root. + + By default, this operation does not return hidden folders. Use a query parameter includeHiddenFolders to include + them in the response. + + https://docs.microsoft.com/en-us/graph/api/user-list-mailfolders?view=graph-rest-1.0&tabs=http + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/mailFolders", params=params) + + @token_required + def create_mail_folder(self, display_name: str, is_hidden: bool = False) -> Response: + """Use this API to create a new mail folder in the root folder of the user's mailbox. + + https://docs.microsoft.com/en-us/graph/api/user-post-mailfolders?view=graph-rest-1.0&tabs=http + + Args: + display_name (str): Query. + is_hidden (bool, optional): Is the folder hidden. Defaults to False. + + Returns: + Response: Microsoft Graph Response. + """ + data = { + "displayName": display_name, + "isHidden": is_hidden, + } + return self._client._post(self._client.base_url + "me/mailFolders", json=data) diff --git a/microsoftgraph/notes.py b/microsoftgraph/notes.py new file mode 100644 index 0000000..4cb9db2 --- /dev/null +++ b/microsoftgraph/notes.py @@ -0,0 +1,87 @@ +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response + + +class Notes(object): + def __init__(self, client) -> None: + """Use the OneNote REST API. + + https://docs.microsoft.com/en-us/graph/api/resources/onenote-api-overview?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def list_notebooks(self, params: dict = None) -> Response: + """Retrieve a list of notebook objects. + + https://docs.microsoft.com/en-us/graph/api/onenote-list-notebooks?view=graph-rest-1.0&tabs=http + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/onenote/notebooks", params=params) + + @token_required + def get_notebook(self, notebook_id: str, params: dict = None) -> Response: + """Retrieve the properties and relationships of a notebook object. + + https://docs.microsoft.com/en-us/graph/api/notebook-get?view=graph-rest-1.0&tabs=http + + Args: + notebook_id (str): The unique identifier of the notebook. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/onenote/notebooks/" + notebook_id, params=params) + + @token_required + def list_sections(self, notebook_id: str, params: dict = None) -> Response: + """Retrieve a list of onenoteSection objects from the specified notebook. + + https://docs.microsoft.com/en-us/graph/api/notebook-list-sections?view=graph-rest-1.0&tabs=http + + Args: + notebook_id (str): The unique identifier of the notebook. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = "me/onenote/notebooks/{}/sections".format(notebook_id) + return self._client._get(self._client.base_url + url, params=params) + + @token_required + def list_pages(self, params: dict = None) -> Response: + """Retrieve a list of page objects. + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me/onenote/pages", params=params) + + @token_required + def create_page(self, section_id: str, files: list) -> Response: + """Create a new page in the specified section. + + https://docs.microsoft.com/en-us/graph/api/section-post-pages?view=graph-rest-1.0 + + Args: + section_id (str): The unique identifier of the section. + files (list): Attachments. + + Returns: + Response: Microsoft Graph Response. + """ + url = "me/onenote/sections/{}/pages".format(section_id) + return self._client._post(self._client.base_url + url, files=files) diff --git a/microsoftgraph/response.py b/microsoftgraph/response.py new file mode 100644 index 0000000..1255012 --- /dev/null +++ b/microsoftgraph/response.py @@ -0,0 +1,31 @@ +from datetime import datetime, timedelta + + +class Response: + def __init__(self, original) -> None: + self.original = original + + if "application/json" in self.original.headers.get("Content-Type", ""): + self.data = self.original.json() + else: + self.data = self.original.content + + def __repr__(self) -> str: + return "".format(self.status_code) + + @property + def status_code(self): + return self.original.status_code + + @property + def throttling(self) -> datetime: + """Microsoft Graph throttling + + https://docs.microsoft.com/en-us/graph/throttling + + Returns: + datetime: Retry after. + """ + if "Retry-After" in self.original.headers: + return datetime.now() + timedelta(seconds=self.original.headers["Retry-After"]) + return None diff --git a/microsoftgraph/users.py b/microsoftgraph/users.py new file mode 100644 index 0000000..fbc34cb --- /dev/null +++ b/microsoftgraph/users.py @@ -0,0 +1,32 @@ +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response + + +class Users(object): + def __init__(self, client) -> None: + """Working with users in Microsoft Graph. + + https://docs.microsoft.com/en-us/graph/api/resources/users?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def get_me(self, params: dict = None) -> Response: + """Retrieve the properties and relationships of user object. + + Note: Getting a user returns a default set of properties only (businessPhones, displayName, givenName, id, + jobTitle, mail, mobilePhone, officeLocation, preferredLanguage, surname, userPrincipalName). + Use $select to get the other properties and relationships for the user object. + + https://docs.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http + + Args: + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._get(self._client.base_url + "me", params=params) diff --git a/microsoftgraph/utils.py b/microsoftgraph/utils.py new file mode 100644 index 0000000..a8f0a8f --- /dev/null +++ b/microsoftgraph/utils.py @@ -0,0 +1,7 @@ +from datetime import datetime + + +def format_time(value: datetime, is_webhook: bool = False) -> str: + if is_webhook: + return value.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + return value.strftime("%Y-%m-%dT%H:%M:%S") diff --git a/microsoftgraph/webhooks.py b/microsoftgraph/webhooks.py new file mode 100644 index 0000000..f19b12b --- /dev/null +++ b/microsoftgraph/webhooks.py @@ -0,0 +1,86 @@ +from datetime import datetime + +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response +from microsoftgraph.utils import format_time + + +class Webhooks(object): + def __init__(self, client) -> None: + """Set up notifications for changes in user data. + + https://docs.microsoft.com/en-us/graph/webhooks?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def create_subscription( + self, + change_type: str, + notification_url: str, + resource: str, + expiration_datetime: datetime, + client_state: str = None, + ) -> Response: + """Creates a subscription to start receiving notifications for a resource. + + https://docs.microsoft.com/en-us/graph/webhooks#creating-a-subscription + + Args: + change_type (str): The event type that caused the notification. For example, created on mail receive, or + updated on marking a message read. + notification_url (str): Url to receive notifications. + resource (str): The URI of the resource relative to https://graph.microsoft.com. + expiration_datetime (datetime): The expiration time for the subscription. + client_state (str, optional): The clientState property specified in the subscription request. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + if isinstance(expiration_datetime, datetime): + expiration_datetime = format_time(expiration_datetime, is_webhook=True) + + data = { + "changeType": change_type, + "notificationUrl": notification_url, + "resource": resource, + "expirationDateTime": expiration_datetime, + "clientState": client_state, + } + return self._client._post(self._client.base_url + "subscriptions", json=data) + + @token_required + def renew_subscription(self, subscription_id: str, expiration_datetime: datetime) -> Response: + """Renews a subscription to keep receiving notifications for a resource. + + https://docs.microsoft.com/en-us/graph/webhooks#renewing-a-subscription + + Args: + subscription_id (str): Subscription ID. + expiration_datetime (datetime): Expiration date. + + Returns: + Response: Microsoft Graph Response. + """ + if isinstance(expiration_datetime, datetime): + expiration_datetime = format_time(expiration_datetime, is_webhook=True) + + data = {"expirationDateTime": expiration_datetime} + return self._client._patch(self._client.base_url + "subscriptions/{}".format(subscription_id), json=data) + + @token_required + def delete_subscription(self, subscription_id: str) -> Response: + """Deletes a subscription to stop receiving notifications for a resource. + + https://docs.microsoft.com/en-us/graph/webhooks#deleting-a-subscription + + Args: + subscription_id (str): Subscription ID. + + Returns: + Response: Microsoft Graph Response. + """ + return self._client._delete(self._client.base_url + "subscriptions/{}".format(subscription_id)) diff --git a/microsoftgraph/workbooks.py b/microsoftgraph/workbooks.py new file mode 100644 index 0000000..0ba0ffb --- /dev/null +++ b/microsoftgraph/workbooks.py @@ -0,0 +1,313 @@ +from urllib.parse import quote_plus + +from microsoftgraph.decorators import token_required +from microsoftgraph.response import Response + + +class Workbooks(object): + def __init__(self, client) -> None: + """Working with Excel in Microsoft Graph + + https://docs.microsoft.com/en-us/graph/api/resources/excel?view=graph-rest-1.0 + + Args: + client (Client): Library Client. + """ + self._client = client + + @token_required + def create_session(self, workbook_id: str, **kwargs) -> Response: + """Create a new workbook session. + + https://docs.microsoft.com/en-us/graph/api/workbook-createsession?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/createSession".format(workbook_id) + return self._client._post(url, **kwargs) + + @token_required + def refresh_session(self, workbook_id: str, **kwargs) -> Response: + """Refresh an existing workbook session. + + https://docs.microsoft.com/en-us/graph/api/workbook-refreshsession?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/refreshSession".format(workbook_id) + return self._client._post(url, **kwargs) + + @token_required + def close_session(self, workbook_id: str, **kwargs) -> Response: + """Close an existing workbook session. + + https://docs.microsoft.com/en-us/graph/api/workbook-closesession?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/closeSession".format(workbook_id) + return self._client._post(url, **kwargs) + + @token_required + def list_worksheets(self, workbook_id: str, params: dict = None, **kwargs) -> Response: + """Retrieve a list of worksheet objects. + + https://docs.microsoft.com/en-us/graph/api/workbook-list-worksheets?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets".format(workbook_id) + return self._client._get(url, params=params, **kwargs) + + @token_required + def list_names(self, workbook_id: str, params: dict = None, **kwargs) -> Response: + """Retrieve a list of nameditem objects. + + https://docs.microsoft.com/en-us/graph/api/workbook-list-names?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/names".format(workbook_id) + return self._client._get(url, params=params, **kwargs) + + @token_required + def add_worksheet(self, workbook_id: str, **kwargs) -> Response: + """Adds a new worksheet to the workbook. + + https://docs.microsoft.com/en-us/graph/api/worksheetcollection-add?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/add".format(workbook_id) + return self._client._post(url, **kwargs) + + @token_required + def get_worksheet(self, workbook_id: str, worksheet_id: str, **kwargs) -> Response: + """Retrieve the properties and relationships of worksheet object. + + https://docs.microsoft.com/en-us/graph/api/worksheet-get?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}".format( + workbook_id, quote_plus(worksheet_id) + ) + return self._client._get(url, **kwargs) + + @token_required + def update_worksheet(self, workbook_id: str, worksheet_id: str, **kwargs) -> Response: + """Update the properties of worksheet object. + + https://docs.microsoft.com/en-us/graph/api/worksheet-update?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}".format( + workbook_id, quote_plus(worksheet_id) + ) + return self._client._patch(url, **kwargs) + + @token_required + def list_charts(self, workbook_id: str, worksheet_id: str, params: dict = None, **kwargs) -> Response: + """Retrieve a list of chart objects. + + https://docs.microsoft.com/en-us/graph/api/worksheet-list-charts?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/charts".format( + workbook_id, quote_plus(worksheet_id) + ) + return self._client._get(url, params=params, **kwargs) + + @token_required + def add_chart(self, workbook_id: str, worksheet_id: str, **kwargs) -> Response: + """Creates a new chart. + + https://docs.microsoft.com/en-us/graph/api/chartcollection-add?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/charts/add".format( + workbook_id, quote_plus(worksheet_id) + ) + return self._client._post(url, **kwargs) + + @token_required + def list_tables(self, workbook_id: str, params: dict = None, **kwargs) -> Response: + """Retrieve a list of table objects. + + https://docs.microsoft.com/en-us/graph/api/workbook-list-tables?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/tables".format(workbook_id) + return self._client._get(url, params=params, **kwargs) + + @token_required + def add_table(self, workbook_id: str, **kwargs) -> Response: + """Create a new table. + + https://docs.microsoft.com/en-us/graph/api/tablecollection-add?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/tables/add".format(workbook_id) + return self._client._post(url, **kwargs) + + @token_required + def create_column(self, workbook_id: str, worksheet_id: str, table_id: str, **kwargs) -> Response: + """Create a new TableColumn. + + https://docs.microsoft.com/en-us/graph/api/table-post-columns?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + table_id (str): Excel table ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/tables/{2}/columns".format( + workbook_id, quote_plus(worksheet_id), table_id + ) + return self._client._post(url, **kwargs) + + @token_required + def create_row(self, workbook_id: str, worksheet_id: str, table_id: str, **kwargs) -> Response: + """Adds rows to the end of a table. + + https://docs.microsoft.com/en-us/graph/api/table-post-rows?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + table_id (str): Excel table ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/tables/{2}/rows".format( + workbook_id, quote_plus(worksheet_id), table_id + ) + return self._client._post(url, **kwargs) + + @token_required + def list_rows(self, workbook_id: str, table_id: str, params: dict = None, **kwargs) -> Response: + """Retrieve a list of tablerow objects. + + https://docs.microsoft.com/en-us/graph/api/table-list-rows?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + table_id (str): Excel table ID. + params (dict, optional): Query. Defaults to None. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/tables/{1}/rows".format(workbook_id, table_id) + return self._client._get(url, params=params, **kwargs) + + # @token_required + # def excel_get_cell(self, item_id, worksheet_id, params=None, **kwargs): + # url = self.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/Cell(row='1', column='A')".format(item_id, quote_plus(worksheet_id)) + # return self._get(url, params=params, **kwargs) + + # @token_required + # def excel_add_cell(self, item_id, worksheet_id, **kwargs): + # url = self.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/rows".format(item_id, worksheet_id) + # return self._patch(url, **kwargs) + + @token_required + def get_range(self, workbook_id: str, worksheet_id: str, **kwargs) -> Response: + """Gets the range object specified by the address or name. + + https://docs.microsoft.com/en-us/graph/api/worksheet-range?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/range(address='A1:B2')".format( + workbook_id, quote_plus(worksheet_id) + ) + return self._client._get(url, **kwargs) + + @token_required + def update_range(self, workbook_id: str, worksheet_id: str, **kwargs) -> Response: + """Update the properties of range object. + + https://docs.microsoft.com/en-us/graph/api/range-update?view=graph-rest-1.0&tabs=http + + Args: + workbook_id (str): Excel file ID. + worksheet_id (str): Excel worksheet ID. + + Returns: + Response: Microsoft Graph Response. + """ + url = self._client.base_url + "me/drive/items/{0}/workbook/worksheets/{1}/range(address='A1:B2')".format( + workbook_id, quote_plus(worksheet_id) + ) + return self._client._patch(url, **kwargs) diff --git a/setup.py b/setup.py index 47b355e..0bbed7a 100644 --- a/setup.py +++ b/setup.py @@ -6,17 +6,19 @@ def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -setup(name='microsoftgraph-python', - version='0.1.7', - description='API wrapper for Microsoft Graph written in Python', - long_description=read('README.md'), - url='https://github.com/GearPlug/microsoftgraph-python', - long_description_content_type="text/markdown", - author='Miguel Ferrer, Nerio Rincon, Yordy Gelvez', - author_email='ingferrermiguel@gmail.com', - license='MIT', - packages=['microsoftgraph'], - install_requires=[ - 'requests', - ], - zip_safe=False) +setup( + name="microsoftgraph-python", + version="1.0.0", + description="API wrapper for Microsoft Graph written in Python", + long_description=read("README.md"), + url="https://github.com/GearPlug/microsoftgraph-python", + long_description_content_type="text/markdown", + author="Miguel Ferrer, Nerio Rincon, Yordy Gelvez", + author_email="ingferrermiguel@gmail.com", + license="MIT", + packages=["microsoftgraph"], + install_requires=[ + "requests", + ], + zip_safe=False, +)