From b38aff2a8e7475cc1245afab33abdaafc766ff69 Mon Sep 17 00:00:00 2001 From: Rob Weber Date: Wed, 28 Aug 2019 14:48:41 -0500 Subject: [PATCH] move pydrive to it's own addon --- addon.xml | 7 +- resources/lib/pydrive/LICENSE.txt | 185 ------------- resources/lib/pydrive/__init__.py | 0 resources/lib/pydrive/apiattr.py | 174 ------------ resources/lib/pydrive/auth.py | 415 ---------------------------- resources/lib/pydrive/drive.py | 38 --- resources/lib/pydrive/files.py | 322 --------------------- resources/lib/pydrive/settings.py | 192 ------------- resources/lib/pydrive/settings.yaml | 7 - 9 files changed, 2 insertions(+), 1338 deletions(-) delete mode 100644 resources/lib/pydrive/LICENSE.txt delete mode 100644 resources/lib/pydrive/__init__.py delete mode 100644 resources/lib/pydrive/apiattr.py delete mode 100644 resources/lib/pydrive/auth.py delete mode 100644 resources/lib/pydrive/drive.py delete mode 100644 resources/lib/pydrive/files.py delete mode 100644 resources/lib/pydrive/settings.py delete mode 100644 resources/lib/pydrive/settings.yaml diff --git a/addon.xml b/addon.xml index d5e07d6..a58bd05 100644 --- a/addon.xml +++ b/addon.xml @@ -6,11 +6,8 @@ - - - - - + + diff --git a/resources/lib/pydrive/LICENSE.txt b/resources/lib/pydrive/LICENSE.txt deleted file mode 100644 index 5c87881..0000000 --- a/resources/lib/pydrive/LICENSE.txt +++ /dev/null @@ -1,185 +0,0 @@ -Copyright 2013 Google Inc. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/resources/lib/pydrive/__init__.py b/resources/lib/pydrive/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/resources/lib/pydrive/apiattr.py b/resources/lib/pydrive/apiattr.py deleted file mode 100644 index fe99700..0000000 --- a/resources/lib/pydrive/apiattr.py +++ /dev/null @@ -1,174 +0,0 @@ -class ApiAttribute(object): - """A data descriptor that sets and returns values.""" - - def __init__(self, name): - """Create an instance of ApiAttribute. - - :param name: name of this attribute. - :type name: str. - """ - self.name = name - - def __get__(self, obj, type=None): - """Accesses value of this attribute.""" - return obj.attr.get(self.name) - - def __set__(self, obj, value): - """Write value of this attribute.""" - obj.attr[self.name] = value - if obj.dirty.get(self.name) is not None: - obj.dirty[self.name] = True - - def __del__(self, obj=None): - """Delete value of this attribute.""" - if(obj != None): - del obj.attr[self.name] - if obj.dirty.get(self.name) is not None: - del obj.dirty[self.name] - - -class ApiAttributeMixin(object): - """Mixin to initialize required global variables to use ApiAttribute.""" - - def __init__(self): - self.attr = {} - self.dirty = {} - - -class ApiResource(dict): - """Super class of all api resources. - - Inherits and behaves as a python dictionary to handle api resources. - Save clean copy of metadata in self.metadata as a dictionary. - Provides changed metadata elements to efficiently update api resources. - """ - auth = ApiAttribute('auth') - - def __init__(self, *args, **kwargs): - """Create an instance of ApiResource.""" - self.update(*args, **kwargs) - - def __getitem__(self, key): - """Overwritten method of dictionary. - - :param key: key of the query. - :type key: str. - :returns: value of the query. - """ - return dict.__getitem__(self, key) - - def __setitem__(self, key, val): - """Overwritten method of dictionary. - - :param key: key of the query. - :type key: str. - :param val: value of the query. - """ - dict.__setitem__(self, key, val) - - def __repr__(self): - """Overwritten method of dictionary.""" - dictrepr = dict.__repr__(self) - return '%s(%s)' % (type(self).__name__, dictrepr) - - def update(self, *args, **kwargs): - """Overwritten method of dictionary.""" - for k, v in dict(*args, **kwargs).iteritems(): - self[k] = v - - def UpdateMetadata(self, metadata=None): - """Update metadata and mark all of them to be clean.""" - if metadata: - self.update(metadata) - self.metadata = dict(self) - - def GetChanges(self): - """Returns changed metadata elements to update api resources efficiently. - - :returns: dict -- changed metadata elements. - """ - dirty = {} - for key in self: - if self.metadata.get(key) is None: - dirty[key] = self[key] - elif self.metadata[key] != self[key]: - dirty[key] = self[key] - return dirty - - -class ApiResourceList(ApiAttributeMixin, ApiResource): - """Abstract class of all api list resources. - - Inherits ApiResource and builds iterator to list any API resource. - """ - metadata = ApiAttribute('metadata') - - def __init__(self, auth=None, metadata=None): - """Create an instance of ApiResourceList. - - :param auth: authorized GoogleAuth instance. - :type auth: GoogleAuth. - :param metadata: parameter to send to list command. - :type metadata: dict. - """ - ApiAttributeMixin.__init__(self) - ApiResource.__init__(self) - self.auth = auth - self.UpdateMetadata() - if metadata: - self.update(metadata) - - def __iter__(self): - """Returns iterator object. - - :returns: ApiResourceList -- self - """ - return self - - def next(self): - """Make API call to list resources and return them. - - Auto updates 'pageToken' everytime it makes API call and - raises StopIteration when it reached the end of iteration. - - :returns: list -- list of API resources. - :raises: StopIteration - """ - if 'pageToken' in self and self['pageToken'] is None: - raise StopIteration - result = self._GetList() - self['pageToken'] = self.metadata.get('nextPageToken') - return result - - def GetList(self): - """Get list of API resources. - - If 'maxResults' is not specified, it will automatically iterate through - every resources available. Otherwise, it will make API call once and - update 'pageToken'. - - :returns: list -- list of API resources. - """ - if self.get('maxResults') is None: - self['maxResults'] = 1000 - result = [] - for x in self: - result.extend(x) - del self['maxResults'] - return result - else: - return self.next() - - def _GetList(self): - """Helper function which actually makes API call. - - Should be overwritten. - - :raises: NotImplementedError - """ - raise NotImplementedError - - def Reset(self): - """Resets current iteration""" - if 'pageToken' in self: - del self['pageToken'] diff --git a/resources/lib/pydrive/auth.py b/resources/lib/pydrive/auth.py deleted file mode 100644 index aeace4b..0000000 --- a/resources/lib/pydrive/auth.py +++ /dev/null @@ -1,415 +0,0 @@ -import socket -import webbrowser -import httplib2 -import oauth2client.clientsecrets as clientsecrets - -from googleapiclient.discovery import build -from functools import wraps -from oauth2client.client import FlowExchangeError -from oauth2client.client import AccessTokenRefreshError -from oauth2client.client import OAuth2WebServerFlow -from oauth2client.client import OOB_CALLBACK_URN -from oauth2client.file import Storage -from oauth2client.tools import ClientRedirectHandler -from oauth2client.tools import ClientRedirectServer -from oauth2client._helpers import scopes_to_string -from .apiattr import ApiAttribute -from .apiattr import ApiAttributeMixin -from .settings import LoadSettingsFile -from .settings import ValidateSettings -from .settings import SettingsError -from .settings import InvalidConfigError - - -class AuthError(Exception): - """Base error for authentication/authorization errors.""" - - -class InvalidCredentialsError(IOError): - """Error trying to read credentials file.""" - - -class AuthenticationRejected(AuthError): - """User rejected authentication.""" - - -class AuthenticationError(AuthError): - """General authentication error.""" - - -class RefreshError(AuthError): - """Access token refresh error.""" - -def LoadAuth(decoratee): - """Decorator to check if the auth is valid and loads auth if not.""" - @wraps(decoratee) - def _decorated(self, *args, **kwargs): - if self.auth is None: # Initialize auth if needed. - self.auth = GoogleAuth() - if self.auth.access_token_expired: - self.auth.LocalWebserverAuth() - if self.auth.service is None: # Check if drive api is built. - self.auth.Authorize() - return decoratee(self, *args, **kwargs) - return _decorated - -def CheckAuth(decoratee): - """Decorator to check if it requires OAuth2 flow request.""" - @wraps(decoratee) - def _decorated(self, *args, **kwargs): - dirty = False - code = None - save_credentials = self.settings.get('save_credentials') - if self.credentials is None and save_credentials: - self.LoadCredentials() - if self.flow is None: - self.GetFlow() - if self.credentials is None: - code = decoratee(self, *args, **kwargs) - dirty = True - else: - if self.access_token_expired: - if self.credentials.refresh_token is not None: - self.Refresh() - else: - code = decoratee(self, *args, **kwargs) - dirty = True - if code is not None: - self.Auth(code) - if dirty and save_credentials: - self.SaveCredentials() - return _decorated - - -class GoogleAuth(ApiAttributeMixin, object): - """Wrapper class for oauth2client library in google-api-python-client. - - Loads all settings and credentials from one 'settings.yaml' file - and performs common OAuth2.0 related functionality such as authentication - and authorization. - """ - DEFAULT_SETTINGS = { - 'client_config_backend': 'file', - 'client_config_file': 'client_secrets.json', - 'save_credentials': False, - 'oauth_scope': ['https://www.googleapis.com/auth/drive'] - } - CLIENT_CONFIGS_LIST = ['client_id', 'client_secret', 'auth_uri', - 'token_uri', 'revoke_uri', 'redirect_uri'] - settings = ApiAttribute('settings') - client_config = ApiAttribute('client_config') - flow = ApiAttribute('flow') - credentials = ApiAttribute('credentials') - http = ApiAttribute('http') - service = ApiAttribute('service') - - def __init__(self, settings_file='settings.yaml'): - """Create an instance of GoogleAuth. - - This constructor just sets the path of settings file. - It does not actually read the file. - - :param settings_file: path of settings file. 'settings.yaml' by default. - :type settings_file: str. - """ - ApiAttributeMixin.__init__(self) - self.client_config = {} - try: - self.settings = LoadSettingsFile(settings_file) - except SettingsError: - self.settings = self.DEFAULT_SETTINGS - else: - if self.settings is None: - self.settings = self.DEFAULT_SETTINGS - else: - ValidateSettings(self.settings) - - @property - def access_token_expired(self): - """Checks if access token doesn't exist or is expired. - - :returns: bool -- True if access token doesn't exist or is expired. - """ - if self.credentials is None: - return True - return self.credentials.access_token_expired - - @CheckAuth - def LocalWebserverAuth(self, host_name='localhost', - port_numbers=[8080, 8090]): - """Authenticate and authorize from user by creating local webserver and - retrieving authentication code. - - This function is not for webserver application. It creates local webserver - for user from standalone application. - - :param host_name: host name of the local webserver. - :type host_name: str. - :param port_numbers: list of port numbers to be tried to used. - :type port_numbers: list. - :returns: str -- code returned from local webserver - :raises: AuthenticationRejected, AuthenticationError - """ - success = False - port_number = 0 - for port in port_numbers: - port_number = port - try: - httpd = ClientRedirectServer((host_name, port), ClientRedirectHandler) - except socket.error as e: - pass - else: - success = True - break - if success: - oauth_callback = 'http://%s:%s/' % (host_name, port_number) - else: - raise AuthenticationError() - self.flow.redirect_uri = oauth_callback - authorize_url = self.GetAuthUrl() - webbrowser.open(authorize_url, new=1, autoraise=True) - httpd.handle_request() - if 'error' in httpd.query_params: - raise AuthenticationRejected('User rejected authentication') - if 'code' in httpd.query_params: - return httpd.query_params['code'] - else: - raise AuthenticationError('No code found in redirect') - - @CheckAuth - def CommandLineAuth(self): - """Authenticate and authorize from user by printing authentication url - retrieving authentication code from command-line. - - :returns: str -- code returned from commandline. - """ - self.flow.redirect_uri = OOB_CALLBACK_URN - authorize_url = self.GetAuthUrl() - return raw_input('Enter verification code: ').strip() - - def LoadCredentials(self, backend=None): - """Loads credentials or create empty credentials if it doesn't exist. - - :param backend: target backend to save credential to. - :type backend: str. - :raises: InvalidConfigError - """ - if backend is None: - backend = self.settings.get('save_credentials_backend') - if backend is None: - raise InvalidConfigError('Please specify credential backend') - if backend == 'file': - self.LoadCredentialsFile() - else: - raise InvalidConfigError('Unknown save_credentials_backend') - - def LoadCredentialsFile(self, credentials_file=None): - """Loads credentials or create empty credentials if it doesn't exist. - - Loads credentials file from path in settings if not specified. - - :param credentials_file: path of credentials file to read. - :type credentials_file: str. - :raises: InvalidConfigError, InvalidCredentialsError - """ - if credentials_file is None: - credentials_file = self.settings.get('save_credentials_file') - if credentials_file is None: - raise InvalidConfigError('Please specify credentials file to read') - try: - storage = Storage(credentials_file) - self.credentials = storage.get() - except IOError: - raise InvalidCredentialsError('Credentials file cannot be symbolic link') - - def SaveCredentials(self, backend=None): - """Saves credentials according to specified backend. - - If you have any specific credentials backend in mind, don't use this - function and use the corresponding function you want. - - :param backend: backend to save credentials. - :type backend: str. - :raises: InvalidConfigError - """ - if backend is None: - backend = self.settings.get('save_credentials_backend') - if backend is None: - raise InvalidConfigError('Please specify credential backend') - if backend == 'file': - self.SaveCredentialsFile() - else: - raise InvalidConfigError('Unknown save_credentials_backend') - - def SaveCredentialsFile(self, credentials_file=None): - """Saves credentials to the file in JSON format. - - :param credentials_file: destination to save file to. - :type credentials_file: str. - :raises: InvalidConfigError, InvalidCredentialsError - """ - if self.credentials is None: - raise InvalidCredentialsError('No credentials to save') - if credentials_file is None: - credentials_file = self.settings.get('save_credentials_file') - if credentials_file is None: - raise InvalidConfigError('Please specify credentials file to read') - try: - storage = Storage(credentials_file) - storage.put(self.credentials) - self.credentials.set_store(storage) - except CredentialsFileSymbolicLinkError: - raise InvalidCredentialsError('Credentials file cannot be symbolic link') - - def LoadClientConfig(self, backend=None): - """Loads client configuration according to specified backend. - - If you have any specific backend to load client configuration from in mind, - don't use this function and use the corresponding function you want. - - :param backend: backend to load client configuration from. - :type backend: str. - :raises: InvalidConfigError - """ - if backend is None: - backend = self.settings.get('client_config_backend') - if backend is None: - raise InvalidConfigError('Please specify client config backend') - if backend == 'file': - self.LoadClientConfigFile() - elif backend == 'settings': - self.LoadClientConfigSettings() - else: - raise InvalidConfigError('Unknown client_config_backend') - - def LoadClientConfigFile(self, client_config_file=None): - """Loads client configuration file downloaded from APIs console. - - Loads client config file from path in settings if not specified. - - :param client_config_file: path of client config file to read. - :type client_config_file: str. - :raises: InvalidConfigError - """ - if client_config_file is None: - client_config_file = self.settings['client_config_file'] - try: - client_type, client_info = clientsecrets.loadfile(client_config_file) - except clientsecrets.InvalidClientSecretsError as error: - raise InvalidConfigError('Invalid client secrets file %s' % error) - if not client_type in (clientsecrets.TYPE_WEB, - clientsecrets.TYPE_INSTALLED): - raise InvalidConfigError('Unknown client_type of client config file') - try: - config_index = ['client_id', 'client_secret', 'auth_uri', 'token_uri'] - for config in config_index: - self.client_config[config] = client_info[config] - self.client_config['revoke_uri'] = client_info.get('revoke_uri') - self.client_config['redirect_uri'] = client_info['redirect_uris'][0] - except KeyError: - raise InvalidConfigError('Insufficient client config in file') - - def LoadClientConfigSettings(self): - """Loads client configuration from settings file. - - :raises: InvalidConfigError - """ - - for config in self.CLIENT_CONFIGS_LIST: - try: - self.client_config[config] = self.settings['client_config'][config] - - except KeyError: - raise InvalidConfigError('Insufficient client config in settings') - - def GetFlow(self): - """Gets Flow object from client configuration. - - :raises: InvalidConfigError - """ - if not all(config in self.client_config \ - for config in self.CLIENT_CONFIGS_LIST): - self.LoadClientConfig() - constructor_kwargs = { - 'redirect_uri': self.client_config['redirect_uri'], - 'auth_uri': self.client_config['auth_uri'], - 'token_uri': self.client_config['token_uri'], - } - if self.client_config['revoke_uri'] is not None: - constructor_kwargs['revoke_uri'] = self.client_config['revoke_uri'] - self.flow = OAuth2WebServerFlow( - self.client_config['client_id'], - self.client_config['client_secret'], - scopes_to_string(self.settings['oauth_scope']), - **constructor_kwargs) - if self.settings.get('get_refresh_token'): - self.flow.params.update({'access_type': 'offline'}) - - def Refresh(self): - """Refreshes the access_token. - - :raises: RefreshError - """ - if self.credentials is None: - raise RefreshError('No credential to refresh.') - if self.credentials.refresh_token is None: - raise RefreshError('No refresh_token found.' - 'Please set access_type of OAuth to offline.') - if self.http is None: - self.http = httplib2.Http() - try: - self.credentials.refresh(self.http) - except AccessTokenRefreshError as error: - raise RefreshError('Access token refresh failed: %s' % error) - - def GetAuthUrl(self, keys = None): - """Creates authentication url where user visits to grant access. - - :returns: str -- Authentication url. - """ - - if(keys != None): - #update some of the settings in the client_config dict - self.client_config['client_id'] = keys['client_id'] - self.client_config['client_secret'] = keys['client_secret'] - - if self.flow is None: - self.GetFlow() - - return self.flow.step1_get_authorize_url() - - def Auth(self, code): - """Authenticate, authorize, and build service. - - :param code: Code for authentication. - :type code: str. - :raises: AuthenticationError - """ - self.Authenticate(code) - self.Authorize() - - def Authenticate(self, code): - """Authenticates given authentication code back from user. - - :param code: Code for authentication. - :type code: str. - :raises: AuthenticationError - """ - if self.flow is None: - self.GetFlow() - try: - self.credentials = self.flow.step2_exchange(code) - except FlowExchangeError as e: - raise AuthenticationError('OAuth2 code exchange failed: %s' % e) - - def Authorize(self): - """Authorizes and builds service. - - :raises: AuthenticationError - """ - if self.http is None: - self.http = httplib2.Http() - if self.access_token_expired: - raise AuthenticationError('No valid credentials provided to authorize') - self.http = self.credentials.authorize(self.http) - self.service = build('drive', 'v2', http=self.http) diff --git a/resources/lib/pydrive/drive.py b/resources/lib/pydrive/drive.py deleted file mode 100644 index 94233e4..0000000 --- a/resources/lib/pydrive/drive.py +++ /dev/null @@ -1,38 +0,0 @@ -from .apiattr import ApiAttributeMixin -from .files import GoogleDriveFile -from .files import GoogleDriveFileList - - -class GoogleDrive(ApiAttributeMixin, object): - """Main Google Drive class.""" - - def __init__(self, auth=None): - """Create an instance of GoogleDrive. - - :param auth: authorized GoogleAuth instance. - :type auth: pydrive.auth.GoogleAuth. - """ - ApiAttributeMixin.__init__(self) - self.auth = auth - - def CreateFile(self, metadata=None): - """Create an instance of GoogleDriveFile with auth of this instance. - - This method would not upload a file to GoogleDrive. - - :param metadata: file resource to initialize GoogleDriveFile with. - :type metadata: dict. - :returns: pydrive.files.GoogleDriveFile -- initialized with auth of this instance. - """ - return GoogleDriveFile(auth=self.auth, metadata=metadata) - - def ListFile(self, param=None): - """Create an instance of GoogleDriveFileList with auth of this instance. - - This method will not fetch from Files.List(). - - :param param: parameter to be sent to Files.List(). - :type param: dict. - :returns: pydrive.files.GoogleDriveFileList -- initialized with auth of this instance. - """ - return GoogleDriveFileList(auth=self.auth, param=param) diff --git a/resources/lib/pydrive/files.py b/resources/lib/pydrive/files.py deleted file mode 100644 index a7dcab9..0000000 --- a/resources/lib/pydrive/files.py +++ /dev/null @@ -1,322 +0,0 @@ -import io -import mimetypes - -from googleapiclient import errors -from googleapiclient.http import MediaIoBaseUpload -from functools import wraps - -from .apiattr import ApiAttribute -from .apiattr import ApiAttributeMixin -from .apiattr import ApiResource -from .apiattr import ApiResourceList -from .auth import LoadAuth - - -class FileNotUploadedError(RuntimeError): - """Error trying to access metadata of file that is not uploaded.""" - - -class ApiRequestError(IOError): - """Error while making any API requests.""" - - -class FileNotDownloadableError(RuntimeError): - """Error trying to download file that is not downloadable.""" - - -def LoadMetadata(decoratee): - """Decorator to check if the file has metadata and fetches it if not. - - :raises: ApiRequestError, FileNotUploadedError - """ - @wraps(decoratee) - def _decorated(self, *args, **kwargs): - if not self.uploaded: - self.FetchMetadata() - return decoratee(self, *args, **kwargs) - return _decorated - - -class GoogleDriveFileList(ApiResourceList): - """Google Drive FileList instance. - - Equivalent to Files.list() in Drive APIs. - """ - - def __init__(self, auth=None, param=None): - """Create an instance of GoogleDriveFileList.""" - super(GoogleDriveFileList, self).__init__(auth=auth, metadata=param) - - @LoadAuth - def _GetList(self): - """Overwritten method which actually makes API call to list files. - - :returns: list -- list of pydrive.files.GoogleDriveFile. - """ - self.metadata = self.auth.service.files().list(**dict(self)).execute() - result = [] - for file_metadata in self.metadata['items']: - tmp_file = GoogleDriveFile( - auth=self.auth, - metadata=file_metadata, - uploaded=True) - result.append(tmp_file) - return result - - -class GoogleDriveFile(ApiAttributeMixin, ApiResource): - """Google Drive File instance. - - Inherits ApiResource which inherits dict. - Can access and modify metadata like dictionary. - """ - content = ApiAttribute('content') - uploaded = ApiAttribute('uploaded') - metadata = ApiAttribute('metadata') - - def __init__(self, auth=None, metadata=None, uploaded=False): - """Create an instance of GoogleDriveFile. - - :param auth: authorized GoogleAuth instance. - :type auth: pydrive.auth.GoogleAuth - :param metadata: file resource to initialize GoogleDirveFile with. - :type metadata: dict. - :param uploaded: True if this file is confirmed to be uploaded. - :type uploaded: bool. - """ - ApiAttributeMixin.__init__(self) - ApiResource.__init__(self) - self.metadata = {} - self.dirty = {'content': False} - self.auth = auth - self.uploaded = uploaded - if uploaded: - self.UpdateMetadata(metadata) - elif metadata: - self.update(metadata) - - def __getitem__(self, key): - """Overwrites manner of accessing Files resource. - - If this file instance is not uploaded and id is specified, - it will try to look for metadata with Files.get(). - - :param key: key of dictionary query. - :type key: str. - :returns: value of Files resource - :raises: KeyError, FileNotUploadedError - """ - try: - return dict.__getitem__(self, key) - except KeyError as e: - if self.uploaded: - raise KeyError(e) - if self.get('id'): - self.FetchMetadata() - return dict.__getitem__(self, key) - else: - raise FileNotUploadedError() - - def SetContentString(self, content): - """Set content of this file to be a string. - - Creates io.BytesIO instance of utf-8 encoded string. - Sets mimeType to be 'text/plain' if not specified. - - :param content: content of the file in string. - :type content: str. - """ - self.content = io.BytesIO(content.encode('utf-8')) - if self.get('mimeType') is None: - self['mimeType'] = 'text/plain' - - def SetContentFile(self, filename): - """Set content of this file from a file. - - Opens the file specified by this method. - Will be read, uploaded, and closed by Upload() method. - Sets metadata 'title' and 'mimeType' automatically if not specified. - - :param filename: name of the file to be uploaded. - :type filename: str. - """ - self.content = open(filename, 'rb') - - if self.get('title') is None: - self['title'] = filename - if self.get('mimeType') is None: - self['mimeType'] = mimetypes.guess_type(filename)[0] - - def GetContentString(self): - """Get content of this file as a string. - - :returns: str -- utf-8 decoded content of the file - :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError - """ - if self.content is None or type(self.content) is not io.BytesIO: - self.FetchContent() - return self.content.getvalue().decode('utf-8') - - def GetContentFile(self, filename, mimetype=None): - """Save content of this file as a local file. - - :param filename: name of the file to write to. - :type filename: str. - :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError - """ - if self.content is None or type(self.content) is not io.BytesIO: - self.FetchContent(mimetype) - f = open(filename, 'wb') - f.write(self.content.getvalue()) - f.close() - - @LoadAuth - def FetchMetadata(self): - """Download file's metadata from id using Files.get(). - - :raises: ApiRequestError, FileNotUploadedError - """ - file_id = self.metadata.get('id') or self.get('id') - if file_id: - try: - metadata = self.auth.service.files().get(fileId=file_id).execute() - except errors.HttpError as error: - raise ApiRequestError(error) - else: - self.uploaded = True - self.UpdateMetadata(metadata) - else: - raise FileNotUploadedError() - - @LoadMetadata - def FetchContent(self, mimetype=None): - """Download file's content from download_url. - - :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError - """ - download_url = self.metadata.get('downloadUrl') - if download_url: - self.content = io.BytesIO(self._DownloadFromUrl(download_url)) - self.dirty['content'] = False - return - - export_links = self.metadata.get('exportLinks') - if export_links and export_links.get(mimetype): - self.content = io.BytesIO( - self._DownloadFromUrl(export_links.get(mimetype))) - self.dirty['content'] = False - return - - raise FileNotDownloadableError( - 'No downloadLink/exportLinks for mimetype found in metadata') - - def Upload(self, param=None): - """Upload/update file by choosing the most efficient method. - - :param param: additional parameter to upload file. - :type param: dict. - :raises: ApiRequestError - """ - if self.uploaded or self.get('id') is not None: - if self.dirty['content']: - self._FilesUpdate(param=param) - else: - self._FilesPatch(param=param) - else: - self._FilesInsert(param=param) - - def Delete(self): - if self.get('id') is not None: - self.auth.service.files().delete(fileId=self.get('id')).execute() - - @LoadAuth - def _FilesInsert(self, param=None): - """Upload a new file using Files.insert(). - - :param param: additional parameter to upload file. - :type param: dict. - :raises: ApiRequestError - """ - if param is None: - param = {} - param['body'] = self.GetChanges() - try: - if self.dirty['content']: - param['media_body'] = self._BuildMediaBody() - metadata = self.auth.service.files().insert(**param).execute() - except errors.HttpError as error: - raise ApiRequestError(error) - else: - self.uploaded = True - self.dirty['content'] = False - self.UpdateMetadata(metadata) - - @LoadAuth - @LoadMetadata - def _FilesUpdate(self, param=None): - """Update metadata and/or content using Files.Update(). - - :param param: additional parameter to upload file. - :type param: dict. - :raises: ApiRequestError, FileNotUploadedError - """ - if param is None: - param = {} - param['body'] = self.GetChanges() - param['fileId'] = self.metadata.get('id') - try: - if self.dirty['content']: - param['media_body'] = self._BuildMediaBody() - metadata = self.auth.service.files().update(**param).execute() - except errors.HttpError as error: - raise ApiRequestError(error) - else: - self.uploaded = True - self.dirty['content'] = False - self.UpdateMetadata(metadata) - - @LoadAuth - @LoadMetadata - def _FilesPatch(self, param=None): - """Update metadata using Files.Patch(). - - :param param: additional parameter to upload file. - :type param: dict. - :raises: ApiRequestError, FileNotUploadedError - """ - if param is None: - param = {} - param['body'] = self.GetChanges() - param['fileId'] = self.metadata.get('id') - try: - metadata = self.auth.service.files().patch(**param).execute() - except errors.HttpError as error: - raise ApiRequestError(error) - else: - self.UpdateMetadata(metadata) - - def _BuildMediaBody(self): - """Build MediaIoBaseUpload to get prepared to upload content of the file. - - Sets mimeType as 'application/octet-stream' if not specified. - - :returns: MediaIoBaseUpload -- instance that will be used to upload content. - """ - if self.get('mimeType') is None: - self['mimeType'] = 'application/octet-stream' - - return MediaIoBaseUpload(self.content, self['mimeType']) - - @LoadAuth - def _DownloadFromUrl(self, url): - """Download file from url using provided credential. - - :param url: link of the file to download. - :type url: str. - :returns: str -- content of downloaded file in string. - :raises: ApiRequestError - """ - resp, content = self.auth.service._http.request(url) - if resp.status != 200: - raise ApiRequestError('Cannot download file: %s' % resp) - return content diff --git a/resources/lib/pydrive/settings.py b/resources/lib/pydrive/settings.py deleted file mode 100644 index 8940f5f..0000000 --- a/resources/lib/pydrive/settings.py +++ /dev/null @@ -1,192 +0,0 @@ -from yaml import load -from yaml import YAMLError -try: - from yaml import CLoader as Loader -except ImportError: - from yaml import Loader - -SETTINGS_FILE = 'settings.yaml' -SETTINGS_STRUCT = { - 'client_config_backend': { - 'type': str, - 'required': True, - 'default': 'file', - 'dependency': [ - { - 'value': 'file', - 'attribute': ['client_config_file'] - }, - { - 'value': 'settings', - 'attribute': ['client_config'] - } - ] - }, - 'save_credentials': { - 'type': bool, - 'required': True, - 'default': False, - 'dependency': [ - { - 'value': True, - 'attribute': ['save_credentials_backend'] - } - ] - }, - 'get_refresh_token': { - 'type': bool, - 'required': False, - 'default': False - }, - 'client_config_file': { - 'type': str, - 'required': False, - 'default': 'client_secrets.json' - }, - 'save_credentials_backend': { - 'type': str, - 'required': False, - 'dependency': [ - { - 'value': 'file', - 'attribute': ['save_credentials_file'] - } - ] - }, - 'client_config': { - 'type': dict, - 'required': False, - 'struct': { - 'client_id': { - 'type': str, - 'required': True, - 'default':'blank' - }, - 'client_secret': { - 'type': str, - 'required': True, - 'default':'blank' - }, - 'auth_uri': { - 'type': str, - 'required': True, - 'default': 'https://accounts.google.com/o/oauth2/auth' - }, - 'token_uri': { - 'type': str, - 'required': True, - 'default': 'https://accounts.google.com/o/oauth2/token' - }, - 'redirect_uri': { - 'type': str, - 'required': True, - 'default': 'urn:ietf:wg:oauth:2.0:oob' - }, - 'revoke_uri': { - 'type': str, - 'required': True, - 'default': None - } - } - }, - 'oauth_scope': { - 'type': list, - 'required': True, - 'struct': str, - 'default': ['https://www.googleapis.com/auth/drive'] - }, - 'save_credentials_file': { - 'type': str, - 'required': False, - } -} - - -class SettingsError(IOError): - """Error while loading/saving settings""" - - -class InvalidConfigError(IOError): - """Error trying to read client configuration.""" - - -def LoadSettingsFile(filename=SETTINGS_FILE): - """Loads settings file in yaml format given file name. - - :param filename: path for settings file. 'settings.yaml' by default. - :type filename: str. - :raises: SettingsError - """ - try: - stream = file(filename, 'r') - data = load(stream, Loader=Loader) - except (YAMLError, IOError) as e: - raise SettingsError(e) - return data - - -def ValidateSettings(data): - """Validates if current settings is valid. - - :param data: dictionary containing all settings. - :type data: dict. - :raises: InvalidConfigError - """ - _ValidateSettingsStruct(data, SETTINGS_STRUCT) - - -def _ValidateSettingsStruct(data, struct): - """Validates if provided data fits provided structure. - - :param data: dictionary containing settings. - :type data: dict. - :param struct: dictionary containing structure information of settings. - :type struct: dict. - :raises: InvalidConfigError - """ - # Validate required elements of the setting. - for key in struct: - if struct[key]['required']: - _ValidateSettingsElement(data, struct, key) - - -def _ValidateSettingsElement(data, struct, key): - """Validates if provided element of settings data fits provided structure. - - :param data: dictionary containing settings. - :type data: dict. - :param struct: dictionary containing structure information of settings. - :type struct: dict. - :param key: key of the settings element to validate. - :type key: str. - :raises: InvalidConfigError - """ - # Check if data exists. If not, check if default value exists. - value = data.get(key) - data_type = struct[key]['type'] - if value is None: - try: - default = struct[key]['default'] - except KeyError: - raise InvalidConfigError('Missing required setting %s' % key) - else: - data[key] = default - # If data exists, Check type of the data - elif type(value) is not data_type: - raise InvalidConfigError('Setting %s should be type %s' % (key, data_type)) - # If type of this data is dict, check if structure of the data is valid. - if data_type is dict: - _ValidateSettingsStruct(data[key], struct[key]['struct']) - # If type of this data is list, check if all values in the list is valid. - elif data_type is list: - for element in data[key]: - if type(element) is not struct[key]['struct']: - raise InvalidConfigError('Setting %s should be list of %s' % - (key, struct[key]['struct'])) - # Check dependency of this attribute. - dependencies = struct[key].get('dependency') - if dependencies: - for dependency in dependencies: - if value == dependency['value']: - for reqkey in dependency['attribute']: - _ValidateSettingsElement(data, struct, reqkey) diff --git a/resources/lib/pydrive/settings.yaml b/resources/lib/pydrive/settings.yaml deleted file mode 100644 index 1b64f1b..0000000 --- a/resources/lib/pydrive/settings.yaml +++ /dev/null @@ -1,7 +0,0 @@ -client_config_backend: 'settings' -client_config: - client_id: "blank" - client_secret: "blank" -get_refresh_token: True -oauth_scope: - - "https://www.googleapis.com/auth/drive.file" \ No newline at end of file