From 7aa1596b8692e1348e11deb90be7541aec785945 Mon Sep 17 00:00:00 2001 From: Doug Fenstermacher Date: Thu, 24 Sep 2020 17:52:30 -0400 Subject: [PATCH 1/4] converts msgraph.api.GraphAPI to use msal --- README.md | 67 +++++++++++-- msgraph/__init__.py | 2 +- msgraph/api.py | 121 +++-------------------- msgraph/base.py | 26 +++++ msgraph/beta/{bookings.py => booking.py} | 12 ++- msgraph/calendar.py | 13 +-- requirements.txt | 2 +- setup.py | 2 +- 8 files changed, 118 insertions(+), 127 deletions(-) rename msgraph/beta/{bookings.py => booking.py} (98%) diff --git a/README.md b/README.md index 4ca5593..6d462b5 100644 --- a/README.md +++ b/README.md @@ -16,22 +16,75 @@ or, to build locally and install: ## Usage ### Authentication +This library supports multiple approaches to authentication using [msal](https://github.com/AzureAD/microsoft-authentication-library-for-python) -The library currently supports connecting to the API using an SSL certificate: +#### Username & Password ```python -from msgraph import api +import msal -authority_host_uri = 'https://login.microsoftonline.com' +authority_domain = 'https://login.microsoftonline.com' tenant = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' +authority = '%s/%s' % (authority_domain, tenant) resource_uri = 'https://graph.microsoft.com' client_id = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' -client_thumbprint = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' -client_certificate = '' -api_instance = api.GraphAPI.from_certificate(authority_host_uri, tenant, resource_uri, client_id, client_certificate, client_thumbprint) +thumbprint = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' +scopes = [resource_uri + "/.default"] + +app = msal.PublicClientApplication(client_id, authority=authority) + +# check the cache to see if this end user has signed in before +accounts = app.get_accounts(username=username) + +if accounts: + logging.info("Account(s) exists in cache, probably with token too") + result = app.acquire_token_silent(scopes, account=accounts[0]) + +if not accounts or not result: + logging.info("No suitable token exists in cache") + result = app.acquire_token_by_username_password(username, password, scopes=scopes) + +access_token = result.get('access_token') +if not access_token: + raise ValueError('access_token not provided: %r' % result) + +api_instance = api.GraphAPI(resource_uri, access_token) +``` + +#### Client Certificate +```python +import msal +import os + +authority_domain = 'https://login.microsoftonline.com' +tenant = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' +authority = '%s/%s' % (authority_domain, tenant) +resource_uri = 'https://graph.microsoft.com' +client_id = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' +thumbprint = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' +scopes = [resource_uri + "/.default"] + +with open('path/to/cert.pem', 'r') as input_file: + private_key = input_file.read() +client_credential = dict(thumbprint=thumbprint, private_key=private_key) + +msgraph_app = msal.ConfidentialClientApplication(client_id, authority=authority, client_credential=client_credential) + +# lookup a token from cache +result = msgraph_app.acquire_token_silent(scopes, account=None) + +if not result: + print("No suitable token exists in cache. Fetching new token from AAD.") + result = msgraph_app.acquire_token_for_client(scopes=scopes) + +if 'access_token' not in result: + raise ValueError('access_token not provided: %r' % result) + +access_token = result['access_token'] +api_instance = api.GraphAPI(resource_uri, access_token) ``` -**NOTE**: When a `client_certificate` is changed, the `client_thumbprint` and `client_id` values must also be changed +For more example on how to authenticate, see [AzureAD/microsoft-authentication-library-for-python](https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample) ### Using the API to fetch Users diff --git a/msgraph/__init__.py b/msgraph/__init__.py index 14e974f..0404d81 100644 --- a/msgraph/__init__.py +++ b/msgraph/__init__.py @@ -1 +1 @@ -__version__ = '0.2.8' +__version__ = '0.3.0' diff --git a/msgraph/api.py b/msgraph/api.py index 6f72231..3ffb533 100644 --- a/msgraph/api.py +++ b/msgraph/api.py @@ -1,5 +1,4 @@ import logging -import adal import requests from . import exception @@ -7,51 +6,6 @@ logger = logging.getLogger(__name__) -class Token(object): - """ - Wraps the authenticated API token - - Attributes: - expires_in (str): The time from when the token was created until when it expires - expires_on (datetime): The datetime of when the token expires - resource (str): The resource for which a token is valid - token_type (str): The specific type of token - access_token (str): The actual token string used to interface with the API endpoint - """ - __slots__ = ('expires_in', 'expires_on', 'resource', 'token_type', 'access_token') - - def __init__(self, expires_in, expires_on, resource, token_type, access_token): - self.expires_in = expires_in - self.expires_on = expires_on - self.resource = resource - self.token_type = token_type - self.access_token = access_token - - def __str__(self): - return '%s %s' % (self.token_type, self.access_token) - - def __repr__(self): - return '<%s %s resource=%r, token_type=%r, access_token=%r, expires_on=%r>' % (self.__class__.__name__, id(self), self.resource, self.token_type, self.access_token, self.expires_on) - - @classmethod - def from_api(cls, data): - """ - Constructs a Token instance from an API response - - Parameters: - data (dict): The data returned from API response - - Returns: - Token: The Token instance - """ - expires_in = data['expiresIn'] - expires_on = data['expiresOn'] - resource = data['resource'] - token_type = data['tokenType'] - access_token = data['accessToken'] - return cls(expires_in, expires_on, resource, token_type, access_token) - - class GraphAPI(object): """ A wrapper for the Microsoft Graph API @@ -59,37 +13,33 @@ class GraphAPI(object): See https://github.com/Azure-Samples/data-lake-analytics-python-auth-options/blob/master/sample.py#L65-L82 Attributes: - authority_host_uri (str): The service to login through - tenant (str): The tenant ID of the instance resource_uri (str): The host of the API service - client_id (str): The client ID - client_certificate (str): The contents of the authenticating SSL certificate - client_thumbprint (str): The thumbprint corresponding to the client_certificate Example: - import api - authority_host_uri = 'https://login.microsoftonline.com' + from msgraph import api + authority = 'https://login.microsoftonline.com' tenant = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' resource_uri = 'https://graph.microsoft.com' client_id = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' client_thumbprint = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' client_certificate = '-----BEGIN RSA PRIVATE KEY-----...' - endpoint = api.GraphAPI.from_certificate(authority_host_uri, tenant, resource_uri, client_id, client_certificate, client_thumbprint) + client_credential = dict(thumbprint=client_thumbprint, private_key=client_certificate) + app = msal.ConfidentialClientApplication(client_id, authority=authority_host_uri, client_credential=client_credential) + + access_token_data = app.acquire_token_for_client(scopes=scope) + if 'access_token' not in access_token_data: + raise ValueError(access_token_data['error_description']) + instance = api.GraphAPI(resource_uri, access_token) """ - def __init__(self, authority_host_uri, tenant, resource_uri, client_id, access_token, **kwargs): - self.authority_host_uri = authority_host_uri - self.tenant = tenant + def __init__(self, resource_uri, access_token, **kwargs): self.resource_uri = resource_uri - self.client_id = client_id self._access_token = access_token - self.client_certificate = kwargs.get('client_certificate') - self.certificate_footprint = kwargs.get('certificate_footprint') self._session = requests.Session() def __repr__(self): - return '<%s %s authority_host_uri=%r, tenant ID=%r, resource URI=%r, client ID=%r>' % (self.__class__.__name__, id(self), self.authority_host_uri, self.tenant, self.resource_uri, self.client_id) + return '<%s %s resource URI=%r>' % (self.__class__.__name__, id(self), self.resource_uri) def request(self, uri, **kwargs): """ @@ -116,10 +66,9 @@ def request(self, uri, **kwargs): url = '%s/%s/%s' % (self.resource_uri, '%s' % version, uri) else: url = uri - token = str(self._access_token) content_type = kwargs.pop('content_type', 'application/json') headers = { - 'Authorization': token, + 'Authorization': self._access_token, 'Content-Type': content_type } method_specific_headers = kwargs.pop('headers', dict()) @@ -128,7 +77,7 @@ def request(self, uri, **kwargs): try: response = self._session.request(method, url, headers=headers, **kwargs) except Exception as e: - message = '%r %r request unsuccessful: %r' % (url, method, e.message) + message = '%r %r request unsuccessful: %r' % (url, method, e) logger.error(message, exc_info=1) code = getattr(e, 'code', None) raise exception.MicrosoftException(code, message) @@ -145,47 +94,3 @@ def request(self, uri, **kwargs): logger.error(error) raise exception.MicrosoftException(code, message) return data - - @staticmethod - def _authenticate_via_certificate(authority_host_uri, tenant, resource_uri, client_id, client_certificate, certificate_thumbprint): - authority_uri = '%s/%s' % (authority_host_uri, tenant) - try: - context = adal.AuthenticationContext(authority_uri, api_version=None) - data = context.acquire_token_with_client_certificate(resource_uri, client_id, client_certificate, certificate_thumbprint) - except adal.adal_error.AdalError as e: - if isinstance(e.error_response, dict): - code = e.error_response.get('error_codes') - message = e.error_response['error_description'] - else: - code = e.error_response - message = str(e) - logger.error(repr(e), exc_info=True) - raise exception.MicrosoftAuthenticationException(code, message) - except Exception as e: - raise e - else: - access_token = Token.from_api(data) - return access_token - - @classmethod - def from_certificate(cls, authority_host_uri, tenant, resource_uri, client_id, client_certificate, certificate_thumbprint): - """ - Creates an authenticated instance using an SSL certificate - - Parameters: - authority_host_uri (str): The service to login through - tenant (str): The tenant ID of the instance - resource_uri (str): The host of the API service - client_id (str): The client ID - client_certificate (str): The contents of the authenticating SSL certificate - client_thumbprint (str): The thumbprint corresponding to the client_certificate - - Returns: - GraphAPI: The authenticated API instance - - Raises: - MicrosoftAuthenticationException: failed to authenticate using the provided parameters - Exception: An unknown error occurred - """ - access_token = cls._authenticate_via_certificate(authority_host_uri, tenant, resource_uri, client_id, client_certificate, certificate_thumbprint) - return cls(authority_host_uri, tenant, resource_uri, client_id, access_token, client_certificate=client_certificate, certificate_thumbprint=certificate_thumbprint) diff --git a/msgraph/base.py b/msgraph/base.py index 40c853d..946661d 100644 --- a/msgraph/base.py +++ b/msgraph/base.py @@ -10,8 +10,18 @@ class Base(object): standard_datetime_format = date_format + ' ' + time_format extended_datetime_format = date_format + 'T' + time_format +'.%fZ' + tz_datetime_format = date_format + 'T' + time_format + '%z' + @classmethod def parse_date_time(cls, text): + instance = None + instance = cls.parse_raw_date_time(text) + if not instance: + instance = cls.parse_tz_date_time(text) + return instance + + @classmethod + def parse_raw_date_time(cls, text): instance = None formats = [cls.extended_datetime_format, cls.full_datetime_format, cls.datetime_format, cls.standard_datetime_format, cls.iso_format, cls.date_format] for format in formats: @@ -22,3 +32,19 @@ def parse_date_time(cls, text): else: break return instance + + @classmethod + def parse_tz_date_time(cls, text): + instance = None + formats = [cls.tz_datetime_format] + # if colon in timezone, remove it + if ":" == text[-3:-2]: + text = text[:-3] + text[-2:] + for format in formats: + try: + instance = datetime.strptime(text, format) + except Exception: + pass + else: + break + return instance diff --git a/msgraph/beta/bookings.py b/msgraph/beta/booking.py similarity index 98% rename from msgraph/beta/bookings.py rename to msgraph/beta/booking.py index 7eaed96..019f9f3 100644 --- a/msgraph/beta/bookings.py +++ b/msgraph/beta/booking.py @@ -1,5 +1,6 @@ import logging from msgraph import base +from msgraph import calendar logger = logging.getLogger(__name__) @@ -51,7 +52,12 @@ def cancel(self, api, business, cancellation_message): def from_api(cls, data): id = data['id'] start = data['start'] + if start: + start = calendar.DateTime.from_api(start) + end = data['end'] + if end: + end = calendar.DateTime.from_api(end) duration = data['duration'] customer_id = data['customerId'] customer_name = data['customerName'] @@ -69,13 +75,13 @@ def from_api(cls, data): invoice_amount = data['invoiceAmount'] invoice_date = data['invoiceDate'] prebuffer = data['preBuffer'] - postbuffer = data['postbuffer'] + postbuffer = data['postBuffer'] price_type = data['priceType'] price = data['price'] reminders = data['reminders'] staff_member_ids = data['staffMemberIds'] opt_out_of_customer_email = data['optOutOfCustomerEmail'] - return cls(id, start, end, duration, customer_id, customer_name, customer_email_address, customer_location, customer_phone, customer_notes, service_id, service_name, service_location, service_notes, invoice_id, invoice_url, invoice_status, invoice_amount, invoice_date, prebuffer, postbuffer, price_type, price, price, price_type, reminders, staff_member_ids, opt_out_of_customer_email) + return cls(id, start, end, duration, customer_id, customer_name, customer_email_address, customer_location, customer_phone, customer_notes, service_id, service_name, service_location, service_notes, invoice_id, invoice_url, invoice_status, invoice_amount, invoice_date, prebuffer, postbuffer, price_type, price, reminders, staff_member_ids, opt_out_of_customer_email) @classmethod def get(cls, api, business, **kwargs): @@ -163,7 +169,7 @@ def get(cls, api, **kwargs): output = cls.from_api(data) else: data = api.request(uri, **kwargs) - output = [cls.from_api(data) for item in data.get('value', [])] + output = [cls.from_api(item) for item in data.get('value', [])] return output diff --git a/msgraph/calendar.py b/msgraph/calendar.py index 46fb6e1..91dd27c 100644 --- a/msgraph/calendar.py +++ b/msgraph/calendar.py @@ -324,9 +324,10 @@ def from_api(cls, data): class DateTime(base.Base): - __slots__ = ('date_time', 'time_zone') + __slots__ = ('raw_date_time', 'date_time', 'time_zone') - def __init__(self, date_time, time_zone): + def __init__(self, raw_date_time, date_time, time_zone): + self.raw_date_time = raw_date_time self.date_time = date_time self.time_zone = time_zone @@ -342,10 +343,10 @@ def to_dict(self): @classmethod def from_api(cls, data): - date_time = data['dateTime'] - date_time = cls.parse_date_time(date_time[:26]) + raw_date_time = data['dateTime'] + date_time = cls.parse_date_time(raw_date_time[:26]) time_zone = data['timeZone'] - return cls(date_time, time_zone) + return cls(raw_date_time, date_time, time_zone) class Event(base.Base): @@ -637,7 +638,7 @@ def get(cls, api, **kwargs): return output @classmethod - def instances(cls, api, event, **kwargs): + def event_instances(cls, api, event, **kwargs): """ Fetch the Events from the API endpoint diff --git a/requirements.txt b/requirements.txt index d95aeed..02c8ef9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -adal>=1.2.2 +msal>=1.5.0 requests>=2.12.0 diff --git a/setup.py b/setup.py index f9ae0d2..c941267 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ 'Source': 'https://github.com/WMInfoTech/python-msgraph', 'Tracker': 'https://github.com/WMInfoTech/python-msgraph/issues' }, - install_requires=['adal>=1.2.2', 'requests>=2.12.0'], + install_requires=['msal>=1.5.0', 'requests>=2.12.0'], options={ 'bdist_wheel': { 'universal': True From fad83b1a25645ef61318e1236f336effe369c0be Mon Sep 17 00:00:00 2001 From: Doug Fenstermacher Date: Thu, 1 Oct 2020 16:02:06 -0400 Subject: [PATCH 2/4] adds documentation for the msgraph.beta module and fixes some naming errors in the Bookings API --- msgraph/beta/README.md | 100 +++++++++++++++++++++++++++++++++++++++- msgraph/beta/booking.py | 7 +-- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/msgraph/beta/README.md b/msgraph/beta/README.md index 8ed527e..d3636fd 100644 --- a/msgraph/beta/README.md +++ b/msgraph/beta/README.md @@ -1,4 +1,102 @@ ## Beta Any `msgraph.beta` module is subject to change. According to the Microsoft Graph website: -> APIs under the `/beta` version in Microsoft Graph are subject to change. Use of these APIs in production applications is not supported. +> APIs under the `/beta` version in Microsoft Graph are subject to change. Use of these APIs in production applications is not supported. + + +### Booking +**Last updated**: 2020-10-01 + +The [Microsoft Bookings API](https://docs.microsoft.com/en-us/graph/api/resources/booking-api-overview?view=graph-rest-beta) currently requires user authentication to get access to a business's appointments, customers, services, or staff members. For a working example, see [Azure-Samples/ms-identity-python-webapp](https://github.com/Azure-Samples/ms-identity-python-webapp). + +#### Fetching businesses +To fetch all businesses using `msgraph`, use the `bookings ` module's `Business` class: + +```python +from msgraph import api +from msgraph.beta import booking + +# authenticate and create API instance here +resource_uri = 'https://graph.microsoft.com' +access_token = '' +api_instance = api.GraphAPI(resource_uri, access_token) + +businesses = booking.Business.get(api_instance) +``` + +or to fetch a specific `msgraph.beta.Business` instance, provide the `business` ID as the `business` keyword argument: + +```python +business = booking.Business.get(api_instance, business='MyBusiness@johndoe.onmicrosoft.com') +``` + +#### Fetching Appointment +**Last updated**: 2020-10-01 +To fetch all appointments using `msgraph`, use the `bookings ` module's `Appointment` class: + +```python +from msgraph.beta import booking + +business = booking.Business.get(api_instance, business='MyBusiness@johndoe.onmicrosoft.com') +customers = booking.Appointment.get(api_instance, business) +``` + +or to fetch a specific `msgraph.beta.Appointment` instance, provide the `appointment` ID as the `appointment` keyword argument: + +```python +appointment = booking.Customer.get(api_instance, business, appointment='myappointmentid') +``` + + +#### Fetching Customers +**Last updated**: 2020-10-01 +To fetch all services using `msgraph`, use the `bookings ` module's `Customer` class: + +```python +from msgraph.beta import booking + +business = booking.Business.get(api_instance, business='MyBusiness@johndoe.onmicrosoft.com') +customers = booking.Customer.get(api_instance, business) +``` + +or to fetch a specific `msgraph.beta.Customer` instance, provide the `customer` ID as the `customer` keyword argument: + +```python +customer = booking.Customer.get(api_instance, business, customer='myservice') +``` + + +#### Fetching Services +**Last updated**: 2020-10-01 +To fetch all services using `msgraph`, use the `bookings ` module's `Service` class: + +```python +from msgraph.beta import booking + +business = booking.Business.get(api_instance, business='MyBusiness@johndoe.onmicrosoft.com') +services = booking.Service.get(api_instance, business) +``` + +or to fetch a specific `msgraph.beta.Service` instance, provide the `service` ID as the `service` keyword argument: + +```python +service = booking.Service.get(api_instance, business, service='myservice') +``` + + +#### Fetching Staff Members +**Last updated**: 2020-10-01 +To fetch all services using `msgraph`, use the `bookings ` module's `StaffMember` class: + +```python +from msgraph.beta import booking + +business = booking.Business.get(api_instance, business='MyBusiness@johndoe.onmicrosoft.com') +staff_members = booking.StaffMember.get(api_instance, business) +``` + +or to fetch a specific `msgraph.beta.Service` instance, provide the `StaffMember` ID as the `staff_member` keyword argument: + +```python +staff_member = booking.StaffMember.get(api_instance, business, staff_member='mystaffmemberid') +``` diff --git a/msgraph/beta/booking.py b/msgraph/beta/booking.py index 019f9f3..c68278e 100644 --- a/msgraph/beta/booking.py +++ b/msgraph/beta/booking.py @@ -256,7 +256,7 @@ def __str__(self): return self.id def __repr__(self): - return '<%s %s id=%r, display_name=%r, email_address=%r, description=%r, is_hidden_from_customers=%r>' % (self.__class__.__name__, id(self), self.display_name, self.email_address, self.description, self.is_hidden_from_customers) + return '<%s %s id=%r, display_name=%r, email_address=%r, description=%r, is_hidden_from_customers=%r>' % (self.__class__.__name__, id(self), self.id, self.display_name, self.email_address, self.description, self.is_hidden_from_customers) def update(self, api, business, **kwargs): uri = 'bookingBusinesses/%s/services/%s' % (business, self.id) @@ -271,10 +271,11 @@ def delete(self, api, business, **kwargs): @classmethod def from_api(cls, data): + print(data) id = data['id'] display_name = data['displayName'] description = data['description'] - email_address = data['emailAddress'] + email_address = data.get('emailAddress') is_hidden_from_customers = data['isHiddenFromCustomers'] notes = data['notes'] prebuffer = data['preBuffer'] @@ -349,7 +350,7 @@ def from_api(cls, data): @classmethod def get(cls, api, business, **kwargs): kwargs.setdefault('version', 'beta') - staff_member = kwargs.pop('service', None) + staff_member = kwargs.pop('staff_member', None) if staff_member: uri = 'bookingBusinesses/%s/staffMembers/%s' % (business, staff_member) data = api.request(uri, **kwargs) From 0bbf6234685d23717253c75683a721d23fd97458 Mon Sep 17 00:00:00 2001 From: Doug Fenstermacher Date: Thu, 5 Nov 2020 01:09:36 -0500 Subject: [PATCH 3/4] updates documentation for msgraph.api.GraphAPI shows how to use msgraph.api.GraphAPI to make API requests --- msgraph/api.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/msgraph/api.py b/msgraph/api.py index 3ffb533..481a9e6 100644 --- a/msgraph/api.py +++ b/msgraph/api.py @@ -16,21 +16,23 @@ class GraphAPI(object): resource_uri (str): The host of the API service Example: - from msgraph import api - authority = 'https://login.microsoftonline.com' - tenant = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' - resource_uri = 'https://graph.microsoft.com' - client_id = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' - client_thumbprint = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' - client_certificate = '-----BEGIN RSA PRIVATE KEY-----...' - - client_credential = dict(thumbprint=client_thumbprint, private_key=client_certificate) - app = msal.ConfidentialClientApplication(client_id, authority=authority_host_uri, client_credential=client_credential) - - access_token_data = app.acquire_token_for_client(scopes=scope) - if 'access_token' not in access_token_data: - raise ValueError(access_token_data['error_description']) - instance = api.GraphAPI(resource_uri, access_token) + >>> from msgraph import api + >>> authority = 'https://login.microsoftonline.com' + >>> tenant = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' + >>> resource_uri = 'https://graph.microsoft.com' + >>> client_id = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' + >>> client_thumbprint = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' + >>> client_certificate = '-----BEGIN RSA PRIVATE KEY-----...' + >>> + >>> client_credential = dict(thumbprint=client_thumbprint, private_key=client_certificate) + >>> app = msal.ConfidentialClientApplication(client_id, authority=authority_host_uri, client_credential=client_credential) + >>> + >>> access_token_data = app.acquire_token_for_client(scopes=scope) + >>> if 'access_token' not in access_token_data: + >>> raise ValueError(access_token_data['error_description']) + >>> instance = api.GraphAPI(resource_uri, access_token) + >>> + >>> raw_output = instance.request('users/johndoe@example.com') """ def __init__(self, resource_uri, access_token, **kwargs): From 296375127b260f2cd310c589651a675c07e0b508 Mon Sep 17 00:00:00 2001 From: Doug Fenstermacher Date: Wed, 1 Sep 2021 00:06:42 -0400 Subject: [PATCH 4/4] adds un-commited changes prior to return to office --- upload.sh | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 upload.sh diff --git a/upload.sh b/upload.sh new file mode 100644 index 0000000..e3f458e --- /dev/null +++ b/upload.sh @@ -0,0 +1,9 @@ +!/bin/bash + +python setup.py clean + +python setup.py sdist bdist_wheel + +# check package +twine check dist/* +twine upload dist/*