From a91537cd58ba8a3a0ffb9def05e9d7fe9f85fc85 Mon Sep 17 00:00:00 2001 From: kuzmoyev Date: Fri, 10 Nov 2023 15:04:01 +0100 Subject: [PATCH] Support new credentials file names closes #169 --- docs/source/authentication.rst | 20 ++++++++---- docs/source/getting_started.rst | 8 ++--- gcsa/_services/authentication.py | 32 +++++++++++++++---- gcsa/event.py | 2 +- gcsa/google_calendar.py | 7 ++-- .../test_authentication.py | 31 ++++++++++++++---- 6 files changed, 71 insertions(+), 29 deletions(-) diff --git a/docs/source/authentication.rst b/docs/source/authentication.rst index 470b6e3a..0140c6af 100644 --- a/docs/source/authentication.rst +++ b/docs/source/authentication.rst @@ -8,10 +8,10 @@ There are several ways to authenticate in ``GoogleCalendar``. Credentials file ---------------- -If you have a ``credentials.json`` file (see :ref:`getting_started`), ``GoogleCalendar`` will read all the needed data -to generate the token and refresh-token from it. +If you have a ``credentials.json`` (``client_secret_*.json``) file (see :ref:`getting_started`), ``GoogleCalendar`` +will read all the needed data to generate the token and refresh-token from it. -To read ``credentials.json`` from the default path (``~/.credentials/credentials.json``) use: +To read ``credentials.json`` (``client_secret_*.json``) from the default directory (``~/.credentials``) use: .. code-block:: python @@ -19,7 +19,7 @@ To read ``credentials.json`` from the default path (``~/.credentials/credentials In this case, if ``~/.credentials/token.pickle`` file exists, it will read it and refresh only if needed. If ``token.pickle`` does not exist, it will be created during authentication flow and saved alongside with -``credentials.json`` in ``~/.credentials/token.pickle``. +``credentials.json`` (``client_secret_*.json``) in ``~/.credentials/token.pickle``. To **avoid saving** the token use: @@ -29,15 +29,21 @@ To **avoid saving** the token use: After token is generated during authentication flow, it can be accessed in ``gc.credentials`` field. -To specify ``credentials.json`` file path use ``credentials_path`` parameter: +To specify ``credentials.json`` (``client_secret_*.json``) file path use ``credentials_path`` parameter: .. code-block:: python gc = GoogleCalendar(credentials_path='path/to/credentials.json') +or + +.. code-block:: python + + gc = GoogleCalendar(credentials_path='path/to/client_secret_273833015691-qwerty.apps.googleusercontent.com.json') + Similarly, if ``token.pickle`` file exists in the same folder (``path/to/``), it will be used and refreshed only if -needed. If it doesn't exist, it will be generated and stored alongside the ``credentials.json`` (in -``path/to/token.pickle``). +needed. If it doesn't exist, it will be generated and stored alongside the ``credentials.json`` (``client_secret_*.json``) +(in ``path/to/token.pickle``). To specify different path for the pickled token file use ``token_path`` parameter: diff --git a/docs/source/getting_started.rst b/docs/source/getting_started.rst index 007ad1fa..3eea4848 100644 --- a/docs/source/getting_started.rst +++ b/docs/source/getting_started.rst @@ -33,8 +33,8 @@ Now you need to get your API credentials: .. note:: You will need to enable the "Google Calendar API" for your project. 2. `Configure the OAuth consent screen`_ - 3. `Create a OAuth client ID credential`_ and download the ``credentials.json`` file - 4. Put downloaded ``credentials.json`` file into ``~/.credentials/`` directory + 3. `Create a OAuth client ID credential`_ and download the ``credentials.json`` (``client_secret_*.json``) file + 4. Put downloaded ``credentials.json`` (``client_secret_*.json``) file into ``~/.credentials/`` directory .. _`Create a new Google Cloud Platform (GCP) project`: https://developers.google.com/workspace/guides/create-project @@ -44,7 +44,7 @@ Now you need to get your API credentials: See more options in :ref:`authentication`. - .. note:: You can put ``credentials.json`` file anywhere you want and specify + .. note:: You can put ``credentials.json`` (``client_secret_*.json``) file anywhere you want and specify the path to it in your code afterwords. But remember not to share it (e.g. add it to ``.gitignore``) as it is your private credentials. @@ -52,7 +52,7 @@ See more options in :ref:`authentication`. | On the first run, your application will prompt you to the default browser to get permissions from you to use your calendar. This will create ``token.pickle`` file in the same folder (unless specified otherwise) as your - ``credentials.json``. So don't forget to also add it to ``.gitignore`` if + ``credentials.json`` (``client_secret_*.json``). So don't forget to also add it to ``.gitignore`` if it is in a GIT repository. | If you don't want to save it in ``.pickle`` file, you can use ``save_token=False`` when initializing the ``GoogleCalendar``. diff --git a/gcsa/_services/authentication.py b/gcsa/_services/authentication.py index c94e2cce..0fb04c90 100644 --- a/gcsa/_services/authentication.py +++ b/gcsa/_services/authentication.py @@ -1,5 +1,6 @@ import pickle import os.path +import glob from typing import List from googleapiclient import discovery @@ -32,10 +33,11 @@ def __init__( Credentials with token and refresh token. If specified, ``credentials_path``, ``token_path``, and ``save_token`` are ignored. If not specified, credentials are retrieved from "token.pickle" file (specified in ``token_path`` or - default path) or with authentication flow using secret from "credentials.json" (specified in - ``credentials_path`` or default path) + default path) or with authentication flow using secret from "credentials.json" ("client_secret_*.json") + (specified in ``credentials_path`` or default path) :param credentials_path: - Path to "credentials.json" file. Default: ~/.credentials + Path to "credentials.json" ("client_secret_*.json") file. + Default: ~/.credentials/credentials.json or ~/.credentials/client_secret*.json :param token_path: Existing path to load the token from, or path to save the token after initial authentication flow. Default: "token.pickle" in the same directory as the credentials_path @@ -109,12 +111,28 @@ def _get_credentials( @staticmethod def _get_default_credentials_path() -> str: - """Checks if ".credentials" folder in home directory exists. If not, creates it. - :return: expanded path to .credentials folder + """Checks if `.credentials` folder in home directory exists and contains `credentials.json` or + `client_secret*.json` file. + + :raises ValueError: if `.credentials` folder does not exist, none of `credentials.json` or `client_secret*.json` + files do not exist, or there are multiple `client_secret*.json` files. + :return: expanded path to `credentials.json` or `client_secret*.json` file """ home_dir = os.path.expanduser('~') credential_dir = os.path.join(home_dir, '.credentials') if not os.path.exists(credential_dir): - os.makedirs(credential_dir) + raise FileNotFoundError(f'Default credentials directory "{credential_dir}" does not exist.') credential_path = os.path.join(credential_dir, 'credentials.json') - return credential_path + if os.path.exists(credential_path): + return credential_path + else: + credentials_files = glob.glob(credential_dir + '/client_secret*.json') + if len(credentials_files) > 1: + raise ValueError(f"Multiple credential files found in {credential_dir}.\n" + f"Try specifying the credentials file, e.x.:\n" + f"GoogleCalendar(credentials_path='{credentials_files[0]}')") + elif not credentials_files: + raise FileNotFoundError(f'Credentials file (credentials.json or client_secret*.json)' + f'not found in the default path: "{credential_dir}".') + else: + return credentials_files[0] diff --git a/gcsa/event.py b/gcsa/event.py index c25432c4..e3ed1047 100644 --- a/gcsa/event.py +++ b/gcsa/event.py @@ -278,7 +278,7 @@ def __repr__(self): def __lt__(self, other): def ensure_datetime(d, timezone): - if type(d) == date: + if type(d) is date: return ensure_localisation(datetime(year=d.year, month=d.month, day=d.day), timezone) else: return d diff --git a/gcsa/google_calendar.py b/gcsa/google_calendar.py index eeca51c4..3356c579 100644 --- a/gcsa/google_calendar.py +++ b/gcsa/google_calendar.py @@ -47,10 +47,11 @@ def __init__( Credentials with token and refresh token. If specified, ``credentials_path``, ``token_path``, and ``save_token`` are ignored. If not specified, credentials are retrieved from "token.pickle" file (specified in ``token_path`` or - default path) or with authentication flow using secret from "credentials.json" (specified in - ``credentials_path`` or default path) + default path) or with authentication flow using secret from "credentials.json" ("client_secret_*.json") + (specified in ``credentials_path`` or default path) :param credentials_path: - Path to "credentials.json" file. Default: ~/.credentials + Path to "credentials.json" ("client_secret_*.json") file. + Default: ~/.credentials/credentials.json or ~/.credentials/client_secret*.json :param token_path: Existing path to load the token from, or path to save the token after initial authentication flow. Default: "token.pickle" in the same directory as the credentials_path diff --git a/tests/google_calendar_tests/test_authentication.py b/tests/google_calendar_tests/test_authentication.py index 9e868de3..7991a5ad 100644 --- a/tests/google_calendar_tests/test_authentication.py +++ b/tests/google_calendar_tests/test_authentication.py @@ -13,7 +13,7 @@ class TestGoogleCalendarCredentials(TestCase): def setUp(self): self.setUpPyfakefs() - self.credentials_dir = '/.credentials' + self.credentials_dir = path.join(path.expanduser('~'), '.credentials') self.credentials_path = path.join(self.credentials_dir, 'credentials.json') self.fs.create_dir(self.credentials_dir) self.fs.create_file(self.credentials_path) @@ -53,20 +53,37 @@ def test_with_given_credentials_expired(self): self.assertTrue(gc.credentials.valid) self.assertFalse(gc.credentials.expired) - def test_get_default_credentials_path_exist(self): - self.fs.create_dir(path.join(path.expanduser('~'), '.credentials')) + def test_get_default_credentials_exist(self): self.assertEqual( - path.join(path.expanduser('~'), '.credentials/credentials.json'), + self.credentials_path, GoogleCalendar._get_default_credentials_path() ) def test_get_default_credentials_path_not_exist(self): - self.assertFalse(path.exists(path.join(path.expanduser('~'), '.credentials'))) + self.fs.reset() + with self.assertRaises(FileNotFoundError): + GoogleCalendar._get_default_credentials_path() + + def test_get_default_credentials_not_exist(self): + self.fs.remove(self.credentials_path) + with self.assertRaises(FileNotFoundError): + GoogleCalendar._get_default_credentials_path() + + def test_get_default_credentials_client_secrets(self): + self.fs.remove(self.credentials_path) + client_secret_path = path.join(self.credentials_dir, 'client_secret_1234.json') + self.fs.create_file(client_secret_path) self.assertEqual( - path.join(path.expanduser('~'), '.credentials/credentials.json'), + client_secret_path, GoogleCalendar._get_default_credentials_path() ) - self.assertTrue(path.exists(path.join(path.expanduser('~'), '.credentials'))) + + def test_get_default_credentials_multiple_client_secrets(self): + self.fs.remove(self.credentials_path) + self.fs.create_file(path.join(self.credentials_dir, 'client_secret_1234.json')) + self.fs.create_file(path.join(self.credentials_dir, 'client_secret_12345.json')) + with self.assertRaises(ValueError): + GoogleCalendar._get_default_credentials_path() def test_get_token_valid(self): gc = GoogleCalendar(token_path=self.valid_token_path)