Skip to content

Commit

Permalink
Merge branch 'master' into add/notes-range
Browse files Browse the repository at this point in the history
  • Loading branch information
lavigne958 authored Oct 7, 2024
2 parents 95cbb1e + d606420 commit 58d7368
Show file tree
Hide file tree
Showing 16 changed files with 1,832 additions and 35 deletions.
2 changes: 1 addition & 1 deletion docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Using ``Authlib`` instead of ``google-auth``. Similar to `google.auth.transport.
claims = {'scope': ' '.join(scopes)}
return AssertionSession(
grant_type=AssertionSession.JWT_BEARER_GRANT_TYPE,
token_url=token_url,
token_endpoint=token_url,
issuer=issuer,
audience=token_url,
claims=claims,
Expand Down
30 changes: 30 additions & 0 deletions docs/community.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Community Extensions
====================

.. _gspread-formating-label:

gspread-formating
~~~~~~~~~~~~~~~~~

`gspread-formatting <https://github.com/robin900/gspread-formatting>`_ offers extensive functionality to help you when you go beyond basic format
provided by ``gspread``.


.. _gspread-pandas-label:

Using gspread with pandas
~~~~~~~~~~~~~~~~~~~~~~~~~

You can find the below libraries to use gsrpead with pandas:

* `gspread-pandas <https://github.com/aiguofer/gspread-pandas>`_
* `gspread-dataframe <https://github.com/robin900/gspread-dataframe>`_

.. _gspread-orm-label:

Object Relational Mappers (ORMs)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The `gspread-models <https://github.com/s2t2/gspread-models-py>`_ package provides a straightforward and intuitive model-based
query interface, making it easy to interact with Google Sheets as if it were more like a database.

8 changes: 8 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ Advanced

advanced

Community extensions
--------------------

.. toctree::
:maxdepth: 2

community


API Documentation
---------------------------
Expand Down
75 changes: 69 additions & 6 deletions docs/user-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,73 @@ Check out the api docs for `DataValidationRule`_ and `CondtionType`_ for more de

.. _CondtionType: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/other#ConditionType

.. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule
.. _DataValidationRule: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#DataValidationRule

Extract table
~~~~~~~~~~~~~

Gspread provides a function to extract a data table.
A data table is defined as a rectangular table that stops either on the **first empty** cell or
the **enge of the sheet**.

You can extract table from any address by providing the top left corner of the desired table.

Gspread provides 3 directions for searching the end of the table:

* :attr:`~gspread.utils.TableDirection.right`: extract a single row searching on the right of the starting cell
* :attr:`~gspread.utils.TableDirection.down`: extract a single column searching on the bottom of the starting cell
* :attr:`~gspread.utils.TableDirection.table`: extract a rectangular table by first searching right from starting cell,
then searching down from starting cell.

.. note::

Gspread will not look for empty cell inside the table. it only look at the top row and first column.

Example extracting a table from the below sample sheet:

.. list-table:: Find table
:header-rows: 1

* - ID
- Name
- Universe
- Super power
* - 1
- Batman
- DC
- Very rich
* - 2
- DeadPool
- Marvel
- self healing
* - 3
- Superman
- DC
- super human
* -
- \-
- \-
- \-
* - 5
- Lavigne958
-
- maintains Gspread
* - 6
- Alifee
-
- maintains Gspread

Using the below code will result in rows 2 to 4:

.. code:: python
worksheet.expand("A2")
[
["Batman", "DC", "Very rich"],
["DeadPool", "Marvel", "self healing"],
["Superman", "DC", "super human"],
]
Expand Down Expand Up @@ -390,7 +456,7 @@ Color the background of **A2:B2** cell range in black, change horizontal alignme
The second argument to :meth:`~gspread.models.Worksheet.format` is a dictionary containing the fields to update. A full specification of format options is available at `CellFormat <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/cells#cellformat>`_ in Sheet API Reference.

.. Tip::
`gspread-formatting <https://github.com/robin900/gspread-formatting>`_ offers extensive functionality to help you when you go beyond basics.
for more complex formatting see :ref:`gspread-formating-label`.


Using gspread with pandas
Expand All @@ -412,10 +478,7 @@ Here's a basic example for writing a dataframe to a sheet. With :meth:`~gspread.
worksheet.update([dataframe.columns.values.tolist()] + dataframe.values.tolist())
For advanced pandas use cases check out these libraries:

* `gspread-pandas <https://github.com/aiguofer/gspread-pandas>`_
* `gspread-dataframe <https://github.com/robin900/gspread-dataframe>`_
For advanced pandas use cases check out community section :ref:`gspread-pandas-label`

Using gspread with NumPy
~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
4 changes: 2 additions & 2 deletions gspread/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,8 +386,8 @@ def api_key(token: str, http_client: HTTPClientType = HTTPClient) -> Client:
"""
if GOOGLE_AUTH_API_KEY_AVAILABLE is False:
raise NotImplementedError(
"api_key is only available with package google.auth>=2.4.0."
'Install it with "pip install google-auth>=2.4.0".'
"api_key is only available with package google.auth>=2.15.0. "
'Install it with "pip install google-auth>=2.15.0".'
)
creds = APIKeyCredentials(token)
return Client(auth=creds, http_client=http_client)
32 changes: 20 additions & 12 deletions gspread/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
"""

from typing import Any, Dict, Mapping, Optional, Union
from typing import Any, Mapping

from requests import Response
from requests.exceptions import JSONDecodeError


class UnSupportedExportFormat(Exception):
Expand Down Expand Up @@ -40,20 +41,24 @@ class APIError(GSpreadException):
such as when we attempt to retrieve things that don't exist."""

def __init__(self, response: Response):
super().__init__(self._extract_error(response))
try:
error = response.json()["error"]
except JSONDecodeError:
# in case we failed to parse the error from the API
# build an empty error object to notify the caller
# and keep the exception raise flow running

error = {
"code": -1,
"message": response.text,
"status": "invalid JSON",
}

super().__init__(error)
self.response: Response = response
self.error: Mapping[str, Any] = response.json()["error"]
self.error: Mapping[str, Any] = error
self.code: int = self.error["code"]

def _extract_error(
self, response: Response
) -> Optional[Dict[str, Union[int, str]]]:
try:
errors = response.json()
return dict(errors["error"])
except (AttributeError, KeyError, ValueError):
return None

def __str__(self) -> str:
return "{}: [{}]: {}".format(
self.__class__.__name__, self.code, self.error["message"]
Expand All @@ -62,6 +67,9 @@ def __str__(self) -> str:
def __repr__(self) -> str:
return self.__str__()

def __reduce__(self) -> tuple:
return self.__class__, (self.response,)


class SpreadsheetNotFound(GSpreadException):
"""Trying to open non-existent or inaccessible spreadsheet."""
6 changes: 3 additions & 3 deletions gspread/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ def import_csv(self, file_id: str, data: Union[str, bytes]) -> Any:


class BackOffHTTPClient(HTTPClient):
"""BackoffClient is a gspread client with exponential
"""BackOffHTTPClient is a http client with exponential
backoff retries.
In case a request fails due to some API rate limits,
Expand All @@ -524,12 +524,12 @@ class BackOffHTTPClient(HTTPClient):
prevent the application from failing (by raising an APIError exception).
.. Warning::
This Client is not production ready yet.
This HTTPClient is not production ready yet.
Use it at your own risk !
.. note::
To use with the `auth` module, make sure to pass this backoff
client factory using the ``client_factory`` parameter of the
http client using the ``http_client`` parameter of the
method used.
.. note::
Expand Down
131 changes: 130 additions & 1 deletion gspread/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ class ValidationConditionType(StrEnum):
filter_expression = "FILTER_EXPRESSION"


class TableDirection(StrEnum):
table = "TABLE"
down = "DOWN"
right = "RIGHT"


def convert_credentials(credentials: Credentials) -> Credentials:
module = credentials.__module__
cls = credentials.__class__.__name__
Expand Down Expand Up @@ -306,7 +312,7 @@ def numericise_all(
:param list values: Input row
:param bool empty2zero: (optional) Whether or not to return empty cells
as 0 (zero). Defaults to ``False``.
:param str default_blank: Which value to use for blank cells,
:param Any default_blank: Which value to use for blank cells,
defaults to empty string.
:param bool allow_underscores_in_numeric_literals: Whether or not to allow
visual underscores in numeric literals
Expand Down Expand Up @@ -979,6 +985,129 @@ def to_records(
return [dict(zip(headers, row)) for row in values]


def _expand_right(values: List[List[str]], start: int, end: int, row: int) -> int:
"""This is a private function, returning the column index of the last non empty cell
on the given row.
Search starts from ``start`` index column.
Search ends on ``end`` index column.
Searches only in the row pointed by ``row``.
"""
try:
return values[row].index("", start, end) - 1
except ValueError:
return end


def _expand_bottom(values: List[List[str]], start: int, end: int, col: int) -> int:
"""This is a private function, returning the row index of the last non empty cell
on the given column.
Search starts from ``start`` index row.
Search ends on ``end`` index row.
Searches only in the column pointed by ``col``.
"""
for rows in range(start, end):
# in case we try to look further than last row
if rows >= len(values):
return len(values) - 1

# check if cell is empty (or the row => empty cell)
if col >= len(values[rows]) or values[rows][col] == "":
return rows - 1

return end - 1


def find_table(
values: List[List[str]],
start_range: str,
direction: TableDirection = TableDirection.table,
) -> List[List[str]]:
"""Expands a list of values based on non-null adjacent cells.
Expand can be done in 3 directions defined in :class:`~gspread.utils.TableDirection`
* ``TableDirection.right``: expands right until the first empty cell
* ``TableDirection.down``: expands down until the first empty cell
* ``TableDirection.table``: expands right until the first empty cell and down until first empty cell
In case of empty result an empty list is restuned.
When the given ``start_range`` is outside the given matrix of values the exception
:class:`~gspread.exceptions.InvalidInputValue` is raised.
Example::
values = [
['', '', '', '', '' ],
['', 'B2', 'C2', '', 'E2'],
['', 'B3', 'C3', '', 'E3'],
['', '' , '' , '', 'E4'],
]
>>> utils.find_table(TableDirection.table, 'B2')
[
['B2', 'C2'],
['B3', 'C3'],
]
.. note::
the ``TableDirection.table`` will look right from starting cell then look down from starting cell.
It will not check cells located inside the table. This could lead to
potential empty values located in the middle of the table.
.. warning::
Given values must be padded with `''` empty values.
:param list[list] values: values where to find the table.
:param gspread.utils.TableDirection direction: the expand direction.
:param str start_range: the starting cell range.
:rtype list(list): the resulting matrix
"""
row, col = a1_to_rowcol(start_range)

# a1_to_rowcol returns coordinates starting form 1
row -= 1
col -= 1

if row >= len(values):
raise InvalidInputValue(
"given row for start_range is outside given values: start range row ({}) >= rows in values {}".format(
row, len(values)
)
)

if col >= len(values[row]):
raise InvalidInputValue(
"given column for start_range is outside given values: start range column ({}) >= columns in values {}".format(
col, len(values[row])
)
)

if direction == TableDirection.down:
rightMost = col
bottomMost = _expand_bottom(values, row, len(values), col)

if direction == TableDirection.right:
bottomMost = row
rightMost = _expand_right(values, col, len(values[row]), row)

if direction == TableDirection.table:
rightMost = _expand_right(values, col, len(values[row]), row)
bottomMost = _expand_bottom(values, row, len(values), col)

result = []

# build resulting array
for rows in values[row : bottomMost + 1]:
result.append(rows[col : rightMost + 1])

return result


# SHOULD NOT BE NEEDED UNTIL NEXT MAJOR VERSION
# DEPRECATION_WARNING_TEMPLATE = (
# "[Deprecated][in version {v_deprecated}]: {msg_deprecated}"
Expand Down
Loading

0 comments on commit 58d7368

Please sign in to comment.