├── tests ├── __init__.py ├── conf_test.py ├── client_test.py ├── spread_test.py ├── cassettes │ ├── tests.client_test.TestClient.test_email.json │ ├── tests.client_test.TestClient.test_root.json │ ├── tests.client_test.TestClient.test_directories.json │ ├── tests.client_test.TestClient.test_email_no_perm.json │ ├── tests.client_test.TestClient.test_find_folders.json │ ├── tests.client_test.TestClient.test_create_folder_no_parents.json │ ├── tests.client_test.TestClient.test_create_folder.json │ ├── tests.client_test.TestClient.test_find_spreadsheet_files_in_folder.json │ ├── tests.client_test.TestClient.test_list_spreadsheet_files.json │ └── tests.client_test.TestClient.test_find_spreadsheet_files_in_folders.json ├── conftest.py └── util_test.py ├── docs ├── source │ ├── changelog.rst │ ├── contributing.rst │ ├── getting_started.rst │ ├── modules.rst │ ├── index.rst │ ├── gspread_pandas.rst │ ├── configuration.rst │ ├── using.rst │ └── conf.py ├── make.bat └── Makefile ├── push.sh ├── MANIFEST.in ├── requirements.txt ├── gspread_pandas ├── _version.py ├── exceptions.py ├── __init__.py ├── conf.py └── client.py ├── requirements_dev.txt ├── pyproject.toml ├── .pre-commit-config.yaml ├── .gitignore ├── setup.cfg ├── .github └── workflows │ ├── python-publish.yml │ ├── tagged-release.yml │ └── python-package.yml ├── LICENSE.rst ├── setup.py ├── CONTRIBUTING.rst └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/source/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../README.rst 2 | -------------------------------------------------------------------------------- /push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | tox && git push origin master --tags 4 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | Modules 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | gspread_pandas 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.rst 2 | include README.rst 3 | include requirements.txt 4 | include requirements_dev.txt 5 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Reqs 2 | gspread>=5.0.0, <6 3 | pandas>=0.20.0 4 | decorator 5 | google-auth 6 | google-auth-oauthlib 7 | -------------------------------------------------------------------------------- /gspread_pandas/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = "3.3.0" 4 | __version_info__ = tuple([int(x) for x in __version__.split(".")]) 5 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pip 2 | sphinx 3 | sphinx_rtd_theme 4 | coverage 5 | twine 6 | bump2version 7 | tox 8 | flake8 9 | pytest 10 | pre-commit 11 | isort 12 | black 13 | pycryptodome 14 | betamax 15 | betamax_serializers 16 | pytest_mock 17 | oauth2client 18 | wheel 19 | -------------------------------------------------------------------------------- /gspread_pandas/exceptions.py: -------------------------------------------------------------------------------- 1 | class GspreadPandasException(Exception): 2 | pass 3 | 4 | 5 | class ConfigException(GspreadPandasException): 6 | pass 7 | 8 | 9 | class NoWorksheetException(GspreadPandasException): 10 | pass 11 | 12 | 13 | class MissMatchException(GspreadPandasException): 14 | pass 15 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :maxdepth: 5 3 | 4 | getting_started 5 | configuration 6 | using 7 | modules 8 | contributing 9 | 10 | .. toctree:: 11 | :maxdepth: 1 12 | 13 | changelog 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | 4 | [tool.isort] 5 | multi_line_output = 3 6 | include_trailing_comma = true 7 | force_grid_wrap = 0 8 | combine_as_imports = true 9 | line_length = 88 10 | known_third_party = ["Crypto", "betamax", "betamax_serializers", "google", "google_auth_oauthlib", "gspread", "numpy", "oauth2client", "pandas", "pytest", "requests", "setuptools"] 11 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.9 3 | 4 | repos: 5 | - repo: https://github.com/ambv/black 6 | rev: 21.12b0 7 | hooks: 8 | - id: black 9 | - repo: https://gitlab.com/pycqa/flake8 10 | rev: 3.9.2 11 | hooks: 12 | - id: flake8 13 | # - repo: https://github.com/asottile/seed-isort-config 14 | # rev: v2.2.0 15 | # hooks: 16 | # - id: seed-isort-config 17 | - repo: https://github.com/pre-commit/mirrors-isort 18 | rev: v5.10.1 19 | hooks: 20 | - id: isort 21 | additional_dependencies: [toml] 22 | -------------------------------------------------------------------------------- /docs/source/gspread_pandas.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | gspread_pandas 3 | ============== 4 | 5 | Submodules 6 | ========== 7 | 8 | gspread_pandas.spread module 9 | ---------------------------- 10 | 11 | .. automodule:: gspread_pandas.spread 12 | :members: 13 | :undoc-members: 14 | :show-inheritance: 15 | 16 | gspread_pandas.client module 17 | ---------------------------- 18 | 19 | .. automodule:: gspread_pandas.client 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | 24 | gspread_pandas.conf module 25 | -------------------------- 26 | 27 | .. automodule:: gspread_pandas.conf 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # pycharm 7 | .idea/ 8 | .idea 9 | 10 | # Packages 11 | *.egg 12 | *.egg-info 13 | .eggs 14 | .cache 15 | build 16 | eggs 17 | parts 18 | bin 19 | var 20 | sdist 21 | develop-eggs 22 | .installed.cfg 23 | lib 24 | lib64 25 | dist/ 26 | 27 | # Installer logs 28 | pip-log.txt 29 | pip-wheel-metadata/ 30 | 31 | # Unit test / coverage reports 32 | .coverage 33 | .tox 34 | nosetests.xml 35 | .pytest_cache 36 | htmlcov 37 | *,cover 38 | 39 | # Complexity 40 | output/*.html 41 | output/*/index.html 42 | 43 | # Sphinx 44 | docs/_build 45 | 46 | # Cookiecutter 47 | output/ 48 | 49 | # Emacs 50 | *~ 51 | \#* 52 | *# 53 | 54 | # Pyenv 55 | .python-version* 56 | 57 | google_secret.json 58 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.3.0 3 | commit = True 4 | tag = True 5 | 6 | [bdist_wheel] 7 | universal = 1 8 | 9 | [metadata] 10 | description-file = README.rst 11 | 12 | [aliases] 13 | test = pytest 14 | 15 | [flake8] 16 | ignore = E203,E266,W503 17 | max-line-length = 88 18 | max-complexity = 18 19 | select = B,C,E,F,W,T4,B9 20 | 21 | [tox:tox] 22 | envlist = py36, py37, py38, py39, py310, flake8 23 | 24 | [testenv:flake8] 25 | basepython = python 26 | deps = flake8 27 | commands = flake8 gspread_pandas 28 | 29 | [testenv] 30 | setenv = 31 | PYTHONPATH = {toxinidir} 32 | deps = -r requirements_dev.txt 33 | commands = python setup.py test 34 | 35 | [bumpversion:file:gspread_pandas/_version.py] 36 | search = __version__ = "{current_version}" 37 | replace = __version__ = "{new_version}" 38 | 39 | [bumpversion:file:CHANGELOG.rst] 40 | search = [Unreleased] 41 | ------------ 42 | replace = [Unreleased] 43 | ------------ 44 | [{new_version}] - {now:%%Y-%%m-%%d} 45 | ----------------------------- 46 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [created] 14 | 15 | jobs: 16 | deploy: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.x' 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | - name: Build package 31 | run: python -m build 32 | - name: Publish package 33 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 34 | with: 35 | user: __token__ 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /gspread_pandas/__init__.py: -------------------------------------------------------------------------------- 1 | from gspread import Spreadsheet 2 | from gspread.urls import SPREADSHEETS_API_V4_BASE_URL 3 | 4 | from ._version import __version__, __version_info__ 5 | from .client import Client 6 | from .spread import Spread 7 | 8 | __all__ = ["Spread", "Client", "__version__", "__version_info__"] 9 | SPREADSHEET_VALUES_BATCH_URL = SPREADSHEETS_API_V4_BASE_URL + "/%s/values:batchGet" 10 | 11 | 12 | def values_batch_get(self, ranges, params=None): 13 | """ 14 | Lower-level method that directly calls `spreadsheets.values.batchGet. 15 | 16 | `_. 18 | 19 | Parameters 20 | ---------- 21 | rantes : list of strings 22 | List of ranges in the `A1 notation 23 | params : dict 24 | (optional) Query parameters 25 | 26 | Returns 27 | ------- 28 | dict 29 | Response body 30 | """ 31 | if params is None: 32 | params = {} 33 | 34 | params.update(ranges=ranges) 35 | 36 | url = SPREADSHEET_VALUES_BATCH_URL % (self.id) 37 | r = self.client.request("get", url, params=params) 38 | return r.json() 39 | 40 | 41 | Spreadsheet.values_batch_get = values_batch_get 42 | -------------------------------------------------------------------------------- /.github/workflows/tagged-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will create a release from a version tag 2 | 3 | name: Release from Tag 4 | 5 | on: 6 | push: 7 | tags: 8 | - "v*" 9 | 10 | jobs: 11 | tagged-release: 12 | name: "Tagged Release" 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 26 | if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi 27 | - name: Lint with flake8 28 | run: | 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 33 | - name: Test with pytest 34 | run: | 35 | pytest 36 | 37 | - uses: "marvinpinto/action-automatic-releases@latest" 38 | with: 39 | repo_token: "${{ secrets.RELEASE_ACCESS_TOKEN }}" 40 | prerelease: false 41 | files: | 42 | * 43 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Lint & Test 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v2 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 31 | if [ -f requirements_dev.txt ]; then pip install -r requirements_dev.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pytest 41 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Diego Fernandez All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from codecs import open 2 | from os import path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | __version__ = "version read in next line" 7 | exec(open("gspread_pandas/_version.py").read()) 8 | 9 | here = path.abspath(path.dirname(__file__)) 10 | 11 | # Get the long description from the README file 12 | with open(path.join(here, "README.rst"), encoding="utf-8") as f: 13 | long_description = f.read() 14 | 15 | # get the dependencies and installs 16 | with open(path.join(here, "requirements.txt"), encoding="utf-8") as f: 17 | all_reqs = f.read().split("\n") 18 | 19 | install_requires = [x.strip() for x in all_reqs if "git+" not in x] 20 | dependency_links = [ 21 | x.strip().replace("git+", "") for x in all_reqs if x.startswith("git+") 22 | ] 23 | 24 | # get the dependencies and installs 25 | with open(path.join(here, "requirements_dev.txt"), encoding="utf-8") as f: 26 | dev_requires = f.read().split("\n") 27 | 28 | setup( 29 | name="gspread-pandas", 30 | version=__version__, 31 | description=( 32 | "A package to easily open an instance of a Google spreadsheet and " 33 | "interact with worksheets through Pandas DataFrames." 34 | ), 35 | long_description=long_description, 36 | url="https://github.com/aiguofer/gspread-pandas", 37 | download_url="https://github.com/aiguofer/gspread-pandas/tarball/v" + __version__, 38 | license="BSD", 39 | classifiers=[ 40 | "Development Status :: 4 - Beta", 41 | "Intended Audience :: Developers", 42 | "Intended Audience :: Science/Research", 43 | "Programming Language :: Python :: 3", 44 | ], 45 | keywords="gspread pandas google spreadsheets", 46 | packages=find_packages(exclude=["docs", "tests*"]), 47 | include_package_data=True, 48 | author="Diego Fernandez", 49 | install_requires=install_requires, 50 | setup_requires=["pytest-runner"], 51 | tests_require=["pytest"], 52 | extras_require={"dev": dev_requires}, 53 | dependency_links=dependency_links, 54 | author_email="aiguo.fernandez@gmail.com", 55 | ) 56 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Code should be run through black, isort, and flake8 before being merged. Pre-commit 5 | takes care of it for you, but you need to have Python 3 installed to be able to run 6 | black. To contribute, please fork the repo, create a feature branch, push it to your 7 | repo, then create a pull request. 8 | 9 | To install and set up the environment after you fork it (replace `aiguofer` with your 10 | username): 11 | 12 | .. code-block:: console 13 | 14 | $ git clone https://github.com/aiguofer/gspread-pandas.git && cd gspread-pandas 15 | $ pip install -e ".[dev]" 16 | $ pre-commit install 17 | 18 | Testing 19 | ------- 20 | 21 | Our tests levarage `betamax `__ to remember HTTP 22 | interactions with the API. In order to add new tests that change the requests to 23 | betamax, you'll need to have Service Account credentials stored as ``google_secret.json`` 24 | in the root project directory. You can then re-record tests by deleting the necessary 25 | cassetes in ``tests/cassettes`` then running: 26 | 27 | .. code-block:: console 28 | 29 | $ GSPREAD_RECORD=true pytest 30 | 31 | NOTE: Currently, the tests don't do any setup and teardown of expected directories/files 32 | in the Google Drive. My main concern in implementing this is that somehow it might 33 | mistakenly use a specific user's credentials and delete important stuff. If you have 34 | any ideas here I'd be happy to discuss. 35 | 36 | Versions 37 | -------- 38 | 39 | In order to bump versions, we use `bump2version `__. 40 | This will take care of adding an entry in the CHANGELOG for the new version and bumping 41 | the version everywhere it needs to. This will also create a git tag for the specific 42 | version. 43 | 44 | 45 | CI 46 | --- 47 | 48 | CI is managed by Github Actions: 49 | python-package.yml - workflow for testing and linting for each python version on every push 50 | tagged-release.yml - workflow that does ^ and then creates a github release only on tagged push 51 | python-publish.yml - workflow that publishes package to PyPi when a github release is created 52 | -------------------------------------------------------------------------------- /tests/conf_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import PosixPath, WindowsPath 3 | 4 | import pytest 5 | from google.oauth2.credentials import Credentials as OAuth2Credentials 6 | from google.oauth2.service_account import Credentials as ServiceAccountCredentials 7 | 8 | from gspread_pandas import conf, exceptions 9 | 10 | 11 | def test_get_config_dir(): 12 | conf_dir = conf.get_config_dir() 13 | if os.name == "nt": 14 | assert isinstance(conf_dir, WindowsPath) 15 | assert "AppData" in str(conf_dir) 16 | else: 17 | assert isinstance(conf_dir, PosixPath) 18 | assert ".config" in str(conf_dir) 19 | 20 | 21 | class Test_get_config: 22 | def test_no_file(self): 23 | with pytest.raises(IOError): 24 | conf.get_config(file_name="this_file_doesnt_exist") 25 | 26 | def test_with_oauth(self, oauth_config): 27 | c = conf.get_config(*oauth_config) 28 | assert isinstance(c, dict) 29 | assert len(c) == 1 30 | assert len(c[list(c.keys())[0]]) > 1 31 | 32 | def test_with_sa(self, sa_config): 33 | c = conf.get_config(*sa_config) 34 | assert isinstance(c, dict) 35 | assert len(c) > 1 36 | 37 | 38 | class Test_get_creds: 39 | def test_service_account(self, set_sa_config): 40 | creds = conf.get_creds() 41 | assert isinstance(creds, ServiceAccountCredentials) 42 | 43 | def test_oauth_no_key(self, set_oauth_config): 44 | with pytest.raises(exceptions.ConfigException): 45 | conf.get_creds(user=None) 46 | 47 | def test_oauth_first_time(self, mocker, set_oauth_config, creds_json): 48 | mocked = mocker.patch.object(conf.InstalledAppFlow, "run_local_server") 49 | mocked.return_value = OAuth2Credentials.from_authorized_user_info(creds_json) 50 | conf.get_creds() 51 | # python 3.5 doesn't have assert_called_once 52 | assert mocked.call_count == 1 53 | assert (conf.get_config_dir() / "creds" / "default").exists() 54 | 55 | def test_oauth_first_time_no_save(self, mocker, set_oauth_config): 56 | mocker.patch.object(conf.InstalledAppFlow, "run_local_server") 57 | conf.get_creds(save=False) 58 | # python 3.5 doesn't have assert_called_once 59 | assert conf.InstalledAppFlow.run_local_server.call_count == 1 60 | 61 | def test_oauth_default(self, make_creds): 62 | assert isinstance(conf.get_creds(), OAuth2Credentials) 63 | 64 | @pytest.mark.skip(reason="need to fix this test") 65 | def test_bad_config(self, set_sa_config): 66 | with pytest.raises(exceptions.ConfigException): 67 | conf.get_creds(config={"foo": "bar"}) 68 | -------------------------------------------------------------------------------- /tests/client_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from gspread_pandas import Client 4 | 5 | 6 | @pytest.mark.usefixtures("betamax_client") 7 | class TestClient: 8 | # just here just for autocompletion during development, during 9 | # test run this will be overridden by the above fixture 10 | client = Client 11 | 12 | def test_root(self): 13 | root = self.client.root 14 | assert isinstance(root, dict) 15 | assert {"id", "name", "path"} == set(root.keys()) 16 | 17 | def test_directories(self): 18 | dirs = self.client.directories 19 | for d in dirs: 20 | assert isinstance(d, dict) 21 | assert {"name", "id", "path"} == set(d.keys()) 22 | 23 | def test_email(self): 24 | assert isinstance(self.client.auth.service_account_email, str) 25 | 26 | def test_email_no_perm(self, betamax_client_bad_scope, capsys): 27 | betamax_client_bad_scope.email 28 | captured = capsys.readouterr() 29 | assert "Couldn't retrieve" in str(captured) 30 | 31 | def test_list_spreadsheet_files(self): 32 | self.client.refresh_directories() 33 | files = self.client.list_spreadsheet_files() 34 | assert isinstance(files, list) 35 | 36 | for f in files: 37 | assert isinstance(f, dict) 38 | assert {"name", "id", "path"} == set(f.keys()) 39 | 40 | def test_find_spreadsheet_files_in_folder(self): 41 | self.client.refresh_directories() 42 | files = self.client.list_spreadsheet_files_in_folder("root") 43 | 44 | assert isinstance(files, list) 45 | 46 | for f in files: 47 | assert f["path"] == "/" 48 | 49 | def test_find_folders(self): 50 | dirs = self.client.directories 51 | dirs_sub = self.client.find_folders("Sub") 52 | 53 | assert set(d["id"] for d in dirs_sub) < set(d["id"] for d in dirs) 54 | 55 | def test_find_spreadsheet_files_in_folders(self): 56 | files = self.client.find_spreadsheet_files_in_folders("sub") 57 | 58 | assert isinstance(files, dict) 59 | assert all("sub" in k.lower() for k in files.keys()) 60 | assert all(isinstance(v, list) for v in files.values()) 61 | 62 | def test_create_folder(self): 63 | self.client.create_folder("/this/is/a/new/dir") 64 | assert ( 65 | next( 66 | d 67 | for d in self.client.directories 68 | if "/this/is/a/new" in d["path"] and d["name"] == "dir" 69 | ) 70 | is not None 71 | ) 72 | 73 | def test_create_folder_no_parents(self): 74 | with pytest.raises(Exception): 75 | self.client.create_folder("/this/does/not/exist", parents=False) 76 | 77 | @pytest.mark.skip(reason="unsure why test isn't working, but manual testing works") 78 | def test_move_file(self): 79 | test_spread = self.client.create("test") 80 | test_spread_id = test_spread.id 81 | self.client.move_file(test_spread_id, "/this/is/a/new/dir") 82 | 83 | assert test_spread_id not in [ 84 | f["id"] for f in self.client.list_spreadsheet_files_in_folder("root") 85 | ] 86 | 87 | new_dir = next( 88 | d 89 | for d in self.client.directories 90 | if "/this/is/a/new" in d["path"] and d["name"] == "dir" 91 | ) 92 | 93 | assert test_spread_id in [ 94 | f["id"] for f in self.client.list_spreadsheet_files_in_folder(new_dir["id"]) 95 | ] 96 | 97 | self.client.del_spreadsheet(test_spread_id) 98 | -------------------------------------------------------------------------------- /tests/spread_test.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import pytest 3 | from gspread import Worksheet 4 | 5 | from gspread_pandas import Spread, util 6 | 7 | 8 | @pytest.mark.usefixtures("betamax_spread") 9 | class TestSpread: 10 | spread = Spread 11 | 12 | def test_spread(self): 13 | assert isinstance(self.spread, Spread) 14 | assert self.spread.email 15 | assert self.spread.url 16 | assert isinstance(self.spread.sheets, list) 17 | assert isinstance(self.spread.sheet, Worksheet) 18 | assert self.spread.sheets[0].id == self.spread.sheet.id 19 | 20 | def test_open_sheet(self): 21 | sheet = self.spread.sheets[0] 22 | self.spread.open_sheet(0) 23 | by_index = self.spread.sheet 24 | self.spread.open_sheet(sheet) 25 | by_sheet = self.spread.sheet 26 | self.spread.open_sheet(sheet.title) 27 | by_name = self.spread.sheet 28 | 29 | assert by_index.id == by_sheet.id == by_name.id 30 | 31 | def test_df(self): 32 | df = self.spread.sheet_to_df( 33 | header_rows=2, start_row=2, formula_columns=["Total"] 34 | ) 35 | 36 | df_to_sheet_name = "Test df_to_sheet" 37 | 38 | assert isinstance(df.columns, pd.MultiIndex) 39 | assert df.shape == (3, 9) 40 | assert df["Total"][0].startswith("=") 41 | 42 | self.spread.df_to_sheet( 43 | df, 44 | start="A2", 45 | replace=True, 46 | sheet=df_to_sheet_name, 47 | freeze_index=True, 48 | freeze_headers=True, 49 | add_filter=True, 50 | merge_headers=True, 51 | ) 52 | 53 | # ensre values are the same 54 | assert ( 55 | self.spread.sheets[1].get_all_values() 56 | == self.spread.sheets[2].get_all_values() 57 | ) 58 | 59 | sheets_metadata = self.spread._spread_metadata["sheets"] 60 | 61 | # ensure merged cells match 62 | assert util.remove_keys_from_list( 63 | sheets_metadata[1]["merges"], ["sheetId"] 64 | ) == util.remove_keys_from_list(sheets_metadata[2]["merges"], ["sheetId"]) 65 | 66 | # ensure basic filter matches 67 | assert util.remove_keys( 68 | sheets_metadata[1]["basicFilter"]["range"], ["sheetId"] 69 | ) == util.remove_keys(sheets_metadata[2]["basicFilter"]["range"], ["sheetId"]) 70 | 71 | # ensure frozen cols/rows and dims match 72 | assert ( 73 | sheets_metadata[1]["properties"]["gridProperties"] 74 | == sheets_metadata[2]["properties"]["gridProperties"] 75 | ) 76 | 77 | self.spread.open_sheet(df_to_sheet_name) 78 | 79 | self.spread.unmerge_cells() 80 | # sometimes it fetches the data too quickly and it hasn't 81 | # updated 82 | sheets_metadata = self.spread._spread_metadata["sheets"] 83 | 84 | # ensure merged cells don't match 85 | assert util.remove_keys_from_list( 86 | sheets_metadata[1]["merges"], ["sheetId"] 87 | ) != util.remove_keys_from_list( 88 | sheets_metadata[2].get("merges", {}), ["sheetId"] 89 | ) 90 | 91 | raw_sheet = "Raw" 92 | self.spread.df_to_sheet( 93 | df[["Total"]], index=False, sheet=raw_sheet, raw_columns=["Total"] 94 | ) 95 | 96 | assert any(row[0].startswith("=") for row in self.spread.sheet.get_all_values()) 97 | 98 | self.spread.delete_sheet(df_to_sheet_name) 99 | self.spread.delete_sheet(raw_sheet) 100 | -------------------------------------------------------------------------------- /docs/source/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | By default, the configuration will be in ``$HOME/.config/gspread_pandas`` on Nix systems and 5 | ``%APPDATA%\gspread_pandas`` on Windows. Under the default behavior, you must have your Google 6 | client credentials stored in ``google_secret.json`` in that directory. If you're not using a 7 | Service Account, the user credentials will be stored in a subdirectory called ``creds``. 8 | 9 | App Credentials 10 | --------------- 11 | 12 | There's 2 main types of app credentials: OAuth client and Service Account. In order to act as 13 | your own Google user, you will need the OAuth client app credentials. With 14 | this type of credentials, each user will need to grant permissions to your app. When they 15 | grant permissions, their credentials will be stored as described below. 16 | 17 | As a Service Account, the used credentials will be for the service account itself. This means 18 | that you'll be using the service account's e-mail and Google drive. Additionally, it will only 19 | be able to work withSpreadsheets that it has permissions for. Although Service Accounts can be 20 | useful for batch processes, you might generally prefer to work as your own user. 21 | 22 | 23 | User Credentials 24 | ---------------- 25 | 26 | Once you have your client credentials, you can have multiple user 27 | credentials stored in the same machine. This can be useful when you have 28 | a shared server (for example with a Jupyter notebook server) with 29 | multiple people that may want to use the library. The ``user`` parameter to 30 | ``Spread`` must be the key identifying a user's credentials, by default it 31 | will store the creds using ``default`` as the key. The first 32 | ``get_creds`` is called for a specific key, you will have to authenticate 33 | through a text based OAuth prompt; this makes it possible to run on a headless 34 | server through ssh or through a Jupyter notebook. After this, the 35 | credentials for that user will be stored in the ``creds`` subdirectory and the 36 | tokens will berefreshed automatically any time the tool is used. 37 | 38 | Users will only be able to interact with Spreadsheets that they have 39 | access to. 40 | 41 | Authentication 42 | ----------------------- 43 | 44 | In the backend, the library is leveraging 45 | `Google's google-auth `__ to 46 | handle authentication. It conveniently stores everything as described 47 | above so that you don't have to worry about boiler plate code to handle auth. 48 | 49 | When a ``Client`` is instanciated, an ``AuthorizedSession`` is created using the 50 | credentials and this is what's used to make requests to the API. This takes care 51 | of handling token refreshes and retries for you. 52 | 53 | Alternate Workflows 54 | ------------------- 55 | 56 | There's a variety of ways to change the default behavior of how/where configuration is stored. 57 | 58 | The easiest way to change the default location is to set the ``GSPREAD_PANDAS_CONFIG_DIR`` 59 | env variable to the directory where you want to store everything. If you use this, the 60 | client creds will still need to be named ``google_secret.json`` and user creds will still 61 | be stored in the ``creds`` subdirectory. 62 | 63 | If you have different client credentials, you could load them passing in ``conf_dir`` and/or 64 | ``file_name`` to ``gspread_pandas.conf.get_config``. Alternatively, you could pull these from 65 | elsewhere, like a database. Once you have the config, you could then pass that to a 66 | ``Client`` or ``Spread`` instance, or you could get credentials by passing it to 67 | ``gspread_pandas.conf.get_creds``. 68 | 69 | When using a Service Account, the ``user`` param will be ignored in ``Client``, ``Spread`` and 70 | ``get_creds``. Otherwise, this param will be used to store the OAuth2 credentials for each user in the 71 | creds subdirectory. If you generate your credentials elsewhere, you can pass them in to a ``Client`` 72 | or ``Spread``. You can also run through the flow to get OAuth2 and avoid saving them by calling 73 | ``get_creds`` directly. You can also override the ``creds_dir`` if you call this function. 74 | -------------------------------------------------------------------------------- /docs/source/using.rst: -------------------------------------------------------------------------------- 1 | Using Gspread-Pandas 2 | ==================== 3 | 4 | There are two main objects you will interact with in ``gspread-pandas``: the ``Client`` and the ``Spread`` objects. The goal of these objects is to make it easy to work with a variety of concepts in Google Sheets and Pandas DataFrames. A lot of care has gone into documenting functions in code to make code introspection tools useful (displaying documentation, code completion, etc). 5 | 6 | The target audience are Data Analysts and Data Scientists, but this can also be used by Data Engineers or anyone trying to automate workflows with Google Sheets and Pandas. 7 | 8 | Client 9 | ------ 10 | 11 | The ``Client`` extends the ``Client`` object from ``gspread`` to add some functionality. I try to contribute back to the upstream project, but some things don't make it in, and others don't belong. 12 | 13 | The main things that are added by the ``Client`` are: 14 | 15 | - Handling credentials and authentication to reduce boilerplate code. 16 | - Store file paths within drive for more working with files in a more intuitive manner (requires passing ``load_dirs=True`` or calling ``Client.refresh_directories()`` if you've already instantiated a ``Client``) 17 | - A variety of functions to query for and work with Spreadsheets in your Google Drive, mainly: 18 | 19 | - ``list_spreadsheet_files`` 20 | - ``list_spreadsheet_files_in_folder`` 21 | - ``find_folders`` 22 | - ``find_spreadssheet_files_in_folders`` 23 | - ``create_folder`` 24 | - ``move_file`` 25 | - Monkey patch the request to automatically retry when there is a 100 second quota exhausted error. 26 | 27 | You can read more :class:`in the docs for the Client object `. 28 | 29 | Spread 30 | ------ 31 | 32 | The ``Spread`` object represents an open Google Spreadsheet. A ``Spread`` object has multiple Worksheets, and only one can be open at any one time. Any function you call will act on the currently open Worksheet, unless you pass ``sheet=`` when you call the function, in which case it will first open that Worksheet and then perform the action. A ``Spread`` object internally aso holds an instance of a ``Client`` to do the majority of the work. 33 | 34 | The ``Spread`` object does a lot of stuff to make it easier for working with Google Spreadsheets. For example, it can handle merged cells, frozen rows/columns, data filters, multi-level column headers, permissions, and more. Some things can be called individually, others can be passed in as function parameters. It can also work with tuples (for example ``(1, 1)``) or A1 notation for specifying cells. 35 | 36 | Some of the most important properties of a ``Spread`` object are: 37 | 38 | - ``spread``: The currently open Spreadsheet (this is a ``gspread`` object) 39 | - ``sheet``: The currently open Worksheet (this is a ``gspread`` object) 40 | - ``client``: The ``Client`` object. This will be automatically created if one is not passed in, but you can also share the same ``Client`` instance among multiple ``Spread`` objects if you pass it in. 41 | - ``sheets``: The list of all available Worksheets 42 | - ``_sheet_metadata``: We store metadata about the sheet, which includes stuff like merged cells, frozen columns, and frozen rows. This is a private property, but you can refresh this with ``refresh_spread_metadata()`` 43 | 44 | Some of the most useful functions are: 45 | 46 | - ``sheet_to_df``: Create a Pandas DataFrame from a Worksheet 47 | - ``clear_sheet``: Clear out all values and resize a Worksheet 48 | - ``delete_sheet``: Delete a Worksheet 49 | - ``df_to_sheet``: Create a Worksheet from a Pandas DataFrame 50 | - ``freeze``: Freeze a given number of rows and/or columns 51 | - ``add_filter``: Add a filter to the Worksheet for the given range of data 52 | - ``merge_cells``: Merge cells in a Worksheet 53 | - ``unmerge_cells``: Unmerge cells within a range 54 | - ``add_permission``: Add a permission to a Spreadsheet 55 | - ``add_permissions``: Add multiple permissions to a Spreadsheet 56 | - ``list_permissions``: Show all current permissions on the Spreadsheet 57 | - ``move``: Move the Spreadsheet to a different location 58 | 59 | You can read more :class:`in the docs for the Spread object `. 60 | -------------------------------------------------------------------------------- /gspread_pandas/conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from os import environ, name 4 | from pathlib import Path 5 | 6 | from google.oauth2.credentials import Credentials as OAuthCredentials 7 | from google.oauth2.service_account import Credentials as SACredentials 8 | from google_auth_oauthlib.flow import InstalledAppFlow 9 | 10 | from gspread_pandas.exceptions import ConfigException 11 | from gspread_pandas.util import decode 12 | 13 | __all__ = ["default_scope", "get_config", "get_creds"] 14 | if name == "nt": 15 | _default_dir = Path(environ.get("APPDATA")) / "gspread_pandas" 16 | else: 17 | _default_dir = ( 18 | Path(environ.get("XDG_CONFIG_HOME", Path(environ.get("HOME", "")) / ".config")) 19 | / "gspread_pandas" 20 | ) 21 | _default_file = "google_secret.json" 22 | 23 | default_scope = [ 24 | "openid", 25 | "https://www.googleapis.com/auth/drive", 26 | "https://www.googleapis.com/auth/userinfo.email", 27 | "https://www.googleapis.com/auth/spreadsheets", 28 | ] 29 | 30 | CONFIG_DIR_ENV_VAR = "GSPREAD_PANDAS_CONFIG_DIR" 31 | 32 | 33 | def get_config_dir(): 34 | """ 35 | Get the config directory. 36 | 37 | It will first look in the environment variable 38 | GSPREAD_PANDAS_CONFIG_DIR, but if it's not set it'll use 39 | ~/.config/gspread_pandas 40 | """ 41 | return Path(environ.get(CONFIG_DIR_ENV_VAR, _default_dir)) 42 | 43 | 44 | def ensure_path(full_path): 45 | """ 46 | Create path if it doesn't exist. 47 | 48 | Parameters 49 | ---------- 50 | full_path : str 51 | Path to create if needed 52 | 53 | Returns 54 | ------- 55 | None 56 | """ 57 | full_path = Path(full_path) 58 | if not full_path.exists(): 59 | full_path.mkdir(parents=True, exist_ok=True) 60 | 61 | 62 | def get_config(conf_dir=None, file_name=_default_file): 63 | """ 64 | Get config for Google client. Looks in ~/.config/gspread_pandas/google_secret.json 65 | by default but you can override it with conf_dir and file_name. The creds_dir value 66 | will be set to conf_dir/creds and the directory will be created if it doesn't exist; 67 | if you'd like to override that you can do so by changing the 'creds_dir' value in 68 | the dict returned by this function. 69 | 70 | Download json from https://console.developers.google.com/apis/credentials 71 | 72 | Parameters 73 | ---------- 74 | conf_dir : str 75 | Full path to config dir (Default value = get_config_dir()) 76 | file_name : str 77 | (Default value = "google_secret.json") 78 | 79 | Returns 80 | ------- 81 | dict 82 | Dict with necessary contents of google_secret.json 83 | """ 84 | conf_dir = Path(conf_dir).expanduser() if conf_dir else get_config_dir() 85 | cfg_file = conf_dir / file_name 86 | 87 | if not cfg_file.exists(): 88 | raise IOError( 89 | "No Google client config found.\n" 90 | "Please download json from " 91 | "https://console.developers.google.com/apis/credentials and " 92 | "save as {}".format(cfg_file) 93 | ) 94 | 95 | with cfg_file.open() as fp: 96 | cfg = json.load(fp) 97 | 98 | return cfg 99 | 100 | 101 | def get_creds( 102 | user="default", config=None, scope=default_scope, creds_dir=None, save=True 103 | ): 104 | """ 105 | Get google google.auth.credentials.Credentials for the given user. If the user 106 | doesn't have previous creds, they will go through the OAuth flow to get new 107 | credentials which will be saved for later use. Credentials will be saved in 108 | config['creds_dir'], if this value is not set, then they will be stored in a folder 109 | named ``creds`` in the default config dir (either ~/.config/gspread_pandas or. 110 | 111 | $GSPREAD_PANDAS_CONFIG_DIR) 112 | 113 | Alternatively, it will get credentials from a service account. 114 | 115 | Parameters 116 | ---------- 117 | user : str 118 | Unique key indicating user's credentials. This is not necessary when using 119 | a ServiceAccount and will be ignored (Default value = "default") 120 | config : dict 121 | Optional, dict with "client_id", "client_secret", and "redirect_uris" keys for 122 | OAuth or "type", "client_email", "private_key", "private_key_id", and 123 | "client_id" for a Service Account. If None is passed, it will call 124 | :meth:`get_config() ` (Default value = None) 125 | creds_dir : str, Path 126 | Optional, directory to load and store creds from/in. If None, it will use the 127 | ``creds`` subdirectory in the default config location. (Default value = None) 128 | scope : list 129 | Optional, scope to use for Google Auth (Default value = default_scope) 130 | 131 | Returns 132 | ------- 133 | google.auth.credentials.Credentials 134 | Google credentials that can be used with gspread 135 | """ 136 | config = config or get_config() 137 | try: 138 | if "private_key_id" in config: 139 | return SACredentials.from_service_account_info(config, scopes=scope) 140 | 141 | if not isinstance(user, str): 142 | raise ConfigException( 143 | "Need to provide a user key as a string if not using a service account" 144 | ) 145 | 146 | if creds_dir is None: 147 | creds_dir = get_config_dir() / "creds" 148 | 149 | creds_file = Path(creds_dir) / user 150 | 151 | if creds_file.exists(): 152 | # need to convert Path to string for python 2.7 153 | return OAuthCredentials.from_authorized_user_file(str(creds_file)) 154 | 155 | flow = InstalledAppFlow.from_client_config(config, scope) 156 | creds = flow.run_local_server( 157 | host="localhost", 158 | port=8182, 159 | authorization_prompt_message="Please visit this URL: {url}", 160 | success_message="The auth flow is complete; you may close this window.", 161 | open_browser=False, 162 | ) 163 | 164 | if save: 165 | creds_data = { 166 | "refresh_token": creds.refresh_token, 167 | "token_uri": creds.token_uri, 168 | "client_id": creds.client_id, 169 | "client_secret": creds.client_secret, 170 | "scopes": creds.scopes, 171 | } 172 | 173 | ensure_path(creds_dir) 174 | creds_file.write_text(decode(json.dumps(creds_data))) 175 | 176 | return creds 177 | except Exception: 178 | exc_info = sys.exc_info() 179 | raise ConfigException(*exc_info[1:]) 180 | -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_email.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:03", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:03 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:04", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:04 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | } 171 | ], 172 | "recorded_with": "betamax/0.8.1" 173 | } -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_root.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:01", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:01 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:02", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:02 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | } 171 | ], 172 | "recorded_with": "betamax/0.8.1" 173 | } -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_directories.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:02", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:02 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:03", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:03 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | } 171 | ], 172 | "recorded_with": "betamax/0.8.1" 173 | } -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_email_no_perm.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:04", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:04 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:05", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:05 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | } 171 | ], 172 | "recorded_with": "betamax/0.8.1" 173 | } -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_find_folders.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:10", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:10 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:10", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:10 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | } 171 | ], 172 | "recorded_with": "betamax/0.8.1" 173 | } -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_create_folder_no_parents.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:14", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:14 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:14", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:14 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | } 171 | ], 172 | "recorded_with": "betamax/0.8.1" 173 | } -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from pathlib import Path 4 | 5 | import pytest 6 | from betamax import Betamax 7 | from betamax_serializers.pretty_json import PrettyJSONSerializer 8 | from Crypto.PublicKey import RSA 9 | from google.auth.transport.requests import AuthorizedSession 10 | 11 | from gspread_pandas import Client, Spread, conf 12 | from gspread_pandas.util import decode 13 | 14 | pytest.RECORD = os.environ.get("GSPREAD_RECORD") is not None 15 | pytest.DUMMY_TOKEN = "" 16 | 17 | 18 | def configure_betamax(): 19 | Betamax.register_serializer(PrettyJSONSerializer) 20 | with Betamax.configure() as config: 21 | config.cassette_library_dir = "tests/cassettes/" 22 | config.default_cassette_options["serialize_with"] = "prettyjson" 23 | 24 | config.before_record(callback=sanitize_token) 25 | 26 | record_mode = "once" if pytest.RECORD else "none" 27 | 28 | config.default_cassette_options["record_mode"] = record_mode 29 | 30 | 31 | def sanitize_token(interaction, current_cassette): 32 | headers = interaction.data["request"]["headers"] 33 | # google-auth creds set the authorization key using lower case 34 | token = headers.get("authorization") 35 | 36 | if token is None: 37 | return 38 | 39 | interaction.data["request"]["headers"]["authorization"] = [ 40 | "Bearer " + pytest.DUMMY_TOKEN 41 | ] 42 | 43 | 44 | def make_config(tmpdir_factory, config): 45 | # convert to str for python 3.5 compat 46 | f = Path(str(tmpdir_factory.mktemp("conf").join("google_secret.json"))) 47 | f.write_text(decode(json.dumps(config))) 48 | return f.parent, f.name 49 | 50 | 51 | def _get_cassette_name(request): 52 | cassette_name = "" 53 | 54 | if request.module is not None: 55 | cassette_name += request.module.__name__ + "." 56 | 57 | if request.cls is not None: 58 | cassette_name += request.cls.__name__ + "." 59 | 60 | cassette_name += request.function.__name__ 61 | return cassette_name 62 | 63 | 64 | def _set_up_recorder(session, request, cassette_name): 65 | recorder = Betamax(session) 66 | recorder.use_cassette(cassette_name) 67 | recorder.start() 68 | request.addfinalizer(recorder.stop) 69 | 70 | return recorder 71 | 72 | 73 | @pytest.fixture 74 | def betamax_authorizedsession(request, set_test_config): 75 | cassette_name = _get_cassette_name(request) 76 | session = AuthorizedSession(conf.get_creds()) 77 | if pytest.RECORD: 78 | session.credentials.refresh(session._auth_request) 79 | else: 80 | session.credentials.token = pytest.DUMMY_TOKEN 81 | recorder = _set_up_recorder(session, request, cassette_name) 82 | 83 | if request.cls: 84 | request.cls.session = session 85 | request.cls.recorder = recorder 86 | 87 | return session 88 | 89 | 90 | @pytest.fixture 91 | def betamax_client(request, betamax_authorizedsession): 92 | request.cls.client = Client(session=betamax_authorizedsession, load_dirs=True) 93 | return request.cls.client 94 | 95 | 96 | @pytest.fixture 97 | def betamax_spread(request, betamax_client): 98 | request.cls.spread = Spread( 99 | "1u626GkYm1RAJSmHcGyd5_VsHNr_c_IfUcE_W-fQGxIM", sheet=0, client=betamax_client 100 | ) 101 | 102 | return request.cls.spread 103 | 104 | 105 | @pytest.fixture 106 | def betamax_client_bad_scope(request, set_test_config): 107 | cassette_name = _get_cassette_name(request) 108 | session = AuthorizedSession( 109 | conf.get_creds( 110 | scope=[ 111 | "https://www.googleapis.com/auth/spreadsheets", 112 | "https://www.googleapis.com/auth/drive", 113 | ] 114 | ) 115 | ) 116 | 117 | if pytest.RECORD: 118 | session.credentials.refresh(session._auth_request) 119 | else: 120 | session.credentials.token = pytest.DUMMY_TOKEN 121 | recorder = _set_up_recorder(session, request, cassette_name) 122 | client = Client(session=session) 123 | 124 | request.cls.session = session 125 | request.cls.recorder = recorder 126 | request.cls.client = client 127 | 128 | return client 129 | 130 | 131 | @pytest.fixture 132 | def set_test_config(set_sa_config): 133 | if pytest.RECORD: 134 | os.environ[conf.CONFIG_DIR_ENV_VAR] = str(os.getcwd()) 135 | 136 | 137 | @pytest.fixture(scope="session") 138 | def sa_config_json(): 139 | return { 140 | "type": "service_account", 141 | "project_id": "", 142 | "private_key_id": "", 143 | "private_key": RSA.generate(2048).exportKey("PEM").decode(), 144 | "client_email": "", 145 | "client_id": "", 146 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 147 | "token_uri": "https://oauth2.googleapis.com/token", 148 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 149 | "client_x509_cert_url": "", 150 | } 151 | 152 | 153 | @pytest.fixture(scope="session") 154 | def oauth_config_json(): 155 | return { 156 | "installed": { 157 | "client_id": "", 158 | "project_id": "", 159 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 160 | "token_uri": "https://oauth2.googleapis.com/token", 161 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 162 | "client_secret": "", 163 | "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"], 164 | } 165 | } 166 | 167 | 168 | @pytest.fixture(scope="session") 169 | def creds_json(): 170 | return { 171 | "access_token": "", 172 | "client_id": "", 173 | "client_secret": "", 174 | "refresh_token": "", 175 | "token_expiry": "2019-05-25T04:21:52Z", 176 | "token_uri": "https://oauth2.googleapis.com/token", 177 | "user_agent": None, 178 | "revoke_uri": "https://oauth2.googleapis.com/revoke", 179 | "id_token": { 180 | "iss": "https://accounts.google.com", 181 | "azp": "", 182 | "aud": "", 183 | "sub": "", 184 | "email": "", 185 | "email_verified": True, 186 | "at_hash": "MoXn24dfJiPj1RnBRLtLng", 187 | "iat": 1558754512, 188 | "exp": 1558758112, 189 | }, 190 | "id_token_jwt": "", 191 | "token_response": { 192 | "access_token": "", 193 | "expires_in": 3600, 194 | "refresh_token": "", 195 | "scope": "", 196 | "token_type": "Bearer", 197 | "id_token": "", 198 | }, 199 | "scopes": [ 200 | "https://spreadsheets.google.com/feeds", 201 | "https://www.googleapis.com/auth/userinfo.email", 202 | "https://www.googleapis.com/auth/drive", 203 | ], 204 | "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", 205 | "invalid": False, 206 | "_class": "OAuth2Credentials", 207 | "_module": "oauth2client.client", 208 | } 209 | 210 | 211 | @pytest.fixture 212 | def sa_config(tmpdir_factory, sa_config_json): 213 | return make_config(tmpdir_factory, sa_config_json) 214 | 215 | 216 | @pytest.fixture 217 | def oauth_config(tmpdir_factory, oauth_config_json): 218 | return make_config(tmpdir_factory, oauth_config_json) 219 | 220 | 221 | def unset_env(): 222 | os.environ.pop(conf.CONFIG_DIR_ENV_VAR) 223 | 224 | 225 | @pytest.fixture 226 | def set_oauth_config(request, oauth_config): 227 | os.environ[conf.CONFIG_DIR_ENV_VAR] = str(oauth_config[0]) 228 | request.addfinalizer(unset_env) 229 | 230 | 231 | @pytest.fixture 232 | def set_sa_config(request, sa_config): 233 | os.environ[conf.CONFIG_DIR_ENV_VAR] = str(sa_config[0]) 234 | request.addfinalizer(unset_env) 235 | 236 | 237 | @pytest.fixture 238 | def make_creds(oauth_config, set_oauth_config, creds_json): 239 | creds_dir = oauth_config[0] / "creds" 240 | conf.ensure_path(creds_dir) 241 | 242 | creds_dir.joinpath("default").write_text(decode(json.dumps(creds_json))) 243 | 244 | 245 | configure_betamax() 246 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. epub3 to make an epub3 31 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 32 | echo. text to make text files 33 | echo. man to make manual pages 34 | echo. texinfo to make Texinfo files 35 | echo. gettext to make PO message catalogs 36 | echo. changes to make an overview over all changed/added/deprecated items 37 | echo. xml to make Docutils-native XML files 38 | echo. pseudoxml to make pseudoxml-XML files for display purposes 39 | echo. linkcheck to check all external links for integrity 40 | echo. doctest to run all doctests embedded in the documentation if enabled 41 | echo. coverage to run coverage check of the documentation if enabled 42 | echo. dummy to check syntax errors of document sources 43 | goto end 44 | ) 45 | 46 | if "%1" == "clean" ( 47 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 48 | del /q /s %BUILDDIR%\* 49 | goto end 50 | ) 51 | 52 | 53 | REM Check if sphinx-build is available and fallback to Python version if any 54 | %SPHINXBUILD% 1>NUL 2>NUL 55 | if errorlevel 9009 goto sphinx_python 56 | goto sphinx_ok 57 | 58 | :sphinx_python 59 | 60 | set SPHINXBUILD=python -m sphinx.__init__ 61 | %SPHINXBUILD% 2> nul 62 | if errorlevel 9009 ( 63 | echo. 64 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 65 | echo.installed, then set the SPHINXBUILD environment variable to point 66 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 67 | echo.may add the Sphinx directory to PATH. 68 | echo. 69 | echo.If you don't have Sphinx installed, grab it from 70 | echo.http://sphinx-doc.org/ 71 | exit /b 1 72 | ) 73 | 74 | :sphinx_ok 75 | 76 | 77 | if "%1" == "html" ( 78 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 79 | if errorlevel 1 exit /b 1 80 | echo. 81 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 82 | goto end 83 | ) 84 | 85 | if "%1" == "dirhtml" ( 86 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 87 | if errorlevel 1 exit /b 1 88 | echo. 89 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 90 | goto end 91 | ) 92 | 93 | if "%1" == "singlehtml" ( 94 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 95 | if errorlevel 1 exit /b 1 96 | echo. 97 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 98 | goto end 99 | ) 100 | 101 | if "%1" == "pickle" ( 102 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 103 | if errorlevel 1 exit /b 1 104 | echo. 105 | echo.Build finished; now you can process the pickle files. 106 | goto end 107 | ) 108 | 109 | if "%1" == "json" ( 110 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 111 | if errorlevel 1 exit /b 1 112 | echo. 113 | echo.Build finished; now you can process the JSON files. 114 | goto end 115 | ) 116 | 117 | if "%1" == "htmlhelp" ( 118 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 119 | if errorlevel 1 exit /b 1 120 | echo. 121 | echo.Build finished; now you can run HTML Help Workshop with the ^ 122 | .hhp project file in %BUILDDIR%/htmlhelp. 123 | goto end 124 | ) 125 | 126 | if "%1" == "qthelp" ( 127 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 128 | if errorlevel 1 exit /b 1 129 | echo. 130 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 131 | .qhcp project file in %BUILDDIR%/qthelp, like this: 132 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\twitterpandas.qhcp 133 | echo.To view the help file: 134 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\twitterpandas.ghc 135 | goto end 136 | ) 137 | 138 | if "%1" == "devhelp" ( 139 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 140 | if errorlevel 1 exit /b 1 141 | echo. 142 | echo.Build finished. 143 | goto end 144 | ) 145 | 146 | if "%1" == "epub" ( 147 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 148 | if errorlevel 1 exit /b 1 149 | echo. 150 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 151 | goto end 152 | ) 153 | 154 | if "%1" == "epub3" ( 155 | %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 156 | if errorlevel 1 exit /b 1 157 | echo. 158 | echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. 159 | goto end 160 | ) 161 | 162 | if "%1" == "latex" ( 163 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 164 | if errorlevel 1 exit /b 1 165 | echo. 166 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdf" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "latexpdfja" ( 181 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 182 | cd %BUILDDIR%/latex 183 | make all-pdf-ja 184 | cd %~dp0 185 | echo. 186 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 187 | goto end 188 | ) 189 | 190 | if "%1" == "text" ( 191 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 192 | if errorlevel 1 exit /b 1 193 | echo. 194 | echo.Build finished. The text files are in %BUILDDIR%/text. 195 | goto end 196 | ) 197 | 198 | if "%1" == "man" ( 199 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 200 | if errorlevel 1 exit /b 1 201 | echo. 202 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 203 | goto end 204 | ) 205 | 206 | if "%1" == "texinfo" ( 207 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 208 | if errorlevel 1 exit /b 1 209 | echo. 210 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 211 | goto end 212 | ) 213 | 214 | if "%1" == "gettext" ( 215 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 216 | if errorlevel 1 exit /b 1 217 | echo. 218 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 219 | goto end 220 | ) 221 | 222 | if "%1" == "changes" ( 223 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 224 | if errorlevel 1 exit /b 1 225 | echo. 226 | echo.The overview file is in %BUILDDIR%/changes. 227 | goto end 228 | ) 229 | 230 | if "%1" == "linkcheck" ( 231 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 232 | if errorlevel 1 exit /b 1 233 | echo. 234 | echo.Link check complete; look for any errors in the above output ^ 235 | or in %BUILDDIR%/linkcheck/output.txt. 236 | goto end 237 | ) 238 | 239 | if "%1" == "doctest" ( 240 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 241 | if errorlevel 1 exit /b 1 242 | echo. 243 | echo.Testing of doctests in the sources finished, look at the ^ 244 | results in %BUILDDIR%/doctest/output.txt. 245 | goto end 246 | ) 247 | 248 | if "%1" == "coverage" ( 249 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 250 | if errorlevel 1 exit /b 1 251 | echo. 252 | echo.Testing of coverage in the sources finished, look at the ^ 253 | results in %BUILDDIR%/coverage/python.txt. 254 | goto end 255 | ) 256 | 257 | if "%1" == "xml" ( 258 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 259 | if errorlevel 1 exit /b 1 260 | echo. 261 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 262 | goto end 263 | ) 264 | 265 | if "%1" == "pseudoxml" ( 266 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 267 | if errorlevel 1 exit /b 1 268 | echo. 269 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 270 | goto end 271 | ) 272 | 273 | if "%1" == "dummy" ( 274 | %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy 275 | if errorlevel 1 exit /b 1 276 | echo. 277 | echo.Build finished. Dummy builder generates no files. 278 | goto end 279 | ) 280 | 281 | :end 282 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " epub3 to make an epub3" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | @echo " dummy to check syntax errors of document sources" 51 | 52 | .PHONY: clean 53 | clean: 54 | rm -rf $(BUILDDIR)/* 55 | 56 | .PHONY: html 57 | html: 58 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 61 | 62 | .PHONY: dirhtml 63 | dirhtml: 64 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 65 | @echo 66 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 67 | 68 | .PHONY: singlehtml 69 | singlehtml: 70 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 71 | @echo 72 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 73 | 74 | .PHONY: pickle 75 | pickle: 76 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 77 | @echo 78 | @echo "Build finished; now you can process the pickle files." 79 | 80 | .PHONY: json 81 | json: 82 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 83 | @echo 84 | @echo "Build finished; now you can process the JSON files." 85 | 86 | .PHONY: htmlhelp 87 | htmlhelp: 88 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 89 | @echo 90 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 91 | ".hhp project file in $(BUILDDIR)/htmlhelp." 92 | 93 | .PHONY: qthelp 94 | qthelp: 95 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 96 | @echo 97 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 98 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 99 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/twitterpandas.qhcp" 100 | @echo "To view the help file:" 101 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/twitterpandas.qhc" 102 | 103 | .PHONY: applehelp 104 | applehelp: 105 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 106 | @echo 107 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 108 | @echo "N.B. You won't be able to view it unless you put it in" \ 109 | "~/Library/Documentation/Help or install it in your application" \ 110 | "bundle." 111 | 112 | .PHONY: devhelp 113 | devhelp: 114 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 115 | @echo 116 | @echo "Build finished." 117 | @echo "To view the help file:" 118 | @echo "# mkdir -p $$HOME/.local/share/devhelp/twitterpandas" 119 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/twitterpandas" 120 | @echo "# devhelp" 121 | 122 | .PHONY: epub 123 | epub: 124 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 125 | @echo 126 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 127 | 128 | .PHONY: epub3 129 | epub3: 130 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 131 | @echo 132 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 133 | 134 | .PHONY: latex 135 | latex: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo 138 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 139 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 140 | "(use \`make latexpdf' here to do that automatically)." 141 | 142 | .PHONY: latexpdf 143 | latexpdf: 144 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 145 | @echo "Running LaTeX files through pdflatex..." 146 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 147 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 148 | 149 | .PHONY: latexpdfja 150 | latexpdfja: 151 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 152 | @echo "Running LaTeX files through platex and dvipdfmx..." 153 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 154 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 155 | 156 | .PHONY: text 157 | text: 158 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 159 | @echo 160 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 161 | 162 | .PHONY: man 163 | man: 164 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 165 | @echo 166 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 167 | 168 | .PHONY: texinfo 169 | texinfo: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo 172 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 173 | @echo "Run \`make' in that directory to run these through makeinfo" \ 174 | "(use \`make info' here to do that automatically)." 175 | 176 | .PHONY: info 177 | info: 178 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 179 | @echo "Running Texinfo files through makeinfo..." 180 | make -C $(BUILDDIR)/texinfo info 181 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 182 | 183 | .PHONY: gettext 184 | gettext: 185 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 186 | @echo 187 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 188 | 189 | .PHONY: changes 190 | changes: 191 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 192 | @echo 193 | @echo "The overview file is in $(BUILDDIR)/changes." 194 | 195 | .PHONY: linkcheck 196 | linkcheck: 197 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 198 | @echo 199 | @echo "Link check complete; look for any errors in the above output " \ 200 | "or in $(BUILDDIR)/linkcheck/output.txt." 201 | 202 | .PHONY: doctest 203 | doctest: 204 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 205 | @echo "Testing of doctests in the sources finished, look at the " \ 206 | "results in $(BUILDDIR)/doctest/output.txt." 207 | 208 | .PHONY: coverage 209 | coverage: 210 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 211 | @echo "Testing of coverage in the sources finished, look at the " \ 212 | "results in $(BUILDDIR)/coverage/python.txt." 213 | 214 | .PHONY: xml 215 | xml: 216 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 217 | @echo 218 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 219 | 220 | .PHONY: pseudoxml 221 | pseudoxml: 222 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 223 | @echo 224 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 225 | 226 | .PHONY: dummy 227 | dummy: 228 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 229 | @echo 230 | @echo "Build finished. Dummy builder generates no files." 231 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Getting Started 3 | =============== 4 | 5 | .. image:: https://img.shields.io/pypi/v/gspread-pandas.svg 6 | :target: https://pypi.python.org/pypi/gspread-pandas 7 | :alt: PyPI Version 8 | 9 | .. image:: https://readthedocs.org/projects/gspread-pandas/badge/?version=latest 10 | :target: https://gspread-pandas.readthedocs.io/en/latest/?badge=latest 11 | :alt: Documentation Status 12 | 13 | author: Diego Fernandez 14 | 15 | Links: 16 | 17 | - `Documentation `_ 18 | - `Source code `_ 19 | - `Short video tutorial `_ 20 | 21 | .. attention:: Looking for maintainer 22 | 23 | I don't really use this package anymore and with my current lifestyle I don't 24 | have much time to maintain this project. The library as-is only supports 25 | gspread version <6. Updating this to support v6 might involve a fair bit of work 26 | which I unfortunately cant do. I'm happy to work with anyone who might want to 27 | maintain, or I can still accept PRs. 28 | 29 | 30 | Overview 31 | ======== 32 | 33 | A package to easily open an instance of a Google spreadsheet and 34 | interact with worksheets through Pandas DataFrames. It enables you to 35 | easily pull data from Google spreadsheets into DataFrames as well as 36 | push data into spreadsheets from DataFrames. It leverages 37 | `gspread `__ in the backend for 38 | most of the heavylifting, but it has a lot of added functionality 39 | to handle things specific to working with DataFrames as well as 40 | some extra nice to have features. 41 | 42 | The target audience are Data Analysts and Data Scientists, but it can also 43 | be used by Data Engineers or anyone trying to automate workflows with Google 44 | Sheets and Pandas. 45 | 46 | Some key goals/features: 47 | 48 | - Be easy to use interactively, with good docstrings and auto-completion 49 | - Nicely handle headers and indexes (including multi-level headers and merged cells) 50 | - Run on Jupyter, headless server, and/or scripts 51 | - Allow storing different user credentials or using Service Accounts 52 | - Automatically handle token refreshes 53 | - Enable handling of frozen rows and columns 54 | - Enable filling in all merged cells when pulling data 55 | - Nicely handle large data sets and auto-retries 56 | - Enable creation of filters 57 | - Handle retries when exceeding 100 second user quota 58 | - When pushing DataFrames with MultiIndex columns, allow merging or flattening headers 59 | - Ability to handle Spreadsheet permissions 60 | - Ability to specify ``ValueInputOption`` and ``ValueRenderOption`` for specific columns 61 | 62 | Installation / Usage 63 | ==================== 64 | 65 | To install use pip: 66 | 67 | .. code-block:: console 68 | 69 | $ pip install gspread-pandas 70 | 71 | Or clone the repo: 72 | 73 | .. code-block:: console 74 | 75 | $ git clone https://github.com/aiguofer/gspread-pandas.git 76 | $ python setup.py install 77 | 78 | Before using, you will need to download Google client credentials for 79 | your app. 80 | 81 | Client Credentials 82 | ------------------ 83 | 84 | To allow a script to use Google Drive API we need to authenticate our 85 | self towards Google. To do so, we need to create a project, describing 86 | the tool and generate credentials. Please use your web browser and go to 87 | `Google console `__ and : 88 | 89 | - Choose **Create Project** in popup menu on the top. 90 | - A dialog box appears, so give your project a name and click on 91 | **Create** button. 92 | - On the left-side menu click on **API Manager**. 93 | - A table of available APIs is shown. Switch **Drive API** and click on 94 | **Enable API** button. Do the same for **Sheets API**. Other APIs might 95 | be switched off, for our purpose. 96 | - On the left-side menu click on **Credentials**. 97 | - In section **OAuth consent screen** select your email address and 98 | give your product a name. Then click on **Save** button. 99 | - In section **Credentials** click on **Add credentials** and switch 100 | **OAuth client ID** (if you want to use your own account or enable 101 | the use of multiple accounts) or **Service account key** (if you prefer 102 | to have a service account interacting with spreadsheets). 103 | - If you select **OAuth client ID**: 104 | 105 | - Select **Application type** item as **Desktop app** and give it a name. 106 | - Click on **Create** button. 107 | - Click on **Download JSON** icon on the right side of created 108 | **OAuth client IDs** and store the downloaded file on your file system. 109 | - If you select **Service account key** 110 | 111 | - Click on **Service account** dropdown and select **New service account** 112 | - Give it a **Service account name** and ignore the **Role** dropdown 113 | (unless you know you need this for something else, it's not necessary for 114 | working with spreadsheets) 115 | - Note the **Service account ID** as you might need to give that user 116 | permission to interact with your spreadsheets 117 | - Leave **Key type** as **JSON** 118 | - Click **Create** and store the downloaded file on your file system. 119 | - Please be aware, the file contains your private credentials, so take 120 | care of the file in the same way you care of your private SSH key; 121 | Move the downloaded JSON to ``~/.config/gspread_pandas/google_secret.json`` 122 | (or you can configure the directory and file name by directly calling 123 | ``gspread_pandas.conf.get_config`` 124 | 125 | 126 | Thanks to similar project 127 | `df2gspread `__ for this great 128 | description of how to get the client credentials. 129 | 130 | You can read more about it in the `configuration docs 131 | `__ 132 | including how to change the default behavior. 133 | 134 | Example 135 | ======= 136 | 137 | .. code:: python 138 | 139 | import pandas as pd 140 | from gspread_pandas import Spread, Client 141 | 142 | file_name = "http://stats.idre.ucla.edu/stat/data/binary.csv" 143 | df = pd.read_csv(file_name) 144 | 145 | # 'Example Spreadsheet' needs to already exist and your user must have access to it 146 | spread = Spread('Example Spreadsheet') 147 | # This will ask to authenticate if you haven't done so before 148 | 149 | # Display available worksheets 150 | spread.sheets 151 | 152 | # Save DataFrame to worksheet 'New Test Sheet', create it first if it doesn't exist 153 | spread.df_to_sheet(df, index=False, sheet='New Test Sheet', start='A2', replace=True) 154 | spread.update_cells('A1', 'B1', ['Created by:', spread.email]) 155 | print(spread) 156 | # @gmail.com', Spread: 'Example Spreadsheet', Sheet: 'New Test Sheet'> 157 | 158 | # You can now first instanciate a Client separately and query folders and 159 | # instanciate other Spread objects by passing in the Client 160 | client = Client() 161 | # Assumming you have a dir called 'example dir' with sheets in it 162 | available_sheets = client.find_spreadsheet_files_in_folders('example dir') 163 | spreads = [] 164 | for sheet in available_sheets.get('example dir', []): 165 | spreads.append(Spread(sheet['id'], client=client)) 166 | 167 | Troubleshooting 168 | =============== 169 | 170 | EOFError in Rodeo 171 | ----------------- 172 | 173 | If you're trying to use ``gspread_pandas`` from within 174 | `Rodeo `_ you might get an 175 | ``EOFError: EOF when reading a line`` error when trying to pass in the verification 176 | code. The workaround for this is to first verify your account in a regular shell. 177 | Since you're just doing this to get your Oauth token, the spreadsheet doesn't need 178 | to be valid. Just run this in shell: 179 | 180 | .. code:: python 181 | 182 | python -c "from gspread_pandas import Spread; Spread('','')" 183 | 184 | Then follow the instructions to create and store the OAuth creds. 185 | 186 | 187 | This action would increase the number of cells in the workbook above the limit of 10000000 cells. 188 | ------------------------------------------------------------------------------------------------- 189 | 190 | IMO, Google sheets is not the right tool for large datasets. However, there's probably good reaons 191 | you might have to use it in such cases. When uploading a large DataFrame, you might run into this 192 | error. 193 | 194 | By default, ``Spread.df_to_sheet`` will add rows and/or columns needed to accomodate the DataFrame. 195 | Since a new sheet contains a fairly large number of columns, if you're uploading a DF with lots of 196 | rows you might exceed the max number of cells in a worksheet even if your data does not. In order 197 | to fix this you have 2 options: 198 | 199 | 1. The easiest is to pass ``replace=True``, which will first resize the worksheet and clear out all values. 200 | 2. Another option is to first resize to 1x1 using ``Spread.sheet.resize(1, 1)`` and then do ``df_to_sheet`` 201 | 202 | There's a strange caveat with resizing, so going to 1x1 first is recommended (``replace=True`` already does this). To read more see `this issue `_ 203 | -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_create_folder.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:13", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:13 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:13", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:13 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | }, 171 | { 172 | "recorded_at": "2022-01-18T21:58:13", 173 | "request": { 174 | "body": { 175 | "encoding": "utf-8", 176 | "string": "" 177 | }, 178 | "headers": { 179 | "Accept": [ 180 | "*/*" 181 | ], 182 | "Accept-Encoding": [ 183 | "gzip, deflate" 184 | ], 185 | "Connection": [ 186 | "keep-alive" 187 | ], 188 | "User-Agent": [ 189 | "python-requests/2.26.0" 190 | ], 191 | "authorization": [ 192 | "Bearer " 193 | ] 194 | }, 195 | "method": "GET", 196 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 197 | }, 198 | "response": { 199 | "body": { 200 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 201 | "encoding": "UTF-8" 202 | }, 203 | "headers": { 204 | "Alt-Svc": [ 205 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 206 | ], 207 | "Cache-Control": [ 208 | "no-cache, no-store, max-age=0, must-revalidate" 209 | ], 210 | "Content-Encoding": [ 211 | "gzip" 212 | ], 213 | "Content-Security-Policy": [ 214 | "frame-ancestors 'self'" 215 | ], 216 | "Content-Type": [ 217 | "application/json; charset=UTF-8" 218 | ], 219 | "Date": [ 220 | "Tue, 18 Jan 2022 21:58:13 GMT" 221 | ], 222 | "Expires": [ 223 | "Mon, 01 Jan 1990 00:00:00 GMT" 224 | ], 225 | "Pragma": [ 226 | "no-cache" 227 | ], 228 | "Server": [ 229 | "GSE" 230 | ], 231 | "Transfer-Encoding": [ 232 | "chunked" 233 | ], 234 | "Vary": [ 235 | "Origin", 236 | "X-Origin" 237 | ], 238 | "X-Content-Type-Options": [ 239 | "nosniff" 240 | ], 241 | "X-Frame-Options": [ 242 | "SAMEORIGIN" 243 | ], 244 | "X-XSS-Protection": [ 245 | "1; mode=block" 246 | ] 247 | }, 248 | "status": { 249 | "code": 200, 250 | "message": "OK" 251 | }, 252 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 253 | } 254 | } 255 | ], 256 | "recorded_with": "betamax/0.8.1" 257 | } -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # gspread-pandas documentation build configuration file, created by 5 | # cookiecutter pipproject 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import os 17 | import sys 18 | 19 | __version__ = "version read in next line" 20 | exec(open("../../gspread_pandas/_version.py").read()) 21 | 22 | # If extensions (or modules to document with autodoc) are in another directory, 23 | # add these directories to sys.path here. If the directory is relative to the 24 | # documentation root, use os.path.abspath to make it absolute, like shown here. 25 | sys.path.insert(0, os.path.abspath("../..")) 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.napoleon", 38 | "sphinx.ext.autosectionlabel", 39 | "sphinx.ext.intersphinx", 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ["_templates"] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = ".rst" 49 | 50 | # The encoding of source files. 51 | # source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = "index" 55 | 56 | # General information about the project. 57 | project = "gspread-pandas" 58 | copyright = "2016, Diego Fernandez" 59 | author = "Diego Fernandez" 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = __version__ 67 | # The full version, including alpha/beta/rc tags. 68 | release = __version__ 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | # today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | # This patterns also effect to html_static_path and html_extra_path 86 | exclude_patterns = [] 87 | 88 | # The reST default role (used for this markup: `text`) to use for all 89 | # documents. 90 | # default_role = None 91 | 92 | # If true, '()' will be appended to :func: etc. cross-reference text. 93 | # add_function_parentheses = True 94 | 95 | # If true, the current module name will be prepended to all description 96 | # unit titles (such as .. function::). 97 | # add_module_names = True 98 | 99 | # If true, sectionauthor and moduleauthor directives will be shown in the 100 | # output. They are ignored by default. 101 | # show_authors = False 102 | 103 | # The name of the Pygments (syntax highlighting) style to use. 104 | pygments_style = "sphinx" 105 | 106 | # A list of ignored prefixes for module index sorting. 107 | # modindex_common_prefix = [] 108 | 109 | # If true, keep warnings as "system message" paragraphs in the built documents. 110 | # keep_warnings = False 111 | 112 | # If true, `todo` and `todoList` produce output, else they produce nothing. 113 | todo_include_todos = False 114 | 115 | 116 | # -- Options for HTML output ---------------------------------------------- 117 | 118 | # The theme to use for HTML and HTML Help pages. See the documentation for 119 | # a list of builtin themes. 120 | html_theme = "sphinx_rtd_theme" 121 | 122 | # Theme options are theme-specific and customize the look and feel of a theme 123 | # further. For a list of options available for each theme, see the 124 | # documentation. 125 | # html_theme_options = {} 126 | 127 | # Add any paths that contain custom themes here, relative to this directory. 128 | # html_theme_path = [] 129 | 130 | # The name for this set of Sphinx documents. 131 | # " v documentation" by default. 132 | # html_title = 'gspread-pandas v0.1' 133 | 134 | # A shorter title for the navigation bar. Default is the same as html_title. 135 | # html_short_title = None 136 | 137 | # The name of an image file (relative to this directory) to place at the top 138 | # of the sidebar. 139 | # html_logo = None 140 | 141 | # The name of an image file (relative to this directory) to use as a favicon of 142 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 143 | # pixels large. 144 | # html_favicon = None 145 | 146 | # Add any paths that contain custom static files (such as style sheets) here, 147 | # relative to this directory. They are copied after the builtin static files, 148 | # so a file named "default.css" will overwrite the builtin "default.css". 149 | html_static_path = ["_static"] 150 | 151 | # Add any extra paths that contain custom files (such as robots.txt or 152 | # .htaccess) here, relative to this directory. These files are copied 153 | # directly to the root of the documentation. 154 | # html_extra_path = [] 155 | 156 | # If not None, a 'Last updated on:' timestamp is inserted at every page 157 | # bottom, using the given strftime format. 158 | # The empty string is equivalent to '%b %d, %Y'. 159 | # html_last_updated_fmt = None 160 | 161 | # If true, SmartyPants will be used to convert quotes and dashes to 162 | # typographically correct entities. 163 | # html_use_smartypants = True 164 | 165 | # Custom sidebar templates, maps document names to template names. 166 | # html_sidebars = {} 167 | 168 | # Additional templates that should be rendered to pages, maps page names to 169 | # template names. 170 | # html_additional_pages = {} 171 | 172 | # If false, no module index is generated. 173 | # html_domain_indices = True 174 | 175 | # If false, no index is generated. 176 | # html_use_index = True 177 | 178 | # If true, the index is split into individual pages for each letter. 179 | # html_split_index = False 180 | 181 | # If true, links to the reST sources are added to the pages. 182 | # html_show_sourcelink = True 183 | 184 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 185 | # html_show_sphinx = True 186 | 187 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 188 | # html_show_copyright = True 189 | 190 | # If true, an OpenSearch description file will be output, and all pages will 191 | # contain a tag referring to it. The value of this option must be the 192 | # base URL from which the finished HTML is served. 193 | # html_use_opensearch = '' 194 | 195 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 196 | # html_file_suffix = None 197 | 198 | # Language to be used for generating the HTML full-text search index. 199 | # Sphinx supports the following languages: 200 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 201 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 202 | # html_search_language = 'en' 203 | 204 | # A dictionary with options for the search language support, empty by default. 205 | # 'ja' uses this config value. 206 | # 'zh' user can custom change `jieba` dictionary path. 207 | # html_search_options = {'type': 'default'} 208 | 209 | # The name of a javascript file (relative to the configuration directory) that 210 | # implements a search results scorer. If empty, the default will be used. 211 | # html_search_scorer = 'scorer.js' 212 | 213 | # Output file base name for HTML help builder. 214 | htmlhelp_basename = "gspread-pandasdoc" 215 | 216 | # -- Options for LaTeX output --------------------------------------------- 217 | 218 | latex_elements = { 219 | # The paper size ('letterpaper' or 'a4paper'). 220 | # 'papersize': 'letterpaper', 221 | # The font size ('10pt', '11pt' or '12pt'). 222 | # 'pointsize': '10pt', 223 | # Additional stuff for the LaTeX preamble. 224 | # 'preamble': '', 225 | # Latex figure (float) alignment 226 | # 'figure_align': 'htbp', 227 | } 228 | 229 | # Grouping the document tree into LaTeX files. List of tuples 230 | # (source start file, target name, title, 231 | # author, documentclass [howto, manual, or own class]). 232 | latex_documents = [ 233 | ( 234 | master_doc, 235 | "gspread-pandas.tex", 236 | "gspread-pandas Documentation", 237 | "Diego Fernandez", 238 | "manual", 239 | ) 240 | ] 241 | 242 | # The name of an image file (relative to this directory) to place at the top of 243 | # the title page. 244 | # latex_logo = None 245 | 246 | # For "manual" documents, if this is true, then toplevel headings are parts, 247 | # not chapters. 248 | # latex_use_parts = False 249 | 250 | # If true, show page references after internal links. 251 | # latex_show_pagerefs = False 252 | 253 | # If true, show URL addresses after external links. 254 | # latex_show_urls = False 255 | 256 | # Documents to append as an appendix to all manuals. 257 | # latex_appendices = [] 258 | 259 | # If false, no module index is generated. 260 | # latex_domain_indices = True 261 | 262 | 263 | # -- Options for manual page output --------------------------------------- 264 | 265 | # One entry per manual page. List of tuples 266 | # (source start file, name, description, authors, manual section). 267 | man_pages = [ 268 | (master_doc, "gspread-pandas", "gspread-pandas Documentation", [author], 1) 269 | ] 270 | 271 | # If true, show URL addresses after external links. 272 | # man_show_urls = False 273 | 274 | 275 | # -- Options for Texinfo output ------------------------------------------- 276 | 277 | # Grouping the document tree into Texinfo files. List of tuples 278 | # (source start file, target name, title, author, 279 | # dir menu entry, description, category) 280 | texinfo_documents = [ 281 | ( 282 | master_doc, 283 | "gspread-pandas", 284 | "gspread-pandas Documentation", 285 | author, 286 | "gspread-pandas", 287 | "One line description of project.", 288 | "Miscellaneous", 289 | ) 290 | ] 291 | 292 | # Documents to append as an appendix to all manuals. 293 | # texinfo_appendices = [] 294 | 295 | # If false, no module index is generated. 296 | # texinfo_domain_indices = True 297 | 298 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 299 | # texinfo_show_urls = 'footnote' 300 | 301 | # If true, do not generate a @detailmenu in the "Top" node's menu. 302 | # texinfo_no_detailmenu = False 303 | 304 | autodoc_member_order = "groupwise" 305 | autoclass_content = "both" 306 | -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_find_spreadsheet_files_in_folder.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:08", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:08 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:09", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:08 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | }, 171 | { 172 | "recorded_at": "2022-01-18T21:58:09", 173 | "request": { 174 | "body": { 175 | "encoding": "utf-8", 176 | "string": "" 177 | }, 178 | "headers": { 179 | "Accept": [ 180 | "*/*" 181 | ], 182 | "Accept-Encoding": [ 183 | "gzip, deflate" 184 | ], 185 | "Connection": [ 186 | "keep-alive" 187 | ], 188 | "User-Agent": [ 189 | "python-requests/2.26.0" 190 | ], 191 | "authorization": [ 192 | "Bearer " 193 | ] 194 | }, 195 | "method": "GET", 196 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 197 | }, 198 | "response": { 199 | "body": { 200 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 201 | "encoding": "UTF-8" 202 | }, 203 | "headers": { 204 | "Alt-Svc": [ 205 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 206 | ], 207 | "Cache-Control": [ 208 | "no-cache, no-store, max-age=0, must-revalidate" 209 | ], 210 | "Content-Encoding": [ 211 | "gzip" 212 | ], 213 | "Content-Security-Policy": [ 214 | "frame-ancestors 'self'" 215 | ], 216 | "Content-Type": [ 217 | "application/json; charset=UTF-8" 218 | ], 219 | "Date": [ 220 | "Tue, 18 Jan 2022 21:58:09 GMT" 221 | ], 222 | "Expires": [ 223 | "Mon, 01 Jan 1990 00:00:00 GMT" 224 | ], 225 | "Pragma": [ 226 | "no-cache" 227 | ], 228 | "Server": [ 229 | "GSE" 230 | ], 231 | "Transfer-Encoding": [ 232 | "chunked" 233 | ], 234 | "Vary": [ 235 | "Origin", 236 | "X-Origin" 237 | ], 238 | "X-Content-Type-Options": [ 239 | "nosniff" 240 | ], 241 | "X-Frame-Options": [ 242 | "SAMEORIGIN" 243 | ], 244 | "X-XSS-Protection": [ 245 | "1; mode=block" 246 | ] 247 | }, 248 | "status": { 249 | "code": 200, 250 | "message": "OK" 251 | }, 252 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 253 | } 254 | }, 255 | { 256 | "recorded_at": "2022-01-18T21:58:09", 257 | "request": { 258 | "body": { 259 | "encoding": "utf-8", 260 | "string": "" 261 | }, 262 | "headers": { 263 | "Accept": [ 264 | "*/*" 265 | ], 266 | "Accept-Encoding": [ 267 | "gzip, deflate" 268 | ], 269 | "Connection": [ 270 | "keep-alive" 271 | ], 272 | "User-Agent": [ 273 | "python-requests/2.26.0" 274 | ], 275 | "authorization": [ 276 | "Bearer " 277 | ] 278 | }, 279 | "method": "GET", 280 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%27root%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 281 | }, 282 | "response": { 283 | "body": { 284 | "base64_string": "H4sIAAAAAAAAAL3WSc+iSAAG4Pv3K4znJkFAxL6xg+wCskwmBtkX2fdO//exJ90XZ47qASpVpKqe1EtR/PjabKO0CLvt981fX5vNj8e12abBo7rd4Qm6ajAFsYMp1qkatFUgjflFE3etOuMZgg4pcFMc+Azx22//diy9e/irax92/e+m2mvDsv8z/qMBxHnM1ZAakGBeMfOjesG3v578/bj9/PZsGA7kdM+YvupDhQGDCJMz2wN4sTql+gpcsMvI0jGexEksvc3gmom8ivKw8iaHTBYVdQnfNAdXPossa7P5zOARYaNnNzTfZmjrVohPRaM2ehJzCz4GdI0Hueo1bZ/zfm6U043aeTg0xW8zdNLBbmxbu3Bn2ExaaRePIGu4qbV6syqcslEz846ibNfAngzGw7Ahi/Qxu/EyzmplDncd3ZW0L0s6zaKHya5RM/W6QIh9nAEJLMR+zQAd/D+OXrehF3RJGL7Io0ZSpXeNRuKCsIfaHlNbai/pQCVddwUMAb5okRapk4r8/KqSVb1sqmijtGmcll7xGg+Nm7xfczzoqIkzE4wQNMHgrQJt7qfEp7oqPCOu1zFJ9bw+r3UcRF4uKYwd8pOGohmN6p441PF5PChwBGVoRvis3UKL4vofyckVg7Rx9G5JCKxYC1cuW2CPFnu+1EK+zLTithSkUmRBOn0kpxjRBtO/m5gwe/bRYv1BXRCMAURrMJJc41BDYLNriUXW8+fltQ4cvaFqhw5dcdrxiB4OHtXcjvlVKZdYE3RPz2MHVwuAFZ4d78mpzNu1dLuo4Pj+KNRSNMQOkQHLWgozHKTWTUx4Kj1Mdxb/SE4XPoSEvbzGJkhiOq3T1gFHU7IF3CLS4xHRnYnkpmu1/MfzWocokfScWc5Y3HM7s5NGMRuNd+6g0F7UWat6ySyBnojBW/WZ/bQ71BLrsxPKeN75SObI6jc2oxc7JtmlJ6/VjdGieJw7PP8mvGs/papPRHexq/WEC2x5JI6CupujMsSU9DS6XpA4Fg655+ej8rUOuTDKJWpjEJD4ggrH+0DG8o0ETjnlGV62moqGE3CVgrTzkZxWF3P3WmoSnapiJmJw10GCRCyowBKz62PArQGGlGOXDc/r8p6cVthm1IW9GwFxEWiMHeHGdvcc51s5Po81rh7dUOngvVXRr8zp61H8/PoHREPEJfkKAAA=", 285 | "encoding": "UTF-8" 286 | }, 287 | "headers": { 288 | "Alt-Svc": [ 289 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 290 | ], 291 | "Cache-Control": [ 292 | "no-cache, no-store, max-age=0, must-revalidate" 293 | ], 294 | "Content-Encoding": [ 295 | "gzip" 296 | ], 297 | "Content-Security-Policy": [ 298 | "frame-ancestors 'self'" 299 | ], 300 | "Content-Type": [ 301 | "application/json; charset=UTF-8" 302 | ], 303 | "Date": [ 304 | "Tue, 18 Jan 2022 21:58:09 GMT" 305 | ], 306 | "Expires": [ 307 | "Mon, 01 Jan 1990 00:00:00 GMT" 308 | ], 309 | "Pragma": [ 310 | "no-cache" 311 | ], 312 | "Server": [ 313 | "GSE" 314 | ], 315 | "Transfer-Encoding": [ 316 | "chunked" 317 | ], 318 | "Vary": [ 319 | "Origin", 320 | "X-Origin" 321 | ], 322 | "X-Content-Type-Options": [ 323 | "nosniff" 324 | ], 325 | "X-Frame-Options": [ 326 | "SAMEORIGIN" 327 | ], 328 | "X-XSS-Protection": [ 329 | "1; mode=block" 330 | ] 331 | }, 332 | "status": { 333 | "code": 200, 334 | "message": "OK" 335 | }, 336 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%27root%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 337 | } 338 | } 339 | ], 340 | "recorded_with": "betamax/0.8.1" 341 | } -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_list_spreadsheet_files.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:06", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:06 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:07", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:07 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | }, 171 | { 172 | "recorded_at": "2022-01-18T21:58:07", 173 | "request": { 174 | "body": { 175 | "encoding": "utf-8", 176 | "string": "" 177 | }, 178 | "headers": { 179 | "Accept": [ 180 | "*/*" 181 | ], 182 | "Accept-Encoding": [ 183 | "gzip, deflate" 184 | ], 185 | "Connection": [ 186 | "keep-alive" 187 | ], 188 | "User-Agent": [ 189 | "python-requests/2.26.0" 190 | ], 191 | "authorization": [ 192 | "Bearer " 193 | ] 194 | }, 195 | "method": "GET", 196 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 197 | }, 198 | "response": { 199 | "body": { 200 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 201 | "encoding": "UTF-8" 202 | }, 203 | "headers": { 204 | "Alt-Svc": [ 205 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 206 | ], 207 | "Cache-Control": [ 208 | "no-cache, no-store, max-age=0, must-revalidate" 209 | ], 210 | "Content-Encoding": [ 211 | "gzip" 212 | ], 213 | "Content-Security-Policy": [ 214 | "frame-ancestors 'self'" 215 | ], 216 | "Content-Type": [ 217 | "application/json; charset=UTF-8" 218 | ], 219 | "Date": [ 220 | "Tue, 18 Jan 2022 21:58:07 GMT" 221 | ], 222 | "Expires": [ 223 | "Mon, 01 Jan 1990 00:00:00 GMT" 224 | ], 225 | "Pragma": [ 226 | "no-cache" 227 | ], 228 | "Server": [ 229 | "GSE" 230 | ], 231 | "Transfer-Encoding": [ 232 | "chunked" 233 | ], 234 | "Vary": [ 235 | "Origin", 236 | "X-Origin" 237 | ], 238 | "X-Content-Type-Options": [ 239 | "nosniff" 240 | ], 241 | "X-Frame-Options": [ 242 | "SAMEORIGIN" 243 | ], 244 | "X-XSS-Protection": [ 245 | "1; mode=block" 246 | ] 247 | }, 248 | "status": { 249 | "code": 200, 250 | "message": "OK" 251 | }, 252 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 253 | } 254 | }, 255 | { 256 | "recorded_at": "2022-01-18T21:58:08", 257 | "request": { 258 | "body": { 259 | "encoding": "utf-8", 260 | "string": "" 261 | }, 262 | "headers": { 263 | "Accept": [ 264 | "*/*" 265 | ], 266 | "Accept-Encoding": [ 267 | "gzip, deflate" 268 | ], 269 | "Connection": [ 270 | "keep-alive" 271 | ], 272 | "User-Agent": [ 273 | "python-requests/2.26.0" 274 | ], 275 | "authorization": [ 276 | "Bearer " 277 | ] 278 | }, 279 | "method": "GET", 280 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 281 | }, 282 | "response": { 283 | "body": { 284 | "base64_string": "H4sIAAAAAAAAAL2X2Y6r2BWG789TlOq6kcAGDLnDYObRzEQRYh7NPBha/e5xHyVS5JNEKaXsC0BsbcSn9a/h37//+PhMizoZP//y8dcfHx+/P66PzyJ+vH5CM3pAmcq9QVeC129sxGwx4lsjKw9+5HOpGV18G0g15s5Jn7/9/LAJbsmfnzJjNyRBDKhBEwfjh5GM04f+c2nMk2T6x+4uGJJm+uevHwuQxp0GadTyE7wBeHjQpey21tChQ8BrXLu19vnnvr89bn/89gxLrGeFHPgjwO4bcb/xpVjg0oBdBH9sGWjnU2TmhIW/iuv6BDs96F5BRHMSEOgTwRpJJK0oRCIGkgQ+sqfMaaPvuRHwdFfA2TkG30RE5OiuHakDM5tiV6jx0MbSUlmaCA3qnShhdC6AUHGP1wP3BSKQ4DBPgztAOnKKWeGqRfwXBpj1j5VV6yMmZ1mqQmoXG3R3HSXWL6v2QiUEU3IIrF7R56R6VVTmE7neSnpqp0ShwTjF5NIJAE5s+ULfAQuzFuaSEXmWZ18h+lpUPDOXd1Ged85k4dWm0jHn+v7kyVeRYRymutNEenbQq5eYL2MYukHI+LpXez3P2I1Y4ktHxJUa9MNUcVFlNGtIQQFxWLOXMfQobCrKcZC60eZu1AJEhBEaLWCsjLXjDJ5fPYuvTNJ3vxKH/yc7Useu+Z2fk0MyBuEUkbI6hVNjx/dN7qLh0ocw6pzKPOvGNxGN0snpHUez2OvRzAcJyhaQMbzC3oO7KvDlopnVSFGOZ2BPRD/7MFkXDxbj2yTb7dJl/cXbScfaivUuBpjsGR3d7dsBdvA7IIG1OO0loD+3uf91LHyNR02lVh97jSQEATkME6YOFCLpQCv5UH08AJFokzapk4r8XM5k220fbfqhDEVWNEH9PTwXwuSijuVAV83d+5kW4j6eg124mMiaR9TYJlfYC0Y6b5/j870cJ5GTGwpj5orXULS8oHogzl12XU7KMT2UaHmOGGc4bIoXvUUnT4yL3tXHLT9j9V57cjMACFojXKMlXFNqdbjVpFKXcfE8sF+jUwZrsxndTEy4Bw5uM9GsbjBGA6I9G3mlsaghMKXfYKn93Hq+l4NAQ1Qd0XmseYiD9WQOqD7EK19ptkwT9ECvMpdQa4ARnjleo1NTDXvjjWnNchMudFI6Z+65BLa9Ee7HuLBDMeeo4rTeGOItOllcchAQec9MkMT0i36xTwRakAPg1ameLbDuriS7+u32C8/3cogSebmXtrvUt8opnbxXzF7j3BsoDJZ619pJMhtgOmdg2L6nnqBTJzERs6J0EFxxsoL3qHdovYboHCr4YNCNxaY4gj09m7tX1VOhRuf0Jo6dnrOxIy9nXFChe9okmFLwixfEuWsTB+/6bCe+l0OujWZLhwwEJK6mkuU2k5kckgBfUYERlLupaMT52BbgxX2LTruHeYhWmOdRVTETNlh/lg4iFrdggzkdHrN7jMHNMpbzc1xeo9N+dGh1Y25GfLaEC8Ysx97xEJaN7Iq4Lx2h4l6ijEfEbi8v1QkRs1mxrCMWrFcUMuJriSkd4fGlcq/qwhPiCzPoJ/eM/NJ/X2W0+EwRu/3kmo9EXTKDoDbBUwTJan3UWPCQul3yLUV3WmGqNxHhFFv7x+vJc5kGnOmMGSSp0Mn4NszOYTnmgbQIKXzdMepdR8pzB2maLuEnmbBWZhqUUBnd0G5EyX4YZxQ46onA1H5/8d5F1EHUfmeAkuk9OiprWdGnlaTWR2kZAn4Hq0G6HllLBNHx2R6/ikg36mSlsWul2PdViiap6yFHBM7BkDJCqjMUotMHtKI9/l2Z3U1dtcigD4w+I/GbdXCadr0WcJLPUEslm95keKe33aC+61DT0GATtgxhLfpJqGTX4YZy4WqIhJKezs4edQIZXN71dX3uQq8iojwP93EtO3laF5ab2Owhd5Hlrtcah+Plpl9xPQXy7HJ6nu+vIrK0LcvtbtWAYvNl72x2j0OoSLKUXQ7ZypvluvssTrc78u8cxyuINNiUbhgYC1DfpC2IlNJiJSIfZk4QHmsf21VUi5tE6LhfPcftQVI02YdeFR9qMI7J+B8JD26Uh8iZPHQOlLqlNUFSfjNw2YB9CgSxf5kqPx6PP378HQRYdrbWFAAA", 285 | "encoding": "UTF-8" 286 | }, 287 | "headers": { 288 | "Alt-Svc": [ 289 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 290 | ], 291 | "Cache-Control": [ 292 | "no-cache, no-store, max-age=0, must-revalidate" 293 | ], 294 | "Content-Encoding": [ 295 | "gzip" 296 | ], 297 | "Content-Security-Policy": [ 298 | "frame-ancestors 'self'" 299 | ], 300 | "Content-Type": [ 301 | "application/json; charset=UTF-8" 302 | ], 303 | "Date": [ 304 | "Tue, 18 Jan 2022 21:58:08 GMT" 305 | ], 306 | "Expires": [ 307 | "Mon, 01 Jan 1990 00:00:00 GMT" 308 | ], 309 | "Pragma": [ 310 | "no-cache" 311 | ], 312 | "Server": [ 313 | "GSE" 314 | ], 315 | "Transfer-Encoding": [ 316 | "chunked" 317 | ], 318 | "Vary": [ 319 | "Origin", 320 | "X-Origin" 321 | ], 322 | "X-Content-Type-Options": [ 323 | "nosniff" 324 | ], 325 | "X-Frame-Options": [ 326 | "SAMEORIGIN" 327 | ], 328 | "X-XSS-Protection": [ 329 | "1; mode=block" 330 | ] 331 | }, 332 | "status": { 333 | "code": 200, 334 | "message": "OK" 335 | }, 336 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 337 | } 338 | } 339 | ], 340 | "recorded_with": "betamax/0.8.1" 341 | } -------------------------------------------------------------------------------- /tests/cassettes/tests.client_test.TestClient.test_find_spreadsheet_files_in_folders.json: -------------------------------------------------------------------------------- 1 | { 2 | "http_interactions": [ 3 | { 4 | "recorded_at": "2022-01-18T21:58:11", 5 | "request": { 6 | "body": { 7 | "encoding": "utf-8", 8 | "string": "" 9 | }, 10 | "headers": { 11 | "Accept": [ 12 | "*/*" 13 | ], 14 | "Accept-Encoding": [ 15 | "gzip, deflate" 16 | ], 17 | "Connection": [ 18 | "keep-alive" 19 | ], 20 | "User-Agent": [ 21 | "python-requests/2.26.0" 22 | ], 23 | "authorization": [ 24 | "Bearer " 25 | ] 26 | }, 27 | "method": "GET", 28 | "uri": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 29 | }, 30 | "response": { 31 | "body": { 32 | "base64_string": "H4sIAAAAAAAAAKvmUlDKTFGyUlAycPS0iAo0KdD1Nfb0D822DAhzVNIByuYl5qaC5H0rFVyKMstSlbhquQCVEs8GNgAAAA==", 33 | "encoding": "UTF-8" 34 | }, 35 | "headers": { 36 | "Alt-Svc": [ 37 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 38 | ], 39 | "Cache-Control": [ 40 | "no-cache, no-store, max-age=0, must-revalidate" 41 | ], 42 | "Content-Encoding": [ 43 | "gzip" 44 | ], 45 | "Content-Security-Policy": [ 46 | "frame-ancestors 'self'" 47 | ], 48 | "Content-Type": [ 49 | "application/json; charset=UTF-8" 50 | ], 51 | "Date": [ 52 | "Tue, 18 Jan 2022 21:58:11 GMT" 53 | ], 54 | "Expires": [ 55 | "Mon, 01 Jan 1990 00:00:00 GMT" 56 | ], 57 | "Pragma": [ 58 | "no-cache" 59 | ], 60 | "Server": [ 61 | "GSE" 62 | ], 63 | "Transfer-Encoding": [ 64 | "chunked" 65 | ], 66 | "Vary": [ 67 | "Origin", 68 | "X-Origin" 69 | ], 70 | "X-Content-Type-Options": [ 71 | "nosniff" 72 | ], 73 | "X-Frame-Options": [ 74 | "SAMEORIGIN" 75 | ], 76 | "X-XSS-Protection": [ 77 | "1; mode=block" 78 | ] 79 | }, 80 | "status": { 81 | "code": 200, 82 | "message": "OK" 83 | }, 84 | "url": "https://www.googleapis.com/drive/v3/files/root?fields=name%2Cid" 85 | } 86 | }, 87 | { 88 | "recorded_at": "2022-01-18T21:58:11", 89 | "request": { 90 | "body": { 91 | "encoding": "utf-8", 92 | "string": "" 93 | }, 94 | "headers": { 95 | "Accept": [ 96 | "*/*" 97 | ], 98 | "Accept-Encoding": [ 99 | "gzip, deflate" 100 | ], 101 | "Connection": [ 102 | "keep-alive" 103 | ], 104 | "User-Agent": [ 105 | "python-requests/2.26.0" 106 | ], 107 | "authorization": [ 108 | "Bearer " 109 | ] 110 | }, 111 | "method": "GET", 112 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 113 | }, 114 | "response": { 115 | "body": { 116 | "base64_string": "H4sIAAAAAAAAAL2VyY6jRgBA7/0VLZ8HCbDZ5gbGYMDsO1GE2GyMKWMWg81o/j3uKDl0Rw3uRJMDIEr1qCfxCn68vC72xzJrF99ff3t5ff3xOF4Xx/Rxu0CqGkvyBkvI9kBGo10YvGzaDE3g2lKkZQ8svv05+xyB7G1+emz+GrlETXbu/n7mYwBhDCuSTjJ3o2HIcs5ASQJkzeMH2m74Arqji7d5vz9OP799tJhF31ucs+FTC0HBfYgTRFNHXE6vDT3T1a7eH/FlEeiyn05YzKLvLaJPHa4yeW7rA59jYgufwsSt+D6pN3xDqsBd6v6Ewyz63uHYfiqRsHV9hFNJ0/YZK7AFpLimXd3OQSXmolb2ExKz6HuJLp/Q8HaJQA1bEWs0BWUgYUhkxAlVwGRwwEZyMaExZEMmbJGoby+UbpZDFK+JKEylAWj9mkCFL7R5HM9AheKTCbtCI6oU5nnSYW/f75VN2kBLJixm0efb3BArvuR6/WQcYlRqaC/aDVeJCvd4iEN36zZhMYs+2ybwCVdf9VYSbNQywq3icrIohVGTO8lzrK9POMyiT7e5u1y7MbpZicVHKb/dAJcz1TuwpChwHcklJiRm0f+nTdzVePckMtmwAWi5iqUrOUKpXI0jq2OPnfOFNqH8cmFjXIMHrLydrbze9kqIMVKCrNWWE/MJi1n0+Ta3YdmjS58+8X5MiVG4VZAVpKM+zZwbZX2PJyxm0WfbXEZuCZP4KrS6gg43mICIsVhW+2tV2tU1OUw4zKJPtxllXjegxcDhTlO2PXEltiRRSzoLcW4titSExCz6fJswLZCBvrpA8lJQ7ROlOfTEwrpANHKr58TqDlExasoHMJQIesFgIy398uPOnKpRaTYwbe50mzhzRQUJLkdzR5cweEfHQ1OZ6mAWfb7GoCtTKWaEtN0roB7BNUIlzQ+5buzhimeCCYtZ9NkasdH1Vadb5na3w66OAcO+hTn4KuqGUi2lqb/4LPp0jXdWlbO1KBfc1mmtdJTdAgSGDXGDonKHpTohMYv+qhpRP8ljjFmjFw/Z+4XTIXIOHn8KaxWyMEzSHxa2srZDf8U3OhBVMAYesdlG4tIvOM4U1i6NsbU3ruud5X7wMK/xm8rnPbBHz2wqCNARU+EGRhlyyLeQS/cCfcyN1VQPc+jXTETOPoA8N5d7SPX6nVl4W9ZpsJba55BRQmDCZBb9msl/eDuz6D8r+Vd5vjwuP1/+ABElPDIFDQAA", 117 | "encoding": "UTF-8" 118 | }, 119 | "headers": { 120 | "Alt-Svc": [ 121 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 122 | ], 123 | "Cache-Control": [ 124 | "no-cache, no-store, max-age=0, must-revalidate" 125 | ], 126 | "Content-Encoding": [ 127 | "gzip" 128 | ], 129 | "Content-Security-Policy": [ 130 | "frame-ancestors 'self'" 131 | ], 132 | "Content-Type": [ 133 | "application/json; charset=UTF-8" 134 | ], 135 | "Date": [ 136 | "Tue, 18 Jan 2022 21:58:11 GMT" 137 | ], 138 | "Expires": [ 139 | "Mon, 01 Jan 1990 00:00:00 GMT" 140 | ], 141 | "Pragma": [ 142 | "no-cache" 143 | ], 144 | "Server": [ 145 | "GSE" 146 | ], 147 | "Transfer-Encoding": [ 148 | "chunked" 149 | ], 150 | "Vary": [ 151 | "Origin", 152 | "X-Origin" 153 | ], 154 | "X-Content-Type-Options": [ 155 | "nosniff" 156 | ], 157 | "X-Frame-Options": [ 158 | "SAMEORIGIN" 159 | ], 160 | "X-XSS-Protection": [ 161 | "1; mode=block" 162 | ] 163 | }, 164 | "status": { 165 | "code": 200, 166 | "message": "OK" 167 | }, 168 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.folder%27&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 169 | } 170 | }, 171 | { 172 | "recorded_at": "2022-01-18T21:58:11", 173 | "request": { 174 | "body": { 175 | "encoding": "utf-8", 176 | "string": "" 177 | }, 178 | "headers": { 179 | "Accept": [ 180 | "*/*" 181 | ], 182 | "Accept-Encoding": [ 183 | "gzip, deflate" 184 | ], 185 | "Connection": [ 186 | "keep-alive" 187 | ], 188 | "User-Agent": [ 189 | "python-requests/2.26.0" 190 | ], 191 | "authorization": [ 192 | "Bearer " 193 | ] 194 | }, 195 | "method": "GET", 196 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%271ZJOmzZX7EHaJ3YjFFSICWA5DqXzCqLTW%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 197 | }, 198 | "response": { 199 | "body": { 200 | "base64_string": "H4sIAAAAAAAAAKvmUlBKy8xJLVayUoiO5arlAgAFrPVGEQAAAA==", 201 | "encoding": "UTF-8" 202 | }, 203 | "headers": { 204 | "Alt-Svc": [ 205 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 206 | ], 207 | "Cache-Control": [ 208 | "no-cache, no-store, max-age=0, must-revalidate" 209 | ], 210 | "Content-Encoding": [ 211 | "gzip" 212 | ], 213 | "Content-Security-Policy": [ 214 | "frame-ancestors 'self'" 215 | ], 216 | "Content-Type": [ 217 | "application/json; charset=UTF-8" 218 | ], 219 | "Date": [ 220 | "Tue, 18 Jan 2022 21:58:11 GMT" 221 | ], 222 | "Expires": [ 223 | "Mon, 01 Jan 1990 00:00:00 GMT" 224 | ], 225 | "Pragma": [ 226 | "no-cache" 227 | ], 228 | "Server": [ 229 | "GSE" 230 | ], 231 | "Transfer-Encoding": [ 232 | "chunked" 233 | ], 234 | "Vary": [ 235 | "Origin", 236 | "X-Origin" 237 | ], 238 | "X-Content-Type-Options": [ 239 | "nosniff" 240 | ], 241 | "X-Frame-Options": [ 242 | "SAMEORIGIN" 243 | ], 244 | "X-XSS-Protection": [ 245 | "1; mode=block" 246 | ] 247 | }, 248 | "status": { 249 | "code": 200, 250 | "message": "OK" 251 | }, 252 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%271ZJOmzZX7EHaJ3YjFFSICWA5DqXzCqLTW%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 253 | } 254 | }, 255 | { 256 | "recorded_at": "2022-01-18T21:58:12", 257 | "request": { 258 | "body": { 259 | "encoding": "utf-8", 260 | "string": "" 261 | }, 262 | "headers": { 263 | "Accept": [ 264 | "*/*" 265 | ], 266 | "Accept-Encoding": [ 267 | "gzip, deflate" 268 | ], 269 | "Connection": [ 270 | "keep-alive" 271 | ], 272 | "User-Agent": [ 273 | "python-requests/2.26.0" 274 | ], 275 | "authorization": [ 276 | "Bearer " 277 | ] 278 | }, 279 | "method": "GET", 280 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%2715DiXSro-mAaBo6R59RM_Gs-WAvIAihR4%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 281 | }, 282 | "response": { 283 | "body": { 284 | "base64_string": "H4sIAAAAAAAAAKvmUlBKy8xJLVayUoiO5arlAgAFrPVGEQAAAA==", 285 | "encoding": "UTF-8" 286 | }, 287 | "headers": { 288 | "Alt-Svc": [ 289 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 290 | ], 291 | "Cache-Control": [ 292 | "no-cache, no-store, max-age=0, must-revalidate" 293 | ], 294 | "Content-Encoding": [ 295 | "gzip" 296 | ], 297 | "Content-Security-Policy": [ 298 | "frame-ancestors 'self'" 299 | ], 300 | "Content-Type": [ 301 | "application/json; charset=UTF-8" 302 | ], 303 | "Date": [ 304 | "Tue, 18 Jan 2022 21:58:12 GMT" 305 | ], 306 | "Expires": [ 307 | "Mon, 01 Jan 1990 00:00:00 GMT" 308 | ], 309 | "Pragma": [ 310 | "no-cache" 311 | ], 312 | "Server": [ 313 | "GSE" 314 | ], 315 | "Transfer-Encoding": [ 316 | "chunked" 317 | ], 318 | "Vary": [ 319 | "Origin", 320 | "X-Origin" 321 | ], 322 | "X-Content-Type-Options": [ 323 | "nosniff" 324 | ], 325 | "X-Frame-Options": [ 326 | "SAMEORIGIN" 327 | ], 328 | "X-XSS-Protection": [ 329 | "1; mode=block" 330 | ] 331 | }, 332 | "status": { 333 | "code": 200, 334 | "message": "OK" 335 | }, 336 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%2715DiXSro-mAaBo6R59RM_Gs-WAvIAihR4%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 337 | } 338 | }, 339 | { 340 | "recorded_at": "2022-01-18T21:58:12", 341 | "request": { 342 | "body": { 343 | "encoding": "utf-8", 344 | "string": "" 345 | }, 346 | "headers": { 347 | "Accept": [ 348 | "*/*" 349 | ], 350 | "Accept-Encoding": [ 351 | "gzip, deflate" 352 | ], 353 | "Connection": [ 354 | "keep-alive" 355 | ], 356 | "User-Agent": [ 357 | "python-requests/2.26.0" 358 | ], 359 | "authorization": [ 360 | "Bearer " 361 | ] 362 | }, 363 | "method": "GET", 364 | "uri": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%271JFUgmhhS3f-OXvLSjXHDVr5s9fh-Rl-m%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 365 | }, 366 | "response": { 367 | "body": { 368 | "base64_string": "H4sIAAAAAAAAAKvmUlBKy8xJLVayUoiO5arlAgAFrPVGEQAAAA==", 369 | "encoding": "UTF-8" 370 | }, 371 | "headers": { 372 | "Alt-Svc": [ 373 | "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\"" 374 | ], 375 | "Cache-Control": [ 376 | "no-cache, no-store, max-age=0, must-revalidate" 377 | ], 378 | "Content-Encoding": [ 379 | "gzip" 380 | ], 381 | "Content-Security-Policy": [ 382 | "frame-ancestors 'self'" 383 | ], 384 | "Content-Type": [ 385 | "application/json; charset=UTF-8" 386 | ], 387 | "Date": [ 388 | "Tue, 18 Jan 2022 21:58:12 GMT" 389 | ], 390 | "Expires": [ 391 | "Mon, 01 Jan 1990 00:00:00 GMT" 392 | ], 393 | "Pragma": [ 394 | "no-cache" 395 | ], 396 | "Server": [ 397 | "GSE" 398 | ], 399 | "Transfer-Encoding": [ 400 | "chunked" 401 | ], 402 | "Vary": [ 403 | "Origin", 404 | "X-Origin" 405 | ], 406 | "X-Content-Type-Options": [ 407 | "nosniff" 408 | ], 409 | "X-Frame-Options": [ 410 | "SAMEORIGIN" 411 | ], 412 | "X-XSS-Protection": [ 413 | "1; mode=block" 414 | ] 415 | }, 416 | "status": { 417 | "code": 200, 418 | "message": "OK" 419 | }, 420 | "url": "https://www.googleapis.com/drive/v3/files?q=mimeType%3D%27application%2Fvnd.google-apps.spreadsheet%27+and+%271JFUgmhhS3f-OXvLSjXHDVr5s9fh-Rl-m%27+in+parents&pageSize=1000&fields=files%28name%2Cid%2Cparents%29&supportsAllDrives=True&includeItemsFromAllDrives=True" 421 | } 422 | } 423 | ], 424 | "recorded_with": "betamax/0.8.1" 425 | } -------------------------------------------------------------------------------- /tests/util_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | import warnings 3 | 4 | import numpy as np 5 | import pandas as pd 6 | import pytest 7 | from google.oauth2 import credentials, service_account 8 | from gspread.client import Client 9 | from gspread.exceptions import APIError 10 | from oauth2client.client import OAuth2Credentials 11 | from oauth2client.service_account import ServiceAccountCredentials 12 | 13 | from gspread_pandas import util 14 | 15 | TEST = 0 16 | ANSWER = 1 17 | DEMO_SHEET = "1u626GkYm1RAJSmHcGyd5_VsHNr_c_IfUcE_W-fQGxIM" 18 | 19 | 20 | @pytest.fixture 21 | def df(): 22 | data = [[1, 2], [3, 4]] 23 | cols = ["col1", "col2"] 24 | df = pd.DataFrame(data, columns=cols) 25 | df.index.name = "test_index" 26 | return df 27 | 28 | 29 | @pytest.fixture 30 | def df_empty(): 31 | return pd.DataFrame() 32 | 33 | 34 | @pytest.fixture 35 | def df_multiheader(): 36 | data = [[1, 2], [3, 4]] 37 | cols = pd.MultiIndex.from_tuples([("col1", "subcol1"), ("col1", "subcol2")]) 38 | return pd.DataFrame(data, columns=cols) 39 | 40 | 41 | @pytest.fixture 42 | def df_multiheader_w_index(): 43 | data = [[1, 2], [3, 4]] 44 | cols = pd.MultiIndex.from_tuples([("col1", "subcol1"), ("col1", "subcol2")]) 45 | df = pd.DataFrame(data, columns=cols) 46 | df.index.name = "test_index" 47 | return df.reset_index() 48 | 49 | 50 | @pytest.fixture 51 | def df_multiheader_w_multiindex(): 52 | data = [[1, 2], [3, 4]] 53 | cols = pd.MultiIndex.from_tuples([("col1", "subcol1"), ("col1", "subcol2")]) 54 | ix = pd.MultiIndex.from_tuples( 55 | [("row1", "subrow1"), ("row1", "subrow2")], names=["l1", "l2"] 56 | ) 57 | df = pd.DataFrame(data, columns=cols, index=ix) 58 | return df.reset_index() 59 | 60 | 61 | @pytest.fixture 62 | def df_multiheader_w_unnamed_multiindex(): 63 | data = [[1, 2], [3, 4]] 64 | cols = pd.MultiIndex.from_tuples([("col1", "subcol1"), ("col1", "subcol2")]) 65 | ix = pd.MultiIndex.from_tuples([("row1", "subrow1"), ("row1", "subrow2")]) 66 | df = pd.DataFrame(data, columns=cols, index=ix) 67 | return df.reset_index() 68 | 69 | 70 | @pytest.fixture 71 | def df_multiheader_blank_top(): 72 | data = [[1], [3]] 73 | cols = pd.MultiIndex.from_tuples([("", "subcol1")]) 74 | return pd.DataFrame(data, columns=cols) 75 | 76 | 77 | @pytest.fixture 78 | def df_multiheader_blank_bottom(): 79 | data = [[1], [3]] 80 | cols = pd.MultiIndex.from_tuples([("col1", "")]) 81 | return pd.DataFrame(data, columns=cols) 82 | 83 | 84 | @pytest.fixture 85 | def data_multiheader(): 86 | data = [ 87 | ["", "col1", "col1"], 88 | ["test_index", "subcol1", "subcol2"], 89 | [1, 2, 3], 90 | [4, 5, 6], 91 | ] 92 | return data 93 | 94 | 95 | @pytest.fixture 96 | def data_multiheader_top(): 97 | data = [ 98 | ["test_index", "col1", "col1"], 99 | ["", "subcol1", "subcol2"], 100 | [1, 2, 3], 101 | [4, 5, 6], 102 | ] 103 | return data 104 | 105 | 106 | @pytest.fixture 107 | def data_empty(): 108 | return [[]] 109 | 110 | 111 | class Test_parse_sheet_index: 112 | def test_normal(self, df): 113 | assert util.parse_sheet_index(df, 1).index.name == "col1" 114 | 115 | def test_noop(self, df): 116 | assert util.parse_sheet_index(df, 0).index.name == "test_index" 117 | 118 | def test_multiheader(self, df_multiheader): 119 | assert util.parse_sheet_index(df_multiheader, 1).index.name == "subcol1" 120 | 121 | def test_multiheader2(self, df_multiheader): 122 | assert util.parse_sheet_index(df_multiheader, 2).index.name == "subcol2" 123 | 124 | def test_multiheader_blank_top(self, df_multiheader_blank_top): 125 | assert ( 126 | util.parse_sheet_index(df_multiheader_blank_top, 1).index.name == "subcol1" 127 | ) 128 | 129 | def test_multiheader_blank_bottom(self, df_multiheader_blank_bottom): 130 | assert ( 131 | util.parse_sheet_index(df_multiheader_blank_bottom, 1).index.name == "col1" 132 | ) 133 | 134 | 135 | class Test_parse_df_col_names: 136 | def test_empty_no_index(self, df_empty): 137 | assert util.parse_df_col_names(df_empty, False) == [[]] 138 | 139 | def test_normal_no_index(self, df): 140 | assert util.parse_df_col_names(df, False) == [["col1", "col2"]] 141 | 142 | def test_multiheader_no_index(self, df_multiheader): 143 | expected = [["col1", "col1"], ["subcol1", "subcol2"]] 144 | assert util.parse_df_col_names(df_multiheader, False) == expected 145 | 146 | def test_multiheader_w_index(self, df_multiheader_w_index): 147 | expected = [["", "col1", "col1"], ["test_index", "subcol1", "subcol2"]] 148 | assert util.parse_df_col_names(df_multiheader_w_index, True) == expected 149 | 150 | def test_multiheader_w_multiindex(self, df_multiheader_w_multiindex): 151 | expected = [["", "", "col1", "col1"], ["l1", "l2", "subcol1", "subcol2"]] 152 | assert util.parse_df_col_names(df_multiheader_w_multiindex, True, 2) == expected 153 | 154 | def test_multiheader_no_index_flatten(self, df_multiheader): 155 | expected = [["col1 subcol1", "col1 subcol2"]] 156 | assert ( 157 | util.parse_df_col_names(df_multiheader, False, flatten_sep=" ") == expected 158 | ) 159 | 160 | def test_multiheader_w_index_flatten(self, df_multiheader_w_index): 161 | expected = [["test_index", "col1 subcol1", "col1 subcol2"]] 162 | assert ( 163 | util.parse_df_col_names(df_multiheader_w_index, True, flatten_sep=" ") 164 | == expected 165 | ) 166 | 167 | def test_multiheader_w_multiindex_flatten(self, df_multiheader_w_multiindex): 168 | expected = [["l1", "l2", "col1 subcol1", "col1 subcol2"]] 169 | assert ( 170 | util.parse_df_col_names( 171 | df_multiheader_w_multiindex, True, 2, flatten_sep=" " 172 | ) 173 | == expected 174 | ) 175 | 176 | def test_multiheader_w_unnamed_multiindex_flatten( 177 | self, df_multiheader_w_unnamed_multiindex 178 | ): 179 | expected = [["", "", "col1 subcol1", "col1 subcol2"]] 180 | assert ( 181 | util.parse_df_col_names( 182 | df_multiheader_w_unnamed_multiindex, True, 2, flatten_sep=" " 183 | ) 184 | == expected 185 | ) 186 | 187 | 188 | class Test_parse_sheet_headers: 189 | def test_empty(self, data_empty): 190 | assert util.parse_sheet_headers(data_empty, 0) is None 191 | 192 | def test_normal(self, data_multiheader): 193 | expected = pd.Index(["", "col1", "col1"]) 194 | assert util.parse_sheet_headers(data_multiheader, 1).equals(expected) 195 | 196 | def test_multiheader(self, data_multiheader): 197 | """Note that 'test_index' should be shifted up.""" 198 | expected = pd.MultiIndex.from_arrays( 199 | [["test_index", "col1", "col1"], ["", "subcol1", "subcol2"]] 200 | ) 201 | assert util.parse_sheet_headers(data_multiheader, 2).equals(expected) 202 | 203 | def test_multiheader3(self, data_multiheader): 204 | """Note that 'test_index' and 1 should be shifted up.""" 205 | expected = pd.MultiIndex.from_arrays( 206 | [["test_index", "col1", "col1"], [1, "subcol1", "subcol2"], ["", 2, 3]] 207 | ) 208 | assert util.parse_sheet_headers(data_multiheader, 3).equals(expected) 209 | 210 | 211 | def test_chunks(): 212 | tests = [ 213 | (([], 1), []), 214 | (([1], 1), [[1]]), 215 | (([1, 2], 1), [[1], [2]]), 216 | (([1, 2, 3], 2), [[1, 2], [3]]), 217 | ] 218 | for test in tests: 219 | assert [chunk for chunk in util.chunks(*test[TEST])] == test[ANSWER] 220 | 221 | 222 | def test_get_cell_as_tuple(): 223 | tests = [("A1", (1, 1)), ((1, 1), (1, 1))] 224 | for test in tests: 225 | assert util.get_cell_as_tuple(test[TEST]) == test[ANSWER] 226 | 227 | bad_tests = [ 228 | "This is a bad cell string", 229 | (1.0, 1.0), 230 | (1, 1, 1), 231 | {"x": "y"}, 232 | 10000000, 233 | ] 234 | 235 | for test in bad_tests: 236 | with pytest.raises(TypeError): 237 | util.get_cell_as_tuple(test) 238 | 239 | 240 | def test_create_filter_request(): 241 | ret = util.create_filter_request("", "A1", "A1") 242 | assert isinstance(ret, dict) 243 | 244 | 245 | def test_create_frozen_request(): 246 | ret = util.create_frozen_request("", 1, 2) 247 | assert isinstance(ret, dict) 248 | 249 | 250 | def test_create_merge_cells_request(): 251 | ret = util.create_merge_cells_request("", "A1", "A1") 252 | assert isinstance(ret, dict) 253 | 254 | 255 | def test_create_unmerge_cells_request(): 256 | ret = util.create_unmerge_cells_request("", "A1", "A1") 257 | assert isinstance(ret, dict) 258 | 259 | 260 | def test_create_merge_headers_request(df_multiheader_blank_bottom): 261 | ret = util.create_merge_headers_request( 262 | "", df_multiheader_blank_bottom.columns, "A1", 0 263 | ) 264 | assert isinstance(ret, list) 265 | 266 | 267 | def test_deprecate(recwarn): 268 | with pytest.deprecated_call() as calls: 269 | util.DEPRECATION_WARNINGS_ENABLED = False 270 | util.deprecate("") 271 | assert len(calls) == 1 272 | 273 | util.DEPRECATION_WARNINGS_ENABLED = True 274 | util.deprecate("") 275 | assert warnings.filters[0][0] == "default" 276 | assert len(calls) == 2 277 | 278 | util.DEPRECATION_WARNINGS_ENABLED = False 279 | util.deprecate("") 280 | assert warnings.filters[0][0] == "ignore" 281 | assert len(calls) == 3 282 | 283 | 284 | def test_monkey_patch_request(betamax_authorizedsession): 285 | c = Client(betamax_authorizedsession.credentials, betamax_authorizedsession) 286 | s = c.open_by_key(DEMO_SHEET) 287 | 288 | # error gets turned into multiple lines by prettyjson serializer in betamax 289 | # need to a regex class that includes newlines 290 | with pytest.raises(APIError, match=r".*Read requests.*RESOURCE_EXHAUSTED.*"): 291 | for i in range(200): 292 | s.fetch_sheet_metadata() 293 | 294 | retry_delay = 10 if pytest.RECORD else 0 295 | 296 | util.monkey_patch_request(c, retry_delay) 297 | 298 | for i in range(200): 299 | s.fetch_sheet_metadata() 300 | 301 | 302 | def test_get_col_merge_ranges(): 303 | ix = pd.MultiIndex.from_arrays( 304 | [ 305 | ["col1", "col1", "col2", "col2"], 306 | ["subcol1", "subcol1", "subcol1", "subcol1"], 307 | ["subsubcol1", "subsubcol2", "subsubcol2", "subsubcol2"], 308 | ] 309 | ) 310 | assert util.get_merge_ranges(ix) == [ 311 | [(0, 1), (2, 3)], 312 | [(0, 1), (2, 3)], 313 | [(2, 3)], 314 | ] 315 | 316 | 317 | def test_fillna(df): 318 | df.loc[2] = [None, None] 319 | df.loc[3] = [np.NaN, np.NaN] 320 | df["col1"] = df["col1"].astype("category") 321 | 322 | assert ( 323 | util.fillna(df, "n\a").loc[2].tolist() 324 | == util.fillna(df, "n\a").loc[3].tolist() 325 | == ["n\a", "n\a"] 326 | ) 327 | 328 | 329 | def test_get_range(): 330 | assert util.get_range("a1", (3, 3)) == "A1:C3" 331 | 332 | 333 | def test_get_contiguous_ranges(): 334 | tests = [ 335 | ([0], []), 336 | ([0, 0], [(0, 1)]), 337 | ([0, 1], []), 338 | ([0, 0, 0], [(0, 2)]), 339 | ([0, 0, 1], [(0, 1)]), 340 | ([0, 1, 1], [(1, 2)]), 341 | ([0, 1, 0], []), 342 | ([0, 0, 1, 1], [(0, 1), (2, 3)]), 343 | ([0, 0, 1, 1, 0, 0], [(0, 1), (2, 3), (4, 5)]), 344 | ([0, 1, 1, 1, 1, 0], [(1, 4)]), 345 | ] 346 | 347 | for test in tests: 348 | assert util.get_contiguous_ranges(test[TEST], 0, len(test[0])) == test[ANSWER] 349 | 350 | 351 | def test_convert(creds_json, sa_config_json): 352 | oauth = OAuth2Credentials.from_json(json.dumps(creds_json)) 353 | sa = ServiceAccountCredentials.from_json_keyfile_dict(sa_config_json) 354 | 355 | assert isinstance(util.convert_credentials(oauth), credentials.Credentials) 356 | assert isinstance(util.convert_credentials(sa), service_account.Credentials) 357 | 358 | 359 | def test_parse_permissions(): 360 | tests = [ 361 | ( 362 | "aiguo.fernandez@gmail.com", 363 | { 364 | "value": "aiguo.fernandez@gmail.com", 365 | "perm_type": "user", 366 | "role": "reader", 367 | }, 368 | ), 369 | ( 370 | "aiguofer.com|owner", 371 | {"value": "aiguofer.com", "perm_type": "domain", "role": "owner"}, 372 | ), 373 | ("anyone|writer", {"perm_type": "anyone", "role": "writer"}), 374 | ( 375 | "difernan@redhat.com|no", 376 | { 377 | "value": "difernan@redhat.com", 378 | "perm_type": "user", 379 | "role": "reader", 380 | "notify": False, 381 | }, 382 | ), 383 | ("anyone|link", {"perm_type": "anyone", "role": "reader", "with_link": True}), 384 | ] 385 | 386 | for test in tests: 387 | assert util.parse_permission(test[TEST]) == test[ANSWER] 388 | 389 | 390 | def test_remove_keys(): 391 | tests = [ 392 | ([{}], {}), 393 | ([{}, ["stuff"]], {}), 394 | ([{"foo": "bar", "bar": "foo"}, ["foo"]], {"bar": "foo"}), 395 | ([{"foo": "bar", "bar": "foo"}, ["foo", "bar"]], {}), 396 | ([{"foo": "bar", "bar": "foo"}], {"foo": "bar", "bar": "foo"}), 397 | ([{"foo": "bar", "bar": "foo"}, ["doesntexist"]], {"foo": "bar", "bar": "foo"}), 398 | ] 399 | 400 | for test in tests: 401 | assert util.remove_keys(*test[TEST]) == test[ANSWER] 402 | 403 | 404 | def test_remove_keys_from_list(): 405 | tests = [ 406 | ( 407 | [[{"foo": "bar", "bar": "foo"}, {"foo": "fighter"}], ["foo"]], 408 | [{"bar": "foo"}, {}], 409 | ), 410 | ( 411 | [[{"foo": "bar", "bar": "foo"}, {"foo": "fighter"}], ["bar"]], 412 | [{"foo": "bar"}, {"foo": "fighter"}], 413 | ), 414 | ([[], ["foo", "bar"]], []), 415 | ([[]], []), 416 | ] 417 | 418 | for test in tests: 419 | assert util.remove_keys_from_list(*test[TEST]) == test[ANSWER] 420 | 421 | 422 | def test_add_paths(): 423 | tests = [ 424 | ( 425 | [ 426 | {"id": "root"}, 427 | [ 428 | {"id": "1", "name": "sub", "parents": ["root"]}, 429 | {"id": "2", "name": "subsub", "parents": ["1"]}, 430 | {"id": "3", "name": "sub1", "parents": ["root"]}, 431 | {"id": "4", "name": "subsubsub", "parents": ["2"]}, 432 | ], 433 | ], 434 | [ 435 | {"id": "1", "name": "sub", "parents": ["root"], "path": "/sub"}, 436 | {"id": "2", "name": "subsub", "parents": ["1"], "path": "/sub/subsub"}, 437 | {"id": "3", "name": "sub1", "parents": ["root"], "path": "/sub1"}, 438 | { 439 | "id": "4", 440 | "name": "subsubsub", 441 | "parents": ["2"], 442 | "path": "/sub/subsub/subsubsub", 443 | }, 444 | ], 445 | ) 446 | ] 447 | 448 | for test in tests: 449 | util.add_paths(*test[TEST]) 450 | assert test[TEST][1] == test[ANSWER] 451 | 452 | 453 | def test_folders_to_create(): 454 | dirs = [ 455 | {"id": "1", "name": "sub", "parents": ["root"], "path": "/sub"}, 456 | {"id": "2", "name": "subsub", "parents": ["1"], "path": "/sub/subsub"}, 457 | {"id": "3", "name": "sub1", "parents": ["root"], "path": "/sub1"}, 458 | { 459 | "id": "4", 460 | "name": "subsubsub", 461 | "parents": ["2"], 462 | "path": "/sub/subsub/subsubsub", 463 | }, 464 | ] 465 | 466 | tests = [ 467 | ("/does/not/exist", ({"id": "root"}, ["does", "not", "exist"])), 468 | ("/sub/does/not/exist", (dirs[0], ["does", "not", "exist"])), 469 | ("/sub/subsub", (dirs[1], [])), 470 | ] 471 | 472 | for test in tests: 473 | assert util.folders_to_create(test[TEST], dirs) == test[ANSWER] 474 | -------------------------------------------------------------------------------- /gspread_pandas/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from google.auth.credentials import Credentials 3 | from google.auth.transport.requests import AuthorizedSession 4 | from gspread import Spreadsheet 5 | from gspread.client import Client as ClientV4 6 | from gspread.exceptions import APIError, SpreadsheetNotFound 7 | from gspread.utils import finditem 8 | 9 | from gspread_pandas.conf import default_scope, get_creds 10 | from gspread_pandas.util import ( 11 | add_paths, 12 | convert_credentials, 13 | folders_to_create, 14 | monkey_patch_request, 15 | remove_keys_from_list, 16 | ) 17 | 18 | __all__ = ["Client"] 19 | 20 | 21 | class Client(ClientV4): 22 | """ 23 | The gspread_pandas :class:`Client` extends :class:`Client ` 24 | and authenticates using credentials stored in ``gspread_pandas`` config. 25 | 26 | This class also adds a few convenience methods to explore the user's google drive 27 | for spreadsheets. 28 | 29 | Parameters 30 | ---------- 31 | user : str 32 | optional, string indicating the key to a users credentials, 33 | which will be stored in a file (by default they will be stored in 34 | ``~/.config/gspread_pandas/creds/`` but can be modified with 35 | ``creds_dir`` property in config). If using a Service Account, this 36 | will be ignored. (default "default") 37 | config : dict 38 | optional, if you want to provide an alternate configuration, 39 | see :meth:`get_config ` 40 | (default None) 41 | scope : list 42 | optional, if you'd like to provide your own scope 43 | (default default_scope) 44 | creds : google.auth.credentials.Credentials 45 | optional, pass credentials if you have those already (default None) 46 | session : google.auth.transport.requests.AuthorizedSession 47 | optional, pass a google.auth.transport.requests.AuthorizedSession or a 48 | requests.Session and creds (default None) 49 | load_dirs : bool 50 | optional, whether you want to load directories and paths on instanciation. 51 | if you refresh directories later or perform an action that requires them, 52 | they will be loaded at that time. For speed, this is disabled by default 53 | (default False) 54 | """ 55 | 56 | _email = None 57 | _root = None 58 | _dirs = None 59 | _load_dirs = False 60 | 61 | def __init__( 62 | self, 63 | user="default", 64 | config=None, 65 | scope=default_scope, 66 | creds=None, 67 | session=None, 68 | load_dirs=False, 69 | ): 70 | #: `(list)` - Feeds included for the OAuth2 scope 71 | self.scope = scope 72 | 73 | if isinstance(session, requests.Session): 74 | credentials = getattr(session, "credentials", creds) 75 | if not credentials: 76 | raise TypeError( 77 | "If you provide a session, you must also provide credentials" 78 | ) 79 | else: 80 | if isinstance(creds, Credentials): 81 | credentials = creds 82 | elif creds is not None and "oauth2client" in creds.__module__: 83 | credentials = convert_credentials(creds) 84 | elif isinstance(user, str): 85 | credentials = get_creds(user, config, self.scope) 86 | else: 87 | raise TypeError( 88 | "Need to provide user as a string or credentials as " 89 | "google.auth.credentials.Credentials" 90 | ) 91 | session = AuthorizedSession(credentials) 92 | super().__init__(credentials, session) 93 | 94 | monkey_patch_request(self) 95 | 96 | self._root = self._drive_request(file_id="root", params={"fields": "name,id"}) 97 | self._root["path"] = "/" 98 | 99 | if load_dirs: 100 | self.refresh_directories() 101 | 102 | @property 103 | def root(self): 104 | """`(dict)` - the info for the top level Drive directory for current user""" 105 | return self._root 106 | 107 | def _get_dirs(self, strip_parents=True): 108 | """ 109 | Helper function to fetch directories if they haven't been yet. 110 | 111 | It will strip the parents by default for the `directories` 112 | property 113 | """ 114 | if not self._load_dirs: 115 | self.refresh_directories() 116 | 117 | if strip_parents: 118 | # this will make a copy, if we intend to modify the values 119 | # internally, pass strip_parents = False 120 | return remove_keys_from_list(self._dirs + [self.root], ["parents"]) 121 | else: 122 | return self._dirs + [self.root] 123 | 124 | directories = property( 125 | _get_dirs, 126 | doc=( 127 | "`(list)` - list of dicts for all avaliable " 128 | "directories for the current user" 129 | ), 130 | ) 131 | 132 | @property 133 | def email(self): 134 | """`(str)` - E-mail for the currently authenticated user""" 135 | if not self._email: 136 | try: 137 | self._email = self.request( 138 | "get", "https://www.googleapis.com/userinfo/v2/me" 139 | ).json()["email"] 140 | except Exception: 141 | print( 142 | """ 143 | Couldn't retrieve email. Delete credentials and authenticate again 144 | """ 145 | ) 146 | 147 | return self._email 148 | 149 | def refresh_directories(self): 150 | """Refresh list of directories for the current user.""" 151 | self._load_dirs = True 152 | q = "mimeType='application/vnd.google-apps.folder'" 153 | self._dirs = self._query_drive(q) 154 | root_dirs = [self.root] 155 | 156 | for dir_ in self._dirs: 157 | # these are top level shared drives 158 | if "parents" not in dir_: 159 | dir_["path"] = dir_["name"] 160 | root_dirs.append(dir_) 161 | 162 | for root_dir in root_dirs: 163 | add_paths(root_dir, self._dirs) 164 | 165 | def login(self): 166 | """Override login since AuthorizedSession now takes care of automatically 167 | refreshing tokens when needed.""" 168 | 169 | def _query_drive(self, q): 170 | files = [] 171 | page_token = "" 172 | params = { 173 | "q": q, 174 | "pageSize": 1000, 175 | "fields": "files(name,id,parents)", 176 | "supportsAllDrives": True, 177 | "includeItemsFromAllDrives": True, 178 | } 179 | 180 | while page_token is not None: 181 | if page_token: 182 | params["pageToken"] = page_token 183 | 184 | res = self._drive_request("get", params=params) 185 | files.extend(res.get("files", [])) 186 | page_token = res.get("nextPageToken", None) 187 | 188 | return files 189 | 190 | def _drive_request( 191 | self, method="get", file_id=None, params=None, data=None, headers=None 192 | ): 193 | url = "https://www.googleapis.com/drive/v3/files" 194 | if file_id: 195 | url += "/{}".format(file_id) 196 | try: 197 | res = self.request(method, url, params=params, json=data) 198 | if res.text: 199 | return res.json() 200 | except APIError as e: 201 | if "scopes" in e.response.text: 202 | print( 203 | "Your credentials don't have Drive API access, ignoring " 204 | "drive specific functionality (Note this includes searching " 205 | "spreadsheets by name)" 206 | ) 207 | return {} 208 | raise 209 | 210 | def open(self, title): 211 | """ 212 | Opens a spreadsheet. 213 | 214 | :param title: A title of a spreadsheet. 215 | :type title: str 216 | 217 | :returns: a :class:`~gspread.spreadsheet.Spreadsheet` instance. 218 | 219 | If there's more than one spreadsheet with same title the first one 220 | will be opened. 221 | 222 | :raises gspread.SpreadsheetNotFound: if no spreadsheet with 223 | specified `title` is found. 224 | 225 | >>> c = gspread.authorize(credentials) 226 | >>> c.open('My fancy spreadsheet') 227 | """ 228 | try: 229 | properties = finditem( 230 | lambda x: x["name"] == title, self.list_spreadsheet_files(title) 231 | ) 232 | 233 | # Drive uses different terminology 234 | properties["title"] = properties["name"] 235 | 236 | return Spreadsheet(self, properties) 237 | except StopIteration: 238 | raise SpreadsheetNotFound 239 | 240 | def list_spreadsheet_files(self, title=None): 241 | """ 242 | Return all spreadsheets that the user has access to. 243 | 244 | Parameters 245 | ---------- 246 | title : str 247 | name of the spreadsheet, if none is passed it'll return every file 248 | (default None) 249 | 250 | Returns 251 | ------- 252 | list 253 | List of spreadsheets. Each spreadsheet is a dict with the following keys: 254 | id, kind, mimeType, and name. 255 | """ 256 | q = "mimeType='application/vnd.google-apps.spreadsheet'" 257 | if title: 258 | q += ' and name = "{}"'.format(title) 259 | return self._list_spreadsheet_files(q) 260 | 261 | def list_spreadsheet_files_in_folder(self, folder_id): 262 | """ 263 | Return all spreadsheets that the user has access to in a sepcific folder. 264 | 265 | Parameters 266 | ---------- 267 | folder_id : str 268 | ID of a folder, see :meth:`find_folders ` 269 | 270 | Returns 271 | ------- 272 | list 273 | List of spreadsheets. Each spreadsheet is a dict with the following keys: 274 | id, kind, mimeType, and name. 275 | """ 276 | q = ( 277 | "mimeType='application/vnd.google-apps.spreadsheet'" 278 | " and '{}' in parents".format(folder_id) 279 | ) 280 | 281 | return self._list_spreadsheet_files(q) 282 | 283 | def _list_spreadsheet_files(self, q): 284 | """Helper function to actually run a query, add paths if needed, and remove 285 | unwanted keys from results.""" 286 | files = self._query_drive(q) 287 | 288 | if self._load_dirs: 289 | self._add_path_to_files(files) 290 | 291 | return remove_keys_from_list(files, ["parents"]) 292 | 293 | def _add_path_to_files(self, files): 294 | """Add path to files by looking up the parent dir and its path.""" 295 | for fil3 in files: 296 | try: 297 | # if a file is in multiple directories then it'll 298 | # have multiple parents. However, this adds complexity 299 | # so we'll just choose the first one to build the path 300 | parent = next( 301 | directory 302 | for directory in self._get_dirs(False) 303 | if directory["id"] in fil3.get("parents", None) 304 | ) 305 | 306 | if parent is None: 307 | # This is a top level shared drive 308 | fil3["path"] = f"{fil3['name']}/" 309 | else: 310 | fil3["path"] = parent.get("path", "/") 311 | except StopIteration: 312 | # Files that are visible to a ServiceAccount but not 313 | # in the root will not have the 'parents' property 314 | fil3["path"] = None 315 | 316 | def find_folders(self, folder_name_query=""): 317 | """ 318 | Return all folders that the user has access to containing ``folder_name_query`` 319 | in the name. 320 | 321 | Parameters 322 | ---------- 323 | folder_name_query : str 324 | Case insensitive string to search in folder name. If empty, 325 | it will return all folders. 326 | 327 | Returns 328 | ------- 329 | list 330 | List of folders. Each folder is a dict with the following keys: 331 | id, kind, mimeType, and name. 332 | """ 333 | return [ 334 | folder 335 | for folder in self.directories 336 | if folder_name_query.lower() in folder["name"].lower() 337 | ] 338 | 339 | def find_spreadsheet_files_in_folders(self, folder_name_query): 340 | """ 341 | Return all spreadsheets that the user has access to in all the folders that 342 | contain ``folder_name_query`` in the name. Returns as a dict with each key being 343 | the folder name and the value being a list of spreadsheet files. 344 | 345 | Parameters 346 | ---------- 347 | folder_name_query : str 348 | Case insensitive string to search in folder name 349 | 350 | Returns 351 | ------- 352 | dict 353 | Spreadsheets in each folder. Each entry is a dict with the folder name as 354 | the key and a list of spreadsheets as the value. Each spreadsheet is a dict 355 | with the following keys: id, kind, mimeType, and name. 356 | """ 357 | 358 | return { 359 | res["name"]: self.list_spreadsheet_files_in_folder(res["id"]) 360 | for res in self.find_folders(folder_name_query) 361 | } 362 | 363 | def create_folder(self, path, parents=True): 364 | """ 365 | Create a new folder in your Google drive. 366 | 367 | Parameters 368 | ---------- 369 | path : str 370 | folder path 371 | parents : bool 372 | if True, create parent folders as needed (Default value = True) 373 | 374 | Returns 375 | ------- 376 | dict 377 | information for the created directory 378 | """ 379 | parent, to_create = folders_to_create(path, self._get_dirs(False)) 380 | 381 | if len(to_create) > 1 and parents is not True: 382 | raise Exception( 383 | "If you want to create nested directories pass parents=True" 384 | ) 385 | 386 | for directory in to_create: 387 | parent = self._drive_request( 388 | "post", 389 | params={"fields": "name,id,parents"}, 390 | data={ 391 | "mimeType": "application/vnd.google-apps.folder", 392 | "name": directory, 393 | "parents": [parent["id"]], 394 | }, 395 | headers={"Content-Type": "application/json"}, 396 | ) 397 | 398 | self.refresh_directories() 399 | return parent 400 | 401 | def move_file(self, file_id, path, create=False): 402 | """ 403 | Move a file to the given path. 404 | 405 | Parameters 406 | ---------- 407 | file_id : str 408 | file id 409 | path : str 410 | folder path. A path starting with `/` will use your drive, for shared drives 411 | the path will start with `/` (no leading /) 412 | create : bool 413 | whether to create any missing folders (Default value = False) 414 | 415 | Returns 416 | ------- 417 | """ 418 | if path == "/": 419 | folder_id = "root" 420 | else: 421 | parent, missing = folders_to_create(path, self._get_dirs(False)) 422 | if missing: 423 | if not create: 424 | raise Exception("Folder does not exist") 425 | 426 | parent = self.create_folder(path) 427 | folder_id = parent["id"] 428 | 429 | old_parents = self._drive_request( 430 | "get", file_id, params={"fields": "parents"} 431 | ).get("parents", []) 432 | 433 | params = {"addParents": folder_id, "removeParents": ",".join(old_parents)} 434 | self._drive_request("patch", file_id, params) 435 | --------------------------------------------------------------------------------