22import logging
33import os
44import time
5+ from typing import Optional , Callable , Union
56
67from oauthlib .oauth2 import TokenExpiredError , WebApplicationClient , BackendApplicationClient , LegacyApplicationClient
78from requests import Session
@@ -68,64 +69,68 @@ class Protocol:
6869 _oauth_scope_prefix = '' # Prefix for scopes
6970 _oauth_scopes = {} # Dictionary of {scopes_name: [scope1, scope2]}
7071
71- def __init__ (self , * , protocol_url = None , api_version = None ,
72- default_resource = None ,
73- casing_function = None , protocol_scope_prefix = None ,
74- timezone = None , ** kwargs ):
72+ def __init__ (self , * , protocol_url : Optional [str ] = None ,
73+ api_version : Optional [str ] = None ,
74+ default_resource : Optional [str ] = None ,
75+ casing_function : Optional [Callable ] = None ,
76+ protocol_scope_prefix : Optional [str ] = None ,
77+ timezone : Union [Optional [str ], Optional [ZoneInfo ]] = None , ** kwargs ):
7578 """ Create a new protocol object
7679
77- :param str protocol_url: the base url used to communicate with the
80+ :param protocol_url: the base url used to communicate with the
7881 server
79- :param str api_version: the api version
80- :param str default_resource: the default resource to use when there is
82+ :param api_version: the api version
83+ :param default_resource: the default resource to use when there is
8184 nothing explicitly specified during the requests
82- :param function casing_function: the casing transform function to be
85+ :param casing_function: the casing transform function to be
8386 used on api keywords (camelcase / pascalcase)
84- :param str protocol_scope_prefix: prefix url for scopes
85- :param datetime. timezone.utc or str timezone : preferred timezone, defaults to the
86- system timezone or fallback to UTC
87+ :param protocol_scope_prefix: prefix url for scopes
88+ :param timezone: preferred timezone, if not provided will default
89+ to the system timezone or fallback to UTC
8790 :raises ValueError: if protocol_url or api_version are not supplied
8891 """
8992 if protocol_url is None or api_version is None :
9093 raise ValueError (
9194 'Must provide valid protocol_url and api_version values' )
92- self .protocol_url = protocol_url or self ._protocol_url
93- self .protocol_scope_prefix = protocol_scope_prefix or ''
94- self .api_version = api_version
95- self .service_url = '{}{}/' .format (protocol_url , api_version )
96- self .default_resource = default_resource or ME_RESOURCE
97- self .use_default_casing = True if casing_function is None else False
98- self .casing_function = casing_function or camelcase
95+ self .protocol_url : str = protocol_url or self ._protocol_url
96+ self .protocol_scope_prefix : str = protocol_scope_prefix or ''
97+ self .api_version : str = api_version
98+ self .service_url : str = '{}{}/' .format (protocol_url , api_version )
99+ self .default_resource : str = default_resource or ME_RESOURCE
100+ self .use_default_casing : bool = True if casing_function is None else False
101+ self .casing_function : Callable = casing_function or camelcase
102+
99103 if timezone :
100104 if isinstance (timezone , str ):
101105 # convert string to ZoneInfo
102106 try :
103107 timezone = ZoneInfo (timezone )
104- except ZoneInfoNotFoundError :
105- log .debug (f'Timezone { timezone } could not be found. Will default to UTC .' )
106- timezone = ZoneInfo ( 'UTC' )
108+ except ZoneInfoNotFoundError as e :
109+ log .error (f'Timezone { timezone } could not be found.' )
110+ raise e
107111 else :
108112 if not isinstance (timezone , ZoneInfo ):
109113 raise ValueError (f'The timezone parameter must be either a string or a valid ZoneInfo instance.' )
114+
110115 # get_localzone() from tzlocal will try to get the system local timezone and if not will return UTC
111- self .timezone = timezone or get_localzone ()
112- self .max_top_value = 500 # Max $top parameter value
116+ self .timezone : ZoneInfo = timezone or get_localzone ()
117+ self .max_top_value : int = 500 # Max $top parameter value
113118
114119 # define any keyword that can be different in this protocol
115- # for example, attachments Odata type differs between Outlook
120+ # for example, attachments OData type differs between Outlook
116121 # rest api and graph: (graph = #microsoft.graph.fileAttachment and
117122 # outlook = #Microsoft.OutlookServices.FileAttachment')
118- self .keyword_data_store = {}
123+ self .keyword_data_store : dict = {}
119124
120- def get_service_keyword (self , keyword ) :
125+ def get_service_keyword (self , keyword : str ) -> str :
121126 """ Returns the data set to the key in the internal data-key dict
122127
123- :param str keyword: key to get value for
128+ :param keyword: key to get value for
124129 :return: value of the keyword
125130 """
126131 return self .keyword_data_store .get (keyword , None )
127132
128- def convert_case (self , key ) :
133+ def convert_case (self , key : str ) -> str :
129134 """ Returns a key converted with this protocol casing method
130135
131136 Converts case to send/read from the cloud
@@ -137,30 +142,26 @@ def convert_case(self, key):
137142
138143 Default case in this API is lowerCamelCase
139144
140- :param str key: a dictionary key to convert
145+ :param key: a dictionary key to convert
141146 :return: key after case conversion
142- :rtype: str
143147 """
144148 return key if self .use_default_casing else self .casing_function (key )
145149
146150 @staticmethod
147- def to_api_case (key ) :
151+ def to_api_case (key : str ) -> str :
148152 """ Converts key to snake_case
149153
150- :param str key: key to convert into snake_case
154+ :param key: key to convert into snake_case
151155 :return: key after case conversion
152- :rtype: str
153156 """
154157 return snakecase (key )
155158
156- def get_scopes_for (self , user_provided_scopes ) :
159+ def get_scopes_for (self , user_provided_scopes : Optional [ Union [ list , str , tuple ]]) -> list :
157160 """ Returns a list of scopes needed for each of the
158161 scope_helpers provided, by adding the prefix to them if required
159162
160163 :param user_provided_scopes: a list of scopes or scope helpers
161- :type user_provided_scopes: list or tuple or str
162164 :return: scopes with url prefix added
163- :rtype: list
164165 :raises ValueError: if unexpected datatype of scopes are passed
165166 """
166167 if user_provided_scopes is None :
@@ -170,8 +171,7 @@ def get_scopes_for(self, user_provided_scopes):
170171 user_provided_scopes = [user_provided_scopes ]
171172
172173 if not isinstance (user_provided_scopes , (list , tuple )):
173- raise ValueError (
174- "'user_provided_scopes' must be a list or a tuple of strings" )
174+ raise ValueError ("'user_provided_scopes' must be a list or a tuple of strings" )
175175
176176 scopes = set ()
177177 for app_part in user_provided_scopes :
@@ -180,7 +180,7 @@ def get_scopes_for(self, user_provided_scopes):
180180
181181 return list (scopes )
182182
183- def prefix_scope (self , scope ) :
183+ def prefix_scope (self , scope : Union [ tuple , str ]) -> str :
184184 """ Inserts the protocol scope prefix if required"""
185185 if self .protocol_scope_prefix :
186186 if isinstance (scope , tuple ):
@@ -275,7 +275,6 @@ def __init__(self, api_version='v2.0', default_resource=None,
275275
276276
277277class MSBusinessCentral365Protocol (Protocol ):
278-
279278 """ A Microsoft Business Central Protocol Implementation
280279 https://docs.microsoft.com/en-us/dynamics-nav/api-reference/v1.0/endpoints-apis-for-dynamics
281280 """
@@ -285,7 +284,7 @@ class MSBusinessCentral365Protocol(Protocol):
285284 _oauth_scopes = DEFAULT_SCOPES
286285 _protocol_scope_prefix = 'https://api.businesscentral.dynamics.com/'
287286
288- def __init__ (self , api_version = 'v1.0' , default_resource = None ,environment = None ,
287+ def __init__ (self , api_version = 'v1.0' , default_resource = None , environment = None ,
289288 ** kwargs ):
290289 """ Create a new Microsoft Graph protocol object
291290
@@ -299,7 +298,7 @@ def __init__(self, api_version='v1.0', default_resource=None,environment=None,
299298 """
300299 if environment :
301300 _version = "2.0"
302- _environment = "/" + environment
301+ _environment = "/" + environment
303302 else :
304303 _version = "1.0"
305304 _environment = ''
@@ -385,7 +384,8 @@ def __init__(self, credentials, *, scopes=None,
385384 if not isinstance (credentials , tuple ) or len (credentials ) != 1 or (not credentials [0 ]):
386385 raise ValueError ('Provide client id only for public or password flow credentials' )
387386 else :
388- if not isinstance (credentials , tuple ) or len (credentials ) != 2 or (not credentials [0 ] and not credentials [1 ]):
387+ if not isinstance (credentials , tuple ) or len (credentials ) != 2 or (
388+ not credentials [0 ] and not credentials [1 ]):
389389 raise ValueError ('Provide valid auth credentials' )
390390
391391 self ._auth_flow_type = auth_flow_type # 'authorization', 'credentials', 'certificate', 'password', or 'public'
@@ -440,12 +440,12 @@ def set_proxy(self, proxy_server, proxy_port, proxy_username,
440440 if proxy_server and proxy_port :
441441 if proxy_username and proxy_password :
442442 proxy_uri = "{}:{}@{}:{}" .format (proxy_username ,
443- proxy_password ,
444- proxy_server ,
445- proxy_port )
443+ proxy_password ,
444+ proxy_server ,
445+ proxy_port )
446446 else :
447447 proxy_uri = "{}:{}" .format (proxy_server ,
448- proxy_port )
448+ proxy_port )
449449
450450 if proxy_http_only is False :
451451 self .proxy = {
@@ -823,7 +823,8 @@ def _internal_request(self, request_obj, url, method, **kwargs):
823823 log .debug ('Server Error: {}' .format (str (e )))
824824 if self .raise_http_errors :
825825 if error_message :
826- raise HTTPError ('{} | Error Message: {}' .format (e .args [0 ], error_message ), response = response ) from None
826+ raise HTTPError ('{} | Error Message: {}' .format (e .args [0 ], error_message ),
827+ response = response ) from None
827828 else :
828829 raise e
829830 else :
@@ -925,7 +926,7 @@ def __del__(self):
925926 But this is not an issue because this connections will be automatically closed.
926927 """
927928 if hasattr (self , 'session' ) and self .session is not None :
928- self .session .close ()
929+ self .session .close ()
929930
930931
931932def oauth_authentication_flow (client_id , client_secret , scopes = None ,
0 commit comments