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