diff --git a/gspread/client.py b/gspread/client.py index 0255c598e..96f3d5914 100644 --- a/gspread/client.py +++ b/gspread/client.py @@ -39,7 +39,9 @@ def __init__( ) -> None: self.http_client = http_client(auth, session) - def set_timeout(self, timeout: Optional[Union[float, Tuple[float, float]]] = None): + def set_timeout( + self, timeout: Optional[Union[float, Tuple[float, float]]] = None + ) -> None: """How long to wait for the server to send data before giving up, as a float, or a ``(connect timeout, read timeout)`` tuple. @@ -76,7 +78,7 @@ def list_spreadsheet_files( return files def _list_spreadsheet_files( - self, title=None, folder_id=None + self, title: Optional[str] = None, folder_id: Optional[str] = None ) -> Tuple[List[Dict[str, Any]], Response]: files = [] page_token = "" @@ -316,9 +318,9 @@ def copy( continue new_spreadsheet.share( - email_address=p["emailAddress"], - perm_type=p["type"], - role=p["role"], + email_address=str(p["emailAddress"]), + perm_type=str(p["type"]), + role=str(p["role"]), notify=False, ) diff --git a/gspread/http_client.py b/gspread/http_client.py index 3a96c5bc8..7cc802dd2 100644 --- a/gspread/http_client.py +++ b/gspread/http_client.py @@ -284,7 +284,9 @@ def spreadsheets_sheets_copy_to( r = self.request("post", url, json=body) return r.json() - def fetch_sheet_metadata(self, id: str, params: Optional[ParamsType] = None) -> Any: + def fetch_sheet_metadata( + self, id: str, params: Optional[ParamsType] = None + ) -> Mapping[str, Any]: """Similar to :method spreadsheets_get:`gspread.http_client.spreadsheets_get`, get the spreadsheet form the API but by default **does not get the cells data**. It only retrieve the the metadata from the spreadsheet. diff --git a/gspread/spreadsheet.py b/gspread/spreadsheet.py index bad613d87..60158c394 100644 --- a/gspread/spreadsheet.py +++ b/gspread/spreadsheet.py @@ -7,10 +7,13 @@ """ import warnings -from typing import Any, Dict, Union +from typing import Any, Dict, Generator, Iterable, List, Mapping, Optional, Union +from requests import Response + +from .cell import Cell from .exceptions import WorksheetNotFound -from .http_client import HTTPClient +from .http_client import HTTPClient, ParamsType from .urls import DRIVE_FILES_API_V3_URL, SPREADSHEET_DRIVE_URL from .utils import ExportFormat, finditem from .worksheet import Worksheet @@ -19,37 +22,40 @@ class Spreadsheet: """The class that represents a spreadsheet.""" - def __init__(self, http_client: HTTPClient, properties: Dict[str, Any]): + def __init__(self, http_client: HTTPClient, properties: Dict[str, Union[str, Any]]): self.client = http_client self._properties = properties metadata = self.fetch_sheet_metadata() + import pprint + + pprint.pprint(metadata) self._properties.update(metadata["properties"]) @property - def id(self): + def id(self) -> str: """Spreadsheet ID.""" return self._properties["id"] @property - def title(self): + def title(self) -> str: """Spreadsheet title.""" return self._properties["title"] @property - def url(self): + def url(self) -> str: """Spreadsheet URL.""" return SPREADSHEET_DRIVE_URL % self.id @property - def creationTime(self): + def creationTime(self) -> str: """Spreadsheet Creation time.""" if "createdTime" not in self._properties: self.update_drive_metadata() return self._properties["createdTime"] @property - def lastUpdateTime(self): + def lastUpdateTime(self) -> str: """Spreadsheet last updated time. Only updated on initialisation. For actual last updated time, use get_lastUpdateTime().""" @@ -62,31 +68,31 @@ def lastUpdateTime(self): return self._properties["modifiedTime"] @property - def timezone(self): + def timezone(self) -> str: """Spreadsheet timeZone""" return self._properties["timeZone"] @property - def locale(self): + def locale(self) -> str: """Spreadsheet locale""" return self._properties["locale"] @property - def sheet1(self): + def sheet1(self) -> Worksheet: """Shortcut property for getting the first worksheet.""" return self.get_worksheet(0) - def __iter__(self): + def __iter__(self) -> Generator[Worksheet, None, None]: yield from self.worksheets() - def __repr__(self): + def __repr__(self) -> str: return "<{} {} id:{}>".format( self.__class__.__name__, repr(self.title), self.id, ) - def batch_update(self, body): + def batch_update(self, body: Mapping[str, Any]) -> Any: """Lower-level method that directly calls `spreadsheets/:batchUpdate `_. :param dict body: `Batch Update Request body `_. @@ -97,7 +103,9 @@ def batch_update(self, body): """ return self.client.batch_update(self.id, body) - def values_append(self, range, params, body): + def values_append( + self, range: str, params: ParamsType, body: Mapping[str, Any] + ) -> Any: """Lower-level method that directly calls `spreadsheets//values:append `_. :param str range: The `A1 notation `_ @@ -111,7 +119,7 @@ def values_append(self, range, params, body): """ return self.client.values_append(self.id, range, params, body) - def values_clear(self, range): + def values_clear(self, range: str) -> Any: """Lower-level method that directly calls `spreadsheets//values:clear `_. :param str range: The `A1 notation `_ of the values to clear. @@ -122,7 +130,11 @@ def values_clear(self, range): """ return self.client.values_clear(self.id, range) - def values_batch_clear(self, params=None, body=None): + def values_batch_clear( + self, + params: Optional[ParamsType] = None, + body: Optional[Mapping[str, Any]] = None, + ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchClear` :param dict params: (optional) `Values Batch Clear Query parameters `_. @@ -131,7 +143,7 @@ def values_batch_clear(self, params=None, body=None): """ return self.client.values_batch_clear(self.id, params, body) - def values_get(self, range, params=None): + def values_get(self, range: str, params: Optional[ParamsType] = None) -> Any: """Lower-level method that directly calls `GET spreadsheets//values/ `_. :param str range: The `A1 notation `_ of the values to retrieve. @@ -143,7 +155,9 @@ def values_get(self, range, params=None): """ return self.client.values_get(self.id, range, params=params) - def values_batch_get(self, ranges, params=None): + def values_batch_get( + self, ranges: List[str], params: Optional[ParamsType] = None + ) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchGet `_. :param list ranges: List of ranges in the `A1 notation `_ of the values to retrieve. @@ -153,7 +167,12 @@ def values_batch_get(self, ranges, params=None): """ return self.client.values_batch_get(self.id, ranges, params=params) - def values_update(self, range, params=None, body=None): + def values_update( + self, + range: str, + params: Optional[ParamsType] = None, + body: Optional[Mapping[str, Any]] = None, + ) -> Any: """Lower-level method that directly calls `PUT spreadsheets//values/ `_. :param str range: The `A1 notation `_ of the values to update. @@ -178,7 +197,7 @@ def values_update(self, range, params=None, body=None): """ return self.client.values_update(self.id, range, params=params, body=body) - def values_batch_update(self, body=None): + def values_batch_update(self, body: Optional[Mapping[str, Any]] = None) -> Any: """Lower-level method that directly calls `spreadsheets//values:batchUpdate `_. :param dict body: (optional) `Values Batch Update Request body `_. @@ -187,17 +206,21 @@ def values_batch_update(self, body=None): """ return self.client.values_batch_update(self.id, body=body) - def _spreadsheets_get(self, params=None): + def _spreadsheets_get(self, params: Optional[ParamsType] = None) -> Any: """A method stub that directly calls `spreadsheets.get `_.""" return self.client.spreadsheets_get(self.id, params=params) - def _spreadsheets_sheets_copy_to(self, sheet_id, destination_spreadsheet_id): + def _spreadsheets_sheets_copy_to( + self, sheet_id: int, destination_spreadsheet_id: str + ) -> Any: """Lower-level method that directly calls `spreadsheets.sheets.copyTo `_.""" return self.client.spreadsheets_sheets_copy_to( self.id, sheet_id, destination_spreadsheet_id ) - def fetch_sheet_metadata(self, params=None): + def fetch_sheet_metadata( + self, params: Optional[ParamsType] = None + ) -> Mapping[str, Any]: """Similar to :method spreadsheets_get:`gspread.http_client.spreadsheets_get`, get the spreadsheet form the API but by default **does not get the cells data**. It only retrieve the the metadata from the spreadsheet. @@ -209,7 +232,7 @@ def fetch_sheet_metadata(self, params=None): """ return self.client.fetch_sheet_metadata(self.id, params=params) - def get_worksheet(self, index): + def get_worksheet(self, index: int) -> Worksheet: """Returns a worksheet with specified `index`. :param index: An index of a worksheet. Indexes start from zero. @@ -233,7 +256,7 @@ def get_worksheet(self, index): except (KeyError, IndexError): raise WorksheetNotFound("index {} not found".format(index)) - def get_worksheet_by_id(self, id: Union[str, int]): + def get_worksheet_by_id(self, id: Union[str, int]) -> Worksheet: """Returns a worksheet with specified `worksheet id`. :param id: The id of a worksheet. it can be seen in the url as the value of the parameter 'gid'. @@ -264,7 +287,7 @@ def get_worksheet_by_id(self, id: Union[str, int]): except (StopIteration, KeyError): raise WorksheetNotFound("id {} not found".format(worksheet_id_int)) - def worksheets(self, exclude_hidden: bool = False): + def worksheets(self, exclude_hidden: bool = False) -> List[Worksheet]: """Returns a list of all :class:`worksheets ` in a spreadsheet. @@ -284,7 +307,7 @@ def worksheets(self, exclude_hidden: bool = False): worksheets = [w for w in worksheets if not w.isSheetHidden] return worksheets - def worksheet(self, title): + def worksheet(self, title: str) -> Worksheet: """Returns a worksheet with specified `title`. :param title: A title of a worksheet. If there're multiple @@ -312,7 +335,9 @@ def worksheet(self, title): except (StopIteration, KeyError): raise WorksheetNotFound(title) - def add_worksheet(self, title, rows, cols, index=None): + def add_worksheet( + self, title: str, rows: int, cols: int, index: Optional[int] = None + ) -> Worksheet: """Adds a new worksheet to a spreadsheet. :param title: A title of a new worksheet. @@ -326,7 +351,9 @@ def add_worksheet(self, title, rows, cols, index=None): :returns: a newly created :class:`worksheets `. """ - body = { + body: Dict[ + str, List[Dict[str, Dict[str, Dict[str, Union[str, int, Dict[str, int]]]]]] + ] = { "requests": [ { "addSheet": { @@ -354,11 +381,11 @@ def add_worksheet(self, title, rows, cols, index=None): def duplicate_sheet( self, - source_sheet_id, - insert_sheet_index=None, - new_sheet_id=None, - new_sheet_name=None, - ): + source_sheet_id: int, + insert_sheet_index: Optional[int] = None, + new_sheet_id: Optional[int] = None, + new_sheet_name: Optional[str] = None, + ) -> Worksheet: """Duplicates the contents of a sheet. :param int source_sheet_id: The sheet ID to duplicate. @@ -388,7 +415,7 @@ def duplicate_sheet( new_sheet_name=new_sheet_name, ) - def del_worksheet(self, worksheet): + def del_worksheet(self, worksheet: Worksheet) -> Any: """Deletes a worksheet from a spreadsheet. :param worksheet: The worksheet to be deleted. @@ -398,7 +425,7 @@ def del_worksheet(self, worksheet): return self.client.batch_update(self.id, body) - def del_worksheet_by_id(self, worksheet_id: Union[str, int]): + def del_worksheet_by_id(self, worksheet_id: Union[str, int]) -> Any: """ Deletes a Worksheet by id """ @@ -411,7 +438,9 @@ def del_worksheet_by_id(self, worksheet_id: Union[str, int]): return self.client.batch_update(self.id, body) - def reorder_worksheets(self, worksheets_in_desired_order): + def reorder_worksheets( + self, worksheets_in_desired_order: Iterable[Worksheet] + ) -> Any: """Updates the ``index`` property of each Worksheet to reflect its index in the provided sequence of Worksheets. @@ -448,13 +477,13 @@ def reorder_worksheets(self, worksheets_in_desired_order): def share( self, - email_address, - perm_type, - role, - notify=True, - email_message=None, - with_link=False, - ): + email_address: str, + perm_type: str, + role: str, + notify: bool = True, + email_message: Optional[str] = None, + with_link: bool = False, + ) -> Response: """Share the spreadsheet with other accounts. :param email_address: user or group e-mail address, domain name @@ -492,7 +521,7 @@ def share( with_link=with_link, ) - def export(self, format=ExportFormat.PDF): + def export(self, format: ExportFormat = ExportFormat.PDF) -> bytes: """Export the spreadsheet in the given format. :param str file_id: A key of a spreadsheet to export @@ -517,11 +546,11 @@ def export(self, format=ExportFormat.PDF): """ return self.client.export(self.id, format) - def list_permissions(self): + def list_permissions(self) -> List[Dict[str, Union[str, bool]]]: """Lists the spreadsheet's permissions.""" return self.client.list_permissions(self.id) - def remove_permissions(self, value, role="any"): + def remove_permissions(self, value: str, role: str = "any") -> List[str]: """Remove permissions from a user or domain. :param value: User or domain to remove permissions from @@ -542,8 +571,8 @@ def remove_permissions(self, value, role="any"): key = "emailAddress" if "@" in value else "domain" - filtered_id_list = [ - p["id"] + filtered_id_list: List[str] = [ + str(p["id"]) for p in permission_list if p.get(key) == value and (p["role"] == role or role == "any") ] @@ -553,7 +582,7 @@ def remove_permissions(self, value, role="any"): return filtered_id_list - def transfer_ownership(self, permission_id): + def transfer_ownership(self, permission_id: str) -> Response: """Transfer the ownership of this file to a new user. It is necessary to first create the permission with the new owner's email address, @@ -581,7 +610,7 @@ def transfer_ownership(self, permission_id): return self.client.request("patch", url, json=payload) - def accept_ownership(self, permission_id): + def accept_ownership(self, permission_id: str) -> Response: """Accept the pending ownership request on that file. It is necessary to edit the permission with the pending ownership. @@ -601,13 +630,13 @@ def accept_ownership(self, permission_id): "role": "owner", } - params = { + params: ParamsType = { "transferOwnership": True, } return self.client.request("patch", url, json=payload, params=params) - def named_range(self, named_range): + def named_range(self, named_range: str) -> List[Cell]: """return a list of :class:`gspread.cell.Cell` objects from the specified named range. @@ -619,13 +648,13 @@ def named_range(self, named_range): # This is only here to provide better user experience. return self.sheet1.range(named_range) - def list_named_ranges(self): + def list_named_ranges(self) -> List[Any]: """Lists the spreadsheet's named ranges.""" return self.fetch_sheet_metadata(params={"fields": "namedRanges"}).get( "namedRanges", [] ) - def update_title(self, title): + def update_title(self, title: str) -> Any: """Renames the spreadsheet. :param str title: A new title. @@ -645,7 +674,7 @@ def update_title(self, title): self._properties["title"] = title return res - def update_timezone(self, timezone): + def update_timezone(self, timezone: str) -> Any: """Updates the current spreadsheet timezone. Can be any timezone in CLDR format such as "America/New_York" or a custom time zone such as GMT-07:00. @@ -666,7 +695,7 @@ def update_timezone(self, timezone): self._properties["timeZone"] = timezone return res - def update_locale(self, locale): + def update_locale(self, locale: str) -> Any: """Update the locale of the spreadsheet. Can be any of the ISO 639-1 language codes, such as: de, fr, en, ... Or an ISO 639-2 if no ISO 639-1 exists. @@ -692,9 +721,9 @@ def update_locale(self, locale): self._properties["locale"] = locale return res - def list_protected_ranges(self, sheetid): + def list_protected_ranges(self, sheetid: int) -> List[Any]: """Lists the spreadsheet's protected named ranges""" - sheets = self.fetch_sheet_metadata( + sheets: List[Mapping[str, Any]] = self.fetch_sheet_metadata( params={"fields": "sheets.properties,sheets.protectedRanges"} )["sheets"] diff --git a/gspread/utils.py b/gspread/utils.py index 59d25818d..02aa12f6d 100644 --- a/gspread/utils.py +++ b/gspread/utils.py @@ -244,7 +244,7 @@ def numericise( def numericise_all( - values: List[Optional[AnyStr]], + values: List[AnyStr], empty2zero: bool = False, default_blank: Any = "", allow_underscores_in_numeric_literals: bool = False, @@ -521,7 +521,7 @@ def column_letter_to_index(column: str) -> int: return index -def cast_to_a1_notation(method: Callable[..., Any]) -> Callable[..., Any]: +def cast_to_a1_notation(method: Callable[..., T]) -> Callable[..., T]: """Decorator function casts wrapped arguments to A1 notation in range method calls. """ @@ -709,7 +709,12 @@ def is_scalar(x: Any) -> bool: return isinstance(x, str) or not isinstance(x, Sequence) -def combined_merge_values(worksheet_metadata, values, start_row_index, start_col_index): +def combined_merge_values( + worksheet_metadata: Mapping[str, Any], + values: List[List[Any]], + start_row_index: int, + start_col_index: int, +) -> List[List[Any]]: """For each merged region, replace all values with the value of the top-left cell of the region. e.g., replaces [ @@ -733,6 +738,9 @@ def combined_merge_values(worksheet_metadata, values, start_row_index, start_col :param start_col_index: The index of the first column of the values in the worksheet. e.g., if the values are in columns C-E, this should be 2. + + :returns: matrix of values with merged coordinates filled according to top-left value + :rtype: list(list(any)) """ merges = worksheet_metadata.get("merges", []) # each merge has "startRowIndex", "endRowIndex", "startColumnIndex", "endColumnIndex diff --git a/gspread/worksheet.py b/gspread/worksheet.py index 5710a4f83..dd1b2ad73 100644 --- a/gspread/worksheet.py +++ b/gspread/worksheet.py @@ -114,7 +114,7 @@ class ValueRange(list): It will be instantiated using the response from the sheet API. """ - _json: MutableMapping[str, Any] = {} + _json: MutableMapping[str, str] = {} @classmethod def from_json(cls: Type[ValueRangeType], json: Mapping[str, Any]) -> ValueRangeType: @@ -449,7 +449,7 @@ def get_values( maintain_size: bool = False, pad_values: bool = True, return_type: GridRangeType = GridRangeType.ListOfLists, - ) -> List[List[T]]: + ) -> Union[ValueRange, List[List[Any]]]: """Alias for :meth:`~gspread.worksheet.Worksheet.get`... with ``return_type`` set to ``List[List[Any]]`` @@ -477,7 +477,7 @@ def get_all_values( maintain_size: bool = False, pad_values: bool = True, return_type: GridRangeType = GridRangeType.ListOfLists, - ) -> List[List[T]]: + ) -> Union[ValueRange, List[List[Any]]]: """Alias to :meth:`~gspread.worksheet.Worksheet.get_values`""" return self.get_values( range_name=range_name, @@ -492,13 +492,13 @@ def get_all_values( def get_all_records( self, - head=1, - expected_headers=None, - value_render_option=None, - default_blank="", - numericise_ignore=[], - allow_underscores_in_numeric_literals=False, - empty2zero=False, + head: int = 1, + expected_headers: Optional[List[str]] = None, + value_render_option: Optional[ValueRenderOption] = None, + default_blank: str = "", + numericise_ignore: Iterable[Union[str, int]] = [], + allow_underscores_in_numeric_literals: bool = False, + empty2zero: bool = False, ) -> List[Dict[str, Union[int, float, str]]]: """Returns a list of dictionaries, all of them having the contents of the spreadsheet with the head row as keys and each of these @@ -598,7 +598,7 @@ def get_all_records( empty2zero, default_blank, allow_underscores_in_numeric_literals, - numericise_ignore, + numericise_ignore, # type: ignore ) for row in values ] @@ -616,7 +616,7 @@ def row_values( major_dimension: Optional[Dimension] = None, value_render_option: Optional[ValueRenderOption] = None, date_time_render_option: Optional[DateTimeOption] = None, - ) -> List[Optional[Union[int, float, str]]]: + ) -> List[str]: """Returns a list of all values in a `row`. Empty cells in this list will be rendered as :const:`None`. @@ -826,7 +826,7 @@ def get( maintain_size: bool = False, pad_values: bool = False, return_type: GridRangeType = GridRangeType.ValueRange, - ) -> Union[ValueRange, List[List[Any]]]: + ) -> Union[ValueRange, List[List[str]]]: """Reads values of a single range or a cell of a sheet. Returns a ValueRange (list of lists) containing all values from a specified range or cell @@ -2091,12 +2091,14 @@ def add_protected_range( "description": description, "warningOnly": warning_only, "requestingUserCanEdit": requesting_user_can_edit, - "editors": None - if warning_only - else { - "users": editor_users_emails, - "groups": editor_groups_emails, - }, + "editors": ( + None + if warning_only + else { + "users": editor_users_emails, + "groups": editor_groups_emails, + } + ), } } } @@ -2395,7 +2397,7 @@ def freeze( return res @cast_to_a1_notation - def set_basic_filter(self, name: Optional[str] = None): + def set_basic_filter(self, name: Optional[str] = None) -> Any: """Add a basic filter to the worksheet. If a range or boundaries are passed, the filter will be limited to the given range. @@ -2538,7 +2540,7 @@ def copy_to( ) @cast_to_a1_notation - def merge_cells(self, name: str, merge_type: str = MergeType.merge_all): + def merge_cells(self, name: str, merge_type: str = MergeType.merge_all) -> Any: """Merge cells. :param str name: Range name in A1 notation, e.g. 'A1:A5'.