-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgdoc.py
More file actions
267 lines (214 loc) · 7.89 KB
/
gdoc.py
File metadata and controls
267 lines (214 loc) · 7.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# Google Docs Interface for use in gdocsfs
# Author: Pranav Kumar <[email protected]>
#
# Base64 encoding is used for content - Docs doesn't play well with escape chars
#
# We need both the Docs and Drive APIs - Docs for creating/editing files and
# Drive for deleting them. As a result, we need two credential files and
# service objects.
from __future__ import print_function
import base64
import pickle
import re
import os.path
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
# If modifying these scopes, delete the pickled token files
SCOPES = ['https://www.googleapis.com/auth/drive']
# Credentials for docs and drive
CRED_DOCS = 'cred_docs.pickle'
CRED_DRIVE = 'cred_drive.pickle'
# Tokens for docs and drive
TOKEN_DOCS = 'token_docs.pickle'
TOKEN_DRIVE = 'token_drive.pickle'
# The ID of a sample document called Blank
DOCUMENT_ID = '1dRvSqJzTekzn_rY__8cnQ7B6w8QNnz94pmnsH8Ml3Iw'
# The service objects for Docs and Drive
SERVICE = None
DRIVE_SERVICE = None
# Docs is 1-indexed... have to add 1 to all write indices
WRITE_OFFSET = 1
###############
### HELPERS ###
###############
# byte array -> string representation
def bytes_to_string(byte_seq):
result = ''
for b in byte_seq:
result += f'{str(b)},'
return result
# string rep -> byte array
def string_to_bytes(string):
split = string.split(',')
return bytes(int(s) for s in split if s.isdigit())
# decoded_bytes = base64.b64decode(b64)
# decoded_str = str(decoded_bytes, 'utf-8')
# return decoded_str
######################
### INITIALIZATION ###
######################
def init_service_docs():
"""Initializes Docs API Service"""
global SERVICE
creds = None
# The file token_docs.pickle stores the user's access and refresh tokens,
# and is created automatically when the authorization flow completes for the
# first time.
if os.path.exists(TOKEN_DOCS):
with open(TOKEN_DOCS, 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CRED_DOCS, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(TOKEN_DOCS, 'wb') as token:
pickle.dump(creds, token)
SERVICE = build('docs', 'v1', credentials=creds)
def init_service_drive():
"""Initializes Drive API Service"""
global DRIVE_SERVICE
creds = None
# The file token_drive.pickle stores the user's access and refresh tokens,
# and is created automatically when the authorization flow completes for the
# first time.
if os.path.exists(TOKEN_DRIVE):
with open(TOKEN_DRIVE, 'rb') as token:
creds = pickle.load(token)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CRED_DRIVE, SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open(TOKEN_DRIVE, 'wb') as token:
pickle.dump(creds, token)
DRIVE_SERVICE = build('drive', 'v3', credentials=creds)
# Initialize services
def initialize():
init_service_docs()
init_service_drive()
###########################
### READING AND WRITING ###
###########################
# Parses one paragraph element and gets all text
# Adapted from: https://developers.google.com/docs/api/samples/extract-text
def read_paragraph_element(element):
"""Returns the text in the given ParagraphElement.
Args:
element: a ParagraphElement from a Google Doc.
"""
text_run = element.get('textRun')
if not text_run:
return ''
return text_run.get('content')
# Parses document and reads num_bytes bytes starting at offset
# Adapted from: https://developers.google.com/docs/api/samples/extract-text
# Returns tuple: (byte array representing content, length of document content)
def read_strucutural_elements(elements, offset, num_bytes=None):
# There should only be one paragraph
assert(len(elements) == 1)
elem = elements[0]
paras = elem.get('paragraph')
elems = paras.get('elements')
# There should only be one element per paragraph
assert(len(elems) == 1)
text = read_paragraph_element(elems[0])
total_content_len = len(text)
# Get rid of newlines
text = re.sub('[\n]', '', text)
# Convert to array of bytes
byte_seq = string_to_bytes(text)
assert(text == bytes_to_string(byte_seq))
if num_bytes:
return byte_seq[offset : offset + num_bytes], total_content_len
else:
return byte_seq, total_content_len
# Args: doc id (string), offset (int), num_bytes (int/None)
#
# Reads num_bytes bytes of doc starting from offset
# (If num_bytes is None, reads the whole file by default)
#
# Returns tuple: (contents as byte array, length of content in doc)
def read_doc(doc_id, offset, num_bytes=None):
global SERVICE
# Get document
document = SERVICE.documents().get(documentId=doc_id).execute()
# Read all elements
elements = document.get('body').get('content')
# The first element is always a null element which doesn't matter
elements = elements[1:]
# contents: byte array
contents, total_len = read_strucutural_elements(elements, offset, num_bytes)
# Get the sequence of bytes by splitting on the comma
return contents, total_len
# Args: doc id (string), offset (int), content to write (byte-string)
#
# Writes given content to specified doc at given offset
def write_doc(doc_id, offset, content):
global SERVICE
# Since we store in base64, offset must be mapped too. Most straightforward
# way to do this is to read the doc, insert the string at the offset, then
# write it back in
# current_contents: byte array
current_contents, total_content_len = read_doc(doc_id, 0, None)
# Insert the contents at offset in current contents
new_contents = current_contents[:offset] + content +\
current_contents[offset:]
# Clear all existing contents
delete = {
'deleteContentRange': {
'range': {
'startIndex': WRITE_OFFSET,
'endIndex': total_content_len
}
}
}
# Encode new contents to string form
byte_str = bytes_to_string(new_contents)
# No empty bytes
assert(',,' not in byte_str)
# Insert new contents
insert = {
'insertText': {
'location': {
'index': WRITE_OFFSET,
},
'text': byte_str
}
}
# Don't delete anything if the file is blank
requests = [ delete ] if total_content_len > 1 else []
if byte_str: requests.append(insert)
# If any deletion or insertion needs to be performed
if requests:
result = SERVICE.documents().batchUpdate(
documentId=doc_id, body={'requests': requests}).execute()
#############################
### CREATION AND DELETION ###
#############################
# Creates doc with given title and contents
# Returns created document ID
def create_doc(title, contents=None):
global SERVICE
# Construct document
doc = {
'title': title,
}
# Create the document
doc_obj = SERVICE.documents().create(body=doc).execute()
doc_id = doc_obj.get('documentId')
# Write to it if contents is not None
if contents:
write_doc(doc_id, 0, contents)
return doc_id
# Deletes document with given document ID
def delete_doc(doc_id):
global DRIVE_SERVICE
DRIVE_SERVICE.files().delete(fileId=doc_id).execute()