forked from nithinmurali/pygsheets
-
Notifications
You must be signed in to change notification settings - Fork 0
/
spreadsheet.py
422 lines (333 loc) · 17.4 KB
/
spreadsheet.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# -*- coding: utf-8 -*-.
"""
pygsheets.spreadsheet
~~~~~~~~~~~~~~~~~~~~~
This module represents an entire spreadsheet. Which can have several worksheets.
"""
import logging
import warnings
from pygsheets.worksheet import Worksheet
from pygsheets.datarange import DataRange
from pygsheets.exceptions import (WorksheetNotFound, RequestError,
InvalidArgumentValue, InvalidUser)
from pygsheets.custom_types import *
from pygsheets.developer_metadata import DeveloperMetadataLookupDataFilter, DeveloperMetadata
class Spreadsheet(object):
""" A class for a spreadsheet object."""
worksheet_cls = Worksheet
def __init__(self, client, jsonsheet=None, id=None):
"""The spreadsheet is used to store and manipulate metadata and load specific sheets.
:param client: The client which is responsible to connect the sheet with the remote.
:param jsonsheet: The json-dict representation of the spreadsheet as returned by Google Sheets API v4.
:param id: Id of this spreadsheet
"""
if type(jsonsheet) != dict and jsonsheet is not None:
raise InvalidArgumentValue("jsonsheet")
self.logger = logging.getLogger(__name__)
self.client = client
self._sheet_list = []
self._jsonsheet = jsonsheet
self._id = id
self._title = ''
self._named_ranges = []
self.fetch_properties(jsonsheet)
self.default_parse = True
@property
def id(self):
"""Id of the spreadsheet."""
return self._id
@property
def title(self):
"""Title of the spreadsheet."""
return self._title
@title.setter
def title(self, value):
self._title = value
self._jsonsheet['properties']['title'] = value
self.update_properties()
@property
def locale(self):
"""Locale of the spreadsheet."""
return self._jsonsheet['properties']['locale']
@locale.setter
def locale(self, value):
self._locale = value
self._jsonsheet['properties']['locale'] = value
self.update_properties()
@property
def sheet1(self):
"""Direct access to the first worksheet."""
return self.worksheet()
@property
def url(self):
"""Url of the spreadsheet."""
return "https://docs.google.com/spreadsheets/d/"+self.id
@property
def named_ranges(self):
"""All named ranges in this spreadsheet."""
return [DataRange(namedjson=x, name=x['name'], worksheet=self.worksheet('id', x['range'].get('sheetId', 0)))
for x in self._named_ranges]
@property
def protected_ranges(self):
"""All protected ranges in this spreadsheet."""
response = self.client.sheet.get(spreadsheet_id=self.id, fields='sheets(properties.sheetId,protectedRanges)')
return [DataRange(protectedjson=x, worksheet=self.worksheet('id', sheet['properties']['sheetId']))
for sheet in response['sheets']
for x in sheet.get('protectedRanges', [])]
@property
def defaultformat(self):
"""Default cell format used."""
return self._defaultFormat
@property
def updated(self):
"""Last time the spreadsheet was modified using RFC 3339 format."""
return self.client.drive.get_update_time(self.id)
def update_properties(self):
"""
Update the sheet properties in cloud
"""
request = {
"updateSpreadsheetProperties": {"properties": self._jsonsheet['properties'], "fields": "*"}}
self.client.sheet.batch_update(self.id, request, fields='*')
def fetch_properties(self, jsonsheet=None, fetch_sheets=True):
"""Update all properties of this spreadsheet with the remote.
The provided json representation must be the same as the Google Sheets v4 Response. If no sheet is given this
will simply fetch all data from remote and update the local representation.
Reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets
:param jsonsheet: Used to update the spreadsheet.
:param fetch_sheets: Fetch sheets from remote.
"""
if not jsonsheet and len(self.id) > 1:
self._jsonsheet = self.client.open_as_json(self.id)
elif not jsonsheet and len(self.id) == 0:
raise InvalidArgumentValue('jsonsheet')
self._id = self._jsonsheet['spreadsheetId']
if fetch_sheets:
self._fetch_sheets(self._jsonsheet)
self._title = self._jsonsheet['properties']['title']
self._defaultFormat = self._jsonsheet['properties']['defaultFormat']
self.client.spreadsheetId = self._id
self._named_ranges = self._jsonsheet.get('namedRanges', [])
def _fetch_sheets(self, jsonsheet=None):
"""Update the sheets stored in this spreadsheet."""
self._sheet_list = []
if not jsonsheet:
jsonsheet = self.client.open_as_json(self.id)
for sheet in jsonsheet.get('sheets'):
self._sheet_list.append(self.worksheet_cls(self, sheet))
def worksheets(self, sheet_property=None, value=None, force_fetch=False):
"""Get worksheets matching the specified property.
:param sheet_property: Property used to filter ('title', 'index', 'id').
:param value: Value of the property.
:param force_fetch: Fetch data from remote.
:returns: List of :class:`Worksheets <Worksheet>`.
"""
if not sheet_property and not value:
return self._sheet_list
if sheet_property not in ['title', 'index', 'id']:
raise InvalidArgumentValue('sheet_property')
elif sheet_property in ['index', 'id']:
value = int(value)
sheets = [x for x in self._sheet_list if getattr(x, sheet_property) == value]
if not len(sheets) > 0 or force_fetch:
self._fetch_sheets()
sheets = [x for x in self._sheet_list if getattr(x, sheet_property) == value]
if not len(sheets) > 0:
raise WorksheetNotFound()
return sheets
def worksheet(self, property='index', value=0):
"""Returns the worksheet with the specified index, title or id.
If several worksheets with the same property are found the first is returned. This may not be the same
worksheet every time.
Example: Getting a worksheet named 'Annual bonuses'
>>> sht = client.open('Sample one')
>>> worksheet = sht.worksheet('title','Annual bonuses')
:param property: The searched property.
:param value: Value of the property.
:returns: :class:`Worksheets <Worksheet>`.
"""
return self.worksheets(property, value)[0]
def worksheet_by_title(self, title):
"""Returns worksheet by title.
:param title: Title of the sheet
:returns: :class:`Worksheets <Worksheet>`.
"""
return self.worksheet('title', title)
def add_worksheet(self, title, rows=100, cols=26, src_tuple=None, src_worksheet=None, index=None):
"""Creates or copies a worksheet and adds it to this spreadsheet.
When creating only a title is needed. Rows & columns can be adjusted to match your needs.
Index can be specified to set position of the sheet.
When copying another worksheet supply the spreadsheet id & worksheet id and the worksheet wrapped in a Worksheet
class.
:param title: Title of the worksheet.
:param rows: Number of rows which should be initialized (default 100)
:param cols: Number of columns which should be initialized (default 26)
:param src_tuple: Tuple of (spreadsheet id, worksheet id) specifying the worksheet to copy.
:param src_worksheet: The source worksheet.
:param index: Tab index of the worksheet.
:returns: :class:`Worksheets <Worksheet>`.
"""
jsheet = dict()
if src_tuple:
jsheet['properties'] = self.client.sheet.sheets_copy_to(src_tuple[0], src_tuple[1], self.id)
wks = self.worksheet_cls(self, jsheet)
wks.title = title
wks.index = index
elif src_worksheet:
if type(src_worksheet) != Worksheet:
raise InvalidArgumentValue("src_worksheet")
jsheet['properties'] = self.client.sheet.sheets_copy_to(src_worksheet.spreadsheet.id, src_worksheet.id, self.id)
wks = self.worksheet_cls(self, jsheet)
wks.title = title
wks.index = index
else:
request = {"addSheet": {"properties": {'title': title, "gridProperties": {"rowCount": rows, "columnCount": cols}}}}
if index is not None:
request["addSheet"]["properties"]["index"] = index
result = self.client.sheet.batch_update(self.id, request, fields='replies/addSheet')
jsheet['properties'] = result['replies'][0]['addSheet']['properties']
wks = self.worksheet_cls(self, jsheet)
self._sheet_list.append(wks)
return wks
def del_worksheet(self, worksheet):
"""Deletes the worksheet from this spreadsheet.
:param worksheet: The :class:`worksheets <Worksheet>` to be deleted.
"""
if worksheet not in self.worksheets():
raise WorksheetNotFound
request = {"deleteSheet": {'sheetId': worksheet.id}}
self.client.sheet.batch_update(self.id, request)
self._sheet_list.remove(worksheet)
def replace(self, pattern, replacement=None, **kwargs):
"""Replace values in any cells matched by pattern in all worksheets.
Keyword arguments not specified will use the default value. If the spreadsheet is -
Unlinked:
Uses `self.find(pattern, **kwargs)` to find the cells and then replace the values in each cell.
Linked:
The replacement will be done by a `findReplaceRequest <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request#findreplacerequest>`_
as defined by the Google Sheets API. After the request the local copy is updated.
:param pattern: Match cell values.
:param replacement: Value used as replacement.
:arg searchByRegex: Consider pattern a regex pattern. (default False)
:arg matchCase: Match case sensitive. (default False)
:arg matchEntireCell: Only match on full match. (default False)
:arg includeFormulas: Match fields with formulas too. (default False)
"""
for wks in self.worksheets():
wks.replace(pattern, replacement=replacement, **kwargs)
def find(self, pattern, **kwargs):
"""Searches through all worksheets.
Search all worksheets with the options given. If an option is not given, the default will be used.
Will return a list of cells for each worksheet packed into a list. If a worksheet has no cell which
matches pattern an empty list is added.
:param pattern: The value to search.
:arg searchByRegex: Consider pattern a regex pattern. (default False)
:arg matchCase: Match case sensitive. (default False)
:arg matchEntireCell: Only match on full match. (default False)
:arg includeFormulas: Match fields with formulas too. (default False)
:returns: A list of lists of :class:`Cells <Cell>`
"""
found_cells = []
for sheet in self.worksheets():
found_cells.append(sheet.find(pattern, **kwargs))
return found_cells
def share(self, email_or_domain, role='reader', type='user', **kwargs):
"""Share this file with a user, group or domain.
User and groups need an e-mail address and domain needs a domain for a permission.
Share sheet with a person and send an email message.
>>> spreadsheet.share('example@gmail.com', role='commenter', type='user', emailMessage='Here is the spreadsheet we talked about!')
Make sheet public with read only access:
>>> spreadsheet.share('', role='reader', type='anyone')
:param email_or_domain: The email address or domain this file should be shared to.
:param role: The role of the new permission.
:param type: The type of the new permission.
:param kwargs: Optional arguments. See DriveAPIWrapper.create_permission documentation for details.
"""
if type in ['user', 'group']:
kwargs['emailAddress'] = email_or_domain
elif type == 'domain':
kwargs['domain'] = email_or_domain
self.client.drive.create_permission(self.id, role=role, type=type, **kwargs)
@property
def permissions(self):
"""Permissions for this file."""
return self.client.drive.list_permissions(self.id)
def remove_permission(self, email_or_domain, permission_id=None):
"""Remove a permission from this sheet.
All permissions associated with this email or domain are deleted.
:param email_or_domain: Email or domain of the permission.
:param permission_id: (optional) permission id if a specific permission should be deleted.
"""
if permission_id is not None:
self.client.drive.delete_permission(self.id, permission_id=permission_id)
else:
for permission in self.permissions:
if email_or_domain in [permission.get('domain', ''), permission.get('emailAddress', '')]:
self.client.drive.delete_permission(self.id, permission_id=permission['id'])
def export(self, file_format=ExportType.CSV, path='', filename=''):
"""Export all worksheets.
The filename must have an appropriate file extension. Each sheet will be exported into a separate file.
The filename is extended (before the extension) with the index number of the worksheet to not overwrite
each file.
:param file_format: ExportType.<?>
:param path: Path to the directory where the file will be stored. (default: current working directory)
:param filename: Filename (default: spreadsheet id)
"""
self.client.drive.export(self, file_format=file_format, filename=filename, path=path)
def delete(self):
"""Deletes this spreadsheet.
Leaves the local copy intact. The deleted spreadsheet is permanently removed from your drive
and not moved to the trash.
"""
self.client.drive.delete(self.id)
def get_developer_metadata(self, key=None, search_sheets=False):
"""
Fetch developer metadata associated with this spreadsheet
:param key: The key of the metadata to fetch. If unspecified, all metadata will be returned
:param search_sheets: Set to True to also include worksheets in the metadata search
"""
spreadsheet_id = None if search_sheets else self.id
data_filter = DeveloperMetadataLookupDataFilter(spreadsheet_id, meta_key=key)
results = self.client.sheet.developer_metadata_search(self.id, data_filter.to_json())
metadata = []
if results:
for result in results["matchedDeveloperMetadata"]:
meta_id = result["developerMetadata"]["metadataId"]
key = result["developerMetadata"]["metadataKey"]
value = result["developerMetadata"]["metadataValue"]
sheet_id = result["developerMetadata"]["location"].get("sheetId", None)
metadata.append(DeveloperMetadata(meta_id, key, value, self.client, self.id, sheet_id))
return metadata
def create_developer_metadata(self, key, value=None):
"""
Create a new developer metadata associated with this spreadsheet
Will return None when in batch mode, otherwise will return a DeveloperMetadata object
:param key: the key of the metadata to be created
:param value: the value of the metadata to be created (optional)
"""
return DeveloperMetadata.new(key, value, self.client, self.id)
def custom_request(self, request, fields, **kwargs):
"""
Send a custom batch update request to this spreadsheet.
These requests have to be properly constructed. All possible requests are documented in the reference.
Reference: api docs <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/request>`__
:param request: One or several requests as dictionaries.
:param fields: Fields which should be included in the response.
:param kwargs: Any other params according to refrence.
:return: json response <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/response> __
"""
return self.client.sheet.batch_update(self.id, request, fields=fields, **kwargs)
def to_json(self):
"""Return this spreadsheet as json resource."""
return self.client.open_as_json(self.id)
def __repr__(self):
return '<%s %s Sheets:%s>' % (self.__class__.__name__,
repr(self.title), len(self._sheet_list))
def __eq__(self, other):
return self.id == other.id
def __iter__(self):
for sheet in self.worksheets():
yield(sheet)
def __getitem__(self, item):
if type(item) == int:
return self.worksheet('index', item)