iterative / PyDrive2

Google Drive API Python wrapper library. Maintained fork of PyDrive.
https://docs.iterative.ai/PyDrive2
Other
580 stars 69 forks source link

Is there a way to authenticate without LocalWebserverAuth() ? #230

Closed matiasrebori closed 2 years ago

matiasrebori commented 2 years ago

Hi, is there a way to authenticate to Drive without using the LocalWebserverAuth() method, because it promps a login page from the browser and i dont want this behaviour. I have a flask application and one feature is to save images to a specific google drive folder (uses a single organization account), this runs in a linux vm. i have a client_secrets.json file, this is the JSON downloaded from Google Developers Console, also i set the settings.yaml file to create a credentials.json file to automate the consequents authentications. The problem is i dont want to manually login for the first time if there isnt a credentials.json file. Im using the next piece of code to login to google drive

from pydrive2.auth import GoogleAuth
from pydrive2.drive import GoogleDrive

CREDENTIALS_NAME = 'credentials.json'
DRIVE_FOLDER = 'xxxxxxxxxxxxxxxxxxxx'

def login():
    GoogleAuth.DEFAULT_SETTINGS['client_config_file'] = CREDENTIALS_NAME
    # create an instance of GoogleAuth.
    gauth = GoogleAuth()
    # loads credentials or create empty credentials if it doesn't exist.
    gauth.LoadCredentialsFile(CREDENTIALS_NAME)

    if gauth.credentials is None:
        # authenticate and authorize from user by creating local web server and retrieving authentication code.
        gauth.LocalWebserverAuth()
    elif gauth.access_token_expired:
        # refreshes the access_token.
        gauth.Refresh()
    else:
        # authorizes and builds service.
        gauth.Authorize()
    # saves credentials to the file in JSON format.
    gauth.SaveCredentialsFile(CREDENTIALS_NAME)
    # create an instance of Google Drive.
    gdrive = GoogleDrive(gauth)
    return gdrive

I have a code to get data from Google Sheets and only uses the JSON file from Google Developers Console, in this code i named this file credentials.json. So i think theres probably a similiar way to authenticate to Google Drive.

def load_sheet(page_name: str) -> list | None:
    data_range: str = page_name + DATA_RANGE
    # service account
    sa = gspread.service_account('credentials.json')
    # credentials
    creds = sa.auth
    # load sheet
    try:
        service = build('sheets', 'v4', credentials=creds)

        # Call the Sheets API
        sheet = service.spreadsheets()
        result = sheet.values().get(spreadsheetId=SAMPLE_SPREADSHEET_ID,
                                    range=data_range).execute()
        values = result.get('values', [])
        if not values:
            print('No data found.')
            return
        return values

    except HttpError as err:
        print(err)
        quit()
shcheklein commented 2 years ago

Hey, yes service account - see example here https://github.com/iterative/PyDrive2/issues/21 or https://docs.iterative.ai/PyDrive2/pydrive2/#pydrive2.auth.GoogleAuth.LocalWebserverAuth . Please give one of those a try and let us know if something doesn't work.

Also, see how fsspec - https://github.com/iterative/PyDrive2/blob/main/pydrive2/fs/spec.py#L132

matiasrebori commented 2 years ago

Thanks you for your help, the example you provided is working. One last question i have a function to upload files like this

def upload_file(filepath, filename, id_drive_folder=DRIVE_FOLDER):
    """
    upload a file to desired folder.

    :param filepath: ubicacion del archivo
    :param filename: nombre del archivo
    :param id_drive_folder: id de la carpeta del drive
    :return:
    """
    gdrive = login()
    metadata = {
        'parents': [
            {"kind": "drive#fileLink", "id": id_drive_folder}
        ],
        'title': f'{filename}'
    }
    # create file
    archivo = gdrive.CreateFile(metadata=metadata)
    # set the content of the file
    archivo.SetContentFile(filepath)
    # upload the file to google drive
    archivo.Upload()

this creates a new instance of GoogleDrive() for every call when doing gdrive = login(), is this approach good or is better to have only one instance of GoogleDrive() authenticated when the application starts i.g. instanciate only one time gdrive = login() and use this instance to all the consequent calls

matiasrebori commented 2 years ago

@shcheklein im trying to upload a base64 image stored in memory, but SetContentFile doesnt accepts io.BytesIO(). Exists a way to uploads files with io.BytesIO?

shcheklein commented 2 years ago

is this approach good or is better to have only one instance of GoogleDrive() authenticated when the application starts i.g. instanciate only one time gdrive = login() and use this instance to all the consequent calls

One instance should be enough. You don't need to do the whole workflow every time (it'll be cached, but anyway you don't need this)

shcheklein commented 2 years ago

im trying to upload a base64 image stored in memory, but SetContentFile doesnt accepts io.BytesIO(). Exists a way to uploads files with io.BytesIO?

yes, you could do SetContentString, or you could do even something like:

file.content = io.BytesIO(...)
files.Upload()
matiasrebori commented 2 years ago

im trying to upload a base64 image stored in memory, but SetContentFile doesnt accepts io.BytesIO(). Exists a way to uploads files with io.BytesIO?

yes, you could do SetContentString, or you could do even something like:

file.content = io.BytesIO(...)
files.Upload()

thanks for your help. I couldn't make it work with SetContentString(). It worked with the second recommendation.

a function receives the param bytes_file

      # create file
      archivo = gdrive.CreateFile(metadata=metadata)
      # set the content of the file
      archivo.content = bytes_file
      # upload the file to google drive
      archivo.Upload()

then to execute

    import base64
    import io

    file = open(r'C:\Users\xxx\Downloads\base64.txt', 'r')
    # read base64 string
    base64_string: str = file.read()
    # decode to data bytes
    image_bytes: bytes = base64.b64decode(base64_string)
    # Buffered I/O implementation using an in-memory bytes buffer.
    image_file = io.BytesIO(image_bytes)
    # upload file
    # file, filename, mimetype 
    upload_media(image_bytes, 'test2_4', 'image/jpeg')
matiasrebori commented 2 years ago

@shcheklein if you want i can contribute to the docs with how to authenticate with a Service Account and how to upload media. Also i think SetContentFile can easily support io.BytesIO(), or we could do another method like SetMediaFile() or something like this.

shcheklein commented 2 years ago

@matiasrebori that would very helpful, thanks.

Also i think SetContentFile can easily support io.BytesIO(),

I'm not sure about this tbh. It's quite explicit and does one thing. SetContentString or we may introduce another wrapper SetContentStream or something. But also, I think it's fine that you could do self.content = - for now. We can document it also.

matiasrebori commented 2 years ago

Ok, I'm not advanced on GitHub, should I open a new issue to link later with a PR?

shcheklein commented 2 years ago

Creating a PR if fine an enough!

matiasrebori commented 2 years ago

is this approach good or is better to have only one instance of GoogleDrive() authenticated when the application starts i.g. instanciate only one time gdrive = login() and use this instance to all the consequent calls

One instance should be enough. You don't need to do the whole workflow every time (it'll be cached, but anyway you don't need this)

Hi, do you know if the low level API googleapiclient also caches or is this a feature of pyDrive ?` Another question, i made a function to get a specific file by name and mimetype

archivo = gdrive.ListFile(query).GetList()
query = {'q': f"title = '{filename}' and mimeType='{mimetype}'"}

the name param in q search files using the low level API v3 works but it doesn't in pyDrive HttpError 400 Invalid query idk if this is wrong or right, instead I find that the param title worked with pyDrive as exact replacement for name. When i have a little more time i'll work in the PR :)

shcheklein commented 2 years ago

Hi, do you know if the low level API googleapiclient also caches or is this a feature of pyDrive ?`

I don't think googleapiclient has any mechanism to cache credentials, refresh tokens, etc.

My point was that, even if you do multiple instances it should be also fine if you use some default settings since PyDrive will create a files with credentials that it'll try to reuse next time.