1""" 2gspread.client 3~~~~~~~~~~~~~~ 4 5This module contains Client class responsible for communicating with 6Google API. 7 8""" 9 10from google.auth.transport.requests import AuthorizedSession 11 12from .exceptions import APIError, SpreadsheetNotFound 13from .spreadsheet import Spreadsheet 14from .urls import ( 15 DRIVE_FILES_API_V2_URL, 16 DRIVE_FILES_API_V3_URL, 17 DRIVE_FILES_UPLOAD_API_V2_URL, 18) 19from .utils import convert_credentials, extract_id_from_url, finditem 20 21 22class Client: 23 """An instance of this class communicates with Google API. 24 25 :param auth: An OAuth2 credential object. Credential objects 26 created by `google-auth <https://github.com/googleapis/google-auth-library-python>`_. 27 28 :param session: (optional) A session object capable of making HTTP requests 29 while persisting some parameters across requests. 30 Defaults to `google.auth.transport.requests.AuthorizedSession <https://google-auth.readthedocs.io/en/latest/reference/google.auth.transport.requests.html#google.auth.transport.requests.AuthorizedSession>`_. 31 32 >>> c = gspread.Client(auth=OAuthCredentialObject) 33 """ 34 35 def __init__(self, auth, session=None): 36 if auth is not None: 37 self.auth = convert_credentials(auth) 38 self.session = session or AuthorizedSession(self.auth) 39 else: 40 self.session = session 41 42 def login(self): 43 from google.auth.transport.requests import Request 44 45 self.auth.refresh(Request(self.session)) 46 47 self.session.headers.update({"Authorization": "Bearer %s" % self.auth.token}) 48 49 def request( 50 self, 51 method, 52 endpoint, 53 params=None, 54 data=None, 55 json=None, 56 files=None, 57 headers=None, 58 ): 59 response = getattr(self.session, method)( 60 endpoint, 61 json=json, 62 params=params, 63 data=data, 64 files=files, 65 headers=headers, 66 ) 67 68 if response.ok: 69 return response 70 else: 71 raise APIError(response) 72 73 def list_spreadsheet_files(self, title=None, folder_id=None): 74 files = [] 75 page_token = "" 76 url = DRIVE_FILES_API_V3_URL 77 78 q = 'mimeType="application/vnd.google-apps.spreadsheet"' 79 if title: 80 q += ' and name = "{}"'.format(title) 81 if folder_id: 82 q += ' and parents in "{}"'.format(folder_id) 83 84 params = { 85 "q": q, 86 "pageSize": 1000, 87 "supportsAllDrives": True, 88 "includeItemsFromAllDrives": True, 89 "fields": "kind,nextPageToken,files(id,name,createdTime,modifiedTime)", 90 } 91 92 while page_token is not None: 93 if page_token: 94 params["pageToken"] = page_token 95 96 res = self.request("get", url, params=params).json() 97 files.extend(res["files"]) 98 page_token = res.get("nextPageToken", None) 99 100 return files 101 102 def open(self, title, folder_id=None): 103 """Opens a spreadsheet. 104 105 :param str title: A title of a spreadsheet. 106 :param str folder_id: (optional) If specified can be used to filter 107 spreadsheets by parent folder ID. 108 :returns: a :class:`~gspread.models.Spreadsheet` instance. 109 110 If there's more than one spreadsheet with same title the first one 111 will be opened. 112 113 :raises gspread.SpreadsheetNotFound: if no spreadsheet with 114 specified `title` is found. 115 116 >>> gc.open('My fancy spreadsheet') 117 """ 118 try: 119 properties = finditem( 120 lambda x: x["name"] == title, 121 self.list_spreadsheet_files(title, folder_id), 122 ) 123 124 # Drive uses different terminology 125 properties["title"] = properties["name"] 126 127 return Spreadsheet(self, properties) 128 except StopIteration: 129 raise SpreadsheetNotFound 130 131 def open_by_key(self, key): 132 """Opens a spreadsheet specified by `key` (a.k.a Spreadsheet ID). 133 134 :param str key: A key of a spreadsheet as it appears in a URL in a browser. 135 :returns: a :class:`~gspread.models.Spreadsheet` instance. 136 137 >>> gc.open_by_key('0BmgG6nO_6dprdS1MN3d3MkdPa142WFRrdnRRUWl1UFE') 138 """ 139 return Spreadsheet(self, {"id": key}) 140 141 def open_by_url(self, url): 142 """Opens a spreadsheet specified by `url`. 143 144 :param str url: URL of a spreadsheet as it appears in a browser. 145 146 :returns: a :class:`~gspread.models.Spreadsheet` instance. 147 148 :raises gspread.SpreadsheetNotFound: if no spreadsheet with 149 specified `url` is found. 150 151 >>> gc.open_by_url('https://docs.google.com/spreadsheet/ccc?key=0Bm...FE&hl') 152 """ 153 return self.open_by_key(extract_id_from_url(url)) 154 155 def openall(self, title=None): 156 """Opens all available spreadsheets. 157 158 :param str title: (optional) If specified can be used to filter 159 spreadsheets by title. 160 161 :returns: a list of :class:`~gspread.models.Spreadsheet` instances. 162 """ 163 spreadsheet_files = self.list_spreadsheet_files(title) 164 165 if title: 166 spreadsheet_files = [ 167 spread for spread in spreadsheet_files if title == spread["name"] 168 ] 169 170 return [ 171 Spreadsheet(self, dict(title=x["name"], **x)) for x in spreadsheet_files 172 ] 173 174 def create(self, title, folder_id=None): 175 """Creates a new spreadsheet. 176 177 :param str title: A title of a new spreadsheet. 178 179 :param str folder_id: Id of the folder where we want to save 180 the spreadsheet. 181 182 :returns: a :class:`~gspread.models.Spreadsheet` instance. 183 184 """ 185 payload = { 186 "name": title, 187 "mimeType": "application/vnd.google-apps.spreadsheet", 188 } 189 190 params = { 191 "supportsAllDrives": True, 192 } 193 194 if folder_id is not None: 195 payload["parents"] = [folder_id] 196 197 r = self.request("post", DRIVE_FILES_API_V3_URL, json=payload, params=params) 198 spreadsheet_id = r.json()["id"] 199 return self.open_by_key(spreadsheet_id) 200 201 def copy(self, file_id, title=None, copy_permissions=False, folder_id=None): 202 """Copies a spreadsheet. 203 204 :param str file_id: A key of a spreadsheet to copy. 205 :param str title: (optional) A title for the new spreadsheet. 206 207 :param bool copy_permissions: (optional) If True, copy permissions from 208 the original spreadsheet to the new spreadsheet. 209 210 :param str folder_id: Id of the folder where we want to save 211 the spreadsheet. 212 213 :returns: a :class:`~gspread.models.Spreadsheet` instance. 214 215 .. versionadded:: 3.1.0 216 217 .. note:: 218 219 If you're using custom credentials without the Drive scope, you need to add 220 ``https://www.googleapis.com/auth/drive`` to your OAuth scope in order to use 221 this method. 222 223 Example:: 224 225 scope = [ 226 'https://www.googleapis.com/auth/spreadsheets', 227 'https://www.googleapis.com/auth/drive' 228 ] 229 230 Otherwise, you will get an ``Insufficient Permission`` error 231 when you try to copy a spreadsheet. 232 233 """ 234 url = "{}/{}/copy".format(DRIVE_FILES_API_V2_URL, file_id) 235 236 payload = { 237 "title": title, 238 "mimeType": "application/vnd.google-apps.spreadsheet", 239 } 240 241 if folder_id is not None: 242 payload["parents"] = [{"id": folder_id}] 243 244 params = {"supportsAllDrives": True} 245 r = self.request("post", url, json=payload, params=params) 246 spreadsheet_id = r.json()["id"] 247 248 new_spreadsheet = self.open_by_key(spreadsheet_id) 249 250 if copy_permissions is True: 251 original = self.open_by_key(file_id) 252 253 permissions = original.list_permissions() 254 for p in permissions: 255 if p.get("deleted"): 256 continue 257 try: 258 new_spreadsheet.share( 259 value=p["emailAddress"], 260 perm_type=p["type"], 261 role=p["role"], 262 notify=False, 263 ) 264 except Exception: 265 pass 266 267 return new_spreadsheet 268 269 def del_spreadsheet(self, file_id): 270 """Deletes a spreadsheet. 271 272 :param str file_id: a spreadsheet ID (a.k.a file ID). 273 """ 274 url = "{}/{}".format(DRIVE_FILES_API_V3_URL, file_id) 275 276 params = {"supportsAllDrives": True} 277 self.request("delete", url, params=params) 278 279 def import_csv(self, file_id, data): 280 """Imports data into the first page of the spreadsheet. 281 282 :param str data: A CSV string of data. 283 284 Example: 285 286 .. code:: 287 288 # Read CSV file contents 289 content = open('file_to_import.csv', 'r').read() 290 291 gc.import_csv(spreadsheet.id, content) 292 293 .. note:: 294 295 This method removes all other worksheets and then entirely 296 replaces the contents of the first worksheet. 297 298 """ 299 headers = {"Content-Type": "text/csv"} 300 url = "{}/{}".format(DRIVE_FILES_UPLOAD_API_V2_URL, file_id) 301 302 self.request( 303 "put", 304 url, 305 data=data, 306 params={ 307 "uploadType": "media", 308 "convert": True, 309 "supportsAllDrives": True, 310 }, 311 headers=headers, 312 ) 313 314 def list_permissions(self, file_id): 315 """Retrieve a list of permissions for a file. 316 317 :param str file_id: a spreadsheet ID (aka file ID). 318 """ 319 url = "{}/{}/permissions".format(DRIVE_FILES_API_V2_URL, file_id) 320 321 params = {"supportsAllDrives": True} 322 r = self.request("get", url, params=params) 323 324 return r.json()["items"] 325 326 def insert_permission( 327 self, 328 file_id, 329 value, 330 perm_type, 331 role, 332 notify=True, 333 email_message=None, 334 with_link=False, 335 ): 336 """Creates a new permission for a file. 337 338 :param str file_id: a spreadsheet ID (aka file ID). 339 :param value: user or group e-mail address, domain name 340 or None for 'default' type. 341 :type value: str, None 342 :param str perm_type: (optional) The account type. 343 Allowed values are: ``user``, ``group``, ``domain``, ``anyone`` 344 :param str role: (optional) The primary role for this user. 345 Allowed values are: ``owner``, ``writer``, ``reader`` 346 :param str notify: (optional) Whether to send an email to the target 347 user/domain. 348 :param str email_message: (optional) An email message to be sent 349 if ``notify=True``. 350 :param bool with_link: (optional) Whether the link is required for this 351 permission to be active. 352 353 Examples:: 354 355 # Give write permissions to otto@example.com 356 357 gc.insert_permission( 358 '0BmgG6nO_6dprnRRUWl1UFE', 359 'otto@example.org', 360 perm_type='user', 361 role='writer' 362 ) 363 364 # Make the spreadsheet publicly readable 365 366 gc.insert_permission( 367 '0BmgG6nO_6dprnRRUWl1UFE', 368 None, 369 perm_type='anyone', 370 role='reader' 371 ) 372 373 """ 374 375 url = "{}/{}/permissions".format(DRIVE_FILES_API_V2_URL, file_id) 376 377 payload = { 378 "value": value, 379 "type": perm_type, 380 "role": role, 381 "withLink": with_link, 382 } 383 384 params = { 385 "sendNotificationEmails": notify, 386 "emailMessage": email_message, 387 "supportsAllDrives": "true", 388 } 389 390 self.request("post", url, json=payload, params=params) 391 392 def remove_permission(self, file_id, permission_id): 393 """Deletes a permission from a file. 394 395 :param str file_id: a spreadsheet ID (aka file ID.) 396 :param str permission_id: an ID for the permission. 397 """ 398 url = "{}/{}/permissions/{}".format( 399 DRIVE_FILES_API_V2_URL, file_id, permission_id 400 ) 401 402 params = {"supportsAllDrives": True} 403 self.request("delete", url, params=params) 404