From 7a45474bcc8762a6ce6e6c28bd8a18aec0d930d455114bf8ef1adb6f2bba59e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=C3=A9ta=20Machov=C3=A1?= Date: Wed, 4 Jun 2025 10:16:28 +0000 Subject: [PATCH 1/3] - Convert to pip-based build OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-pydrive2?expand=0&rev=12 --- .gitattributes | 23 + .gitignore | 1 + PyDrive2-1.18.1.tar.gz | 3 + migrate-to-google-auth.patch | 1420 ++++++++++++++++++++++++++++++++++ python-pydrive2.changes | 64 ++ python-pydrive2.spec | 84 ++ 6 files changed, 1595 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 PyDrive2-1.18.1.tar.gz create mode 100644 migrate-to-google-auth.patch create mode 100644 python-pydrive2.changes create mode 100644 python-pydrive2.spec diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9b03811 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +## Default LFS +*.7z filter=lfs diff=lfs merge=lfs -text +*.bsp filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.gem filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.jar filter=lfs diff=lfs merge=lfs -text +*.lz filter=lfs diff=lfs merge=lfs -text +*.lzma filter=lfs diff=lfs merge=lfs -text +*.obscpio filter=lfs diff=lfs merge=lfs -text +*.oxt filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.rpm filter=lfs diff=lfs merge=lfs -text +*.tbz filter=lfs diff=lfs merge=lfs -text +*.tbz2 filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.ttf filter=lfs diff=lfs merge=lfs -text +*.txz filter=lfs diff=lfs merge=lfs -text +*.whl filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57affb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.osc diff --git a/PyDrive2-1.18.1.tar.gz b/PyDrive2-1.18.1.tar.gz new file mode 100644 index 0000000..fdfd7e6 --- /dev/null +++ b/PyDrive2-1.18.1.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49dca8842f7c3d63fd35ab55492af2a8b5b39f1cc811042fe37ffbed1c48303f +size 60629 diff --git a/migrate-to-google-auth.patch b/migrate-to-google-auth.patch new file mode 100644 index 0000000..78b660b --- /dev/null +++ b/migrate-to-google-auth.patch @@ -0,0 +1,1420 @@ +From 2789fda4c4ee7aac002f561819f2cec68e838239 Mon Sep 17 00:00:00 2001 +From: junpeng-jp +Date: Sat, 6 Aug 2022 21:25:26 +0800 +Subject: [PATCH 01/11] Migrate to Google Auth + +--- + pydrive2/auth.py | 606 +++++++++--------- + pydrive2/auth_helpers.py | 58 ++ + pydrive2/storage.py | 146 +++++ + pydrive2/test/settings/default_user.yaml | 11 + + .../settings/default_user_no_refresh.yaml | 9 + + pydrive2/test/test_oauth.py | 28 +- + pydrive2/test/test_token_expiry.py | 77 +++ + 7 files changed, 616 insertions(+), 319 deletions(-) + create mode 100644 pydrive2/auth_helpers.py + create mode 100644 pydrive2/storage.py + create mode 100644 pydrive2/test/settings/default_user.yaml + create mode 100644 pydrive2/test/settings/default_user_no_refresh.yaml + create mode 100644 pydrive2/test/test_token_expiry.py + +Index: PyDrive2-1.16.2/pydrive2/auth.py +=================================================================== +--- PyDrive2-1.16.2.orig/pydrive2/auth.py ++++ PyDrive2-1.16.2/pydrive2/auth.py +@@ -1,28 +1,27 @@ + import json +-import webbrowser + import httplib2 +-import oauth2client.clientsecrets as clientsecrets + import threading + + 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.contrib.dictionary_storage import DictionaryStorage +-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 ++import google.oauth2.credentials ++import google.oauth2.service_account ++from .storage import FileBackend, DictionaryBackend ++ + from .settings import LoadSettingsFile + from .settings import ValidateSettings + 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 ++ ++ ++_CLIENT_AUTH_PROMPT_MESSAGE = "Please visit this URL:\n{url}\n" ++ + + class AuthError(Exception): + """Base error for authentication/authorization errors.""" +@@ -45,23 +44,16 @@ class RefreshError(AuthError): + + + def LoadAuth(decoratee): +- """Decorator to check if the auth is valid and loads auth if not.""" ++ """ ++ Decorator to check the self.auth & self.http object in a decorated API call. ++ Loads a new GoogleAuth or Http object if needed. ++ """ + + @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() + + # Ensure that a thread-safe HTTP object is provided. + if ( +@@ -71,77 +63,20 @@ def LoadAuth(decoratee): + and "http" in kwargs["param"] + and kwargs["param"]["http"] is not None + ): ++ # overwrites the HTTP objects used by the Gdrive API object + 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 ++ # If HTTP object not specified, resuse HTTP from self.auth.thread_local ++ self.http = self.auth.authorized_http + + 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): ++class GoogleAuth: + """Wrapper class for oauth2client library in google-api-python-client. + + Loads all settings and credentials from one 'settings.yaml' file +@@ -155,37 +90,24 @@ class GoogleAuth(ApiAttributeMixin): + "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") +- flow = ApiAttribute("flow") +- credentials = ApiAttribute("credentials") +- http = ApiAttribute("http") +- service = ApiAttribute("service") +- auth_method = ApiAttribute("auth_method") + + def __init__( + self, settings_file="settings.yaml", http_timeout=None, settings=None + ): + """Create an instance of GoogleAuth. + ++ This constructor parses just the yaml settings file. ++ All other config & auth related objects are lazily loaded (see properties section) ++ + :param settings_file: path of settings file. 'settings.yaml' by default. + :type settings_file: str. + :param settings: settings dict. + :type settings: dict. + """ + self.http_timeout = http_timeout +- ApiAttributeMixin.__init__(self) + self.thread_local = threading.local() +- self.client_config = {} + + if settings is None and settings_file: + try: +@@ -196,21 +118,92 @@ class GoogleAuth(ApiAttributeMixin): + self.settings = settings or self.DEFAULT_SETTINGS + ValidateSettings(self.settings) + +- storages, default = self._InitializeStoragesFromSettings() +- self._storages = storages +- self._default_storage = default ++ self._service = None ++ self._client_config = None ++ self._oauth_type = None ++ self._flow = None ++ self._storage_registry = {} ++ self._default_storage = None ++ self._credentials = None ++ ++ @property ++ def service(self): ++ if not self._service: ++ 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 default_storage(self): ++ if not self.settings.get("save_credentials"): ++ return None ++ ++ if not self._default_storage: ++ self._InitializeStoragesFromSettings() ++ return self._default_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.default_storage: ++ try: ++ self.LoadCredentials() ++ return self._credentials ++ except FileNotFoundError: ++ pass ++ ++ self.LocalWebserverAuth() ++ ++ elif self.oauth_type == "service": ++ self.ServiceAuth() ++ else: ++ raise InvalidConfigError( ++ "Only web, installed, service oauth is supported" ++ ) ++ ++ return self._credentials ++ ++ @property ++ def authorized_http(self): ++ # returns a thread-safe, local, cached HTTP object ++ if not getattr(self.thread_local, "http", None): ++ # If HTTP object not available in thread_local, ++ # create and store Authorized Http object in thread_local storage ++ self.thread_local.http = self.Get_Http_Object() ++ ++ return self.thread_local.http ++ ++ # 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 ++ return not self.credentials.valid + +- @CheckAuth + def LocalWebserverAuth( + self, + host_name="localhost", +@@ -238,119 +231,100 @@ class GoogleAuth(ApiAttributeMixin): + :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_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" ++ + port_number = 0 + for port in port_numbers: + port_number = port + try: +- httpd = ClientRedirectServer( +- (bind_addr or host_name, port), ClientRedirectHandler ++ self._credentials = self.flow.run_local_server( ++ host=bind_addr or host_name, ++ port=port_number, ++ authorization_prompt_message=_CLIENT_AUTH_PROMPT_MESSAGE, ++ open_browser=launch_browser, ++ **additional_config, ++ ) ++ except OSError as e: ++ print( ++ "Port {} is in use. Trying a different port".format(port) + ) +- except OSError: +- pass +- else: +- success = True +- break +- if success: +- oauth_callback = f"http://{host_name}:{port_number}/" +- else: +- print( +- "Failed to start a local web server. Please check your firewall" +- ) +- print( +- "settings and locally running programs that may be blocking or" +- ) +- 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 ++ 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 uri") ++ ++ except OAuth2Error as e: ++ # catch all other oauth 2 errors ++ print("Authentication request was rejected") ++ raise AuthenticationRejected("User rejected authentication") ++ ++ # if any port results in successful auth, we're done ++ if self._credentials: ++ if self.default_storage: ++ self.SaveCredentials() ++ ++ return ++ ++ # If we have tried all ports and could not find a port ++ print("Failed to start a local web server. Please check your firewall") ++ print("settings and locally running programs that may be blocking or") ++ print("using configured ports. Default ports are 8080 and 8090.") ++ raise AuthenticationError("None of the specified ports are available") ++ + 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() +- print("Go to the following link in your browser:") +- print() +- print(" " + authorize_url) +- print() +- return input("Enter verification code: ").strip() ++ raise DeprecationWarning( ++ "The command line auth has been deprecated. " ++ "The recommended alternative is to use local webserver auth with a loopback address." ++ ) + +- @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"]) + keyfile_name = self.client_config.get("client_json_file_path") + keyfile_dict = self.client_config.get("client_json_dict") + keyfile_json = self.client_config.get("client_json") + ++ # setting the subject for domain-wide delegation ++ additional_config = {} ++ additional_config["subject"] = self.client_config.get( ++ "client_user_email" ++ ) ++ additional_config["scopes"] = self.settings["oauth_scope"] ++ + if not keyfile_dict and keyfile_json: + # Compensating for missing ServiceAccountCredentials.from_json_keyfile + keyfile_dict = json.loads(keyfile_json) + + if keyfile_dict: +- self.credentials = ( +- ServiceAccountCredentials.from_json_keyfile_dict( +- keyfile_dict=keyfile_dict, scopes=scopes +- ) ++ self._credentials = google.oauth2.service_account.Credentials.from_service_account_info( ++ keyfile_dict, **additional_config + ) + elif keyfile_name: +- self.credentials = ( +- ServiceAccountCredentials.from_json_keyfile_name( +- filename=keyfile_name, scopes=scopes +- ) ++ self._credentials = google.oauth2.service_account.Credentials.from_service_account_file( ++ keyfile_name, **additional_config + ) + 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, +- ) +- +- user_email = self.client_config.get("client_user_email") +- if user_email: +- self.credentials = self.credentials.create_delegated( +- sub=user_email +- ) ++ raise AuthenticationError("Invalid service credentials") + + def _InitializeStoragesFromSettings(self): +- result = {"file": None, "dictionary": None} + backend = self.settings.get("save_credentials_backend") + save_credentials = self.settings.get("save_credentials") + if backend == "file": +@@ -359,7 +333,9 @@ class GoogleAuth(ApiAttributeMixin): + raise InvalidConfigError( + "Please specify credentials file to read" + ) +- result[backend] = Storage(credentials_file) ++ ++ self._storage_registry[backend] = FileBackend(credentials_file) ++ + elif backend == "dictionary": + creds_dict = self.settings.get("save_credentials_dict") + if creds_dict is None: +@@ -369,12 +345,14 @@ class GoogleAuth(ApiAttributeMixin): + if creds_key is None: + raise InvalidConfigError("Please specify credentials key") + +- result[backend] = DictionaryStorage(creds_dict, creds_key) ++ self._storage_registry[backend] = DictionaryBackend(creds_dict) ++ + elif save_credentials: + raise InvalidConfigError( + "Unknown save_credentials_backend: %s" % backend + ) +- return result, result.get(backend) ++ ++ self._default_storage = self._storage_registry.get(backend) + + def LoadCredentials(self, backend=None): + """Loads credentials or create empty credentials if it doesn't exist. +@@ -404,7 +382,8 @@ class GoogleAuth(ApiAttributeMixin): + :raises: InvalidConfigError, InvalidCredentialsError + """ + if credentials_file is None: +- self._default_storage = self._storages["file"] ++ self._default_storage = self._storage_registry["file"] ++ credentials_file = self.settings.get("save_credentials_file") + if self._default_storage is None: + raise InvalidConfigError( + "Backend `file` is not configured, specify " +@@ -412,30 +391,49 @@ class GoogleAuth(ApiAttributeMixin): + "file or pass an explicit value" + ) + else: +- self._default_storage = Storage(credentials_file) ++ self._default_storage = FileBackend(credentials_file) + + try: +- self.credentials = self._default_storage.get() ++ auth_info = self.default_storage.read_credentials() ++ except FileNotFoundError: ++ # if credential 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: ++ 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"), ++ ) + + def _LoadCredentialsDictionary(self): +- self._default_storage = self._storages["dictionary"] ++ self._default_storage = self._storage_registry["dictionary"] + if self._default_storage is None: + raise InvalidConfigError( + "Backend `dictionary` is not configured, specify " + "credentials dict and key to read in the settings file" + ) + +- self.credentials = self._default_storage.get() ++ creds_key = self.settings.get("save_credentials_key") + +- if self.credentials: +- self.credentials.set_store(self._default_storage) ++ self._credentials = self.default_storage.read_credentials(creds_key) + + def SaveCredentials(self, backend=None): + """Saves credentials according to specified backend. +@@ -465,39 +463,39 @@ class GoogleAuth(ApiAttributeMixin): + :type credentials_file: str. + :raises: InvalidConfigError, InvalidCredentialsError + """ +- if self.credentials is None: ++ if self._credentials is None: + 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) ++ ++ storage = self._storage_registry["file"] + + try: +- storage.put(self.credentials) ++ storage.store_credentials(self._credentials) ++ + except OSError: + raise InvalidCredentialsError( + "Credentials file cannot be symbolic link" + ) + + def _SaveCredentialsDictionary(self): +- if self.credentials is None: ++ if self._credentials is None: + raise InvalidCredentialsError("No credentials to save") + +- storage = self._storages["dictionary"] ++ storage = self._storage_registry["dictionary"] + if storage is None: + raise InvalidConfigError( + "Backend `dictionary` is not configured, specify " + "credentials dict and key to write in the settings file" + ) + +- storage.put(self.credentials) ++ creds_key = self.settings.get("save_credentials_key") ++ storage.store_credentials(self._credentials, creds_key) + + def LoadClientConfig(self, backend=None): + """Loads client configuration according to specified backend. +@@ -535,45 +533,18 @@ class GoogleAuth(ApiAttributeMixin): + """ + 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") ++ with open(client_config_file, "r") as json_file: ++ client_config = json.load(json_file) + +- # Service auth related fields. +- service_auth_config = ["client_email"] + 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 error: ++ raise InvalidConfigError("Invalid client secrets file: %s" % error) ++ ++ self._client_config = checked_config ++ self._oauth_type = client_type + + def LoadServiceConfigSettings(self): + """Loads client configuration from settings. +@@ -585,11 +556,12 @@ class GoogleAuth(ApiAttributeMixin): + "client_json", + "client_pkcs12_file_path", + ] ++ service_config = {} + + for config in configs: + value = self.settings["service_config"].get(config) + if value: +- self.client_config[config] = value ++ service_config[config] = value + break + else: + raise InvalidConfigError( +@@ -597,91 +569,91 @@ class GoogleAuth(ApiAttributeMixin): + ) + + if config == "client_pkcs12_file_path": +- self.SERVICE_CONFIGS_LIST.append("client_service_email") ++ # see https://github.com/googleapis/google-auth-library-python/issues/288 ++ 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" ++ ) + + for config in self.SERVICE_CONFIGS_LIST: + try: +- self.client_config[config] = self.settings["service_config"][ ++ service_config[config] = self.settings["service_config"].get( + 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) + +- def GetFlow(self): ++ # 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, scopes=None, **kwargs): + """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"} ++ if not scopes: ++ 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=scopes, ++ **kwargs, + ) + ++ 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." +- ) +- 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) ++ raise DeprecationWarning( ++ "Manual refresh is deprecated as the" ++ "new google auth library handles refresh automatically" ++ ) + +- def GetAuthUrl(self): ++ def GetAuthUrl(self, redirect_uri="http://localhost:8080/"): + """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." ++ ) ++ ++ self.flow.redirect_uri = redirect_uri ++ ++ return self.flow.authorization_url() + + def Auth(self, code): + """Authenticate, authorize, and build service. +@@ -691,7 +663,6 @@ class GoogleAuth(ApiAttributeMixin): + :raises: AuthenticationError + """ + self.Authenticate(code) +- self.Authorize() + + def Authenticate(self, code): + """Authenticates given authentication code back from user. +@@ -700,12 +671,35 @@ class GoogleAuth(ApiAttributeMixin): + :type code: str. + :raises: AuthenticationError + """ +- if self.flow is None: +- self.GetFlow() ++ from urllib.parse import unquote ++ ++ 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) ++ self.flow.fetch_token(code=unquote(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") from e ++ ++ self._credentials = self.flow.credentials ++ ++ # save credentials if there's a default storage ++ if self.default_storage: ++ self.SaveCredentials() ++ + print("Authentication successful.") + + def _build_http(self): +@@ -728,24 +722,16 @@ class GoogleAuth(ApiAttributeMixin): + + :raises: AuthenticationError + """ +- if self.access_token_expired: +- raise AuthenticationError( +- "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 ++ raise DeprecationWarning( ++ "Manual authorization of HTTP will be deprecated as the" ++ "new google auth library handles the adding to relevant oauth headers automatically" + ) + + def Get_Http_Object(self): +- """Create and authorize an httplib2.Http object. Necessary for +- thread-safety. ++ """ ++ Helper function to get a new Authorized Http object. + :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()) +Index: PyDrive2-1.16.2/pydrive2/auth_helpers.py +=================================================================== +--- /dev/null ++++ PyDrive2-1.16.2/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 +Index: PyDrive2-1.16.2/pydrive2/storage.py +=================================================================== +--- /dev/null ++++ PyDrive2-1.16.2/pydrive2/storage.py +@@ -0,0 +1,116 @@ ++import os ++import json ++import warnings ++import threading ++ ++ ++_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 credentials""" ++ ++ def _read_credentials(self, **kwargs): ++ """Specific implementation of how credentials are retrieved from backend""" ++ return NotImplementedError ++ ++ def _store_credentials(self, credential, **kwargs): ++ """Specific implementation of how credentials are written to backend""" ++ return NotImplementedError ++ ++ def _delete_credentials(self, **kwargs): ++ """Specific implementation of how credentials are deleted from backend""" ++ return NotImplementedError ++ ++ def read_credentials(self, **kwargs): ++ """Reads a credential config from the backend and ++ returns the config as a dictionary ++ :return: A dictionary of the credentials ++ """ ++ return self._read_credentials(**kwargs) ++ ++ def store_credentials(self, credential, **kwargs): ++ """Write a credential to the backend""" ++ self._store_credentials(credential, **kwargs) ++ ++ def delete_credentials(self, **kwargs): ++ """Delete credential. ++ Frees any resources associated with storing the credential ++ """ ++ self._delete_credentials(**kwargs) ++ ++ ++class FileBackend(CredentialBackend): ++ """Read and write credential to a specific file backend with Thread-locking""" ++ ++ def __init__(self, filename): ++ self._filename = filename ++ self._thread_lock = threading.Lock() ++ ++ def _create_file_if_needed(self, filename): ++ """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(filename): ++ old_umask = os.umask(0o177) ++ try: ++ open(filename, "a+b").close() ++ finally: ++ os.umask(old_umask) ++ ++ def _read_credentials(self, **kwargs): ++ """Reads a local json file and parses the information into a info dictionary.""" ++ with self._thread_lock: ++ validate_file(self._filename) ++ with open(self._filename, "r") as json_file: ++ return json.load(json_file) ++ ++ def _store_credentials(self, credentials, **kwargs): ++ """Writes current credentials to a local json file.""" ++ with self._thread_lock: ++ # write new credentials to the temp file ++ dirname, filename = os.path.split(self._filename) ++ temp_path = os.path.join(dirname, "temp_{}".format(filename)) ++ self._create_file_if_needed(temp_path) ++ ++ with open(temp_path, "w") as json_file: ++ json_file.write(credentials.to_json()) ++ ++ # replace the existing credential file ++ os.replace(temp_path, self._filename) ++ ++ def _delete_credentials(self, **kwargs): ++ """Delete credentials file.""" ++ with self._thread_lock: ++ os.unlink(self._filename) ++ ++ ++class DictionaryBackend(CredentialBackend): ++ """Read and write credentials to a dictionary backend""" ++ ++ def __init__(self, dictionary): ++ self._dictionary = dictionary ++ ++ def _read_credentials(self, key): ++ """Reads a local json file and parses the information into a info dictionary.""" ++ return self._dictionary.get(key) ++ ++ def _store_credentials(self, credentials, key): ++ """Writes current credentials to a local json file.""" ++ self._dictionary[key] = credentials.to_json() ++ ++ def _delete_credentials(self, key): ++ """Delete Credentials file.""" ++ self._dictionary.pop(key, None) +Index: PyDrive2-1.16.2/pydrive2/test/settings/default_user.yaml +=================================================================== +--- /dev/null ++++ PyDrive2-1.16.2/pydrive2/test/settings/default_user.yaml +@@ -0,0 +1,11 @@ ++client_config_backend: file ++client_config_file: /tmp/pydrive2/user.json ++ ++save_credentials: True ++save_credentials_backend: file ++save_credentials_file: credentials/default_user.dat ++ ++oauth_scope: ++ - https://www.googleapis.com/auth/drive ++ ++get_refresh_token: True +\ No newline at end of file +Index: PyDrive2-1.16.2/pydrive2/test/settings/default_user_no_refresh.yaml +=================================================================== +--- /dev/null ++++ PyDrive2-1.16.2/pydrive2/test/settings/default_user_no_refresh.yaml +@@ -0,0 +1,9 @@ ++client_config_backend: file ++client_config_file: /tmp/pydrive2/user.json ++ ++save_credentials: True ++save_credentials_backend: file ++save_credentials_file: credentials/default_user_no_refresh.dat ++ ++oauth_scope: ++ - https://www.googleapis.com/auth/drive +Index: PyDrive2-1.16.2/pydrive2/test/test_oauth.py +=================================================================== +--- PyDrive2-1.16.2.orig/pydrive2/test/test_oauth.py ++++ PyDrive2-1.16.2/pydrive2/test/test_oauth.py +@@ -10,7 +10,7 @@ from pydrive2.test.test_util import ( + settings_file_path, + GDRIVE_USER_CREDENTIALS_DATA, + ) +-from oauth2client.file import Storage ++from ..storage import FileBackend + + + def setup_module(module): +@@ -24,6 +24,7 @@ def test_01_LocalWebserverAuthWithClient + # 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") +@@ -37,6 +38,7 @@ def test_02_LocalWebserverAuthWithClient + # 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") +@@ -50,6 +52,7 @@ def test_03_LocalWebServerAuthWithNoCred + 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) + +@@ -61,6 +64,7 @@ def test_04_CommandLineAuthWithClientCon + # 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") +@@ -72,6 +76,7 @@ def test_05_ConfigFromSettingsWithoutOau + # 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) + +@@ -81,7 +86,7 @@ def test_06_ServiceAuthFromSavedCredenti + setup_credentials("credentials/6.dat") + ga = GoogleAuth(settings_file_path("test_oauth_test_06.yaml")) + ga.ServiceAuth() +- assert not ga.access_token_expired ++ assert ga.credentials + time.sleep(1) + + +@@ -92,13 +97,14 @@ def test_07_ServiceAuthFromSavedCredenti + # Delete old credentials file + delete_file(credentials_file) + assert not os.path.exists(credentials_file) ++ # For Service Auth, credentials are created with no token (treated as expired) ++ # JWT token is not populated until the first request where auto-refresh happens ++ # assert not ga.access_token_expired + ga.ServiceAuth() +- assert os.path.exists(credentials_file) +- # Secondary auth should be made only using the previously saved +- # login info ++ assert ga.credentials + ga = GoogleAuth(settings_file_path("test_oauth_test_07.yaml")) + ga.ServiceAuth() +- assert not ga.access_token_expired ++ assert ga.credentials + time.sleep(1) + + +@@ -108,6 +114,7 @@ def test_08_ServiceAuthFromJsonFileNoCre + ga = GoogleAuth(settings_file_path("test_oauth_test_08.yaml")) + assert not ga.settings["save_credentials"] + ga.ServiceAuth() ++ assert ga.credentials + time.sleep(1) + + +@@ -120,10 +127,9 @@ def test_09_SaveLoadCredentialsUsesDefau + # 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() +- ga.LoadCredentials() +- ga.SaveCredentials() ++ assert ga.credentials + assert spy.call_count == 0 + + +@@ -142,14 +148,14 @@ def test_10_ServiceAuthFromSavedCredenti + } + ga = GoogleAuth(settings=settings) + ga.ServiceAuth() +- assert not ga.access_token_expired ++ assert ga.credentials + assert creds_dict + first_creds_dict = creds_dict.copy() + # Secondary auth should be made only using the previously saved + # login info + ga = GoogleAuth(settings=settings) + ga.ServiceAuth() +- assert not ga.access_token_expired ++ assert ga.credentials + assert creds_dict == first_creds_dict + time.sleep(1) + +Index: PyDrive2-1.16.2/pydrive2/test/test_token_expiry.py +=================================================================== +--- /dev/null ++++ PyDrive2-1.16.2/pydrive2/test/test_token_expiry.py +@@ -0,0 +1,77 @@ ++import pytest ++from pydrive2.auth import GoogleAuth ++from pydrive2.drive import GoogleDrive ++from pydrive2.test.test_util import ( ++ settings_file_path, ++ setup_credentials, ++ pydrive_retry, ++ delete_file, ++) ++from google.auth.exceptions import RefreshError ++ ++ ++@pytest.fixture ++def googleauth_refresh(): ++ setup_credentials() ++ # Delete old credentials file ++ delete_file("credentials/default_user.dat") ++ ga = GoogleAuth(settings_file_path("default_user.yaml")) ++ ga.LocalWebserverAuth() ++ ++ return ga ++ ++ ++@pytest.fixture ++def googleauth_no_refresh(): ++ setup_credentials() ++ # Delete old credentials file ++ delete_file("credentials/default_user_no_refresh.dat") ++ ga = GoogleAuth(settings_file_path("default_user_no_refresh.yaml")) ++ ga.LocalWebserverAuth() ++ ++ return ga ++ ++ ++@pytest.mark.manual ++def test_01_TokenExpiryWithRefreshToken(googleauth_refresh): ++ gdrive = GoogleDrive(googleauth_refresh) ++ ++ about_object = pydrive_retry(gdrive.GetAbout) ++ assert about_object is not None ++ ++ # save the first access token for comparison ++ token1 = gdrive.auth.credentials.token ++ ++ # simulate token expiry by deleting the underlying token ++ gdrive.auth.credentials.token = None ++ ++ # credential object should still exist but access token expired ++ assert gdrive.auth.credentials ++ assert gdrive.auth.access_token_expired ++ ++ about_object = pydrive_retry(gdrive.GetAbout) ++ assert about_object is not None ++ ++ # save the second access token for comparison ++ token2 = gdrive.auth.credentials.token ++ ++ assert token1 != token2 ++ ++ ++@pytest.mark.manual ++def test_02_TokenExpiryWithoutRefreshToken(googleauth_no_refresh): ++ gdrive = GoogleDrive(googleauth_no_refresh) ++ ++ about_object = pydrive_retry(gdrive.GetAbout) ++ assert about_object is not None ++ ++ # simulate token expiry by deleting the underlying token ++ gdrive.auth.credentials.token = None ++ ++ # credential object should still exist but access token expired ++ assert gdrive.auth.credentials ++ assert gdrive.auth.access_token_expired ++ ++ # as credentials have no refresh token, this would fail ++ with pytest.raises(RefreshError) as e_info: ++ about_object = pydrive_retry(gdrive.GetAbout) +Index: PyDrive2-1.16.2/setup.py +=================================================================== +--- PyDrive2-1.16.2.orig/setup.py ++++ PyDrive2-1.16.2/setup.py +@@ -37,7 +37,8 @@ setup( + long_description_content_type="text/x-rst", + install_requires=[ + "google-api-python-client >= 1.12.5", +- "oauth2client >= 4.0.0", ++ "google-auth", ++ "google-auth-oauthlib", + "PyYAML >= 3.0", + "pyOpenSSL >= 19.1.0", + ], +Index: PyDrive2-1.16.2/pydrive2/test/test_oauth_custom.py +=================================================================== +--- /dev/null ++++ PyDrive2-1.16.2/pydrive2/test/test_oauth_custom.py +@@ -0,0 +1,42 @@ ++import pytest ++import os ++from pydrive2.auth import GoogleAuth ++from pydrive2.drive import GoogleDrive ++from pydrive2.test.test_util import ( ++ settings_file_path, ++ setup_credentials, ++ delete_file, ++) ++ ++ ++@pytest.fixture ++def googleauth_preauth(): ++ setup_credentials() ++ # Delete old credentials file ++ delete_file("credentials/default_user.dat") ++ ga = GoogleAuth(settings_file_path("default_user.yaml")) ++ ++ return ga ++ ++ ++@pytest.mark.manual ++def test_01_CustomAuthWithSavingOfCredentials(googleauth_preauth): ++ ++ credentials_file = googleauth_preauth.settings["save_credentials_file"] ++ ++ assert not os.path.exists(credentials_file) ++ ++ auth_url, state = googleauth_preauth.GetAuthUrl() ++ print("please visit this url: {}".format(auth_url)) ++ ++ googleauth_preauth.Authenticate(input("Please enter the auth code: ")) ++ ++ # credentials have been loaded ++ assert googleauth_preauth.credentials ++ # check that credentials file has been saved ++ assert os.path.exists(credentials_file) ++ ++ gdrive = GoogleDrive(googleauth_preauth) ++ ++ about_object = gdrive.GetAbout() ++ assert about_object is not None diff --git a/python-pydrive2.changes b/python-pydrive2.changes new file mode 100644 index 0000000..ec55dfe --- /dev/null +++ b/python-pydrive2.changes @@ -0,0 +1,64 @@ +------------------------------------------------------------------- +Wed Jun 4 10:13:50 UTC 2025 - Markéta Machová + +- Convert to pip-based build + +------------------------------------------------------------------- +Tue Dec 26 20:28:13 UTC 2023 - Dirk Müller + +- update to 1.18.1: + * fix(fs): remove DVC-specific broken cache optimization + * **Full Changelog**: + https://github.com/iterative/PyDrive2/compare/1.16.2...1.18.1 + +------------------------------------------------------------------- +Thu Dec 14 20:55:41 UTC 2023 - Dirk Müller + +- update to 1.18.0: + * Delete .github/release-drafter.yml + * Delete .github/workflows/release-drafter.yml + * **Full Changelog**: + https://github.com/iterative/PyDrive2/compare/1.16.2...1.18.0 + +------------------------------------------------------------------- +Thu Dec 7 09:17:01 UTC 2023 - Markéta Machová + +- Update to 1.16.2 (bsc#1217858, CVE-2023-49297) + * auth: add dictionary storage + * auth: rename client_creds_dict -> client_json_dict + * fs: simplify auth + * fs: hide gdrive_* methods + * fs: add acknowledge_abuse parameter + * remove six + * Implement mv method in GDriveFileSystem + * fs: use itertools.chain.from_iterable instead of funcy.py3.cat + * add bind_addr parameter to LocalWebserverAuth + * drop Python 3.7 support + * Merge pull request from GHSA-v5f6-hjmf-9mc5 +- Drop merged modernize.patch +- Rebase migrate-to-google-auth.patch + * pr#180 was closed in favor of pr#221, which was closed as stale + +------------------------------------------------------------------- +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 + +- Disabled tests - they all fail on OBS because they need network + connectivity and a Google service account + +------------------------------------------------------------------- +Mon Jul 5 19:13:43 UTC 2021 - Martin Wilck + +- python-pydrive2 1.8.3 + * Initial package created with py2pack diff --git a/python-pydrive2.spec b/python-pydrive2.spec new file mode 100644 index 0000000..37d3dc0 --- /dev/null +++ b/python-pydrive2.spec @@ -0,0 +1,84 @@ +# +# spec file for package python-pydrive2 +# +# Copyright (c) 2025 SUSE LLC +# +# All modifications and additions to the file contributed by third parties +# remain the property of their copyright owners, unless otherwise agreed +# upon. The license for this file, and modifications and additions to the +# file, is the same license as for the pristine package itself (unless the +# license for the pristine package is not an Open Source License, in which +# case the license is the MIT License). An "Open Source License" is a +# license that conforms to the Open Source Definition (Version 1.9) +# published by the Open Source Initiative. + +# Please submit bugfixes or comments via https://bugs.opensuse.org/ +# + + +Name: python-pydrive2 +Version: 1.18.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 +# PATCH-FIX-UPSTREAM https://github.com/iterative/PyDrive2/pull/221 Migrating to Google Auth Library +Patch1: migrate-to-google-auth.patch +BuildRequires: %{python_module pip} +BuildRequires: %{python_module setuptools} +BuildRequires: %{python_module wheel} +BuildRequires: fdupes +BuildRequires: python-rpm-macros +Requires: python-PyYAML >= 3.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 +BuildArch: noarch +## tests fail in OBS environment +# SECTION test requirements +BuildRequires: %{python_module google-auth >= 2.6.6} +BuildRequires: %{python_module google-auth-httplib2 >= 0.1.0} +BuildRequires: %{python_module google-auth-oauthlib >= 0.5.1} +#BuildRequires: %{python_module google-api-python-client >= 1.12.5} +#BuildRequires: %{python_module PyYAML >= 3.0} +#BuildRequires: %{python_module black} +#BuildRequires: %{python_module flake8-docstrings} +#BuildRequires: %{python_module flake8} +#BuildRequires: %{python_module fsspec} +#BuildRequires: %{python_module pyOpenSSL >= 19.1.0} +#BuildRequires: %{python_module pytest-mock} +#BuildRequires: %{python_module pytest} +#BuildRequires: %{python_module timeout-decorator} +#BuildRequires: %{python_module tqdm} +# /SECTION +%python_subpackages + +%description +PyDrive2 is a wrapper library of google-api-python-client that simplifies many +common Google Drive API V2 tasks. It is an actively maintained fork of PyDrive. +By the authors and maintainers of the Git for Data - DVC project. + +%prep +%setup -q -n PyDrive2-%{version} +%autopatch -p1 + +%build +%pyproject_wheel + +%install +%pyproject_install +%python_expand %fdupes %{buildroot}%{$python_sitelib} + +%check +# The pydrive2 tests require network connectivity and a Google service account + +%files %{python_files} +%doc CHANGES README.rst +%license LICENSE +%{python_sitelib}/PyDrive2*-info +%{python_sitelib}/pydrive2 + +%changelog From 53451ad568eb8242e93bdd7af8d1fa2241ab93bec332aa1dba040d812154a7d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=C3=A9ta=20Machov=C3=A1?= Date: Wed, 4 Jun 2025 10:20:06 +0000 Subject: [PATCH 2/3] fix files OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-pydrive2?expand=0&rev=13 --- python-pydrive2.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-pydrive2.spec b/python-pydrive2.spec index 37d3dc0..b6416e6 100644 --- a/python-pydrive2.spec +++ b/python-pydrive2.spec @@ -78,7 +78,7 @@ By the authors and maintainers of the Git for Data - DVC project. %files %{python_files} %doc CHANGES README.rst %license LICENSE -%{python_sitelib}/PyDrive2*-info +%{python_sitelib}/[Pp]y[Dd]rive2-%{version}*-info %{python_sitelib}/pydrive2 %changelog From c8e97f10358597b72f955e73fb933ab9ddac9e998e9cd39b622e371219c68900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=C3=A9ta=20Machov=C3=A1?= Date: Mon, 9 Jun 2025 08:54:27 +0000 Subject: [PATCH 3/3] fix files OBS-URL: https://build.opensuse.org/package/show/devel:languages:python/python-pydrive2?expand=0&rev=14 --- python-pydrive2.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-pydrive2.spec b/python-pydrive2.spec index b6416e6..6d0dbf9 100644 --- a/python-pydrive2.spec +++ b/python-pydrive2.spec @@ -78,7 +78,7 @@ By the authors and maintainers of the Git for Data - DVC project. %files %{python_files} %doc CHANGES README.rst %license LICENSE -%{python_sitelib}/[Pp]y[Dd]rive2-%{version}*-info +%{python_sitelib}/[Pp]y[Dd]rive2*-info %{python_sitelib}/pydrive2 %changelog