diff --git a/PyDrive2-1.10.1.tar.gz b/PyDrive2-1.10.1.tar.gz new file mode 100644 index 0000000..da4a644 --- /dev/null +++ b/PyDrive2-1.10.1.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac29d6da1eff3e5f14d3da0af10ff0bf3d12448427ee4e99d2f8336663455483 +size 48501 diff --git a/PyDrive2-1.8.3.tar.gz b/PyDrive2-1.8.3.tar.gz deleted file mode 100644 index b9fd240..0000000 --- a/PyDrive2-1.8.3.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d2217ba2feabcd13c97269b7e83ac975e33c1110bc2f4bd7d36b3e74349c011d -size 10998124 diff --git a/migrate-to-google-auth.patch b/migrate-to-google-auth.patch new file mode 100644 index 0000000..5d72acc --- /dev/null +++ b/migrate-to-google-auth.patch @@ -0,0 +1,1793 @@ +From 0236bf917d3103e95eafdf5c839d652e2a75c790 Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:12:34 +0800 +Subject: [PATCH 1/9] Update setup.py with new google-auth dependencies + +--- + setup.py | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/setup.py b/setup.py +index 06982d0..2b2d952 100644 +--- a/setup.py ++++ b/setup.py +@@ -33,6 +33,10 @@ + "google-api-python-client >= 1.12.5", + "six >= 1.13.0", + "oauth2client >= 4.0.0", ++ "google-auth >= 2.6.6", ++ "google-auth-httplib2 >= 0.1.0", ++ "google-auth-oauthlib >= 0.5.1", ++ "filelock >= 3.7.0", + "PyYAML >= 3.0", + "pyOpenSSL >= 19.1.0", + ], + +From 0b675594b81092191a8b52ab7606b73df7773ed0 Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:26:36 +0800 +Subject: [PATCH 2/9] Update threadsafe code following google recommendations + +--- + pydrive2/auth.py | 52 +++++++++++++++-------------------------------- + pydrive2/drive.py | 6 +++++- + pydrive2/files.py | 35 ++++++++++++++++++------------- + 3 files changed, 42 insertions(+), 51 deletions(-) + +diff --git a/pydrive2/auth.py b/pydrive2/auth.py +index a1b8b2f..08625e8 100644 +--- a/pydrive2/auth.py ++++ b/pydrive2/auth.py +@@ -21,6 +21,7 @@ + from .settings import SettingsError + from .settings import InvalidConfigError + ++from google_auth_httplib2 import AuthorizedHttp + + class AuthError(Exception): + """Base error for authentication/authorization errors.""" +@@ -61,23 +62,7 @@ def _decorated(self, *args, **kwargs): + if self.auth.service is None: + self.auth.Authorize() + +- # Ensure that a thread-safe HTTP object is provided. +- if ( +- kwargs is not None +- and "param" in kwargs +- and kwargs["param"] is not None +- and "http" in kwargs["param"] +- and kwargs["param"]["http"] is not None +- ): +- self.http = kwargs["param"]["http"] +- del kwargs["param"]["http"] +- +- else: +- # If HTTP object not specified, create or resuse an HTTP +- # object from the thread local storage. +- if not getattr(self.auth.thread_local, "http", None): +- self.auth.thread_local.http = self.auth.Get_Http_Object() +- self.http = self.auth.thread_local.http ++ + + return decoratee(self, *args, **kwargs) + +@@ -173,15 +158,14 @@ class GoogleAuth(ApiAttributeMixin): + def __init__(self, settings_file="settings.yaml", http_timeout=None): + """Create an instance of GoogleAuth. + +- This constructor just sets the path of settings file. +- It does not actually read the file. ++ This constructor parses just he yaml settings file. ++ All other settings are lazy + + :param settings_file: path of settings file. 'settings.yaml' by default. + :type settings_file: str. + """ + self.http_timeout = http_timeout + ApiAttributeMixin.__init__(self) +- self.thread_local = threading.local() + self.client_config = {} + try: + self.settings = LoadSettingsFile(settings_file) +@@ -196,6 +180,15 @@ def __init__(self, settings_file="settings.yaml", http_timeout=None): + # Only one (`file`) backend is supported now + self._default_storage = self._storages["file"] + ++ self._service = None ++ ++ # Lazy loading, read-only properties ++ @property ++ def service(self): ++ if not self._service: ++ self._service = build("drive", "v2", cache_discovery=False) ++ return self._service ++ + @property + def access_token_expired(self): + """Checks if access token doesn't exist or is expired. +@@ -597,12 +590,8 @@ def Refresh(self): + "No refresh_token found." + "Please set access_type of OAuth to offline." + ) +- if self.http is None: +- self.http = self._build_http() +- try: +- self.credentials.refresh(self.http) +- except AccessTokenRefreshError as error: +- raise RefreshError("Access token refresh failed: %s" % error) ++ ++ + + def GetAuthUrl(self): + """Creates authentication url where user visits to grant access. +@@ -663,19 +652,10 @@ def Authorize(self): + "No valid credentials provided to authorize" + ) + +- if self.http is None: +- self.http = self._build_http() +- self.http = self.credentials.authorize(self.http) +- self.service = build( +- "drive", "v2", http=self.http, cache_discovery=False +- ) +- + def Get_Http_Object(self): + """Create and authorize an httplib2.Http object. Necessary for + thread-safety. + :return: The http object to be used in each call. + :rtype: httplib2.Http + """ +- http = self._build_http() +- http = self.credentials.authorize(http) +- return http ++ return AuthorizedHttp(self.credentials, http=self._build_http()) +diff --git a/pydrive2/drive.py b/pydrive2/drive.py +index 8189ab1..b70462f 100644 +--- a/pydrive2/drive.py ++++ b/pydrive2/drive.py +@@ -46,4 +46,8 @@ def GetAbout(self): + + :returns: A dictionary of Google Drive information like user, usage, quota etc. + """ +- return self.auth.service.about().get().execute(http=self.http) ++ return ( ++ self.auth.service.about() ++ .get() ++ .execute(http=self.auth.Get_Http_Object()) ++ ) +diff --git a/pydrive2/files.py b/pydrive2/files.py +index 4716c7d..abf4724 100644 +--- a/pydrive2/files.py ++++ b/pydrive2/files.py +@@ -82,7 +82,7 @@ def _GetList(self): + self.metadata = ( + self.auth.service.files() + .list(**dict(self)) +- .execute(http=self.http) ++ .execute(http=self.auth.Get_Http_Object()) + ) + except errors.HttpError as error: + raise ApiRequestError(error) +@@ -440,7 +440,7 @@ def FetchMetadata(self, fields=None, fetch_all=False): + # Teamdrive support + supportsAllDrives=True, + ) +- .execute(http=self.http) ++ .execute(http=self.auth.Get_Http_Object()) + ) + except errors.HttpError as error: + raise ApiRequestError(error) +@@ -543,7 +543,7 @@ def InsertPermission(self, new_permission, param=None): + permission = ( + self.auth.service.permissions() + .insert(**param) +- .execute(http=self.http) ++ .execute(http=self.auth.Get_Http_Object()) + ) + except errors.HttpError as error: + raise ApiRequestError(error) +@@ -574,7 +574,7 @@ def GetPermissions(self): + # Teamdrive support + supportsAllDrives=True, + ) +- .execute(http=self.http) ++ .execute(http=self.auth.Get_Http_Object()) + ).get("items") + + if permissions: +@@ -594,13 +594,13 @@ def DeletePermission(self, permission_id): + return self._DeletePermission(permission_id) + + def _WrapRequest(self, request): +- """Replaces request.http with self.http. ++ """Replaces request.http with self.auth.Get_Http_Object(). + + Ensures thread safety. Similar to other places where we call + `.execute(http=self.http)` to pass a client from the thread local storage. + """ +- if self.http: +- request.http = self.http ++ if self.auth: ++ request.http = self.auth.Get_Http_Object() + return request + + @LoadAuth +@@ -624,7 +624,7 @@ def _FilesInsert(self, param=None): + metadata = ( + self.auth.service.files() + .insert(**param) +- .execute(http=self.http) ++ .execute(http=self.auth.Get_Http_Object()) + ) + except errors.HttpError as error: + raise ApiRequestError(error) +@@ -648,7 +648,9 @@ def _FilesUnTrash(self, param=None): + param["supportsAllDrives"] = True + + try: +- self.auth.service.files().untrash(**param).execute(http=self.http) ++ self.auth.service.files().untrash(**param).execute( ++ http=self.auth.Get_Http_Object() ++ ) + except errors.HttpError as error: + raise ApiRequestError(error) + else: +@@ -672,7 +674,9 @@ def _FilesTrash(self, param=None): + param["supportsAllDrives"] = True + + try: +- self.auth.service.files().trash(**param).execute(http=self.http) ++ self.auth.service.files().trash(**param).execute( ++ http=self.auth.Get_Http_Object() ++ ) + except errors.HttpError as error: + raise ApiRequestError(error) + else: +@@ -697,7 +701,9 @@ def _FilesDelete(self, param=None): + param["supportsAllDrives"] = True + + try: +- self.auth.service.files().delete(**param).execute(http=self.http) ++ self.auth.service.files().delete(**param).execute( ++ http=self.auth.Get_Http_Object() ++ ) + except errors.HttpError as error: + raise ApiRequestError(error) + else: +@@ -726,7 +732,7 @@ def _FilesUpdate(self, param=None): + metadata = ( + self.auth.service.files() + .update(**param) +- .execute(http=self.http) ++ .execute(http=self.auth.Get_Http_Object()) + ) + except errors.HttpError as error: + raise ApiRequestError(error) +@@ -756,7 +762,7 @@ def _FilesPatch(self, param=None): + metadata = ( + self.auth.service.files() + .patch(**param) +- .execute(http=self.http) ++ .execute(http=self.auth.Get_Http_Object()) + ) + except errors.HttpError as error: + raise ApiRequestError(error) +@@ -785,7 +791,8 @@ def _DownloadFromUrl(self, url): + :returns: str -- content of downloaded file in string. + :raises: ApiRequestError + """ +- resp, content = self.http.request(url) ++ http = self.auth.Get_Http_Object() ++ resp, content = http.request(url) + if resp.status != 200: + raise ApiRequestError(errors.HttpError(resp, content, uri=url)) + return content + +From 799b00562c376f0b3a4b4cf6d518bd7d25df13da Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:48:52 +0800 +Subject: [PATCH 3/9] Migration to google-auth library (#89) + +--- + pydrive2/auth.py | 489 +++++++++++++++++---------------------- + pydrive2/auth_helpers.py | 58 +++++ + pydrive2/drive.py | 2 - + pydrive2/files.py | 14 -- + pydrive2/settings.py | 5 + + 5 files changed, 275 insertions(+), 293 deletions(-) + create mode 100644 pydrive2/auth_helpers.py + +diff --git a/pydrive2/auth.py b/pydrive2/auth.py +index 08625e8..d70841e 100644 +--- a/pydrive2/auth.py ++++ b/pydrive2/auth.py +@@ -1,9 +1,11 @@ +-import webbrowser + import httplib2 ++import json + import oauth2client.clientsecrets as clientsecrets +-import threading ++import google.oauth2.credentials ++import google.oauth2.service_account + + from googleapiclient.discovery import build ++ + from functools import wraps + from oauth2client.service_account import ServiceAccountCredentials + from oauth2client.client import FlowExchangeError +@@ -21,7 +23,30 @@ + from .settings import SettingsError + from .settings import InvalidConfigError + ++from .auth_helpers import verify_client_config ++from oauthlib.oauth2.rfc6749.errors import OAuth2Error, MissingCodeError ++from google_auth_oauthlib.flow import InstalledAppFlow + from google_auth_httplib2 import AuthorizedHttp ++from warnings import warn ++ ++ ++DEFAULT_SETTINGS = { ++ "client_config_backend": "file", ++ "client_config_file": "client_secrets.json", ++ "save_credentials": False, ++ "oauth_scope": ["https://www.googleapis.com/auth/drive"], ++} ++ ++_CLIENT_AUTH_PROMPT_MESSAGE = "Please visit this URL:\n{url}\n" ++ ++ ++DEFAULT_SETTINGS = { ++ "client_config_backend": "file", ++ "client_config_file": "client_secrets.json", ++ "save_credentials": False, ++ "oauth_scope": ["https://www.googleapis.com/auth/drive"], ++} ++ + + class AuthError(Exception): + """Base error for authentication/authorization errors.""" +@@ -43,87 +68,6 @@ 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): +- # Initialize auth if needed. +- if self.auth is None: +- self.auth = GoogleAuth() +- # Re-create access token if it expired. +- if self.auth.access_token_expired: +- if getattr(self.auth, "auth_method", False) == "service": +- self.auth.ServiceAuth() +- else: +- self.auth.LocalWebserverAuth() +- +- # Initialise service if not built yet. +- if self.auth.service is None: +- self.auth.Authorize() +- +- +- +- return decoratee(self, *args, **kwargs) +- +- return _decorated +- +- +-def CheckServiceAuth(decoratee): +- """Decorator to authorize service account.""" +- +- @wraps(decoratee) +- def _decorated(self, *args, **kwargs): +- self.auth_method = "service" +- dirty = False +- save_credentials = self.settings.get("save_credentials") +- if self.credentials is None and save_credentials: +- self.LoadCredentials() +- if self.credentials is None: +- decoratee(self, *args, **kwargs) +- self.Authorize() +- dirty = True +- elif self.access_token_expired: +- self.Refresh() +- dirty = True +- self.credentials.set_store(self._default_storage) +- if dirty and save_credentials: +- self.SaveCredentials() +- +- 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) +- self.credentials.set_store(self._default_storage) +- if dirty and save_credentials: +- self.SaveCredentials() +- +- return _decorated +- +- + class GoogleAuth(ApiAttributeMixin): + """Wrapper class for oauth2client library in google-api-python-client. + +@@ -132,20 +76,6 @@ class GoogleAuth(ApiAttributeMixin): + 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", +- ] + SERVICE_CONFIGS_LIST = ["client_user_email"] + settings = ApiAttribute("settings") + client_config = ApiAttribute("client_config") +@@ -166,21 +96,23 @@ def __init__(self, settings_file="settings.yaml", http_timeout=None): + """ + self.http_timeout = http_timeout + ApiAttributeMixin.__init__(self) +- self.client_config = {} ++ + try: + self.settings = LoadSettingsFile(settings_file) + except SettingsError: +- self.settings = self.DEFAULT_SETTINGS ++ self.settings = DEFAULT_SETTINGS + else: +- if self.settings is None: +- self.settings = self.DEFAULT_SETTINGS +- else: +- ValidateSettings(self.settings) ++ # if no exceptions ++ ValidateSettings(self.settings) + self._storages = self._InitializeStoragesFromSettings() + # Only one (`file`) backend is supported now + self._default_storage = self._storages["file"] + + self._service = None ++ self._credentials = None ++ self._client_config = None ++ self._oauth_type = None ++ self._flow = None + + # Lazy loading, read-only properties + @property +@@ -189,17 +121,51 @@ def service(self): + self._service = build("drive", "v2", cache_discovery=False) + return self._service + ++ @property ++ def client_config(self): ++ if not self._client_config: ++ self.LoadClientConfig() ++ return self._client_config ++ ++ @property ++ def oauth_type(self): ++ if not self._oauth_type: ++ self.LoadClientConfig() ++ return self._oauth_type ++ ++ @property ++ def flow(self): ++ if not self._flow: ++ self.GetFlow() ++ return self._flow ++ ++ @property ++ def credentials(self): ++ if not self._credentials: ++ if self.oauth_type in ("web", "installed"): ++ self.LocalWebserverAuth() ++ ++ elif self.oauth_type == "service": ++ self.ServiceAuth() ++ else: ++ raise InvalidConfigError( ++ "Only web, installed, service oauth is supported" ++ ) ++ ++ return self._credentials ++ ++ # Other properties + @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: ++ if not self.credentials: + return True +- return self.credentials.access_token_expired + +- @CheckAuth ++ return not self.credentials.valid ++ + def LocalWebserverAuth( + self, host_name="localhost", port_numbers=None, launch_browser=True + ): +@@ -219,27 +185,53 @@ def LocalWebserverAuth( + :raises: AuthenticationRejected, AuthenticationError + """ + if port_numbers is None: +- port_numbers = [ +- 8080, +- 8090, +- ] # Mutable objects should not be default +- # values, as each call's changes are global. +- success = False +- port_number = 0 +- for port in port_numbers: +- port_number = port +- try: +- httpd = ClientRedirectServer( +- (host_name, port), ClientRedirectHandler ++ port_numbers = [8080, 8090] ++ ++ additional_config = {} ++ # offline token request needed to obtain refresh token ++ # make sure that consent is requested ++ if self.settings.get("get_refresh_token"): ++ additional_config["access_type"] = "offline" ++ additional_config["prompt"] = "select_account" ++ ++ try: ++ for port in port_numbers: ++ self._credentials = self.flow.run_local_server( ++ host=host_name, ++ port=port, ++ authorization_prompt_message=_CLIENT_AUTH_PROMPT_MESSAGE, ++ open_browser=launch_browser, ++ **additional_config, + ) +- except OSError: +- pass +- else: +- success = True ++ # if any port results in successful auth, we're done + break +- if success: +- oauth_callback = f"http://{host_name}:{port_number}/" +- else: ++ ++ except OSError as e: ++ # OSError: [WinError 10048] ... ++ # When WSGIServer.allow_reuse_address = False, ++ # raise OSError when binding to a used port ++ ++ # If some other error besides the socket address reuse error ++ if e.errno != 10048: ++ raise ++ ++ print("Port {} is in use. Trying a different port".format(port)) ++ ++ except MissingCodeError as e: ++ # if code is not found in the redirect uri's query parameters ++ print( ++ "Failed to find 'code' in the query parameters of the redirect." ++ ) ++ print("Please check that your redirect uri is correct.") ++ raise AuthenticationError("No code found in redirect") ++ ++ except OAuth2Error as e: ++ # catch oauth 2 errors ++ print("Authentication request was rejected") ++ raise AuthenticationRejected("User rejected authentication") ++ ++ # If we have tried all ports and could not find a port ++ if not self._credentials: + print( + "Failed to start a local web server. Please check your firewall" + ) +@@ -248,30 +240,7 @@ def LocalWebserverAuth( + ) + print("using configured ports. Default ports are 8080 and 8090.") + raise AuthenticationError() +- self.flow.redirect_uri = oauth_callback +- authorize_url = self.GetAuthUrl() +- if launch_browser: +- webbrowser.open(authorize_url, new=1, autoraise=True) +- print("Your browser has been opened to visit:") +- else: +- print("Open your browser to visit:") +- print() +- print(" " + authorize_url) +- print() +- httpd.handle_request() +- if "error" in httpd.query_params: +- print("Authentication request was rejected") +- raise AuthenticationRejected("User rejected authentication") +- if "code" in httpd.query_params: +- return httpd.query_params["code"] +- else: +- print( +- 'Failed to find "code" in the query parameters of the redirect.' +- ) +- print("Try command-line authentication") +- 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. +@@ -286,36 +255,31 @@ def CommandLineAuth(self): + print() + return input("Enter verification code: ").strip() + +- @CheckServiceAuth + def ServiceAuth(self): + """Authenticate and authorize using P12 private key, client id + and client email for a Service account. + :raises: AuthError, InvalidConfigError + """ +- if set(self.SERVICE_CONFIGS_LIST) - set(self.client_config): +- self.LoadServiceConfigSettings() +- scopes = scopes_to_string(self.settings["oauth_scope"]) + client_service_json = self.client_config.get("client_json_file_path") ++ + if client_service_json: +- self.credentials = ( +- ServiceAccountCredentials.from_json_keyfile_name( +- filename=client_service_json, scopes=scopes +- ) ++ additional_config = {} ++ additional_config["subject"] = self.client_config.get( ++ "client_user_email" + ) +- else: +- service_email = self.client_config["client_service_email"] +- file_path = self.client_config["client_pkcs12_file_path"] +- self.credentials = ServiceAccountCredentials.from_p12_keyfile( +- service_account_email=service_email, +- filename=file_path, +- scopes=scopes, ++ additional_config["scopes"] = self.settings["oauth_scope"] ++ ++ self._credentials = google.oauth2.service_account.Credentials.from_service_account_file( ++ client_service_json, **additional_config + ) + +- user_email = self.client_config.get("client_user_email") +- if user_email: +- self.credentials = self.credentials.create_delegated( +- sub=user_email ++ elif self.client_config.get("use_default"): ++ # if no service credential file in yaml settings ++ # default to checking env var `GOOGLE_APPLICATION_CREDENTIALS` ++ credentials, _ = google.auth.default( ++ scopes=self.settings["oauth_scope"] + ) ++ self._credentials = credentials + + def _InitializeStoragesFromSettings(self): + result = {"file": None} +@@ -463,144 +427,102 @@ def LoadClientConfigFile(self, client_config_file=None): + """ + 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 client_type not in ( +- clientsecrets.TYPE_WEB, +- clientsecrets.TYPE_INSTALLED, +- ): +- raise InvalidConfigError( +- "Unknown client_type of client config file" +- ) + +- # General settings. +- 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") +- +- # Service auth related fields. +- service_auth_config = ["client_email"] ++ with open(client_config_file, "r") as json_file: ++ client_config = json.load(json_file) ++ + try: +- for config in service_auth_config: +- self.client_config[config] = client_info[config] +- except KeyError: +- pass # The service auth fields are not present, handling code can go here. ++ # check the format of the loaded client config ++ client_type, checked_config = verify_client_config(client_config) ++ except ValueError as e: ++ raise InvalidConfigError("Invalid client secrets file: %s" % e) ++ ++ self._client_config = checked_config ++ self._oauth_type = client_type + + def LoadServiceConfigSettings(self): + """Loads client configuration from settings file. + :raises: InvalidConfigError + """ +- for file_format in ["json", "pkcs12"]: +- config = f"client_{file_format}_file_path" +- value = self.settings["service_config"].get(config) +- if value: +- self.client_config[config] = value +- break +- else: +- raise InvalidConfigError( +- "Either json or pkcs12 file path required " +- "for service authentication" ++ service_config = self.settings["service_config"] ++ ++ # see https://github.com/googleapis/google-auth-library-python/issues/288 ++ if "client_pkcs12_file_path" in service_config: ++ raise DeprecationWarning( ++ "PKCS#12 files are no longer supported in the new google.auth library. " ++ "Please download a new json service credential file from google cloud console. " ++ "For more info, visit https://github.com/googleapis/google-auth-library-python/issues/288" + ) + +- if file_format == "pkcs12": +- self.SERVICE_CONFIGS_LIST.append("client_service_email") +- +- for config in self.SERVICE_CONFIGS_LIST: +- try: +- self.client_config[config] = self.settings["service_config"][ +- config +- ] +- except KeyError: +- err = "Insufficient service config in settings" +- err += f"\n\nMissing: {config} key." +- raise InvalidConfigError(err) ++ self._client_config = service_config ++ self._oauth_type = "service" + + 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" +- ) ++ ++ try: ++ client_config = self.settings["client_config"] ++ except KeyError as e: ++ raise InvalidConfigError( ++ "Settings does not contain 'client_config'" ++ ) ++ ++ try: ++ _, checked_config = verify_client_config( ++ client_config, with_oauth_type=False ++ ) ++ except ValueError as e: ++ raise InvalidConfigError("Invalid client secrets file: %s" % e) ++ ++ # assumed to be Installed App Flow as the Local Server Auth is appropriate for this type of device ++ self._client_config = checked_config ++ self._oauth_type = "installed" + + 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"], +- "access_type": "online", +- } +- 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", "approval_prompt": "force"} ++ ++ additional_config = {} ++ scopes = self.settings.get("oauth_scope") ++ ++ if self.oauth_type in ("web", "installed"): ++ self._flow = InstalledAppFlow.from_client_config( ++ {self.oauth_type: self.client_config}, ++ scopes, ++ **additional_config, + ) + ++ if self.oauth_type == "service": ++ # In a service oauth2 flow, ++ # the oauth subject does not have to provide any consent via the client ++ pass ++ + 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 +- and self.auth_method != "service" +- ): +- raise RefreshError( +- "No refresh_token found." +- "Please set access_type of OAuth to offline." +- ) +- +- ++ raise DeprecationWarning( ++ "Refresh is now handled automatically within the new google.auth Credential objects. " ++ "There's no need to manually refresh your credentials now." ++ ) + + def GetAuthUrl(self): + """Creates authentication url where user visits to grant access. + + :returns: str -- Authentication url. + """ +- if self.flow is None: +- self.GetFlow() +- return self.flow.step1_get_authorize_url() ++ if self.oauth_type == "service": ++ raise AuthenticationError( ++ "Authentication is not required for service client type." ++ ) ++ ++ return self.flow.authorization_url() + + def Auth(self, code): + """Authenticate, authorize, and build service. +@@ -619,13 +541,26 @@ def Authenticate(self, code): + :type code: str. + :raises: AuthenticationError + """ +- if self.flow is None: +- self.GetFlow() ++ if self.oauth_type == "service": ++ raise AuthenticationError( ++ "Authentication is not required for service client type." ++ ) ++ + try: +- self.credentials = self.flow.step2_exchange(code) +- except FlowExchangeError as e: +- raise AuthenticationError("OAuth2 code exchange failed: %s" % e) +- print("Authentication successful.") ++ self.flow.fetch_token(code=code) ++ ++ except MissingCodeError as e: ++ # if code is not found in the redirect uri's query parameters ++ print( ++ "Failed to find 'code' in the query parameters of the redirect." ++ ) ++ print("Please check that your redirect uri is correct.") ++ raise AuthenticationError("No code found in redirect") ++ ++ except OAuth2Error as e: ++ # catch oauth 2 errors ++ print("Authentication request was rejected") ++ raise AuthenticationRejected("User rejected authentication") + + def _build_http(self): + http = httplib2.Http(timeout=self.http_timeout) +diff --git a/pydrive2/auth_helpers.py b/pydrive2/auth_helpers.py +new file mode 100644 +index 0000000..04e5f67 +--- /dev/null ++++ b/pydrive2/auth_helpers.py +@@ -0,0 +1,58 @@ ++_OLD_CLIENT_CONFIG_KEYS = frozenset( ++ ( ++ "client_id", ++ "client_secret", ++ "auth_uri", ++ "token_uri", ++ "revoke_uri", ++ "redirect_uri", ++ ) ++) ++ ++_CLIENT_CONFIG_KEYS = frozenset( ++ ( ++ "client_id", ++ "client_secret", ++ "auth_uri", ++ "token_uri", ++ "redirect_uris", ++ ) ++) ++ ++ ++def verify_client_config(client_config, with_oauth_type=True): ++ """Verifies that format of the client config ++ loaded from a Google-format client secrets file. ++ """ ++ ++ oauth_type = None ++ config = client_config ++ ++ if with_oauth_type: ++ if "web" in client_config: ++ oauth_type = "web" ++ config = config["web"] ++ ++ elif "installed" in client_config: ++ oauth_type = "installed" ++ config = config["installed"] ++ else: ++ raise ValueError( ++ "Client secrets must be for a web or installed app" ++ ) ++ ++ # This is the older format of client config ++ if _OLD_CLIENT_CONFIG_KEYS.issubset(config.keys()): ++ config["redirect_uris"] = [config["redirect_uri"]] ++ ++ # by default, the redirect uri is the first in the list ++ if "redirect_uri" not in config: ++ config["redirect_uri"] = config["redirect_uris"][0] ++ ++ if "revoke_uri" not in config: ++ config["revoke_uri"] = "https://oauth2.googleapis.com/revoke" ++ ++ if not _CLIENT_CONFIG_KEYS.issubset(config.keys()): ++ raise ValueError("Client secrets is not in the correct format.") ++ ++ return oauth_type, config +diff --git a/pydrive2/drive.py b/pydrive2/drive.py +index b70462f..83ad391 100644 +--- a/pydrive2/drive.py ++++ b/pydrive2/drive.py +@@ -1,7 +1,6 @@ + from .apiattr import ApiAttributeMixin + from .files import GoogleDriveFile + from .files import GoogleDriveFileList +-from .auth import LoadAuth + + + class GoogleDrive(ApiAttributeMixin): +@@ -40,7 +39,6 @@ def ListFile(self, param=None): + """ + return GoogleDriveFileList(auth=self.auth, param=param) + +- @LoadAuth + def GetAbout(self): + """Return information about the Google Drive of the auth instance. + +diff --git a/pydrive2/files.py b/pydrive2/files.py +index abf4724..ab26c72 100644 +--- a/pydrive2/files.py ++++ b/pydrive2/files.py +@@ -12,7 +12,6 @@ + from .apiattr import ApiAttributeMixin + from .apiattr import ApiResource + from .apiattr import ApiResourceList +-from .auth import LoadAuth + + BLOCK_SIZE = 1024 + # Usage: MIME_TYPE_TO_BOM['']['']. +@@ -68,7 +67,6 @@ def __init__(self, auth=None, param=None): + """Create an instance of GoogleDriveFileList.""" + super().__init__(auth=auth, metadata=param) + +- @LoadAuth + def _GetList(self): + """Overwritten method which actually makes API call to list files. + +@@ -287,7 +285,6 @@ def GetContentString( + self.FetchContent(mimetype, remove_bom) + return self.content.getvalue().decode(encoding) + +- @LoadAuth + def GetContentFile( + self, + filename, +@@ -355,7 +352,6 @@ def download(fd, request): + if bom: + self._RemovePrefix(fd, bom) + +- @LoadAuth + def GetContentIOBuffer( + self, + mimetype=None, +@@ -412,7 +408,6 @@ def GetContentIOBuffer( + chunksize=chunksize, + ) + +- @LoadAuth + def FetchMetadata(self, fields=None, fetch_all=False): + """Download file's metadata from id using Files.get(). + +@@ -552,7 +547,6 @@ def InsertPermission(self, new_permission, param=None): + + return permission + +- @LoadAuth + def GetPermissions(self): + """Get file's or shared drive's permissions. + +@@ -603,7 +597,6 @@ def _WrapRequest(self, request): + request.http = self.auth.Get_Http_Object() + return request + +- @LoadAuth + def _FilesInsert(self, param=None): + """Upload a new file using Files.insert(). + +@@ -633,7 +626,6 @@ def _FilesInsert(self, param=None): + self.dirty["content"] = False + self.UpdateMetadata(metadata) + +- @LoadAuth + def _FilesUnTrash(self, param=None): + """Un-delete (Trash) a file using Files.UnTrash(). + :param param: additional parameter to file. +@@ -658,7 +650,6 @@ def _FilesUnTrash(self, param=None): + self.metadata["labels"]["trashed"] = False + return True + +- @LoadAuth + def _FilesTrash(self, param=None): + """Soft-delete (Trash) a file using Files.Trash(). + +@@ -684,7 +675,6 @@ def _FilesTrash(self, param=None): + self.metadata["labels"]["trashed"] = True + return True + +- @LoadAuth + def _FilesDelete(self, param=None): + """Delete a file using Files.Delete() + (WARNING: deleting permanently deletes the file!) +@@ -709,7 +699,6 @@ def _FilesDelete(self, param=None): + else: + return True + +- @LoadAuth + @LoadMetadata + def _FilesUpdate(self, param=None): + """Update metadata and/or content using Files.Update(). +@@ -741,7 +730,6 @@ def _FilesUpdate(self, param=None): + self.dirty["content"] = False + self.UpdateMetadata(metadata) + +- @LoadAuth + @LoadMetadata + def _FilesPatch(self, param=None): + """Update metadata using Files.Patch(). +@@ -782,7 +770,6 @@ def _BuildMediaBody(self): + self.content, self["mimeType"], resumable=True + ) + +- @LoadAuth + def _DownloadFromUrl(self, url): + """Download file from url using provided credential. + +@@ -797,7 +784,6 @@ def _DownloadFromUrl(self, url): + raise ApiRequestError(errors.HttpError(resp, content, uri=url)) + return content + +- @LoadAuth + def _DeletePermission(self, permission_id): + """Deletes the permission remotely, and from the file object itself. + +diff --git a/pydrive2/settings.py b/pydrive2/settings.py +index f5e84ab..e3a46a6 100644 +--- a/pydrive2/settings.py ++++ b/pydrive2/settings.py +@@ -75,6 +75,7 @@ + "client_service_email": {"type": str, "required": False}, + "client_pkcs12_file_path": {"type": str, "required": False}, + "client_json_file_path": {"type": str, "required": False}, ++ "use_default": {"type": bool, "required": False}, + }, + }, + "oauth_scope": { +@@ -107,6 +108,10 @@ def LoadSettingsFile(filename=SETTINGS_FILE): + data = load(stream, Loader=Loader) + except (YAMLError, OSError) as e: + raise SettingsError(e) ++ ++ if not data: ++ raise SettingsError("{} is an empty settings file.".format(filename)) ++ + return data + + + +From 2a0a76709a33a6098bc9095896d3aaabc7b63e10 Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:57:00 +0800 +Subject: [PATCH 4/9] Implement lockfile storage (#89) + +--- + pydrive2/auth.py | 106 ++++++++++++++++++++++++++------------- + pydrive2/storage.py | 117 ++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 188 insertions(+), 35 deletions(-) + create mode 100644 pydrive2/storage.py + +diff --git a/pydrive2/auth.py b/pydrive2/auth.py +index d70841e..32c1c18 100644 +--- a/pydrive2/auth.py ++++ b/pydrive2/auth.py +@@ -104,10 +104,8 @@ def __init__(self, settings_file="settings.yaml", http_timeout=None): + else: + # if no exceptions + ValidateSettings(self.settings) +- self._storages = self._InitializeStoragesFromSettings() +- # Only one (`file`) backend is supported now +- self._default_storage = self._storages["file"] + ++ self._storage = None + self._service = None + self._credentials = None + self._client_config = None +@@ -139,10 +137,28 @@ def flow(self): + self.GetFlow() + return self._flow + ++ @property ++ def storage(self): ++ if not self.settings.get("save_credentials"): ++ return None ++ ++ if not self._storage: ++ self._InitializeStoragesFromSettings() ++ return self._storage ++ + @property + def credentials(self): + if not self._credentials: + if self.oauth_type in ("web", "installed"): ++ # try to load from backend if available ++ # credentials would auto-refresh if expired ++ if self.storage: ++ try: ++ self.LoadCredentials() ++ return self._credentials ++ except FileNotFoundError: ++ pass ++ + self.LocalWebserverAuth() + + elif self.oauth_type == "service": +@@ -282,21 +298,14 @@ def ServiceAuth(self): + self._credentials = credentials + + def _InitializeStoragesFromSettings(self): +- result = {"file": None} +- backend = self.settings.get("save_credentials_backend") +- save_credentials = self.settings.get("save_credentials") +- if backend == "file": +- credentials_file = self.settings.get("save_credentials_file") +- if credentials_file is None: ++ if self.settings.get("save_credentials"): ++ backend = self.settings.get("save_credentials_backend") ++ if backend != "file": + raise InvalidConfigError( +- "Please specify credentials file to read" ++ "Unknown save_credentials_backend: %s" % backend + ) +- result[backend] = Storage(credentials_file) +- elif save_credentials: +- raise InvalidConfigError( +- "Unknown save_credentials_backend: %s" % backend +- ) +- return result ++ ++ self._storage = FileBackend() + + def LoadCredentials(self, backend=None): + """Loads credentials or create empty credentials if it doesn't exist. +@@ -324,25 +333,55 @@ def LoadCredentialsFile(self, credentials_file=None): + :raises: InvalidConfigError, InvalidCredentialsError + """ + if credentials_file is None: +- self._default_storage = self._storages["file"] +- if self._default_storage is None: ++ credentials_file = self.settings.get("save_credentials_file") ++ if credentials_file is None: + raise InvalidConfigError( +- "Backend `file` is not configured, specify " +- "credentials file to read in the settings " +- "file or pass an explicit value" ++ "Please specify credentials file to read" + ) +- else: +- self._default_storage = Storage(credentials_file) + + try: +- self.credentials = self._default_storage.get() ++ auth_info = self.storage.read_credentials(credentials_file) ++ except FileNotFoundError: ++ # if credential found was not found, raise the error for handling ++ raise + except OSError: ++ # catch other errors + raise InvalidCredentialsError( + "Credentials file cannot be symbolic link" + ) + +- if self.credentials: +- self.credentials.set_store(self._default_storage) ++ try: ++ self._credentials = google.oauth2.credentials.Credentials.from_authorized_user_info( ++ auth_info, scopes=auth_info["scopes"] ++ ) ++ except ValueError: ++ # if saved credentials lack a refresh token ++ # handled for backwards compatibility ++ warn( ++ "Loading authorized user credentials without a refresh token is " ++ "not officially supported by google auth library. We recommend that " ++ "you only store refreshable credentials moving forward." ++ ) ++ ++ self._credentials = google.oauth2.credentials.Credentials( ++ token=auth_info.get("token"), ++ token_uri="https://oauth2.googleapis.com/token", # always overrides ++ scopes=auth_info.get("scopes"), ++ client_id=auth_info.get("client_id"), ++ client_secret=auth_info.get("client_secret"), ++ ) ++ ++ # in-case reauth / consent required ++ # create a flow object so that reauth flow can be triggered ++ additional_config = {} ++ if self.credentials.refresh_token: ++ additional_config["access_type"] = "offline" ++ ++ self._flow = InstalledAppFlow.from_client_config( ++ {self.oauth_type: self.client_config}, ++ self.credentials.scopes, ++ **additional_config, ++ ) + + def SaveCredentials(self, backend=None): + """Saves credentials according to specified backend. +@@ -370,22 +409,19 @@ def SaveCredentialsFile(self, credentials_file=None): + :type credentials_file: str. + :raises: InvalidConfigError, InvalidCredentialsError + """ +- if self.credentials is None: ++ if not self.credentials: + raise InvalidCredentialsError("No credentials to save") + + if credentials_file is None: +- storage = self._storages["file"] +- if storage is None: ++ credentials_file = self.settings.get("save_credentials_file") ++ if credentials_file is None: + raise InvalidConfigError( +- "Backend `file` is not configured, specify " +- "credentials file to read in the settings " +- "file or pass an explicit value" ++ "Please specify credentials file to read" + ) +- else: +- storage = Storage(credentials_file) + + try: +- storage.put(self.credentials) ++ self.storage.store_credentials(self.credentials, credentials_file) ++ + except OSError: + raise InvalidCredentialsError( + "Credentials file cannot be symbolic link" +diff --git a/pydrive2/storage.py b/pydrive2/storage.py +new file mode 100644 +index 0000000..ad9f772 +--- /dev/null ++++ b/pydrive2/storage.py +@@ -0,0 +1,117 @@ ++import os ++import json ++import warnings ++from filelock import FileLock ++ ++ ++_SYM_LINK_MESSAGE = "File: {0}: Is a symbolic link." ++_IS_DIR_MESSAGE = "{0}: Is a directory" ++_MISSING_FILE_MESSAGE = "Cannot access {0}: No such file or directory" ++ ++ ++def validate_file(filename): ++ if os.path.islink(filename): ++ raise IOError(_SYM_LINK_MESSAGE.format(filename)) ++ elif os.path.isdir(filename): ++ raise IOError(_IS_DIR_MESSAGE.format(filename)) ++ elif not os.path.isfile(filename): ++ warnings.warn(_MISSING_FILE_MESSAGE.format(filename)) ++ ++ ++class CredentialBackend(object): ++ """Adapter that provides a consistent interface to read and write credential files""" ++ ++ def _read_credentials(self, rpath): ++ """Specific implementation of how the storage object should retrieve a file.""" ++ return NotImplementedError ++ ++ def _store_credentials(self, credential, rpath): ++ """Specific implementation of how the storage object should write a file""" ++ return NotImplementedError ++ ++ def _delete_credentials(self, rpath): ++ """Specific implementation of how the storage object should delete a file.""" ++ return NotImplementedError ++ ++ def read_credentials(self, rpath): ++ """Reads a credential config file and returns the config as a dictionary ++ :param fname: host name of the local web server. ++ :type host_name: str.` ++ :return: A credential file ++ """ ++ return self._read_credentials(rpath) ++ ++ def store_credentials(self, credential, rpath): ++ """Write a credential to ++ The Storage lock must be held when this is called. ++ Args: ++ credentials: Credentials, the credentials to store. ++ """ ++ self._store_credentials(credential, rpath) ++ ++ def delete_credentials(self, rpath): ++ """Delete credential. ++ Frees any resources associated with storing the credential. ++ The Storage lock must *not* be held when this is called. ++ ++ Returns: ++ None ++ """ ++ self._delete_credentials(rpath) ++ ++ ++class FileBackend(CredentialBackend): ++ # https://stackoverflow.com/questions/37084682/is-oauth-thread-safe ++ """Read and write credentials to a file backend with File Locking""" ++ ++ def __init__(self): ++ self._locks = {} ++ ++ def createLock(self, rpath): ++ self._locks[rpath] = FileLock("{}.lock".format(rpath)) ++ ++ def getLock(self, rpath): ++ if rpath not in self._locks: ++ self.createLock(rpath) ++ return self._locks[rpath] ++ ++ def _create_file_if_needed(self, rpath): ++ """Create an empty file if necessary. ++ This method will not initialize the file. Instead it implements a ++ simple version of "touch" to ensure the file has been created. ++ """ ++ if not os.path.exists(rpath): ++ old_umask = os.umask(0o177) ++ try: ++ open(rpath, "a+b").close() ++ finally: ++ os.umask(old_umask) ++ ++ def _read_credentials(self, rpath): ++ """Reads a local json file and parses the information into a info dictionary. ++ Returns: ++ Raises: ++ """ ++ with self.getLock(rpath): ++ with open(rpath, "r") as json_file: ++ return json.load(json_file) ++ ++ def _store_credentials(self, credentials, rpath): ++ """Writes current credentials to a local json file. ++ Args: ++ Raises: ++ """ ++ with self.getLock(rpath): ++ self._create_file_if_needed(rpath) ++ validate_file(rpath) ++ ++ with open(rpath, "w") as json_file: ++ json_file.write(credentials.to_json()) ++ ++ def _delete_credentials(self, rpath): ++ """Delete Credentials file. ++ Args: ++ credentials: Credentials, the credentials to store. ++ """ ++ with self.getLock(rpath): ++ os.unlink(rpath) + +From 9230969d92714251d9bed2dd4d5cd80a96e7d70d Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:51:00 +0800 +Subject: [PATCH 5/9] Cleanup of oauth tests & remove service config read/write + checks + +--- + pydrive2/test/settings/default.yaml | 2 +- + pydrive2/test/settings/test_oauth_test_07.yaml | 2 +- + pydrive2/test/settings/test_oauth_test_08.yaml | 2 +- + pydrive2/test/settings/test_oauth_test_09.yaml | 2 +- + pydrive2/test/test_oauth.py | 13 +++++++------ + 5 files changed, 11 insertions(+), 10 deletions(-) + +diff --git a/pydrive2/test/settings/default.yaml b/pydrive2/test/settings/default.yaml +index ef6e654..b31b91f 100644 +--- a/pydrive2/test/settings/default.yaml ++++ b/pydrive2/test/settings/default.yaml +@@ -1,6 +1,6 @@ + client_config_backend: service + service_config: +- client_json_file_path: /tmp/pydrive2/credentials.json ++ client_json_file_path: tmp/pydrive2/credentials.json + + save_credentials: True + save_credentials_backend: file +diff --git a/pydrive2/test/settings/test_oauth_test_07.yaml b/pydrive2/test/settings/test_oauth_test_07.yaml +index a806515..673a6ec 100644 +--- a/pydrive2/test/settings/test_oauth_test_07.yaml ++++ b/pydrive2/test/settings/test_oauth_test_07.yaml +@@ -1,6 +1,6 @@ + client_config_backend: service + service_config: +- client_json_file_path: /tmp/pydrive2/credentials.json ++ client_json_file_path: tmp/pydrive2/credentials.json + + save_credentials: True + save_credentials_backend: file +diff --git a/pydrive2/test/settings/test_oauth_test_08.yaml b/pydrive2/test/settings/test_oauth_test_08.yaml +index 1fcbc05..f927e53 100644 +--- a/pydrive2/test/settings/test_oauth_test_08.yaml ++++ b/pydrive2/test/settings/test_oauth_test_08.yaml +@@ -1,6 +1,6 @@ + client_config_backend: service + service_config: +- client_json_file_path: /tmp/pydrive2/credentials.json ++ client_json_file_path: tmp/pydrive2/credentials.json + + save_credentials: False + +diff --git a/pydrive2/test/settings/test_oauth_test_09.yaml b/pydrive2/test/settings/test_oauth_test_09.yaml +index 09eca82..b131ca1 100644 +--- a/pydrive2/test/settings/test_oauth_test_09.yaml ++++ b/pydrive2/test/settings/test_oauth_test_09.yaml +@@ -1,6 +1,6 @@ + client_config_backend: service + service_config: +- client_json_file_path: /tmp/pydrive2/credentials.json ++ client_json_file_path: tmp/pydrive2/credentials.json + + save_credentials: True + save_credentials_backend: file +diff --git a/pydrive2/test/test_oauth.py b/pydrive2/test/test_oauth.py +index d876661..b57b2bb 100644 +--- a/pydrive2/test/test_oauth.py ++++ b/pydrive2/test/test_oauth.py +@@ -22,6 +22,7 @@ def test_01_LocalWebserverAuthWithClientConfigFromFile(): + # Test if authentication works with config read from file + ga = GoogleAuth(settings_file_path("test_oauth_test_01.yaml")) + ga.LocalWebserverAuth() ++ assert ga.credentials + assert not ga.access_token_expired + # Test if correct credentials file is created + CheckCredentialsFile("credentials/1.dat") +@@ -35,6 +36,7 @@ def test_02_LocalWebserverAuthWithClientConfigFromSettings(): + # Test if authentication works with config read from settings + ga = GoogleAuth(settings_file_path("test_oauth_test_02.yaml")) + ga.LocalWebserverAuth() ++ assert ga.credentials + assert not ga.access_token_expired + # Test if correct credentials file is created + CheckCredentialsFile("credentials/2.dat") +@@ -48,6 +50,7 @@ def test_03_LocalWebServerAuthWithNoCredentialsSaving(): + ga = GoogleAuth(settings_file_path("test_oauth_test_03.yaml")) + assert not ga.settings["save_credentials"] + ga.LocalWebserverAuth() ++ assert ga.credentials + assert not ga.access_token_expired + time.sleep(1) + +@@ -59,6 +62,7 @@ def test_04_CommandLineAuthWithClientConfigFromFile(): + # Test if authentication works with config read from file + ga = GoogleAuth(settings_file_path("test_oauth_test_04.yaml")) + ga.CommandLineAuth() ++ assert ga.credentials + assert not ga.access_token_expired + # Test if correct credentials file is created + CheckCredentialsFile("credentials/4.dat") +@@ -70,6 +74,7 @@ def test_05_ConfigFromSettingsWithoutOauthScope(): + # Test if authentication works without oauth_scope + ga = GoogleAuth(settings_file_path("test_oauth_test_05.yaml")) + ga.LocalWebserverAuth() ++ assert ga.credentials + assert not ga.access_token_expired + time.sleep(1) + +@@ -79,6 +84,7 @@ def test_06_ServiceAuthFromSavedCredentialsP12File(): + setup_credentials("credentials/6.dat") + ga = GoogleAuth(settings_file_path("test_oauth_test_06.yaml")) + ga.ServiceAuth() ++ assert ga.credentials + assert not ga.access_token_expired + time.sleep(1) + +@@ -91,12 +97,9 @@ def test_07_ServiceAuthFromSavedCredentialsJsonFile(): + delete_file(credentials_file) + assert not os.path.exists(credentials_file) + ga.ServiceAuth() +- assert os.path.exists(credentials_file) +- # Secondary auth should be made only using the previously saved +- # login info + ga = GoogleAuth(settings_file_path("test_oauth_test_07.yaml")) + ga.ServiceAuth() +- assert not ga.access_token_expired ++ assert ga.credentials + time.sleep(1) + + +@@ -120,8 +123,6 @@ def test_09_SaveLoadCredentialsUsesDefaultStorage(mocker): + assert not os.path.exists(credentials_file) + spy = mocker.spy(Storage, "__init__") + ga.ServiceAuth() +- ga.LoadCredentials() +- ga.SaveCredentials() + assert spy.call_count == 0 + + + +From 2a688e28b15cb310ea39faac4375029c62a574ef Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:51:32 +0800 +Subject: [PATCH 6/9] Add oauth test 10: google.auth.default auth + +--- + pydrive2/test/settings/test_oauth_test_10.yaml | 5 +++++ + pydrive2/test/test_oauth.py | 10 ++++++++++ + 2 files changed, 15 insertions(+) + create mode 100644 pydrive2/test/settings/test_oauth_test_10.yaml + +diff --git a/pydrive2/test/settings/test_oauth_test_10.yaml b/pydrive2/test/settings/test_oauth_test_10.yaml +new file mode 100644 +index 0000000..2af85fc +--- /dev/null ++++ b/pydrive2/test/settings/test_oauth_test_10.yaml +@@ -0,0 +1,5 @@ ++client_config_backend: service ++service_config: ++ use_default: True ++oauth_scope: ++ - https://www.googleapis.com/auth/drive +diff --git a/pydrive2/test/test_oauth.py b/pydrive2/test/test_oauth.py +index b57b2bb..40963c1 100644 +--- a/pydrive2/test/test_oauth.py ++++ b/pydrive2/test/test_oauth.py +@@ -126,6 +126,16 @@ def test_09_SaveLoadCredentialsUsesDefaultStorage(mocker): + assert spy.call_count == 0 + + ++def test_10_ServiceAuthFromEnvironmentDefault(): ++ # Test fix for https://github.com/iterative/PyDrive2/issues/163 ++ # Make sure that Load and Save credentials by default reuse the ++ # same Storage (since it defined lock which make it TS) ++ ga = GoogleAuth(settings_file_path("test_oauth_test_10.yaml")) ++ ga.ServiceAuth() ++ assert ga.credentials ++ time.sleep(1) ++ ++ + def CheckCredentialsFile(credentials, no_file=False): + ga = GoogleAuth(settings_file_path("test_oauth_default.yaml")) + ga.LoadCredentialsFile(credentials) + +From a3bc162c59a5044df8983e953331a084a0bc9ffa Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:52:54 +0800 +Subject: [PATCH 7/9] Deprecate command line auth (#173) + +--- + pydrive2/auth.py | 18 +++++++++++------- + 1 file changed, 11 insertions(+), 7 deletions(-) + +diff --git a/pydrive2/auth.py b/pydrive2/auth.py +index 32c1c18..646268c 100644 +--- a/pydrive2/auth.py ++++ b/pydrive2/auth.py +@@ -263,13 +263,17 @@ def CommandLineAuth(self): + + :returns: str -- code returned from commandline. + """ +- self.flow.redirect_uri = OOB_CALLBACK_URN +- authorize_url = self.GetAuthUrl() +- print("Go to the following link in your browser:") +- print() +- print(" " + authorize_url) +- print() +- return input("Enter verification code: ").strip() ++ ++ warn( ++ ( ++ "The command line auth has been deprecated. " ++ "The recommended alternative is to use local webserver auth with a loopback address." ++ ), ++ DeprecationWarning, ++ ) ++ ++ self.LocalWebserverAuth(host_name="127.0.0.1") ++ + + def ServiceAuth(self): + """Authenticate and authorize using P12 private key, client id + +From c9a0f90759df11f3c544c57f69edfc8ce936816e Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sun, 29 May 2022 00:58:10 +0800 +Subject: [PATCH 8/9] Remove dependencies on oauth2client + +--- + pydrive2/auth.py | 18 ++++-------------- + setup.py | 1 - + 2 files changed, 4 insertions(+), 15 deletions(-) + +diff --git a/pydrive2/auth.py b/pydrive2/auth.py +index 646268c..cf59a29 100644 +--- a/pydrive2/auth.py ++++ b/pydrive2/auth.py +@@ -1,21 +1,10 @@ + import httplib2 + import json +-import oauth2client.clientsecrets as clientsecrets + import google.oauth2.credentials + import google.oauth2.service_account + + from googleapiclient.discovery import build +- +-from functools import wraps +-from oauth2client.service_account import ServiceAccountCredentials +-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 pydrive2.storage import FileBackend + from .apiattr import ApiAttribute + from .apiattr import ApiAttributeMixin + from .settings import LoadSettingsFile +@@ -257,6 +246,9 @@ def LocalWebserverAuth( + print("using configured ports. Default ports are 8080 and 8090.") + raise AuthenticationError() + ++ if self.storage: ++ self.SaveCredentials() ++ + def CommandLineAuth(self): + """Authenticate and authorize from user by printing authentication url + retrieving authentication code from command-line. +@@ -274,7 +266,6 @@ def CommandLineAuth(self): + + self.LocalWebserverAuth(host_name="127.0.0.1") + +- + def ServiceAuth(self): + """Authenticate and authorize using P12 private key, client id + and client email for a Service account. +@@ -526,7 +517,6 @@ def GetFlow(self): + + :raises: InvalidConfigError + """ +- + additional_config = {} + scopes = self.settings.get("oauth_scope") + +diff --git a/setup.py b/setup.py +index 2b2d952..74e6d24 100644 +--- a/setup.py ++++ b/setup.py +@@ -32,7 +32,6 @@ + install_requires=[ + "google-api-python-client >= 1.12.5", + "six >= 1.13.0", +- "oauth2client >= 4.0.0", + "google-auth >= 2.6.6", + "google-auth-httplib2 >= 0.1.0", + "google-auth-oauthlib >= 0.5.1", + +From 012f9edfc8f7da06ff74542fbf8938b772e2483f Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Wed, 1 Jun 2022 23:09:53 +0800 +Subject: [PATCH 9/9] Changed mocker.spy to track calls to FileBackend's + __init__ + +--- + pydrive2/test/test_oauth.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/pydrive2/test/test_oauth.py b/pydrive2/test/test_oauth.py +index 40963c1..7e6df5d 100644 +--- a/pydrive2/test/test_oauth.py ++++ b/pydrive2/test/test_oauth.py +@@ -8,7 +8,7 @@ + delete_file, + settings_file_path, + ) +-from oauth2client.file import Storage ++from ..storage import FileBackend + + + def setup_module(module): +@@ -121,7 +121,7 @@ def test_09_SaveLoadCredentialsUsesDefaultStorage(mocker): + # Delete old credentials file + delete_file(credentials_file) + assert not os.path.exists(credentials_file) +- spy = mocker.spy(Storage, "__init__") ++ spy = mocker.spy(FileBackend, "__init__") + ga.ServiceAuth() + assert spy.call_count == 0 + diff --git a/modernize.patch b/modernize.patch new file mode 100644 index 0000000..35c83a2 --- /dev/null +++ b/modernize.patch @@ -0,0 +1,353 @@ +From 2e43e4561d965ce78dc158a02fbdb75ba6c38105 Mon Sep 17 00:00:00 2001 +From: Ruslan Kuprieiev +Date: Wed, 27 Apr 2022 15:31:53 +0300 +Subject: [PATCH] pydrive2: pyupgrade --py37-plus + +--- + pydrive2/apiattr.py | 15 ++++++--------- + pydrive2/auth.py | 14 ++++++-------- + pydrive2/drive.py | 2 +- + pydrive2/files.py | 10 ++++------ + pydrive2/settings.py | 8 +++----- + pydrive2/test/test_drive.py | 1 - + pydrive2/test/test_file.py | 18 ++++++++---------- + pydrive2/test/test_filelist.py | 7 +++---- + setup.py | 4 +--- + 9 files changed, 32 insertions(+), 47 deletions(-) + +diff --git a/pydrive2/apiattr.py b/pydrive2/apiattr.py +index 46845b1..6b66b42 100644 +--- a/pydrive2/apiattr.py ++++ b/pydrive2/apiattr.py +@@ -1,7 +1,4 @@ +-from six import Iterator, iteritems +- +- +-class ApiAttribute(object): ++class ApiAttribute: + """A data descriptor that sets and returns values.""" + + def __init__(self, name): +@@ -32,7 +29,7 @@ def __del__(self, obj=None): + del obj.dirty[self.name] + + +-class ApiAttributeMixin(object): ++class ApiAttributeMixin: + """Mixin to initialize required global variables to use ApiAttribute.""" + + def __init__(self): +@@ -54,7 +51,7 @@ class ApiResource(dict): + + def __init__(self, *args, **kwargs): + """Create an instance of ApiResource.""" +- super(ApiResource, self).__init__() ++ super().__init__() + self.update(*args, **kwargs) + self.metadata = dict(self) + +@@ -79,11 +76,11 @@ def __setitem__(self, key, val): + def __repr__(self): + """Overwritten method of dictionary.""" + dict_representation = dict.__repr__(self) +- return "%s(%s)" % (type(self).__name__, dict_representation) ++ return f"{type(self).__name__}({dict_representation})" + + def update(self, *args, **kwargs): + """Overwritten method of dictionary.""" +- for k, v in iteritems(dict(*args, **kwargs)): ++ for k, v in dict(*args, **kwargs).items(): + self[k] = v + + def UpdateMetadata(self, metadata=None): +@@ -106,7 +103,7 @@ def GetChanges(self): + return dirty + + +-class ApiResourceList(ApiAttributeMixin, ApiResource, Iterator): ++class ApiResourceList(ApiAttributeMixin, ApiResource): + """Abstract class of all api list resources. + + Inherits ApiResource and builds iterator to list any API resource. +diff --git a/pydrive2/auth.py b/pydrive2/auth.py +index 644befb..a1b8b2f 100644 +--- a/pydrive2/auth.py ++++ b/pydrive2/auth.py +@@ -1,8 +1,6 @@ +-import socket + import webbrowser + import httplib2 + import oauth2client.clientsecrets as clientsecrets +-from six.moves import input + import threading + + from googleapiclient.discovery import build +@@ -141,7 +139,7 @@ def _decorated(self, *args, **kwargs): + return _decorated + + +-class GoogleAuth(ApiAttributeMixin, object): ++class GoogleAuth(ApiAttributeMixin): + """Wrapper class for oauth2client library in google-api-python-client. + + Loads all settings and credentials from one 'settings.yaml' file +@@ -241,13 +239,13 @@ def LocalWebserverAuth( + httpd = ClientRedirectServer( + (host_name, port), ClientRedirectHandler + ) +- except socket.error: ++ except OSError: + pass + else: + success = True + break + if success: +- oauth_callback = "http://%s:%s/" % (host_name, port_number) ++ oauth_callback = f"http://{host_name}:{port_number}/" + else: + print( + "Failed to start a local web server. Please check your firewall" +@@ -381,7 +379,7 @@ def LoadCredentialsFile(self, credentials_file=None): + + try: + self.credentials = self._default_storage.get() +- except IOError: ++ except OSError: + raise InvalidCredentialsError( + "Credentials file cannot be symbolic link" + ) +@@ -431,7 +429,7 @@ def SaveCredentialsFile(self, credentials_file=None): + + try: + storage.put(self.credentials) +- except IOError: ++ except OSError: + raise InvalidCredentialsError( + "Credentials file cannot be symbolic link" + ) +@@ -538,7 +536,7 @@ def LoadServiceConfigSettings(self): + ] + except KeyError: + err = "Insufficient service config in settings" +- err += "\n\nMissing: {} key.".format(config) ++ err += f"\n\nMissing: {config} key." + raise InvalidConfigError(err) + + def LoadClientConfigSettings(self): +diff --git a/pydrive2/drive.py b/pydrive2/drive.py +index d8b26aa..8189ab1 100644 +--- a/pydrive2/drive.py ++++ b/pydrive2/drive.py +@@ -4,7 +4,7 @@ + from .auth import LoadAuth + + +-class GoogleDrive(ApiAttributeMixin, object): ++class GoogleDrive(ApiAttributeMixin): + """Main Google Drive class.""" + + def __init__(self, auth=None): +diff --git a/pydrive2/files.py b/pydrive2/files.py +index 096f9bb..4716c7d 100644 +--- a/pydrive2/files.py ++++ b/pydrive2/files.py +@@ -17,9 +17,7 @@ + BLOCK_SIZE = 1024 + # Usage: MIME_TYPE_TO_BOM['']['']. + MIME_TYPE_TO_BOM = { +- "application/vnd.google-apps.document": { +- "text/plain": "\ufeff".encode("utf8") +- } ++ "application/vnd.google-apps.document": {"text/plain": "\ufeff".encode()} + } + + +@@ -68,7 +66,7 @@ class GoogleDriveFileList(ApiResourceList): + + def __init__(self, auth=None, param=None): + """Create an instance of GoogleDriveFileList.""" +- super(GoogleDriveFileList, self).__init__(auth=auth, metadata=param) ++ super().__init__(auth=auth, metadata=param) + + @LoadAuth + def _GetList(self): +@@ -98,7 +96,7 @@ def _GetList(self): + return result + + +-class IoBuffer(object): ++class IoBuffer: + """Lightweight retention of one chunk.""" + + def __init__(self, encoding): +@@ -116,7 +114,7 @@ def read(self): + ) + + +-class MediaIoReadable(object): ++class MediaIoReadable: + def __init__( + self, + request, +diff --git a/pydrive2/settings.py b/pydrive2/settings.py +index 8e79881..f5e84ab 100644 +--- a/pydrive2/settings.py ++++ b/pydrive2/settings.py +@@ -103,9 +103,9 @@ def LoadSettingsFile(filename=SETTINGS_FILE): + :raises: SettingsError + """ + try: +- with open(filename, "r") as stream: ++ with open(filename) as stream: + data = load(stream, Loader=Loader) +- except (YAMLError, IOError) as e: ++ except (YAMLError, OSError) as e: + raise SettingsError(e) + return data + +@@ -158,9 +158,7 @@ def _ValidateSettingsElement(data, struct, key): + 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) +- ) ++ raise InvalidConfigError(f"Setting {key} should be type {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"]) +diff --git a/pydrive2/test/test_drive.py b/pydrive2/test/test_drive.py +index 23b58a6..81498a8 100644 +--- a/pydrive2/test/test_drive.py ++++ b/pydrive2/test/test_drive.py +@@ -1,4 +1,3 @@ +-# -*- coding: utf-8 -*- + import unittest + + from pydrive2.auth import GoogleAuth +diff --git a/pydrive2/test/test_file.py b/pydrive2/test/test_file.py +index e83898c..bf3021a 100644 +--- a/pydrive2/test/test_file.py ++++ b/pydrive2/test/test_file.py +@@ -1,4 +1,3 @@ +-# -*- coding: utf-8 -*- + import filecmp + import os + import unittest +@@ -8,7 +7,6 @@ + from tempfile import mkdtemp + from time import time + +-from six.moves import range + import timeout_decorator + from concurrent.futures import ThreadPoolExecutor, as_completed + from googleapiclient import errors +@@ -699,22 +697,22 @@ def test_Gfile_Conversion_Add_Remove_BOM(self): + + def test_InsertPrefix(self): + # Create BytesIO. +- file_obj = BytesIO("abc".encode("utf8")) ++ file_obj = BytesIO(b"abc") + original_length = len(file_obj.getvalue()) +- char_to_insert = "\ufeff".encode("utf8") ++ char_to_insert = "\ufeff".encode() + + # Insert the prefix. + GoogleDriveFile._InsertPrefix(file_obj, char_to_insert) + modified_length = len(file_obj.getvalue()) + self.assertGreater(modified_length, original_length) +- self.assertEqual(file_obj.getvalue(), "\ufeffabc".encode("utf8")) ++ self.assertEqual(file_obj.getvalue(), "\ufeffabc".encode()) + + def test_InsertPrefixLarge(self): + # Create BytesIO. + test_content = "abc" * 800 + file_obj = BytesIO(test_content.encode("utf-8")) + original_length = len(file_obj.getvalue()) +- char_to_insert = "\ufeff".encode("utf8") ++ char_to_insert = "\ufeff".encode() + + # Insert the prefix. + GoogleDriveFile._InsertPrefix(file_obj, char_to_insert) +@@ -725,22 +723,22 @@ def test_InsertPrefixLarge(self): + + def test_RemovePrefix(self): + # Create BytesIO. +- file_obj = BytesIO("\ufeffabc".encode("utf8")) ++ file_obj = BytesIO("\ufeffabc".encode()) + original_length = len(file_obj.getvalue()) +- char_to_remove = "\ufeff".encode("utf8") ++ char_to_remove = "\ufeff".encode() + + # Insert the prefix. + GoogleDriveFile._RemovePrefix(file_obj, char_to_remove) + modified_length = len(file_obj.getvalue()) + self.assertLess(modified_length, original_length) +- self.assertEqual(file_obj.getvalue(), "abc".encode("utf8")) ++ self.assertEqual(file_obj.getvalue(), b"abc") + + def test_RemovePrefixLarge(self): + # Create BytesIO. + test_content = "\ufeff" + "abc" * 800 + file_obj = BytesIO(test_content.encode("utf8")) + original_length = len(file_obj.getvalue()) +- char_to_remove = "\ufeff".encode("utf8") ++ char_to_remove = "\ufeff".encode() + + # Insert the prefix. + GoogleDriveFile._RemovePrefix(file_obj, char_to_remove) +diff --git a/pydrive2/test/test_filelist.py b/pydrive2/test/test_filelist.py +index 644c13f..15918cd 100644 +--- a/pydrive2/test/test_filelist.py ++++ b/pydrive2/test/test_filelist.py +@@ -1,4 +1,3 @@ +-# -*- coding: utf-8 -*- + import os + import unittest + +@@ -28,7 +27,7 @@ def setup_class(cls): + + def test_01_Files_List_GetList(self): + drive = GoogleDrive(self.ga) +- query = "title = '{}' and trashed = false".format(self.title) ++ query = f"title = '{self.title}' and trashed = false" + for file1 in pydrive_list_item(drive, query): + found = False + for file2 in pydrive_list_item(drive, query): +@@ -38,7 +37,7 @@ def test_01_Files_List_GetList(self): + + def test_02_Files_List_ForLoop(self): + drive = GoogleDrive(self.ga) +- query = "title = '{}' and trashed = false".format(self.title) ++ query = f"title = '{self.title}' and trashed = false" + files = [] + for x in pydrive_list_item( + drive, query, 2 +@@ -84,7 +83,7 @@ def test_File_List_Folders(self): + ) + pydrive_retry(folder1.Upload) + self.file_list.append(folder1) +- query = "title = '{}' and trashed = false".format(self.title) ++ query = f"title = '{self.title}' and trashed = false" + count = 0 + for file1 in pydrive_list_item(drive, query): + self.assertFileInFileList(file1) +diff --git a/setup.py b/setup.py +index 32c9364..06982d0 100644 +--- a/setup.py ++++ b/setup.py +@@ -1,4 +1,3 @@ +-import sys + from setuptools import setup + + # Extra dependecies to run tests +@@ -11,8 +10,7 @@ + "pytest-mock", + ] + +-if sys.version_info >= (3, 6): +- tests_requirements.append("black==22.3.0") ++tests_requirements.append("black==22.3.0") + + setup( + name="PyDrive2", diff --git a/python-pydrive2.changes b/python-pydrive2.changes index f1cedb3..ff83cc2 100644 --- a/python-pydrive2.changes +++ b/python-pydrive2.changes @@ -1,3 +1,15 @@ +------------------------------------------------------------------- +Fri Jun 3 11:11:45 UTC 2022 - Markéta Machová + +- Update to 1.10.1 + * implement a fsspec-based filesystem backend + * fs.get_file: add callback support + * Add option to not launch the browser automatically via LocalWebserver + * make credentials save/load thread safe +- Add patches: + * modernize.patch: support up to Python 3.10 + * migrate-to-google-auth.patch: drop obsolete oauthlib2 requirement + ------------------------------------------------------------------- Mon Jul 5 21:20:11 UTC 2021 - Martin Wilck diff --git a/python-pydrive2.spec b/python-pydrive2.spec index d9fe40f..b39f7b2 100644 --- a/python-pydrive2.spec +++ b/python-pydrive2.spec @@ -1,7 +1,7 @@ # -# spec file for package python-PyDrive2 +# spec file for package python-pydrive2 # -# Copyright (c) 2021 SUSE LLC +# Copyright (c) 2022 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,33 +18,46 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-pydrive2 -Version: 1.8.3 +Version: 1.10.1 Release: 0 Summary: A wrapper library for google-api-python-client License: Apache-2.0 URL: https://github.com/iterative/PyDrive2 Source: https://files.pythonhosted.org/packages/source/P/PyDrive2/PyDrive2-%{version}.tar.gz -BuildRequires: python-rpm-macros +# PATCH-FIX-UPSTREAM https://github.com/iterative/PyDrive2/commit/2e43e4561d965ce78dc158a02fbdb75ba6c38105 pydrive2: modernise to python 3.7+ +Patch0: modernize.patch +# PATCH-FIX-UPSTREAM https://github.com/iterative/PyDrive2/pull/180 Migrating to Google Auth Library +Patch1: migrate-to-google-auth.patch BuildRequires: %{python_module setuptools} +BuildRequires: python-rpm-macros ## tests fail in OBS environment # SECTION test requirements #BuildRequires: %{python_module google-api-python-client >= 1.12.5} -#BuildRequires: %{python_module oauth2client >= 4.0.0} -#BuildRequires: %{python_module pyOpenSSL >= 19.1.0} #BuildRequires: %{python_module PyYAML >= 3.0} -#BuildRequires: %{python_module six >= 1.13.0} -#BuildRequires: %{python_module pytest} -#BuildRequires: %{python_module timeout-decorator} -#BuildRequires: %{python_module funcy >= 1.14} -#BuildRequires: %{python_module flake8} -#BuildRequires: %{python_module flake8-docstrings} #BuildRequires: %{python_module black} +BuildRequires: %{python_module filelock >= 3.7.0} +#BuildRequires: %{python_module flake8-docstrings} +#BuildRequires: %{python_module flake8} +#BuildRequires: %{python_module fsspec} +#BuildRequires: %{python_module funcy >= 1.14} +BuildRequires: %{python_module google-auth-oauthlib >= 0.5.1} +BuildRequires: %{python_module google-auth >= 2.6.6} +BuildRequires: %{python_module google-auth-httplib2 >= 0.1.0} +#BuildRequires: %{python_module pyOpenSSL >= 19.1.0} +#BuildRequires: %{python_module pytest-mock} +#BuildRequires: %{python_module pytest} +#BuildRequires: %{python_module six >= 1.13.0} +#BuildRequires: %{python_module timeout-decorator} +#BuildRequires: %{python_module tqdm} # /SECTION BuildRequires: fdupes -Requires: python-google-api-python-client >= 1.12.5 -Requires: python-oauth2client >= 4.0.0 -Requires: python-pyOpenSSL >= 19.1.0 Requires: python-PyYAML >= 3.0 +Requires: python-filelock >= 3.7.0 +Requires: python-google-api-python-client >= 1.12.5 +Requires: python-google-auth >= 2.6.6 +Requires: python-google-auth-httplib2 >= 0.1.0 +Requires: python-google-auth-oauthlib >= 0.5.1 +Requires: python-pyOpenSSL >= 19.1.0 Requires: python-six >= 1.13.0 BuildArch: noarch %python_subpackages @@ -56,6 +69,7 @@ By the authors and maintainers of the Git for Data - DVC project. %prep %setup -q -n PyDrive2-%{version} +%autopatch -p1 %build %python_build