pydrive-1.3.1/0000775000175000017500000000000013636106554012003 5ustar seb128seb128pydrive-1.3.1/AUTHORS0000664000175000017500000000023313003464542013041 0ustar seb128seb128Please contribute to this project to make Google drive development experience better in Python. * JunYoung Gwak * Scott Blevins * Robin Nabel pydrive-1.3.1/README.rst0000664000175000017500000001015413003464542013463 0ustar seb128seb128PyDrive ------- *PyDrive* is a wrapper library of `google-api-python-client `_ that simplifies many common Google Drive API tasks. Project Info ------------ - Homepage: `https://pypi.python.org/pypi/PyDrive `_ - Documentation: `Official documentation on GitHub pages `_ - Github: `https://github.com/googledrive/PyDrive `_ Features of PyDrive ------------------- - Simplifies OAuth2.0 into just few lines with flexible settings. - Wraps `Google Drive API `_ into classes of each resource to make your program more object-oriented. - Helps common operations else than API calls, such as content fetching and pagination control. How to install -------------- You can install PyDrive with regular ``pip`` command. :: $ pip install PyDrive To install the current development version from GitHub, use: :: $ pip install git+https://github.com/googledrive/PyDrive.git#egg=PyDrive OAuth made easy --------------- Download *client\_secrets.json* from Google API Console and OAuth2.0 is done in two lines. You can customize behavior of OAuth2 in one settings file *settings.yaml*. .. code:: python from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive gauth = GoogleAuth() gauth.LocalWebserverAuth() drive = GoogleDrive(gauth) File management made easy ------------------------- Upload/update the file with one method. PyDrive will do it in the most efficient way. .. code:: python file1 = drive.CreateFile({'title': 'Hello.txt'}) file1.SetContentString('Hello') file1.Upload() # Files.insert() file1['title'] = 'HelloWorld.txt' # Change title of the file file1.Upload() # Files.patch() content = file1.GetContentString() # 'Hello' file1.SetContentString(content+' World!') # 'Hello World!' file1.Upload() # Files.update() file2 = drive.CreateFile() file2.SetContentFile('hello.png') file2.Upload() print('Created file %s with mimeType %s' % (file2['title'], file2['mimeType'])) # Created file hello.png with mimeType image/png file3 = drive.CreateFile({'id': file2['id']}) print('Downloading file %s from Google Drive' % file3['title']) # 'hello.png' file3.GetContentFile('world.png') # Save Drive file as a local file # or download Google Docs files in an export format provided. # downloading a docs document as an html file: docsfile.GetContentFile('test.html', mimetype='text/html') File listing pagination made easy --------------------------------- *PyDrive* handles file listing pagination for you. .. code:: python # Auto-iterate through all files that matches this query file_list = drive.ListFile({'q': "'root' in parents"}).GetList() for file1 in file_list: print('title: %s, id: %s' % (file1['title'], file1['id'])) # Paginate file lists by specifying number of max results for file_list in drive.ListFile({'maxResults': 10}): print 'Received %s files from Files.list()' % len(file_list) # <= 10 for file1 in file_list: print('title: %s, id: %s' % (file1['title'], file1['id'])) Concurrent access made easy --------------------------- All calls made are thread-safe. The underlying implementation in the google-api-client library `is not thread-safe `_, which means that every request has to re-authenticate an http object. You can avoid this overhead by creating your own http object for each thread and re-use it for every call. This can be done as follows: .. code:: python # Create httplib.Http() object. http = drive.auth.Get_Http_Object() # Create file object to upload. file_obj = drive.CreateFile() file_obj['title'] = "file name" # Upload the file and pass the http object into the call to Upload. file_obj.Upload(param={"http": http}) You can specify the http-object in every access method which takes a *param* parameter.pydrive-1.3.1/TODO0000664000175000017500000000000013003464542012451 0ustar seb128seb128pydrive-1.3.1/MANIFEST.in0000664000175000017500000000025613003464542013534 0ustar seb128seb128include AUTHORS include CHANGES include LICENSE include MANIFEST.in include README.rst recursive-include docs * recursive-include pydrive/test * recursive-exclude * *.py[co] pydrive-1.3.1/LICENSE0000664000175000017500000002370413003464542013006 0ustar seb128seb128Copyright 2013 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. pydrive-1.3.1/CONTRIBUTING.rst0000664000175000017500000000245713003464542014444 0ustar seb128seb128Contributing guidelines ======================= How to become a contributor and submit your own code ---------------------------------------------------- Contributor License Agreements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We'd love to accept your patches! Before we can take them, we have to jump a couple of legal hurdles. Please fill out either the individual or corporate Contributor License Agreement (CLA). * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an `individual CLA `_. * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a `corporate CLA `_. Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll be able to accept your pull requests. ***NOTE***: Only original source code from you and other people that have signed the CLA can be accepted into the main repository. Contributing code ~~~~~~~~~~~~~~~~~ If you have improvements to PyDrive, send us your pull requests! For those just getting started, Github has a `howto `_.pydrive-1.3.1/.gitignore0000664000175000017500000000015713003464542013766 0ustar seb128seb128*.pyc *~ pydrive/test/configs/* pydrive/test/credentials/* *.egg-info dist build/ .cache client_secrets.json pydrive-1.3.1/pydrive/0000775000175000017500000000000013003464542013455 5ustar seb128seb128pydrive-1.3.1/pydrive/test/0000775000175000017500000000000013003464542014434 5ustar seb128seb128pydrive-1.3.1/pydrive/test/test_util.py0000664000175000017500000000034413003464542017023 0ustar seb128seb128import random import re newline_pattern = re.compile(r'[\r\n]') def CreateRandomFileName(): hash = random.getrandbits(128) return "%032x" % hash def StripNewlines(string): return newline_pattern.sub("", string) pydrive-1.3.1/pydrive/test/README.rst0000664000175000017500000000110513003464542016120 0ustar seb128seb128Instructions ------------ - Install timeout-decorator and futures (used for parallel upload and download testing). - Create *configs* and *credentials* folder in working directory. - Put *client_secrets.json* file from API console into *configs* folder. - Put *clients_secrets_local.json* from API console, for native device in *configs* folder. - Put different *a.png* and *b.png* file in working directory. - Replace {{ }} sections in *settings/test2.yaml* with the relevant sections from your config file. - For ServiceAuth test, place PKCS12 file in working directory. pydrive-1.3.1/pydrive/test/test_apiattr.py0000664000175000017500000000174513003464542017520 0ustar seb128seb128import unittest from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive class ApiAttributeTest(unittest.TestCase): """Test ApiAttr functions. """ ga = GoogleAuth('settings/test1.yaml') ga.LocalWebserverAuth() first_file = 'a.png' second_file = 'b.png' def test_UpdateMetadataNotInfinitelyNesting(self): # Verify 'metadata' field present. self.assertTrue(self.file1.metadata is not None) self.file1.UpdateMetadata() self.file1.UpdateMetadata() # Verify 'metadata' field still present. self.assertTrue(self.file1.metadata is not None) # Ensure no 'metadata' field in 'metadata' (i.e. nested). self.assertTrue('metadata' not in self.file1.metadata) def setUp(self): self.drive = GoogleDrive(self.ga) self.file1 = self.drive.CreateFile() self.file1.Upload() def tearDown(self): self.file1.Delete() if __name__ == '__main__': unittest.main()pydrive-1.3.1/pydrive/test/test_filelist.py0000664000175000017500000000627213003464542017667 0ustar seb128seb128# -*- coding: utf-8 -*- import os import sys import unittest from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive from pydrive.test import test_util class GoogleDriveFileListTest(unittest.TestCase): """Tests operations of files.GoogleDriveFileList class. Equivalent to Files.list in Google Drive API. """ ga = GoogleAuth('settings/test1.yaml') ga.LocalWebserverAuth() drive = GoogleDrive(ga) def test_01_Files_List_GetList(self): drive = GoogleDrive(self.ga) flist = drive.ListFile({'q': "title = '%s' and trashed = false" % self.title}) files = flist.GetList() # Auto iterate every file for file1 in self.file_list: found = False for file2 in files: if file1['id'] == file2['id']: found = True self.assertEqual(found, True) def test_02_Files_List_ForLoop(self): drive = GoogleDrive(self.ga) flist = drive.ListFile({'q': "title = '%s' and trashed = false"%self.title, 'maxResults': 2}) files = [] for x in flist: # Build iterator to access files simply with for loop self.assertTrue(len(x) <= 2) files.extend(x) for file1 in self.file_list: found = False for file2 in files: if file1['id'] == file2['id']: found = True self.assertEqual(found, True) def test_03_Files_List_GetList_Iterate(self): drive = GoogleDrive(self.ga) flist = drive.ListFile({'q': "title = '%s' and trashed = false"%self.title, 'maxResults': 2}) files = [] while True: try: x = flist.GetList() self.assertTrue(len(x) <= 2) files.extend(x) except StopIteration: break for file1 in self.file_list: found = False for file2 in files: if file1['id'] == file2['id']: found = True self.assertEqual(found, True) def test_File_List_Folders(self): drive = GoogleDrive(self.ga) folder1 = drive.CreateFile( {'mimeType': 'application/vnd.google-apps.folder', 'title': self.title}) folder1.Upload() self.file_list.append(folder1) flist = drive.ListFile({'q': "title = '%s' and trashed = false" % self.title}) count = 0 for _flist in flist: for file1 in _flist: self.assertFileInFileList(file1) count += 1 self.assertTrue(count == 11) # setUp and tearDown methods. # =========================== def setUp(self): title = test_util.CreateRandomFileName() file_list = [] for x in range(0, 10): file1 = self.drive.CreateFile() file1['title'] = title file1.Upload() file_list.append(file1) self.title = title self.file_list = file_list def tearDown(self): # Deleting uploaded files. for file1 in self.file_list: file1.Delete() def assertFileInFileList(self, file_object): found = False for file1 in self.file_list: if file_object['id'] == file1['id']: found = True self.assertEqual(found, True) def DeleteOldFile(self, file_name): try: os.remove(file_name) except OSError: pass if __name__ == '__main__': unittest.main() pydrive-1.3.1/pydrive/test/test_drive.py0000664000175000017500000000107613003464542017162 0ustar seb128seb128# -*- coding: utf-8 -*- import unittest from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive class GoogleDriveTest(unittest.TestCase): """Tests basic operations on meta-data information of the linked Google Drive account. """ ga = GoogleAuth('settings/test1.yaml') ga.LocalWebserverAuth() def test_01_About_Request(self): drive = GoogleDrive(self.ga) about_object = drive.GetAbout() self.assertTrue(about_object is not None, "About object not loading.") if __name__ == '__main__': unittest.main() pydrive-1.3.1/pydrive/test/__init__.py0000664000175000017500000000000013003464542016533 0ustar seb128seb128pydrive-1.3.1/pydrive/test/test_file.py0000664000175000017500000005131113003464542016765 0ustar seb128seb128# -*- coding: utf-8 -*- import filecmp import os import unittest from io import BytesIO from six.moves import range import timeout_decorator from concurrent.futures import ThreadPoolExecutor, as_completed from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive from pydrive.files import ApiRequestError, GoogleDriveFile import test_util class GoogleDriveFileTest(unittest.TestCase): """Tests basic file operations of files.GoogleDriveFile. Upload and download of contents and metadata, and thread-safety checks. Equivalent to Files.insert, Files.update, Files.patch in Google Drive API. """ ga = GoogleAuth('settings/test1.yaml') ga.LocalWebserverAuth() first_file = 'a.png' second_file = 'b.png' def test_01_Files_Insert(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = 'firsttestfile' file1['title'] = filename file1.Upload() # Files.insert self.assertEqual(file1.metadata['title'], filename) file2 = drive.CreateFile({'id': file1['id']}) # Download file from id. self.assertEqual(file2['title'], filename) self.DeleteUploadedFiles(drive, [file1['id']]) def test_02_Files_Insert_Unicode(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = u'첫번째 파일' file1['title'] = filename file1.Upload() # Files.insert self.assertEqual(file1.metadata['title'], filename) file2 = drive.CreateFile({'id': file1['id']}) # Download file from id. self.assertEqual(file2['title'], filename) self.DeleteUploadedFiles(drive, [file1['id']]) def test_03_Files_Insert_Content_String(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = 'secondtestfile' content = 'hello world!' file1['title'] = filename file1.SetContentString(content) file1.Upload() # Files.insert self.assertEqual(file1.GetContentString(), content) file1.FetchContent() # Force download and double check content self.assertEqual(file1.metadata['title'], filename) self.assertEqual(file1.GetContentString(), content) file2 = drive.CreateFile({'id': file1['id']}) # Download file from id. self.assertEqual(file2.GetContentString(), content) self.assertEqual(file2.metadata['title'], filename) self.DeleteUploadedFiles(drive, [file1['id']]) def test_04_Files_Insert_Content_Unicode_String(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = u'두번째 파일' content = u'안녕 세상아!' file1['title'] = filename file1.SetContentString(content) file1.Upload() # Files.insert self.assertEqual(file1.GetContentString(), content) self.assertEqual(file1.metadata['title'], filename) file1.FetchContent() # Force download and double check content. self.assertEqual(file1.GetContentString(), content) file2 = drive.CreateFile({'id': file1['id']}) # Download file from id. self.assertEqual(file2.GetContentString(), content) self.assertEqual(file2.metadata['title'], filename) self.DeleteUploadedFiles(drive, [file1['id']]) def test_05_Files_Insert_Content_File(self): self.DeleteOldFile(self.first_file+'1') self.DeleteOldFile(self.first_file+'2') drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = 'filecontent' file1['title'] = filename file1.SetContentFile(self.first_file) file1.Upload() # Files.insert self.assertEqual(file1.metadata['title'], filename) file1.FetchContent() # Force download and double check content. file1.GetContentFile(self.first_file+'1') self.assertEqual(filecmp.cmp(self.first_file, self.first_file+'1'), True) file2 = drive.CreateFile({'id': file1['id']}) # Download file from id. file2.GetContentFile(self.first_file+'2') self.assertEqual(filecmp.cmp(self.first_file, self.first_file+'2'), True) self.DeleteUploadedFiles(drive, [file1['id']]) def test_06_Files_Patch(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = 'prepatchtestfile' newfilename = 'patchtestfile' file1['title'] = filename file1.Upload() # Files.insert self.assertEqual(file1.metadata['title'], filename) file1['title'] = newfilename file1.Upload() # Files.patch self.assertEqual(file1.metadata['title'], newfilename) file2 = drive.CreateFile({'id': file1['id']}) # Download file from id. file2.FetchMetadata() self.assertEqual(file2.metadata['title'], newfilename) self.DeleteUploadedFiles(drive, [file1['id']]) def test_07_Files_Patch_Skipping_Content(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = 'prepatchtestfile' newfilename = 'patchtestfile' content = 'hello world!' file1['title'] = filename file1.SetContentString(content) file1.Upload() # Files.insert self.assertEqual(file1.metadata['title'], filename) file1['title'] = newfilename file1.Upload() # Files.patch self.assertEqual(file1.metadata['title'], newfilename) self.assertEqual(file1.GetContentString(), content) self.assertEqual(file1.GetContentString(), content) self.DeleteUploadedFiles(drive, [file1['id']]) def test_08_Files_Update_String(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = 'preupdatetestfile' newfilename = 'updatetestfile' content = 'hello world!' newcontent = 'hello new world!' file1['title'] = filename file1.SetContentString(content) file1.Upload() # Files.insert self.assertEqual(file1.metadata['title'], filename) self.assertEqual(file1.GetContentString(), content) file1.FetchContent() # Force download and double check content. self.assertEqual(file1.GetContentString(), content) file1['title'] = newfilename file1.SetContentString(newcontent) file1.Upload() # Files.update self.assertEqual(file1.metadata['title'], newfilename) self.assertEqual(file1.GetContentString(), newcontent) self.assertEqual(file1.GetContentString(), newcontent) self.DeleteUploadedFiles(drive, [file1['id']]) def test_09_Files_Update_File(self): self.DeleteOldFile(self.first_file+'1') self.DeleteOldFile(self.second_file+'1') drive = GoogleDrive(self.ga) file1 = drive.CreateFile() filename = 'preupdatetestfile' newfilename = 'updatetestfile' file1['title'] = filename file1.SetContentFile(self.first_file) file1.Upload() # Files.insert self.assertEqual(file1.metadata['title'], filename) file1.FetchContent() # Force download and double check content. file1.GetContentFile(self.first_file+'1') self.assertEqual(filecmp.cmp(self.first_file, self.first_file+'1'), True) file1['title'] = newfilename file1.SetContentFile(self.second_file) file1.Upload() # Files.update self.assertEqual(file1.metadata['title'], newfilename) file1.GetContentFile(self.second_file+'1') self.assertEqual(filecmp.cmp(self.second_file, self.second_file+'1'), True) self.DeleteUploadedFiles(drive, [file1['id']]) # Tests for Trash/UnTrash/Delete. # =============================== def test_Files_Trash_File(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() self.assertFalse(file1.metadata[u'labels'][u'trashed']) # Download to verify non-trashed state on GDrive. file2 = drive.CreateFile({'id': file1['id']}) file2.FetchMetadata() self.assertFalse(file2.metadata[u'labels'][u'trashed']) file1.Trash() self.assertTrue(file1.metadata[u'labels'][u'trashed']) file2.FetchMetadata() self.assertTrue(file2.metadata[u'labels'][u'trashed']) self.DeleteUploadedFiles(drive, [file1['id']]) def test_Files_Trash_File_Just_ID(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() self.assertFalse(file1.metadata[u'labels'][u'trashed']) # Trash file by ID. file2 = drive.CreateFile({'id': file1['id']}) file2.Trash() # Verify trashed by downloading metadata. file1.FetchMetadata() self.assertTrue(file1.metadata[u'labels'][u'trashed']) self.DeleteUploadedFiles(drive, [file1['id']]) def test_Files_UnTrash_File(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() file1.Trash() self.assertTrue(file1.metadata[u'labels'][u'trashed']) # Verify that file is trashed by downloading metadata. file2 = drive.CreateFile({'id': file1['id']}) file2.FetchMetadata() self.assertTrue(file2.metadata[u'labels'][u'trashed']) # Un-trash the file, and assert local metadata is updated correctly. file1.UnTrash() self.assertFalse(file1.metadata[u'labels'][u'trashed']) # Re-fetch the metadata, and assert file un-trashed on GDrive. file2.FetchMetadata() self.assertFalse(file2.metadata[u'labels'][u'trashed']) self.DeleteUploadedFiles(drive, [file1['id']]) def test_Files_UnTrash_File_Just_ID(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() file1.Trash() self.assertTrue(file1.metadata[u'labels'][u'trashed']) file2 = drive.CreateFile({'id': file1['id']}) file2.UnTrash() # UnTrash without fetching metadata. file1.FetchMetadata() self.assertFalse(file1.metadata[u'labels'][u'trashed']) self.DeleteUploadedFiles(drive, [file1['id']]) def test_Files_Delete_File(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() file2 = drive.CreateFile({'id': file1['id']}) file1.Delete() try: file2.FetchMetadata() self.fail("File not deleted correctly.") except ApiRequestError as e: pass def test_Files_Delete_File_Just_ID(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() file2 = drive.CreateFile({'id': file1['id']}) file2.Delete() try: file1.FetchMetadata() self.fail("File not deleted correctly.") except ApiRequestError as e: pass # Tests for Permissions. # ====================== def test_Files_FetchMetadata_Fields(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() self.assertFalse('permissions' in file1) file1.FetchMetadata('permissions') self.assertTrue('permissions' in file1) file1.Delete() def test_Files_Insert_Permission(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() # Verify only one permission before inserting permission. permissions = file1.GetPermissions() self.assertEqual(len(permissions), 1) self.assertEqual(len(file1['permissions']), 1) # Insert the permission. permission = file1.InsertPermission({'type': 'anyone', 'value': 'anyone', 'role': 'reader'}) self.assertTrue(permission) self.assertEqual(len(file1["permissions"]), 2) self.assertEqual(file1["permissions"][1]["type"], "anyone") permissions = file1.GetPermissions() self.assertEqual(len(file1["permissions"]), 2) self.assertEqual(file1["permissions"][1]["type"], "anyone") self.assertEqual(permissions[1]["type"], "anyone") # Verify remote changes made. file2 = drive.CreateFile({'id': file1['id']}) permissions = file2.GetPermissions() self.assertEqual(len(permissions), 2) self.assertEqual(permissions[1]["type"], "anyone") file1.Delete() def test_Files_Get_Permissions(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() self.assertFalse('permissions' in file1) permissions = file1.GetPermissions() self.assertTrue(permissions is not None) self.assertTrue('permissions' in file1) file1.Delete() def test_Files_Delete_Permission(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() file1.InsertPermission({'type': 'anyone', 'value': 'anyone', 'role': 'reader'}) permissions = file1.GetPermissions() self.assertEqual(len(permissions), 2) self.assertEqual(len(file1['permissions']), 2) file1.DeletePermission(permissions[1]['id']) self.assertEqual(len(file1['permissions']), 1) # Verify remote changes made. file2 = drive.CreateFile({'id': file1['id']}) permissions = file2.GetPermissions() self.assertEqual(len(permissions), 1) file1.Delete() def test_Files_Delete_Permission_Invalid(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() file1.Upload() try: file1.DeletePermission('invalid id') self.fail("Deleting invalid permission not raising exception.") except ApiRequestError as e: pass file1.Delete() def test_GFile_Conversion_Lossless_String(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() # Upload a string, and convert into Google Doc format. test_string = 'Generic, non-exhaustive ASCII test string.' file1.SetContentString(test_string) file1.Upload({'convert': True}) # Download string as plain text. downloaded_string = file1.GetContentString(mimetype='text/plain') self.assertEqual(test_string, downloaded_string, "Strings do not match") # Download content into file and ensure that file content matches original # content string. downloaded_file_name = '_tmp_downloaded_file_name.txt' file1.GetContentFile(downloaded_file_name, mimetype='text/plain') downloaded_string = open(downloaded_file_name).read() self.assertEqual(test_string, downloaded_string, "Strings do not match") # Delete temp file. os.remove(downloaded_file_name) # Tests for GDrive conversion. # ============================ def setup_gfile_conversion_test(self): drive = GoogleDrive(self.ga) file1 = drive.CreateFile() # Create a file to upload. file_name = '_tmp_source_file.txt' downloaded_file_name = '_tmp_downloaded_file_name.txt' original_file_content = 'Generic, non-exhaustive\n ASCII test string.' source_file = open(file_name, mode='w+') source_file.write(original_file_content) source_file.close() original_file_content = test_util.StripNewlines(original_file_content) return file1, file_name, original_file_content, downloaded_file_name def cleanup_gfile_conversion_test(self, file1, file_name, downloaded_file_name): # Delete temporary files. os.path.exists(file_name) and os.remove(file_name) os.path.exists(downloaded_file_name) and os.remove(downloaded_file_name) file1.Delete() # Delete uploaded file. def test_GFile_Conversion_Remove_BOM(self): (file1, file_name, original_file_content, downloaded_file_name) = \ self.setup_gfile_conversion_test() try: # Upload source_file and convert into Google Doc format. file1.SetContentFile(file_name) file1.Upload({'convert': True}) # Download as string. downloaded_content_no_bom = file1.GetContentString(mimetype='text/plain', remove_bom=True) downloaded_content_no_bom = test_util.StripNewlines( downloaded_content_no_bom) self.assertEqual(original_file_content, downloaded_content_no_bom) # Download as file. file1.GetContentFile(downloaded_file_name, remove_bom=True) downloaded_content = open(downloaded_file_name).read() downloaded_content = test_util.StripNewlines(downloaded_content) self.assertEqual(original_file_content, downloaded_content) finally: self.cleanup_gfile_conversion_test(file1, file_name, downloaded_file_name) def test_Gfile_Conversion_Add_Remove_BOM(self): """Tests whether you can switch between the BOM appended and removed version on the fly.""" (file1, file_name, original_file_content, downloaded_file_name) = \ self.setup_gfile_conversion_test() try: file1.SetContentFile(file_name) file1.Upload({'convert': True}) content_bom = file1.GetContentString(mimetype='text/plain') content_no_bom = file1.GetContentString(mimetype='text/plain', remove_bom=True) content_bom_2 = file1.GetContentString(mimetype='text/plain') self.assertEqual(content_bom, content_bom_2) self.assertNotEqual(content_bom, content_no_bom) self.assertTrue(len(content_bom) > len(content_no_bom)) finally: self.cleanup_gfile_conversion_test(file1, file_name, downloaded_file_name) def test_InsertPrefix(self): # Create BytesIO. file_obj = BytesIO('abc') original_length = len(file_obj.getvalue()) char_to_insert = u'\ufeff'.encode('utf8') # Insert the prefix. GoogleDriveFile._InsertPrefix(file_obj, char_to_insert) modified_length = len(file_obj.getvalue()) self.assertGreater(modified_length, original_length) self.assertEqual(file_obj.getvalue(), u'\ufeffabc'.encode('utf8')) def test_InsertPrefixLarge(self): # Create BytesIO. test_content = 'abc' * 800 file_obj = BytesIO(test_content) original_length = len(file_obj.getvalue()) char_to_insert = u'\ufeff'.encode('utf8') # Insert the prefix. GoogleDriveFile._InsertPrefix(file_obj, char_to_insert) modified_length = len(file_obj.getvalue()) self.assertGreater(modified_length, original_length) expected_content = u'\ufeff' + test_content self.assertEqual(file_obj.getvalue(), expected_content.encode('utf8')) def test_RemovePrefix(self): # Create BytesIO. file_obj = BytesIO(u'\ufeffabc'.encode('utf8')) original_length = len(file_obj.getvalue()) char_to_remove = u'\ufeff'.encode('utf8') # Insert the prefix. GoogleDriveFile._RemovePrefix(file_obj, char_to_remove) modified_length = len(file_obj.getvalue()) self.assertLess(modified_length, original_length) self.assertEqual(file_obj.getvalue(), 'abc') def test_RemovePrefixLarge(self): # Create BytesIO. test_content = u'\ufeff' + u'abc' * 800 file_obj = BytesIO(test_content.encode('utf8')) original_length = len(file_obj.getvalue()) char_to_remove = u'\ufeff'.encode('utf8') # Insert the prefix. GoogleDriveFile._RemovePrefix(file_obj, char_to_remove) modified_length = len(file_obj.getvalue()) self.assertLess(modified_length, original_length) self.assertEqual(file_obj.getvalue(), test_content[1:]) # Setup for concurrent upload testing. # ===================================== class UploadWorker: def __init__(self, gdrive_file, generate_http=False): self.gdrive_file = gdrive_file self.param = {} if generate_http: self.param = {"http": gdrive_file.auth.Get_Http_Object()} def run(self): self.gdrive_file.Upload(param=self.param) def _parallel_uploader(self, num_of_uploads, num_of_workers, use_per_thread_http=False): drive = GoogleDrive(self.ga) thread_pool = ThreadPoolExecutor(max_workers=num_of_workers) # Create list of gdrive_files. upload_files = [] remote_name = test_util.CreateRandomFileName() for i in range(num_of_uploads): file_name = self.first_file if i % 2 == 0 else self.second_file up_file = drive.CreateFile() up_file['title'] = remote_name up_file.SetContentFile(file_name) upload_files.append(up_file) # Ensure there are no files with the random file name. files = drive.ListFile(param={'q': "title = '%s' and trashed = false" % remote_name}).GetList() self.assertTrue(len(files) == 0) # Submit upload jobs to ThreadPoolExecutor. futures = [] for i in range(num_of_uploads): upload_worker = self.UploadWorker(upload_files[i], use_per_thread_http) futures.append(thread_pool.submit(upload_worker.run)) # Ensure that all threads a) return, and b) encountered no exceptions. for future in as_completed(futures): self.assertIsNone(future.exception()) thread_pool.shutdown() # Ensure 10 files were uploaded. files = drive.ListFile(param={'q': "title = '%s' and trashed = false" % remote_name}).GetList() self.assertTrue(len(files) == 10) # Remove uploaded files. self.DeleteUploadedFiles(drive, [fi['id'] for fi in upload_files]) @timeout_decorator.timeout(80) def test_Parallel_Files_Insert_File_Auto_Generated_HTTP(self): self._parallel_uploader(10, 10) @timeout_decorator.timeout(80) def test_Parallel_Insert_File_Passed_HTTP(self): self._parallel_uploader(10, 10, True) # Helper functions. # ================= def DeleteOldFile(self, file_name): try: os.remove(file_name) except OSError: pass def DeleteUploadedFiles(self, drive, ids): for element in ids: tmp_file = drive.CreateFile({'id': element}) tmp_file.Delete() if __name__ == '__main__': unittest.main() pydrive-1.3.1/pydrive/test/test_oauth.py0000664000175000017500000000510213003464542017163 0ustar seb128seb128import unittest import os import time from pydrive.auth import GoogleAuth class GoogleAuthTest(unittest.TestCase): """Tests basic OAuth2 operations of auth.GoogleAuth.""" def test_01_LocalWebserverAuthWithClientConfigFromFile(self): # Delete old credentials file self.DeleteOldCredentialsFile('credentials/1.dat') # Test if authentication works with config read from file ga = GoogleAuth('settings/test1.yaml') ga.LocalWebserverAuth() self.assertEqual(ga.access_token_expired, False) # Test if correct credentials file is created self.CheckCredentialsFile('credentials/1.dat') time.sleep(1) def test_02_LocalWebserverAuthWithClientConfigFromSettings(self): # Delete old credentials file self.DeleteOldCredentialsFile('credentials/2.dat') # Test if authentication works with config read from settings ga = GoogleAuth('settings/test2.yaml') ga.LocalWebserverAuth() self.assertEqual(ga.access_token_expired, False) # Test if correct credentials file is created self.CheckCredentialsFile('credentials/2.dat') time.sleep(1) def test_03_LocalWebServerAuthWithNoCredentialsSaving(self): # Delete old credentials file self.DeleteOldCredentialsFile('credentials/4.dat') # Provide wrong credentials file ga = GoogleAuth('settings/test3.yaml') ga.LocalWebserverAuth() self.assertEqual(ga.access_token_expired, False) # Test if correct credentials file is created self.CheckCredentialsFile('credentials/4.dat', no_file=True) time.sleep(1) def test_04_CommandLineAuthWithClientConfigFromFile(self): # Delete old credentials file self.DeleteOldCredentialsFile('credentials/1.dat') # Test if authentication works with config read from file ga = GoogleAuth('settings/test4.yaml') ga.CommandLineAuth() self.assertEqual(ga.access_token_expired, False) # Test if correct credentials file is created self.CheckCredentialsFile('credentials/1.dat') time.sleep(1) def test_05_ConfigFromSettingsWithoutOauthScope(self): # Test if authentication works without oauth_scope ga = GoogleAuth('settings/test5.yaml') ga.LocalWebserverAuth() self.assertEqual(ga.access_token_expired, False) time.sleep(1) def DeleteOldCredentialsFile(self, credentials): try: os.remove(credentials) except OSError: pass def CheckCredentialsFile(self, credentials, no_file=False): ga = GoogleAuth('settings/default.yaml') ga.LoadCredentialsFile(credentials) self.assertEqual(ga.access_token_expired, no_file) if __name__ == '__main__': unittest.main() pydrive-1.3.1/pydrive/test/settings/0000775000175000017500000000000013003464542016274 5ustar seb128seb128pydrive-1.3.1/pydrive/test/settings/test6.yaml0000664000175000017500000000054213003464542020226 0ustar seb128seb128client_config_backend: 'service' service_config: client_user_email: "{{YOUR_USER_EMAIL}}" client_service_email: "{{YOUR_SERVICE_EMAIL}}" client_pkcs12_file_path: "{{YOUR_PKCS12_FILE_PATH}}" save_credentials: True save_credentials_backend: 'file' save_credentials_file: 'credentials/5.dat' oauth_scope: - "https://www.googleapis.com/auth/drive" pydrive-1.3.1/pydrive/test/settings/test3.yaml0000664000175000017500000000024313003464542020221 0ustar seb128seb128client_config_backend: 'file' client_config_file: 'configs/client_secrets.json' save_credentials: False oauth_scope: - 'https://www.googleapis.com/auth/drive' pydrive-1.3.1/pydrive/test/settings/test5.yaml0000664000175000017500000000012013003464542020215 0ustar seb128seb128client_config_backend: 'file' client_config_file: 'configs/client_secrets.json' pydrive-1.3.1/pydrive/test/settings/test1.yaml0000664000175000017500000000035613003464542020224 0ustar seb128seb128client_config_backend: 'file' client_config_file: 'configs/client_secrets.json' save_credentials: True save_credentials_backend: 'file' save_credentials_file: 'credentials/1.dat' oauth_scope: - 'https://www.googleapis.com/auth/drive' pydrive-1.3.1/pydrive/test/settings/test2.yaml0000664000175000017500000000043313003464542020221 0ustar seb128seb128client_config_backend: 'settings' client_config: client_id: "{{YOUR_CLIENT_ID}}" client_secret: "{{YOUR_CLIENT_SECRET}}" save_credentials: True save_credentials_backend: 'file' save_credentials_file: 'credentials/2.dat' oauth_scope: - "https://www.googleapis.com/auth/drive" pydrive-1.3.1/pydrive/test/settings/test4.yaml0000664000175000017500000000036413003464542020226 0ustar seb128seb128client_config_backend: 'file' client_config_file: 'configs/client_secrets_local.json' save_credentials: True save_credentials_backend: 'file' save_credentials_file: 'credentials/1.dat' oauth_scope: - "https://www.googleapis.com/auth/drive" pydrive-1.3.1/pydrive/test/settings/default.yaml0000664000175000017500000000035613003464542020610 0ustar seb128seb128client_config_backend: 'file' client_config_file: 'configs/client_secrets.json' save_credentials: True save_credentials_backend: 'file' save_credentials_file: 'credentials/1.dat' oauth_scope: - 'https://www.googleapis.com/auth/drive' pydrive-1.3.1/pydrive/settings.py0000664000175000017500000001354213003464542015674 0ustar seb128seb128from yaml import load from yaml import YAMLError try: from yaml import CLoader as Loader except ImportError: from yaml import Loader SETTINGS_FILE = 'settings.yaml' SETTINGS_STRUCT = { 'client_config_backend': { 'type': str, 'required': True, 'default': 'file', 'dependency': [ { 'value': 'file', 'attribute': ['client_config_file'] }, { 'value': 'settings', 'attribute': ['client_config'] }, { 'value': 'service', 'attribute': ['service_config'] } ] }, 'save_credentials': { 'type': bool, 'required': True, 'default': False, 'dependency': [ { 'value': True, 'attribute': ['save_credentials_backend'] } ] }, 'get_refresh_token': { 'type': bool, 'required': False, 'default': False }, 'client_config_file': { 'type': str, 'required': False, 'default': 'client_secrets.json' }, 'save_credentials_backend': { 'type': str, 'required': False, 'dependency': [ { 'value': 'file', 'attribute': ['save_credentials_file'] } ] }, 'client_config': { 'type': dict, 'required': False, 'struct': { 'client_id': { 'type': str, 'required': True }, 'client_secret': { 'type': str, 'required': True }, 'auth_uri': { 'type': str, 'required': True, 'default': 'https://accounts.google.com/o/oauth2/auth' }, 'token_uri': { 'type': str, 'required': True, 'default': 'https://accounts.google.com/o/oauth2/token' }, 'redirect_uri': { 'type': str, 'required': True, 'default': 'urn:ietf:wg:oauth:2.0:oob' }, 'revoke_uri': { 'type': str, 'required': True, 'default': None } } }, 'service_config': { 'type': dict, 'required': False, 'struct': { 'client_user_email': { 'type': str, 'required': True, 'default': None }, 'client_service_email': { 'type': str, 'required': True }, 'client_pkcs12_file_path': { 'type': str, 'required': True } } }, 'oauth_scope': { 'type': list, 'required': True, 'struct': str, 'default': ['https://www.googleapis.com/auth/drive'] }, 'save_credentials_file': { 'type': str, 'required': False, } } class SettingsError(IOError): """Error while loading/saving settings""" class InvalidConfigError(IOError): """Error trying to read client configuration.""" def LoadSettingsFile(filename=SETTINGS_FILE): """Loads settings file in yaml format given file name. :param filename: path for settings file. 'settings.yaml' by default. :type filename: str. :raises: SettingsError """ try: stream = open(filename, 'r') data = load(stream, Loader=Loader) except (YAMLError, IOError) as e: raise SettingsError(e) return data def ValidateSettings(data): """Validates if current settings is valid. :param data: dictionary containing all settings. :type data: dict. :raises: InvalidConfigError """ _ValidateSettingsStruct(data, SETTINGS_STRUCT) def _ValidateSettingsStruct(data, struct): """Validates if provided data fits provided structure. :param data: dictionary containing settings. :type data: dict. :param struct: dictionary containing structure information of settings. :type struct: dict. :raises: InvalidConfigError """ # Validate required elements of the setting. for key in struct: if struct[key]['required']: _ValidateSettingsElement(data, struct, key) def _ValidateSettingsElement(data, struct, key): """Validates if provided element of settings data fits provided structure. :param data: dictionary containing settings. :type data: dict. :param struct: dictionary containing structure information of settings. :type struct: dict. :param key: key of the settings element to validate. :type key: str. :raises: InvalidConfigError """ # Check if data exists. If not, check if default value exists. value = data.get(key) data_type = struct[key]['type'] if value is None: try: default = struct[key]['default'] except KeyError: raise InvalidConfigError('Missing required setting %s' % key) else: data[key] = default # If data exists, Check type of the data elif type(value) is not data_type: raise InvalidConfigError('Setting %s should be type %s' % (key, data_type)) # If type of this data is dict, check if structure of the data is valid. if data_type is dict: _ValidateSettingsStruct(data[key], struct[key]['struct']) # If type of this data is list, check if all values in the list is valid. elif data_type is list: for element in data[key]: if type(element) is not struct[key]['struct']: raise InvalidConfigError('Setting %s should be list of %s' % (key, struct[key]['struct'])) # Check dependency of this attribute. dependencies = struct[key].get('dependency') if dependencies: for dependency in dependencies: if value == dependency['value']: for reqkey in dependency['attribute']: _ValidateSettingsElement(data, struct, reqkey) pydrive-1.3.1/pydrive/__init__.py0000664000175000017500000000000013003464542015554 0ustar seb128seb128pydrive-1.3.1/pydrive/auth.py0000664000175000017500000004437013003464542015000 0ustar seb128seb128import socket import webbrowser import httplib2 import oauth2client.clientsecrets as clientsecrets from six.moves import input from apiclient.discovery import build from functools import wraps from oauth2client.service_account import ServiceAccountCredentials from oauth2client.client import FlowExchangeError from oauth2client.client import AccessTokenRefreshError from oauth2client.client import OAuth2WebServerFlow from oauth2client.client import OOB_CALLBACK_URN from oauth2client.file import Storage from oauth2client.tools import ClientRedirectHandler from oauth2client.tools import ClientRedirectServer from oauth2client._helpers import scopes_to_string from .apiattr import ApiAttribute from .apiattr import ApiAttributeMixin from .settings import LoadSettingsFile from .settings import ValidateSettings from .settings import SettingsError from .settings import InvalidConfigError class AuthError(Exception): """Base error for authentication/authorization errors.""" class InvalidCredentialsError(IOError): """Error trying to read credentials file.""" class AuthenticationRejected(AuthError): """User rejected authentication.""" class AuthenticationError(AuthError): """General authentication error.""" class RefreshError(AuthError): """Access token refresh error.""" def LoadAuth(decoratee): """Decorator to check if the auth is valid and loads auth if not.""" @wraps(decoratee) def _decorated(self, *args, **kwargs): # Initialize auth if needed. if self.auth is None: self.auth = GoogleAuth() # Re-create access token if it expired. if self.auth.access_token_expired: if getattr(self, 'auth_method', False) == 'service': self.auth.ServiceAuth() else: self.auth.LocalWebserverAuth() # Initialise service if not built yet. if self.auth.service is None: self.auth.Authorize() # Ensure that a thread-safe HTTP object is provided. if kwargs is not None and \ "param" in kwargs and \ kwargs["param"] is not None and \ "http" in kwargs["param"] and \ kwargs["param"]["http"] is not None: self.http = kwargs["param"]["http"] del kwargs["param"]["http"] else: # If HTTP object not specified, each call creates new HTTP object. self.http = self.auth.Get_Http_Object() return decoratee(self, *args, **kwargs) return _decorated def CheckServiceAuth(decoratee): """Decorator to authorize service account.""" @wraps(decoratee) def _decorated(self, *args, **kwargs): self.auth_method = 'service' dirty = False save_credentials = self.settings.get('save_credentials') if self.credentials is None and save_credentials: self.LoadCredentials() if self.credentials is None: decoratee(self, *args, **kwargs) self.Authorize() dirty = True else: if self.access_token_expired: if self.credentials.refresh_token is not None: self.Refresh() else: decoratee(self, *args, **kwargs) self.Authorize() dirty = True if dirty and save_credentials: self.SaveCredentials() return _decorated def CheckAuth(decoratee): """Decorator to check if it requires OAuth2 flow request.""" @wraps(decoratee) def _decorated(self, *args, **kwargs): dirty = False code = None save_credentials = self.settings.get('save_credentials') if self.credentials is None and save_credentials: self.LoadCredentials() if self.flow is None: self.GetFlow() if self.credentials is None: code = decoratee(self, *args, **kwargs) dirty = True else: if self.access_token_expired: if self.credentials.refresh_token is not None: self.Refresh() else: code = decoratee(self, *args, **kwargs) dirty = True if code is not None: self.Auth(code) if dirty and save_credentials: self.SaveCredentials() return _decorated class GoogleAuth(ApiAttributeMixin, object): """Wrapper class for oauth2client library in google-api-python-client. Loads all settings and credentials from one 'settings.yaml' file and performs common OAuth2.0 related functionality such as authentication and authorization. """ DEFAULT_SETTINGS = { 'client_config_backend': 'file', 'client_config_file': 'client_secrets.json', 'save_credentials': False, 'oauth_scope': ['https://www.googleapis.com/auth/drive'] } CLIENT_CONFIGS_LIST = ['client_id', 'client_secret', 'auth_uri', 'token_uri', 'revoke_uri', 'redirect_uri'] SERVICE_CONFIGS_LIST = ['client_service_email', 'client_user_email', 'client_pkcs12_file_path'] settings = ApiAttribute('settings') client_config = ApiAttribute('client_config') flow = ApiAttribute('flow') credentials = ApiAttribute('credentials') http = ApiAttribute('http') service = ApiAttribute('service') auth_method = ApiAttribute('auth_method') def __init__(self, settings_file='settings.yaml',http_timeout=None): """Create an instance of GoogleAuth. This constructor just sets the path of settings file. It does not actually read the file. :param settings_file: path of settings file. 'settings.yaml' by default. :type settings_file: str. """ self.http_timeout=http_timeout ApiAttributeMixin.__init__(self) self.client_config = {} try: self.settings = LoadSettingsFile(settings_file) except SettingsError: self.settings = self.DEFAULT_SETTINGS else: if self.settings is None: self.settings = self.DEFAULT_SETTINGS else: ValidateSettings(self.settings) @property def access_token_expired(self): """Checks if access token doesn't exist or is expired. :returns: bool -- True if access token doesn't exist or is expired. """ if self.credentials is None: return True return self.credentials.access_token_expired @CheckAuth def LocalWebserverAuth(self, host_name='localhost', port_numbers=None): """Authenticate and authorize from user by creating local web server and retrieving authentication code. This function is not for web server application. It creates local web server for user from standalone application. :param host_name: host name of the local web server. :type host_name: str. :param port_numbers: list of port numbers to be tried to used. :type port_numbers: list. :returns: str -- code returned from local web server :raises: AuthenticationRejected, AuthenticationError """ if port_numbers is None: port_numbers = [8080, 8090] # Mutable objects should not be default # values, as each call's changes are global. success = False port_number = 0 for port in port_numbers: port_number = port try: httpd = ClientRedirectServer((host_name, port), ClientRedirectHandler) except socket.error as e: pass else: success = True break if success: oauth_callback = 'http://%s:%s/' % (host_name, port_number) else: print('Failed to start a local web server. Please check your firewall') print('settings and locally running programs that may be blocking or') print('using configured ports. Default ports are 8080 and 8090.') raise AuthenticationError() self.flow.redirect_uri = oauth_callback authorize_url = self.GetAuthUrl() webbrowser.open(authorize_url, new=1, autoraise=True) print('Your browser has been opened to visit:') print() print(' ' + authorize_url) print() httpd.handle_request() if 'error' in httpd.query_params: print('Authentication request was rejected') raise AuthenticationRejected('User rejected authentication') if 'code' in httpd.query_params: return httpd.query_params['code'] else: print('Failed to find "code" in the query parameters of the redirect.') print('Try command-line authentication') raise AuthenticationError('No code found in redirect') @CheckAuth def CommandLineAuth(self): """Authenticate and authorize from user by printing authentication url retrieving authentication code from command-line. :returns: str -- code returned from commandline. """ self.flow.redirect_uri = OOB_CALLBACK_URN authorize_url = self.GetAuthUrl() print('Go to the following link in your browser:') print() print(' ' + authorize_url) print() return input('Enter verification code: ').strip() @CheckServiceAuth def ServiceAuth(self): """Authenticate and authorize using P12 private key, client id and client email for a Service account. :raises: AuthError, InvalidConfigError """ if set(self.SERVICE_CONFIGS_LIST) - set(self.client_config): self.LoadServiceConfigSettings() scopes = scopes_to_string(self.settings['oauth_scope']) user_email = self.client_config.get('client_user_email') service_email = self.client_config['client_service_email'] file_path = self.client_config['client_pkcs12_file_path'] self.credentials = ServiceAccountCredentials.from_p12_keyfile( service_account_email=service_email, filename=file_path, scopes=scopes) if user_email: self.credentials = self.credentials.create_delegated(sub=user_email) def LoadCredentials(self, backend=None): """Loads credentials or create empty credentials if it doesn't exist. :param backend: target backend to save credential to. :type backend: str. :raises: InvalidConfigError """ if backend is None: backend = self.settings.get('save_credentials_backend') if backend is None: raise InvalidConfigError('Please specify credential backend') if backend == 'file': self.LoadCredentialsFile() else: raise InvalidConfigError('Unknown save_credentials_backend') def LoadCredentialsFile(self, credentials_file=None): """Loads credentials or create empty credentials if it doesn't exist. Loads credentials file from path in settings if not specified. :param credentials_file: path of credentials file to read. :type credentials_file: str. :raises: InvalidConfigError, InvalidCredentialsError """ if credentials_file is None: credentials_file = self.settings.get('save_credentials_file') if credentials_file is None: raise InvalidConfigError('Please specify credentials file to read') try: storage = Storage(credentials_file) self.credentials = storage.get() except IOError: raise InvalidCredentialsError('Credentials file cannot be symbolic link') def SaveCredentials(self, backend=None): """Saves credentials according to specified backend. If you have any specific credentials backend in mind, don't use this function and use the corresponding function you want. :param backend: backend to save credentials. :type backend: str. :raises: InvalidConfigError """ if backend is None: backend = self.settings.get('save_credentials_backend') if backend is None: raise InvalidConfigError('Please specify credential backend') if backend == 'file': self.SaveCredentialsFile() else: raise InvalidConfigError('Unknown save_credentials_backend') def SaveCredentialsFile(self, credentials_file=None): """Saves credentials to the file in JSON format. :param credentials_file: destination to save file to. :type credentials_file: str. :raises: InvalidConfigError, InvalidCredentialsError """ if self.credentials is None: raise InvalidCredentialsError('No credentials to save') if credentials_file is None: credentials_file = self.settings.get('save_credentials_file') if credentials_file is None: raise InvalidConfigError('Please specify credentials file to read') try: storage = Storage(credentials_file) storage.put(self.credentials) self.credentials.set_store(storage) except IOError: raise InvalidCredentialsError('Credentials file cannot be symbolic link') def LoadClientConfig(self, backend=None): """Loads client configuration according to specified backend. If you have any specific backend to load client configuration from in mind, don't use this function and use the corresponding function you want. :param backend: backend to load client configuration from. :type backend: str. :raises: InvalidConfigError """ if backend is None: backend = self.settings.get('client_config_backend') if backend is None: raise InvalidConfigError('Please specify client config backend') if backend == 'file': self.LoadClientConfigFile() elif backend == 'settings': self.LoadClientConfigSettings() elif backend == 'service': self.LoadServiceConfigSettings() else: raise InvalidConfigError('Unknown client_config_backend') def LoadClientConfigFile(self, client_config_file=None): """Loads client configuration file downloaded from APIs console. Loads client config file from path in settings if not specified. :param client_config_file: path of client config file to read. :type client_config_file: str. :raises: InvalidConfigError """ if client_config_file is None: client_config_file = self.settings['client_config_file'] try: client_type, client_info = clientsecrets.loadfile(client_config_file) except clientsecrets.InvalidClientSecretsError as error: raise InvalidConfigError('Invalid client secrets file %s' % error) if not client_type in (clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED): raise InvalidConfigError('Unknown client_type of client config file') # General settings. try: config_index = ['client_id', 'client_secret', 'auth_uri', 'token_uri'] for config in config_index: self.client_config[config] = client_info[config] self.client_config['revoke_uri'] = client_info.get('revoke_uri') self.client_config['redirect_uri'] = client_info['redirect_uris'][0] except KeyError: raise InvalidConfigError('Insufficient client config in file') # Service auth related fields. service_auth_config = ['client_email'] try: for config in service_auth_config: self.client_config[config] = client_info[config] except KeyError: pass # The service auth fields are not present, handling code can go here. def LoadServiceConfigSettings(self): """Loads client configuration from settings file. :raises: InvalidConfigError """ for config in self.SERVICE_CONFIGS_LIST: try: self.client_config[config] = \ self.settings['service_config'][config] except KeyError: err = "Insufficient service config in settings" err += "\n\nMissing: {} key.".format(config) raise InvalidConfigError(err) def LoadClientConfigSettings(self): """Loads client configuration from settings file. :raises: InvalidConfigError """ for config in self.CLIENT_CONFIGS_LIST: try: self.client_config[config] = self.settings['client_config'][config] except KeyError: raise InvalidConfigError('Insufficient client config in settings') def GetFlow(self): """Gets Flow object from client configuration. :raises: InvalidConfigError """ if not all(config in self.client_config \ for config in self.CLIENT_CONFIGS_LIST): self.LoadClientConfig() constructor_kwargs = { 'redirect_uri': self.client_config['redirect_uri'], 'auth_uri': self.client_config['auth_uri'], 'token_uri': self.client_config['token_uri'], } if self.client_config['revoke_uri'] is not None: constructor_kwargs['revoke_uri'] = self.client_config['revoke_uri'] self.flow = OAuth2WebServerFlow( self.client_config['client_id'], self.client_config['client_secret'], scopes_to_string(self.settings['oauth_scope']), **constructor_kwargs) if self.settings.get('get_refresh_token'): self.flow.params.update({ 'access_type': 'offline', 'approval_prompt': 'force' }) def Refresh(self): """Refreshes the access_token. :raises: RefreshError """ if self.credentials is None: raise RefreshError('No credential to refresh.') if self.credentials.refresh_token is None: raise RefreshError('No refresh_token found.' 'Please set access_type of OAuth to offline.') if self.http is None: self.http = httplib2.Http(timeout=self.http_timeout) try: self.credentials.refresh(self.http) except AccessTokenRefreshError as error: raise RefreshError('Access token refresh failed: %s' % error) def GetAuthUrl(self): """Creates authentication url where user visits to grant access. :returns: str -- Authentication url. """ if self.flow is None: self.GetFlow() return self.flow.step1_get_authorize_url() def Auth(self, code): """Authenticate, authorize, and build service. :param code: Code for authentication. :type code: str. :raises: AuthenticationError """ self.Authenticate(code) self.Authorize() def Authenticate(self, code): """Authenticates given authentication code back from user. :param code: Code for authentication. :type code: str. :raises: AuthenticationError """ if self.flow is None: self.GetFlow() try: self.credentials = self.flow.step2_exchange(code) except FlowExchangeError as e: raise AuthenticationError('OAuth2 code exchange failed: %s' % e) print('Authentication successful.') def Authorize(self): """Authorizes and builds service. :raises: AuthenticationError """ if self.http is None: self.http = httplib2.Http(timeout=self.http_timeout) if self.access_token_expired: raise AuthenticationError('No valid credentials provided to authorize') self.http = self.credentials.authorize(self.http) self.service = build('drive', 'v2', http=self.http) def Get_Http_Object(self): """Create and authorize an httplib2.Http object. Necessary for thread-safety. :return: The http object to be used in each call. :rtype: httplib2.Http """ http = httplib2.Http(timeout=self.http_timeout) http = self.credentials.authorize(http) return http pydrive-1.3.1/pydrive/drive.py0000664000175000017500000000301313003464542015135 0ustar seb128seb128from .apiattr import ApiAttributeMixin from .files import GoogleDriveFile from .files import GoogleDriveFileList from .auth import LoadAuth class GoogleDrive(ApiAttributeMixin, object): """Main Google Drive class.""" def __init__(self, auth=None): """Create an instance of GoogleDrive. :param auth: authorized GoogleAuth instance. :type auth: pydrive.auth.GoogleAuth. """ ApiAttributeMixin.__init__(self) self.auth = auth def CreateFile(self, metadata=None): """Create an instance of GoogleDriveFile with auth of this instance. This method would not upload a file to GoogleDrive. :param metadata: file resource to initialize GoogleDriveFile with. :type metadata: dict. :returns: pydrive.files.GoogleDriveFile -- initialized with auth of this instance. """ return GoogleDriveFile(auth=self.auth, metadata=metadata) def ListFile(self, param=None): """Create an instance of GoogleDriveFileList with auth of this instance. This method will not fetch from Files.List(). :param param: parameter to be sent to Files.List(). :type param: dict. :returns: pydrive.files.GoogleDriveFileList -- initialized with auth of this instance. """ return GoogleDriveFileList(auth=self.auth, param=param) @LoadAuth def GetAbout(self): """Return information about the Google Drive of the auth instance. :returns: A dictionary of Google Drive information like user, usage, quota etc. """ return self.auth.service.about().get().execute(http=self.http) pydrive-1.3.1/pydrive/apiattr.py0000664000175000017500000001142213003464542015473 0ustar seb128seb128from six import Iterator, iteritems class ApiAttribute(object): """A data descriptor that sets and returns values.""" def __init__(self, name): """Create an instance of ApiAttribute. :param name: name of this attribute. :type name: str. """ self.name = name def __get__(self, obj, type=None): """Accesses value of this attribute.""" return obj.attr.get(self.name) def __set__(self, obj, value): """Write value of this attribute.""" obj.attr[self.name] = value if obj.dirty.get(self.name) is not None: obj.dirty[self.name] = True def __del__(self, obj=None): """Delete value of this attribute.""" if not obj: return del obj.attr[self.name] if obj.dirty.get(self.name) is not None: del obj.dirty[self.name] class ApiAttributeMixin(object): """Mixin to initialize required global variables to use ApiAttribute.""" def __init__(self): self.attr = {} self.dirty = {} self.http = None # Any element may make requests and will require this # field. class ApiResource(dict): """Super class of all api resources. Inherits and behaves as a python dictionary to handle api resources. Save clean copy of metadata in self.metadata as a dictionary. Provides changed metadata elements to efficiently update api resources. """ auth = ApiAttribute('auth') def __init__(self, *args, **kwargs): """Create an instance of ApiResource.""" super(ApiResource, self).__init__() self.update(*args, **kwargs) self.metadata = dict(self) def __getitem__(self, key): """Overwritten method of dictionary. :param key: key of the query. :type key: str. :returns: value of the query. """ return dict.__getitem__(self, key) def __setitem__(self, key, val): """Overwritten method of dictionary. :param key: key of the query. :type key: str. :param val: value of the query. """ dict.__setitem__(self, key, val) def __repr__(self): """Overwritten method of dictionary.""" dict_representation = dict.__repr__(self) return '%s(%s)' % (type(self).__name__, dict_representation) def update(self, *args, **kwargs): """Overwritten method of dictionary.""" for k, v in iteritems(dict(*args, **kwargs)): self[k] = v def UpdateMetadata(self, metadata=None): """Update metadata and mark all of them to be clean.""" if metadata: self.update(metadata) self.metadata = dict(self) def GetChanges(self): """Returns changed metadata elements to update api resources efficiently. :returns: dict -- changed metadata elements. """ dirty = {} for key in self: if self.metadata.get(key) is None: dirty[key] = self[key] elif self.metadata[key] != self[key]: dirty[key] = self[key] return dirty class ApiResourceList(ApiAttributeMixin, ApiResource, Iterator): """Abstract class of all api list resources. Inherits ApiResource and builds iterator to list any API resource. """ metadata = ApiAttribute('metadata') def __init__(self, auth=None, metadata=None): """Create an instance of ApiResourceList. :param auth: authorized GoogleAuth instance. :type auth: GoogleAuth. :param metadata: parameter to send to list command. :type metadata: dict. """ ApiAttributeMixin.__init__(self) ApiResource.__init__(self) self.auth = auth self.UpdateMetadata() if metadata: self.update(metadata) def __iter__(self): """Returns iterator object. :returns: ApiResourceList -- self """ return self def __next__(self): """Make API call to list resources and return them. Auto updates 'pageToken' every time it makes API call and raises StopIteration when it reached the end of iteration. :returns: list -- list of API resources. :raises: StopIteration """ if 'pageToken' in self and self['pageToken'] is None: raise StopIteration result = self._GetList() self['pageToken'] = self.metadata.get('nextPageToken') return result def GetList(self): """Get list of API resources. If 'maxResults' is not specified, it will automatically iterate through every resources available. Otherwise, it will make API call once and update 'pageToken'. :returns: list -- list of API resources. """ if self.get('maxResults') is None: self['maxResults'] = 1000 result = [] for x in self: result.extend(x) del self['maxResults'] return result else: return next(self) def _GetList(self): """Helper function which actually makes API call. Should be overwritten. :raises: NotImplementedError """ raise NotImplementedError def Reset(self): """Resets current iteration""" if 'pageToken' in self: del self['pageToken'] pydrive-1.3.1/pydrive/files.py0000664000175000017500000004615013003464542015137 0ustar seb128seb128import io import mimetypes from apiclient import errors from apiclient.http import MediaIoBaseUpload from functools import wraps from .apiattr import ApiAttribute from .apiattr import ApiAttributeMixin from .apiattr import ApiResource from .apiattr import ApiResourceList from .auth import LoadAuth BLOCK_SIZE = 1024 # Usage: MIME_TYPE_TO_BOM['']['']. MIME_TYPE_TO_BOM = { 'application/vnd.google-apps.document': { 'text/plain': u'\ufeff'.encode('utf8') } } class FileNotUploadedError(RuntimeError): """Error trying to access metadata of file that is not uploaded.""" class ApiRequestError(IOError): """Error while making any API requests.""" class FileNotDownloadableError(RuntimeError): """Error trying to download file that is not downloadable.""" def LoadMetadata(decoratee): """Decorator to check if the file has metadata and fetches it if not. :raises: ApiRequestError, FileNotUploadedError """ @wraps(decoratee) def _decorated(self, *args, **kwargs): if not self.uploaded: self.FetchMetadata() return decoratee(self, *args, **kwargs) return _decorated class GoogleDriveFileList(ApiResourceList): """Google Drive FileList instance. Equivalent to Files.list() in Drive APIs. """ def __init__(self, auth=None, param=None): """Create an instance of GoogleDriveFileList.""" super(GoogleDriveFileList, self).__init__(auth=auth, metadata=param) @LoadAuth def _GetList(self): """Overwritten method which actually makes API call to list files. :returns: list -- list of pydrive.files.GoogleDriveFile. """ self.metadata = self.auth.service.files().list(**dict(self)).execute( http=self.http) result = [] for file_metadata in self.metadata['items']: tmp_file = GoogleDriveFile( auth=self.auth, metadata=file_metadata, uploaded=True) result.append(tmp_file) return result class GoogleDriveFile(ApiAttributeMixin, ApiResource): """Google Drive File instance. Inherits ApiResource which inherits dict. Can access and modify metadata like dictionary. """ content = ApiAttribute('content') uploaded = ApiAttribute('uploaded') metadata = ApiAttribute('metadata') def __init__(self, auth=None, metadata=None, uploaded=False): """Create an instance of GoogleDriveFile. :param auth: authorized GoogleAuth instance. :type auth: pydrive.auth.GoogleAuth :param metadata: file resource to initialize GoogleDriveFile with. :type metadata: dict. :param uploaded: True if this file is confirmed to be uploaded. :type uploaded: bool. """ ApiAttributeMixin.__init__(self) ApiResource.__init__(self) self.metadata = {} self.dirty = {'content': False} self.auth = auth self.uploaded = uploaded if uploaded: self.UpdateMetadata(metadata) elif metadata: self.update(metadata) self._ALL_FIELDS = 'alternateLink,appDataContents,' \ 'canComment,canReadRevisions,' \ 'copyable,createdDate,defaultOpenWithLink,description,' \ 'downloadUrl,editable,embedLink,etag,explicitlyTrashed,' \ 'exportLinks,fileExtension,fileSize,folderColorRgb,' \ 'fullFileExtension,headRevisionId,iconLink,id,' \ 'imageMediaMetadata,indexableText,isAppAuthorized,kind,' \ 'labels,lastModifyingUser,lastModifyingUserName,' \ 'lastViewedByMeDate,markedViewedByMeDate,md5Checksum,' \ 'mimeType,modifiedByMeDate,modifiedDate,openWithLinks,' \ 'originalFilename,ownedByMe,ownerNames,owners,parents,' \ 'permissions,properties,quotaBytesUsed,selfLink,shareable,' \ 'shared,sharedWithMeDate,sharingUser,spaces,thumbnail,' \ 'thumbnailLink,title,userPermission,version,' \ 'videoMediaMetadata,webContentLink,webViewLink,writersCanShare' self.has_bom = True def __getitem__(self, key): """Overwrites manner of accessing Files resource. If this file instance is not uploaded and id is specified, it will try to look for metadata with Files.get(). :param key: key of dictionary query. :type key: str. :returns: value of Files resource :raises: KeyError, FileNotUploadedError """ try: return dict.__getitem__(self, key) except KeyError as e: if self.uploaded: raise KeyError(e) if self.get('id'): self.FetchMetadata() return dict.__getitem__(self, key) else: raise FileNotUploadedError() def SetContentString(self, content, encoding='utf-8'): """Set content of this file to be a string. Creates io.BytesIO instance of utf-8 encoded string. Sets mimeType to be 'text/plain' if not specified. :param encoding: The encoding to use when setting the content of this file. :type encoding: str :param content: content of the file in string. :type content: str """ self.content = io.BytesIO(content.encode(encoding)) if self.get('mimeType') is None: self['mimeType'] = 'text/plain' def SetContentFile(self, filename): """Set content of this file from a file. Opens the file specified by this method. Will be read, uploaded, and closed by Upload() method. Sets metadata 'title' and 'mimeType' automatically if not specified. :param filename: name of the file to be uploaded. :type filename: str. """ self.content = open(filename, 'rb') if self.get('title') is None: self['title'] = filename if self.get('mimeType') is None: self['mimeType'] = mimetypes.guess_type(filename)[0] def GetContentString(self, mimetype=None, encoding='utf-8', remove_bom=False): """Get content of this file as a string. :param mimetype: The mimetype of the content string. :type mimetype: str :param encoding: The encoding to use when decoding the byte string. :type encoding: str :param remove_bom: Whether to strip a known BOM. :type remove_bom: bool :returns: str -- utf-8 decoded content of the file :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError """ if self.content is None or \ type(self.content) is not io.BytesIO or \ self.has_bom == remove_bom: self.FetchContent(mimetype, remove_bom) return self.content.getvalue().decode(encoding) def GetContentFile(self, filename, mimetype=None, remove_bom=False): """Save content of this file as a local file. :param filename: name of the file to write to. :type filename: str :param mimetype: mimeType of the file. :type mimetype: str :param remove_bom: Whether to remove the byte order marking. :type remove_bom: bool :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError """ if self.content is None or \ type(self.content) is not io.BytesIO or \ self.has_bom == remove_bom: self.FetchContent(mimetype, remove_bom) f = open(filename, 'wb') f.write(self.content.getvalue()) f.close() @LoadAuth def FetchMetadata(self, fields=None, fetch_all=False): """Download file's metadata from id using Files.get(). :param fields: The fields to include, as one string, each entry separated by commas, e.g. 'fields,labels'. :type fields: str :param fetch_all: Whether to fetch all fields. :type fetch_all: bool :raises: ApiRequestError, FileNotUploadedError """ file_id = self.metadata.get('id') or self.get('id') if fetch_all: fields = self._ALL_FIELDS if file_id: try: metadata = self.auth.service.files().get(fileId=file_id, fields=fields)\ .execute(http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: self.uploaded = True self.UpdateMetadata(metadata) else: raise FileNotUploadedError() @LoadMetadata def FetchContent(self, mimetype=None, remove_bom=False): """Download file's content from download_url. :raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError """ download_url = self.metadata.get('downloadUrl') export_links = self.metadata.get('exportLinks') if download_url: self.content = io.BytesIO(self._DownloadFromUrl(download_url)) self.dirty['content'] = False elif export_links and export_links.get(mimetype): self.content = io.BytesIO( self._DownloadFromUrl(export_links.get(mimetype))) self.dirty['content'] = False else: raise FileNotDownloadableError( 'No downloadLink/exportLinks for mimetype found in metadata') if mimetype == 'text/plain' and remove_bom: self._RemovePrefix(self.content, MIME_TYPE_TO_BOM[self['mimeType']][mimetype]) self.has_bom = not remove_bom def Upload(self, param=None): """Upload/update file by choosing the most efficient method. :param param: additional parameter to upload file. :type param: dict. :raises: ApiRequestError """ if self.uploaded or self.get('id') is not None: if self.dirty['content']: self._FilesUpdate(param=param) else: self._FilesPatch(param=param) else: self._FilesInsert(param=param) def Trash(self, param=None): """Move a file to the trash. :raises: ApiRequestError """ self._FilesTrash(param=param) def UnTrash(self, param=None): """Move a file out of the trash. :param param: Additional parameter to file. :type param: dict. :raises: ApiRequestError """ self._FilesUnTrash(param=param) def Delete(self, param=None): """Hard-delete a file. :param param: additional parameter to file. :type param: dict. :raises: ApiRequestError """ self._FilesDelete(param=param) def InsertPermission(self, new_permission): """Insert a new permission. Re-fetches all permissions after call. :param new_permission: The new permission to insert, please see the official Google Drive API guide on permissions.insert for details. :type new_permission: object :return: The permission object. :rtype: object """ file_id = self.metadata.get('id') or self['id'] try: permission = self.auth.service.permissions().insert( fileId=file_id, body=new_permission).execute(http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: self.GetPermissions() # Update permissions field. return permission @LoadAuth def GetPermissions(self): """Downloads all permissions from Google Drive, as this information is not downloaded by FetchMetadata by default. :return: A list of the permission objects. :rtype: object[] """ self.FetchMetadata(fields='permissions') return self.metadata.get('permissions') def DeletePermission(self, permission_id): """Deletes the permission specified by the permission_id. :param permission_id: The permission id. :type permission_id: str :return: True if it succeeds. :rtype: bool """ return self._DeletePermission(permission_id) @LoadAuth def _FilesInsert(self, param=None): """Upload a new file using Files.insert(). :param param: additional parameter to upload file. :type param: dict. :raises: ApiRequestError """ if param is None: param = {} param['body'] = self.GetChanges() try: if self.dirty['content']: param['media_body'] = self._BuildMediaBody() metadata = self.auth.service.files().insert(**param).execute( http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: self.uploaded = True self.dirty['content'] = False self.UpdateMetadata(metadata) @LoadAuth def _FilesUnTrash(self, param=None): """Un-delete (Trash) a file using Files.UnTrash(). :param param: additional parameter to file. :type param: dict. :raises: ApiRequestError """ if param is None: param = {} param['fileId'] = self.metadata.get('id') or self['id'] try: self.auth.service.files().untrash(**param).execute( http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: if self.metadata: self.metadata[u'labels'][u'trashed'] = False return True @LoadAuth def _FilesTrash(self, param=None): """Soft-delete (Trash) a file using Files.Trash(). :param param: additional parameter to file. :type param: dict. :raises: ApiRequestError """ if param is None: param = {} param['fileId'] = self.metadata.get('id') or self['id'] try: self.auth.service.files().trash(**param).execute( http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: if self.metadata: self.metadata[u'labels'][u'trashed'] = True return True @LoadAuth def _FilesDelete(self, param=None): """Delete a file using Files.Delete() (WARNING: deleting permanently deletes the file!) :param param: additional parameter to file. :type param: dict. :raises: ApiRequestError """ if param is None: param = {} param['fileId'] = self.metadata.get('id') or self['id'] try: self.auth.service.files().delete(**param).execute(http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: return True @LoadAuth @LoadMetadata def _FilesUpdate(self, param=None): """Update metadata and/or content using Files.Update(). :param param: additional parameter to upload file. :type param: dict. :raises: ApiRequestError, FileNotUploadedError """ if param is None: param = {} param['body'] = self.GetChanges() param['fileId'] = self.metadata.get('id') try: if self.dirty['content']: param['media_body'] = self._BuildMediaBody() metadata = self.auth.service.files().update(**param).execute( http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: self.uploaded = True self.dirty['content'] = False self.UpdateMetadata(metadata) @LoadAuth @LoadMetadata def _FilesPatch(self, param=None): """Update metadata using Files.Patch(). :param param: additional parameter to upload file. :type param: dict. :raises: ApiRequestError, FileNotUploadedError """ if param is None: param = {} param['body'] = self.GetChanges() param['fileId'] = self.metadata.get('id') try: metadata = self.auth.service.files().patch(**param).execute( http=self.http) except errors.HttpError as error: raise ApiRequestError(error) else: self.UpdateMetadata(metadata) def _BuildMediaBody(self): """Build MediaIoBaseUpload to get prepared to upload content of the file. Sets mimeType as 'application/octet-stream' if not specified. :returns: MediaIoBaseUpload -- instance that will be used to upload content. """ if self.get('mimeType') is None: self['mimeType'] = 'application/octet-stream' return MediaIoBaseUpload(self.content, self['mimeType'], resumable=True) @LoadAuth def _DownloadFromUrl(self, url): """Download file from url using provided credential. :param url: link of the file to download. :type url: str. :returns: str -- content of downloaded file in string. :raises: ApiRequestError """ resp, content = self.http.request(url) if resp.status != 200: raise ApiRequestError('Cannot download file: %s' % resp) return content @LoadAuth def _DeletePermission(self, permission_id): """Deletes the permission remotely, and from the file object itself. :param permission_id: The ID of the permission. :type permission_id: str :return: The permission :rtype: object """ file_id = self.metadata.get('id') or self['id'] try: self.auth.service.permissions().delete( fileId=file_id, permissionId=permission_id).execute() except errors.HttpError as error: raise ApiRequestError(error) else: if 'permissions' in self and 'permissions' in self.metadata: permissions = self['permissions'] is_not_current_permission = lambda per: per['id'] == permission_id permissions = filter(is_not_current_permission, permissions) self['permissions'] = permissions self.metadata['permissions'] = permissions return True @staticmethod def _RemovePrefix(file_object, prefix, block_size=BLOCK_SIZE): """Deletes passed prefix by shifting content of passed file object by to the left. Operation is in-place. Args: file_object (obj): The file object to manipulate. prefix (str): The prefix to insert. block_size (int): The size of the blocks which are moved one at a time. """ prefix_length = len(prefix) # Detect if prefix exists in file. content_start = file_object.read(prefix_length) if content_start == prefix: # Shift content left by prefix length, by copying 1KiB at a time. block_to_write = file_object.read(block_size) current_block_length = len(block_to_write) # Read and write location in separate variables for simplicity. read_location = prefix_length + current_block_length write_location = 0 while current_block_length > 0: # Write next block. file_object.seek(write_location) file_object.write(block_to_write) # Set write location to the next block. write_location += len(block_to_write) # Read next block of input. file_object.seek(read_location) block_to_write = file_object.read(block_size) # Update the current block length and read_location. current_block_length = len(block_to_write) read_location += current_block_length # Truncate the file to its, now shorter, length. file_object.truncate(read_location - prefix_length) @staticmethod def _InsertPrefix(file_object, prefix, block_size=BLOCK_SIZE): """Inserts the passed prefix in the beginning of the file, operation is in-place. Args: file_object (obj): The file object to manipulate. prefix (str): The prefix to insert. """ # Read the first two blocks. first_block = file_object.read(block_size) second_block = file_object.read(block_size) # Pointer to the first byte of the next block to be read. read_location = block_size * 2 # Write BOM. file_object.seek(0) file_object.write(prefix) # {read|write}_location separated for readability. write_location = len(prefix) # Write and read block alternatingly. while len(first_block): # Write first block. file_object.seek(write_location) file_object.write(first_block) # Increment write_location. write_location += block_size # Move second block into first variable. first_block = second_block # Read in the next block. file_object.seek(read_location) second_block = file_object.read(block_size) # Increment read_location. read_location += block_size pydrive-1.3.1/CHANGES0000664000175000017500000000005113003464542012762 0ustar seb128seb128v1.0.0, Aug 16, 2013 -- Initial release. pydrive-1.3.1/examples/0000775000175000017500000000000013003464542013611 5ustar seb128seb128pydrive-1.3.1/examples/strip_bom_example.py0000664000175000017500000000347013003464542017700 0ustar seb128seb128from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive # Authenticate the client. gauth = GoogleAuth() gauth.LocalWebserverAuth() drive = GoogleDrive(gauth) # Create a file, set content, and upload. file1 = drive.CreateFile() original_file_content = 'Generic, non-exhaustive\n ASCII test string.' file1.SetContentString(original_file_content) # {'convert': True} triggers conversion to a Google Drive document. file1.Upload({'convert': True}) # Download the file. file2 = drive.CreateFile({'id': file1['id']}) # Print content before download. print('Original text:') print(bytes(original_file_content.encode('unicode-escape'))) print('Number of chars: %d' % len(original_file_content)) print('') # Original text: # Generic, non-exhaustive\n ASCII test string. # Number of chars: 43 # Download document as text file WITH the BOM and print the contents. content_with_bom = file2.GetContentString(mimetype='text/plain') print('Content with BOM:') print(bytes(content_with_bom.encode('unicode-escape'))) print('Number of chars: %d' % len(content_with_bom)) print('') # Content with BOM: # \ufeffGeneric, non-exhaustive\r\n ASCII test string. # Number of chars: 45 # Download document as text file WITHOUT the BOM and print the contents. content_without_bom = file2.GetContentString(mimetype='text/plain', remove_bom=True) print('Content without BOM:') print(bytes(content_without_bom.encode('unicode-escape'))) print('Number of chars: %d' % len(content_without_bom)) print('') # Content without BOM: # Generic, non-exhaustive\r\n ASCII test string. # Number of chars: 44 # *NOTE*: When downloading a Google Drive document as text file, line-endings # are converted to the Windows-style: \r\n. # Delete the file as necessary. file1.Delete() pydrive-1.3.1/examples/using_folders.py0000664000175000017500000000142113003464542017024 0ustar seb128seb128from pydrive.auth import GoogleAuth from pydrive.drive import GoogleDrive gauth = GoogleAuth() gauth.LocalWebserverAuth() drive = GoogleDrive(gauth) # Create folder. folder_metadata = { 'title' : '', # The mimetype defines this new file as a folder, so don't change this. 'mimeType' : 'application/vnd.google-apps.folder' } folder = drive.CreateFile(folder_metadata) folder.Upload() # Get folder info and print to screen. folder_title = folder['title'] folder_id = folder['id'] print('title: %s, id: %s' % (folder_title, folder_id)) # Upload file to folder. f = drive.CreateFile({"parents": [{"kind": "drive#fileLink", "id": folder_id}]}) # Make sure to add the path to the file to upload below. f.SetContentFile('') f.Upload() pydrive-1.3.1/docs/0000775000175000017500000000000013636106533012730 5ustar seb128seb128pydrive-1.3.1/docs/filemanagement.rst0000664000175000017500000002257113003464542016440 0ustar seb128seb128File management made easy ========================= There are many methods to create and update file metadata and contents. With *PyDrive*, you don't have to care about any of these different API methods. Manipulate file metadata and contents from `GoogleDriveFile`_ object and call `Upload()`_. *PyDrive* will make the optimal API call for you. Upload a new file ----------------- Here is a sample code to upload a file. ``gauth`` is an authenticated `GoogleAuth`_ object. .. code-block:: python from pydrive.drive import GoogleDrive # Create GoogleDrive instance with authenticated GoogleAuth instance. drive = GoogleDrive(gauth) # Create GoogleDriveFile instance with title 'Hello.txt'. file1 = drive.CreateFile({'title': 'Hello.txt'}) file1.Upload() # Upload the file. print('title: %s, id: %s' % (file1['title'], file1['id'])) # title: Hello.txt, id: {{FILE_ID}} Now, you will have a file 'Hello.txt' uploaded to your Google Drive. You can open it from web interface to check its content, 'Hello World!'. Note that `CreateFile()`_ will create `GoogleDriveFile`_ instance but not actually upload a file to Google Drive. You can initialize `GoogleDriveFile`_ object by itself. However, it is not recommended to do so in order to keep authentication consistent. Delete, Trash and un-Trash files -------------------------------- You may want to delete, trash, or un-trash a file. To do this use ``Delete()``, ``Trash()`` or ``UnTrash()`` on a GoogleDriveFile object. *Note:* ``Trash()`` *moves a file into the trash and can be recovered,* ``Delete()`` *deletes the file permanently and immediately.* .. code-block:: python # Create GoogleDriveFile instance and upload it. file1 = drive.CreateFile() file1.Upload() file1.Trash() # Move file to trash. file1.UnTrash() # Move file out of trash. file1.Delete() # Permanently delete the file. Update file metadata -------------------- You can manipulate file metadata from a `GoogleDriveFile`_ object just as you manipulate a ``dict``. The format of file metadata can be found in the Google Drive API documentation: `Files resource`_. Sample code continues from `Upload a new file`_: .. code-block:: python file1['title'] = 'HelloWorld.txt' # Change title of the file. file1.Upload() # Update metadata. print('title: %s' % file1['title']) # title: HelloWorld.txt. Now, the title of your file has changed to 'HelloWorld.txt'. Download file metadata from file ID ----------------------------------- You might want to get file metadata from file ID. In that case, just initialize `GoogleDriveFile`_ with file ID and access metadata from `GoogleDriveFile`_ just as you access ``dict``. Sample code continues from above: .. code-block:: python # Create GoogleDriveFile instance with file id of file1. file2 = drive.CreateFile({'id': file1['id']}) print('title: %s, mimeType: %s' % (file2['title'], file2['mimeType'])) # title: HelloWorld.txt, mimeType: text/plain Handling special metadata ------------------------- Not all metadata can be set with the methods described above. PyDrive gives you access to the metadata of an object through ``file_object.FetchMetadata()``. This function has two optional parameters: ``fields`` and ``fetch_all``. .. code-block:: python file1 = drive.CreateFile({'id': ''}) # Fetches all basic metadata fields, including file size, last modified etc. file1.FetchMetadata() # Fetches all metadata available. file1.FetchMetadata(fetch_all=True) # Fetches the 'permissions' metadata field. file1.FetchMetadata(fields='permissions') # You can update a list of specific fields like this: file1.FetchMetadata(fields='permissions,labels,mimeType') For more information on available metadata fields have a look at the `official documentation`_. Insert permissions __________________ Insert, retrieving or deleting permissions is illustrated by making a file readable to all who have a link to the file. .. code-block:: python file1 = drive.CreateFile() file1.Upload() # Insert the permission. permission = file1.InsertPermission({ 'type': 'anyone', 'value': 'anyone', 'role': 'reader'}) print(file1['alternateLink']) # Display the sharable link. Note: ``InsertPermission()`` calls ``GetPermissions()`` after successfully inserting the permission. You can find more information on the permitted fields of a permission `here `_. This file is now shared and anyone with the link can view it. But what if you want to check whether a file is already shared? List permissions ________________ Permissions can be fetched using the ``GetPermissions()`` function of a ``GoogleDriveFile``, and can be used like so: .. code-block:: python # Create a new file file1 = drive.CreateFile() # Fetch permissions. permissions = file1.GetPermissions() print(permissions) # The permissions are also available as file1['permissions']: print(file1['permissions']) For the more advanced user: ``GetPermissions()`` is a shorthand for: .. code-block:: python # Fetch Metadata, including the permissions field. file1.FetchMetadata(fields='permissions') # The permissions array is now available for further use. print(file1['permissions']) Remove a Permission ___________________ *PyDrive* allows you to remove a specific permission using the ``DeletePermission(permission_id)`` function. This function allows you to delete one permission at a time by providing the permission's ID. .. code-block:: python file1 = drive.CreateFile({'id': ''}) permissions = file1.GetPermissions() # Download file permissions. permission_id = permissions[1]['id'] # Get a permission ID. file1.DeletePermission(permission_id) # Delete the permission. Upload and update file content ------------------------------ Managing file content is as easy as managing file metadata. You can set file content with either `SetContentFile(filename)`_ or `SetContentString(content)`_ and call `Upload()`_ just as you did to upload or update file metadata. Sample code continues from `Download file metadata from file ID`_: .. code-block:: python file4 = drive.CreateFile({'title':'appdata.json', 'mimeType':'application/json'}) file4.SetContentString('{"firstname": "John", "lastname": "Smith"}') file4.Upload() # Upload file. file4.SetContentString('{"firstname": "Claudio", "lastname": "Afshar"}') file4.Upload() # Update content of the file. file5 = drive.CreateFile() # Read file and set it as a content of this instance. file5.SetContentFile('cat.png') file5.Upload() # Upload the file. print('title: %s, mimeType: %s' % (file5['title'], file5['mimeType'])) # title: cat.png, mimeType: image/png **Advanced Users:** If you call SetContentFile and GetContentFile you can can define which character encoding is to be used by using the optional parameter `encoding`. If you, for example, are retrieving a file which is stored on your Google Drive which is encoded with ISO-8859-1, then you can get the content string like so: .. code-block:: python content_string = file4.GetContentString(encoding='ISO-8859-1') Download file content --------------------- Just as you uploaded file content, you can download it using `GetContentFile(filename)`_ or `GetContentString()`_. Sample code continues from above: .. code-block:: python # Initialize GoogleDriveFile instance with file id. file6 = drive.CreateFile({'id': file5['id']}) file6.GetContentFile('catlove.png') # Download file as 'catlove.png'. # Initialize GoogleDriveFile instance with file id. file7 = drive.CreateFile({'id': file4['id']}) content = file7.GetContentString() # content: '{"firstname": "Claudio", "lastname": "Afshar"}' file7.SetContentString(content.replace('lastname', 'familyname')) file7.Upload() # Uploaded content: '{"firstname": "Claudio", "familyname": "Afshar"}' **Advanced users**: Google Drive is `known`_ to add BOM (Byte Order Marks) to the beginning of some files, such as Google Documents downloaded as text files. In some cases confuses parsers and leads to corrupt files. PyDrive can remove the BOM from the beginning of a file when it is downloaded. Just set the `remove_bom` parameter in `GetContentString()` or `GetContentFile()` - see `examples/strip_bom_example.py` in the GitHub repository for an example. .. _`GoogleDriveFile`: ./pydrive.html#pydrive.files.GoogleDriveFile .. _`Upload()`: ./pydrive.html#pydrive.files.GoogleDriveFile.Upload .. _`GoogleAuth`: ./pydrive.html#pydrive.auth.GoogleAuth .. _`CreateFile()`: ./pydrive.html#pydrive.drive.GoogleDrive.CreateFile .. _`Files resource`: https://developers.google.com/drive/v2/reference/files#resource-representations .. _`SetContentFile(filename)`: ./pydrive.html#pydrive.files.GoogleDriveFile.SetContentFile .. _`SetContentString(content)`: ./pydrive.html#pydrive.files.GoogleDriveFile.SetContentString .. _`GetContentFile(filename)`: ./pydrive.html#pydrive.files.GoogleDriveFile.GetContentFile .. _`GetContentString()`: ./pydrive.html#pydrive.files.GoogleDriveFile.GetContentString .. _`official documentation`: https://developers.google.com/drive/v2/reference/files#resource-representations .. _`known`: https://productforums.google.com/forum/#!topic/docs/BJLimQDGtjQpydrive-1.3.1/docs/README0000664000175000017500000000123313003464542013602 0ustar seb128seb128This document outlines how to rebuild the documentation. General setup: - Install Sphinx, `pip install sphinx` or `apt-get install python-sphinx` - Install the theme used for the docs: `pip install sphinx_rtd_theme` - Run `python setup.py build_sphinx --source-dir=docs/ --build-dir=docs/build --all-files` If code files were added, the easiest way to reflect code changes in the documentation by referencing the file from within pydrive.rst. If a non-code related file was added (it has to have the ".rst" ending), then add the file name to the list of names under "Table of Contents" in index.rst. Make sure to add the file name excluding the ".rst" file ending.pydrive-1.3.1/docs/filelist.rst0000664000175000017500000000355113003464542015274 0ustar seb128seb128File listing made easy ============================= *PyDrive* handles paginations and parses response as list of `GoogleDriveFile`_. Get all files which matches the query ------------------------------------- Create `GoogleDriveFileList`_ instance with `parameters of Files.list()`_ as ``dict``. Call `GetList()`_ and you will get all files that matches your query as a list of `GoogleDriveFile`_. .. code-block:: python from pydrive.drive import GoogleDrive drive = GoogleDrive(gauth) # Create GoogleDrive instance with authenticated GoogleAuth instance # Auto-iterate through all files in the root folder. file_list = drive.ListFile({'q': "'root' in parents and trashed=false"}).GetList() for file1 in file_list: print('title: %s, id: %s' % (file1['title'], file1['id'])) You can update metadata or content of these `GoogleDriveFile`_ instances if you need it. Paginate and iterate through files ---------------------------------- *PyDrive* provides Pythonic way of paginating and iterating through list of files. All you have to do is to limit number of results with ``maxResults`` parameter and build ``for`` loop retrieving file list from API each iteration. Sample code continues from above: .. code-block:: python # Paginate file lists by specifying number of max results for file_list in drive.ListFile({'q': 'trashed=true', 'maxResults': 10}): print('Received %s files from Files.list()' % len(file_list)) # <= 10 for file1 in file_list: print('title: %s, id: %s' % (file1['title'], file1['id'])) .. _`GoogleDriveFile`: ./pydrive.html#pydrive.files.GoogleDriveFile .. _`GoogleDriveFileList`: ./pydrive.html#pydrive.files.GoogleDriveFileList .. _`parameters of Files.list()`: https://developers.google.com/drive/v2/reference/files/list#request .. _`GetList()`: ./pydrive.html#pydrive.apiattr.ApiResourceList.GetList pydrive-1.3.1/docs/pydrive.rst0000664000175000017500000000127313003464542015142 0ustar seb128seb128pydrive package =============== pydrive.apiattr module ---------------------- .. automodule:: pydrive.apiattr :members: :undoc-members: :show-inheritance: pydrive.auth module ------------------- .. automodule:: pydrive.auth :members: :undoc-members: :show-inheritance: pydrive.drive module -------------------- .. automodule:: pydrive.drive :members: :undoc-members: :show-inheritance: pydrive.files module -------------------- .. automodule:: pydrive.files :members: :undoc-members: :show-inheritance: pydrive.settings module ----------------------- .. automodule:: pydrive.settings :members: :undoc-members: :show-inheritance: pydrive-1.3.1/docs/oauth.rst0000664000175000017500000001471113003464542014601 0ustar seb128seb128OAuth made easy =============== Authentication in two lines --------------------------- OAuth2.0 is complex and difficult to start with. To make it more simple, *PyDrive* makes all authentication into just two lines. .. code-block:: python from pydrive.auth import GoogleAuth gauth = GoogleAuth() # Create local webserver and auto handles authentication. gauth.LocalWebserverAuth() # Or use the CommandLineAuth(), which provides you with a link to paste # into your browser. The site it leads to then provides you with an # authentication token which you paste into the command line. # Commented out as it is an alternative to the LocalWebserverAuth() above, # and someone will just copy-paste the entire thing into their editor. # gauth.CommandLineAuth() To make this code work, you need to download the application configurations file from APIs Console. Take a look at quickstart_ for detailed instructions. `LocalWebserverAuth()`_ is a built-in method of `GoogleAuth`_ which sets up local webserver to automatically receive authentication code from user and authorizes by itself. You can also use `CommandLineAuth()`_ which manually takes code from user at command line. .. _quickstart: ./quickstart.html#authentication .. _`LocalWebserverAuth()`: ./pydrive.html#pydrive.auth.GoogleAuth.LocalWebserverAuth .. _`GoogleAuth`: ./pydrive.html#pydrive.auth.GoogleAuth .. _`CommandLineAuth()`: ./pydrive.html#pydrive.auth.GoogleAuth.CommandLineAuth Automatic and custom authentication with *settings.yaml* -------------------------------------------------------- Read this section if you need a custom authentication flow, **such as silent authentication on a remote machine**. For an example of such a setup have a look at `Sample settings.yaml`_. OAuth is complicated and it requires a lot of settings. By default, when you don't provide any settings, *PyDrive* will automatically set default values which works for most of the cases. Here are some default settings. - Read client configuration from file *client_secrets.json* - OAuth scope: :code:`https://www.googleapis.com/auth/drive` - Don't save credentials - Don't retrieve refresh token However, you might want to customize these settings while maintaining two lines of clean code. If that is the case, you can make *settings.yaml* file in your working directory and *PyDrive* will read it to customize authentication behavior. These are all the possible fields of a *settings.yaml* file: .. code-block:: python client_config_backend: {{str}} client_config_file: {{str}} client_config: client_id: {{str}} client_secret: {{str}} auth_uri: {{str}} token_uri: {{str}} redirect_uri: {{str}} revoke_uri: {{str}} save_credentials: {{bool}} save_credentials_backend: {{str}} save_credentials_file: {{str}} get_refresh_token: {{bool}} oauth_scope: {{list of str}} Fields explained: :client_config_backend (str): From where to read client configuration(API application settings such as client_id and client_secrets) from. Valid values are 'file' and 'settings'. **Default**: 'file'. **Required**: No. :client_config_file (str): When *client_config_backend* is 'file', path to the file containing client configuration. **Default**: 'client_secrets.json'. **Required**: No. :client_config (dict): Place holding dictionary for client configuration when *client_config_backend* is 'settings'. **Required**: Yes, only if *client_config_backend* is 'settings' :client_config['client_id'] (str): Client ID of the application. **Required**: Yes, only if *client_config_backend* is 'settings' :client_config['client_secret'] (str): Client secret of the application. **Required**: Yes, only if *client_config_backend* is 'settings' :client_config['auth_uri'] (str): The authorization server endpoint URI. **Default**: 'https://accounts.google.com/o/oauth2/auth'. **Required**: No. :client_config['token_uri'] (str): The token server endpoint URI. **Default**: 'https://accounts.google.com/o/oauth2/token'. **Required**: No. :client_config['redirect_uri'] (str): Redirection endpoint URI. **Default**: 'urn:ietf:wg:oauth:2.0:oob'. **Required**: No. :client_config['revoke_uri'] (str): Revoke endpoint URI. **Default**: None. **Required**: No. :save_credentials (bool): True if you want to save credentials. **Default**: False. **Required**: No. :save_credentials_backend (str): Backend to save credentials to. 'file' is the only valid value for now. **Default**: 'file'. **Required**: No. :save_credentials_file (str): Destination of credentials file. **Required**: Yes, only if *save_credentials_backend* is 'file'. :get_refresh_token (bool): True if you want to retrieve refresh token along with access token. **Default**: False. **Required**: No. :oauth_scope (list of str): OAuth scope to authenticate. **Default**: ['https://www.googleapis.com/auth/drive']. **Required**: No. Sample *settings.yaml* ______________________ :: client_config_backend: settings client_config: client_id: 9637341109347.apps.googleusercontent.com client_secret: psDskOoWr1P602PXRTHi save_credentials: True save_credentials_backend: file save_credentials_file: credentials.json get_refresh_token: True oauth_scope: - https://www.googleapis.com/auth/drive.file - https://www.googleapis.com/auth/drive.install Building your own authentication flow ------------------------------------- You might want to build your own authentication flow. For example, you might want to integrate your existing website with Drive API. In that case, you can customize authentication flow as follwing: 1. Get authentication Url from `GetAuthUrl()`_. 2. Ask users to visit the authentication Url and grant access to your application. Retrieve authentication code manually by user or automatically by building your own oauth2callback. 3. Call `Auth(code)`_ with the authentication code you retrieved from step 2. Your *settings.yaml* will work for your customized authentication flow, too. Here is a sample code for your customized authentication flow .. code-block:: python from pydrive.auth import GoogleAuth gauth = GoogleAuth() auth_url = gauth.GetAuthUrl() # Create authentication url user needs to visit code = AskUserToVisitLinkAndGiveCode(auth_url) # Your customized authentication flow gauth.Auth(code) # Authorize and build service from the code .. _`GetAuthUrl()`: ./pydrive.html#pydrive.auth.GoogleAuth.GetAuthUrl .. _`Auth(code)`: ./pydrive.html#pydrive.auth.GoogleAuth.Auth pydrive-1.3.1/docs/quickstart.rst0000664000175000017500000000664613003464542015663 0ustar seb128seb128Quickstart ============================= Authentication -------------- Drive API requires OAuth2.0 for authentication. *PyDrive* makes your life much easier by handling complex authentication steps for you. 1. Go to `APIs Console`_ and make your own project. 2. Search for 'Google Drive API', select the entry, and click 'Enable'. 3. Select 'Credentials' from the left menu, click 'Create Credentials', select 'OAuth client ID'. 4. Now, the product name and consent screen need to be set -> click 'Configure consent screen' and follow the instructions. Once finished: a. Select 'Application type' to be *Web application*. b. Enter an appropriate name. c. Input *http://localhost:8080* for 'Authorized JavaScript origins'. d. Input *http://localhost:8080/* for 'Authorized redirect URIs'. e. Click 'Create'. 5. Click 'Download JSON' on the right side of Client ID to download **client_secret_.json**. The downloaded file has all authentication information of your application. **Rename the file to "client_secrets.json" and place it in your working directory.** Create *quickstart.py* file and copy and paste the following code. .. code-block:: python from pydrive.auth import GoogleAuth gauth = GoogleAuth() gauth.LocalWebserverAuth() # Creates local webserver and auto handles authentication. Run this code with *python quickstart.py* and you will see a web browser asking you for authentication. Click *Accept* and you are done with authentication. For more details, take a look at documentation: `OAuth made easy`_ .. _`APIs Console`: https://console.developers.google.com/iam-admin/projects .. _`OAuth made easy`: ./oauth.html Creating and updating file -------------------------- There are many methods to create and update file metadata and contents. With *PyDrive*, all you have to know is `Upload()`_ method which makes optimal API call for you. Add the following code to your *quickstart.py* and run it. .. code-block:: python from pydrive.drive import GoogleDrive drive = GoogleDrive(gauth) file1 = drive.CreateFile({'title': 'Hello.txt'}) # Create GoogleDriveFile instance with title 'Hello.txt'. file1.SetContentString('Hello World!') # Set content of the file from given string. file1.Upload() This code will create a new file with title *Hello.txt* and its content *Hello World!*. You can see and open this file from `Google Drive`_ if you want. For more details, take a look at documentation: `File management made easy`_ .. _`Upload()`: ./pydrive.html#pydrive.files.GoogleDriveFile.Upload .. _`Google Drive`: https://drive.google.com .. _`File management made easy`: ./filemanagement.html Getting list of files --------------------- *PyDrive* handles paginations and parses response as list of `GoogleDriveFile`_. Let's get title and id of all the files in the root folder of Google Drive. Again, add the following code to *quickstart.py* and execute it. .. code-block:: python # Auto-iterate through all files that matches this query file_list = drive.ListFile({'q': "'root' in parents and trashed=false"}).GetList() for file1 in file_list: print('title: %s, id: %s' % (file1['title'], file1['id'])) You will see title and id of all the files and folders in root folder of your Google Drive. For more details, take a look at documentation: `File listing made easy`_ .. _`GoogleDriveFile`: ./pydrive.html#pydrive.files.GoogleDriveFile .. _`File listing made easy`: ./filelist.html pydrive-1.3.1/docs/conf.py0000664000175000017500000002247213003464542014231 0ustar seb128seb128# -*- coding: utf-8 -*- # # PyDrive documentation build configuration file, created by # sphinx-quickstart on Sun Jun 12 23:01:40 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../')) #exclude_patterns = ['_build', '**tests**', '**spi**'] exclude_dirnames = ["test"] # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'PyDrive' copyright = u'2016, JunYoung Gwak, Scott Blevins, Robin Nabel, Google Inc.' author = u'JunYoung Gwak, Scott Blevins, Robin Nabel' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = u'1.3.1' # The full version, including alpha/beta/rc tags. release = u'1.3.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', 'pydrive/test/*', 'test/*', 'pydrive/test', '../pydrive/test'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'PyDrivedoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'PyDrive.tex', u'PyDrive Documentation', u'JunYoung Gwak, Scott Blevins, Robin Nabel', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'pydrive', u'PyDrive Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'PyDrive', u'PyDrive Documentation', author, 'PyDrive', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False pydrive-1.3.1/docs/index.rst0000664000175000017500000000241613003464542014567 0ustar seb128seb128.. PyDrive documentation master file, created by sphinx-quickstart on Sun Jun 12 23:01:40 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to PyDrive's documentation! =================================== PyDrive is a wrapper library of `google-api-python-client`_ that simplifies many common Google Drive API tasks. Project Info ============ - Homepage: `https://pypi.python.org/pypi/PyDrive `_ - Documentation: `Official documentation on GitHub pages `_ - Github: `https://github.com/googledrive/PyDrive `_ How to install ============== You can install PyDrive with regular ``pip`` command. :: $ pip install PyDrive To install the current development version from GitHub, use: :: $ pip install git+https://github.com/googledrive/PyDrive.git#egg=PyDrive Table of Contents ================= .. toctree:: :maxdepth: 2 quickstart oauth filemanagement filelist pydrive .. _`google-api-python-client`: https://github.com/google/google-api-python-client Indices and tables ================== * :ref:`genindex` * :ref:`search` pydrive-1.3.1/setup.py0000664000175000017500000000106213003464542013504 0ustar seb128seb128from setuptools import setup setup( name='PyDrive', version='1.3.1', author='JunYoung Gwak', author_email='jgwak@dreamylab.com', maintainer='Robin Nabel', maintainer_email='rnabel@ucdavis.edu', packages=['pydrive', 'pydrive.test'], url='https://github.com/googledrive/PyDrive', license='LICENSE', description='Google Drive API made easy.', long_description=open('README.rst').read(), install_requires=[ "google-api-python-client >= 1.2", "oauth2client >= 4.0.0", "PyYAML >= 3.0", ], ) pydrive-1.3.1/tox.ini0000664000175000017500000000033513003464542013307 0ustar seb128seb128[tox] envlist = py26, py27, py33, py34 [testenv] changedir = {toxinidir}/pydrive/test deps = pytest httplib2 PyYAML git+https://github.com/google/google-api-python-client.git commands = py.test -v -s