arcade-google 0.0.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ import base64
2
+ import json
3
+ from email.message import EmailMessage
4
+ from email.mime.text import MIMEText
5
+ from typing import Annotated, Optional
6
+
7
+ from google.oauth2.credentials import Credentials
8
+ from googleapiclient.discovery import build
9
+ from googleapiclient.errors import HttpError
10
+
11
+ from arcade.core.schema import ToolContext
12
+ from arcade.sdk import tool
13
+ from arcade.sdk.auth import Google
14
+ from arcade.sdk.error import RetryableToolError
15
+ from arcade_google.tools.utils import (
16
+ DateRange,
17
+ build_query_string,
18
+ fetch_messages,
19
+ get_draft_url,
20
+ get_email_in_trash_url,
21
+ get_sent_email_url,
22
+ parse_draft_email,
23
+ parse_email,
24
+ )
25
+
26
+
27
+ # Email sending tools
28
+ @tool(
29
+ requires_auth=Google(
30
+ scopes=["https://www.googleapis.com/auth/gmail.send"],
31
+ )
32
+ )
33
+ async def send_email(
34
+ context: ToolContext,
35
+ subject: Annotated[str, "The subject of the email"],
36
+ body: Annotated[str, "The body of the email"],
37
+ recipient: Annotated[str, "The recipient of the email"],
38
+ cc: Annotated[Optional[list[str]], "CC recipients of the email"] = None,
39
+ bcc: Annotated[Optional[list[str]], "BCC recipients of the email"] = None,
40
+ ) -> Annotated[str, "A confirmation message with the sent email ID and URL"]:
41
+ """
42
+ Send an email using the Gmail API.
43
+ """
44
+
45
+ # Set up the Gmail API client
46
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
47
+
48
+ message = EmailMessage()
49
+ message.set_content(body)
50
+ message["To"] = recipient
51
+ message["Subject"] = subject
52
+ if cc:
53
+ message["Cc"] = ", ".join(cc)
54
+ if bcc:
55
+ message["Bcc"] = ", ".join(bcc)
56
+
57
+ # Encode the message in base64
58
+ encoded_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
59
+
60
+ # Create the email
61
+ email = {"raw": encoded_message}
62
+
63
+ # Send the email
64
+ sent_message = service.users().messages().send(userId="me", body=email).execute()
65
+ return f"Email with ID {sent_message['id']} sent: {get_sent_email_url(sent_message['id'])}"
66
+
67
+
68
+ @tool(
69
+ requires_auth=Google(
70
+ scopes=["https://www.googleapis.com/auth/gmail.send"],
71
+ )
72
+ )
73
+ async def send_draft_email(
74
+ context: ToolContext, email_id: Annotated[str, "The ID of the draft to send"]
75
+ ) -> Annotated[str, "A confirmation message with the sent email ID and URL"]:
76
+ """
77
+ Send a draft email using the Gmail API.
78
+ """
79
+
80
+ # Set up the Gmail API client
81
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
82
+
83
+ # Send the draft email
84
+ sent_message = service.users().drafts().send(userId="me", body={"id": email_id}).execute()
85
+
86
+ # Construct the URL to the sent email
87
+ return (
88
+ f"Draft email with ID {sent_message['id']} sent: {get_sent_email_url(sent_message['id'])}"
89
+ )
90
+
91
+
92
+ # Draft Management Tools
93
+ @tool(
94
+ requires_auth=Google(
95
+ scopes=["https://www.googleapis.com/auth/gmail.compose"],
96
+ )
97
+ )
98
+ async def write_draft_email(
99
+ context: ToolContext,
100
+ subject: Annotated[str, "The subject of the draft email"],
101
+ body: Annotated[str, "The body of the draft email"],
102
+ recipient: Annotated[str, "The recipient of the draft email"],
103
+ cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
104
+ bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
105
+ ) -> Annotated[str, "A confirmation message with the draft email ID and URL"]:
106
+ """
107
+ Compose a new email draft using the Gmail API.
108
+ """
109
+ # Set up the Gmail API client
110
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
111
+
112
+ message = MIMEText(body)
113
+ message["to"] = recipient
114
+ message["subject"] = subject
115
+ if cc:
116
+ message["Cc"] = ", ".join(cc)
117
+ if bcc:
118
+ message["Bcc"] = ", ".join(bcc)
119
+
120
+ # Encode the message in base64
121
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
122
+
123
+ # Create the draft
124
+ draft = {"message": {"raw": raw_message}}
125
+
126
+ draft_message = service.users().drafts().create(userId="me", body=draft).execute()
127
+ return (
128
+ f"Draft email with ID {draft_message['id']} created: {get_draft_url(draft_message['id'])}"
129
+ )
130
+
131
+
132
+ @tool(
133
+ requires_auth=Google(
134
+ scopes=["https://www.googleapis.com/auth/gmail.compose"],
135
+ )
136
+ )
137
+ async def update_draft_email(
138
+ context: ToolContext,
139
+ draft_email_id: Annotated[str, "The ID of the draft email to update."],
140
+ subject: Annotated[str, "The subject of the draft email"],
141
+ body: Annotated[str, "The body of the draft email"],
142
+ recipient: Annotated[str, "The recipient of the draft email"],
143
+ cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
144
+ bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
145
+ ) -> Annotated[str, "A confirmation message with the updated draft email ID and URL"]:
146
+ """
147
+ Update an existing email draft using the Gmail API.
148
+ """
149
+
150
+ # Set up the Gmail API client
151
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
152
+
153
+ message = MIMEText(body)
154
+ message["to"] = recipient
155
+ message["subject"] = subject
156
+ if cc:
157
+ message["Cc"] = ", ".join(cc)
158
+ if bcc:
159
+ message["Bcc"] = ", ".join(bcc)
160
+
161
+ # Encode the message in base64
162
+ raw_message = base64.urlsafe_b64encode(message.as_bytes()).decode()
163
+
164
+ # Update the draft
165
+ draft = {"id": draft_email_id, "message": {"raw": raw_message}}
166
+
167
+ updated_draft_message = (
168
+ service.users().drafts().update(userId="me", id=draft_email_id, body=draft).execute()
169
+ )
170
+ return f"Draft email with ID {updated_draft_message['id']} updated: {get_draft_url(updated_draft_message['id'])}"
171
+
172
+
173
+ @tool(
174
+ requires_auth=Google(
175
+ scopes=["https://www.googleapis.com/auth/gmail.compose"],
176
+ )
177
+ )
178
+ async def delete_draft_email(
179
+ context: ToolContext,
180
+ draft_email_id: Annotated[str, "The ID of the draft email to delete"],
181
+ ) -> Annotated[str, "A confirmation message indicating successful deletion"]:
182
+ """
183
+ Delete a draft email using the Gmail API.
184
+ """
185
+
186
+ # Set up the Gmail API client
187
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
188
+
189
+ # Delete the draft
190
+ service.users().drafts().delete(userId="me", id=draft_email_id).execute()
191
+ return f"Draft email with ID {draft_email_id} deleted successfully."
192
+
193
+
194
+ # Email Management Tools
195
+ @tool(
196
+ requires_auth=Google(
197
+ scopes=["https://www.googleapis.com/auth/gmail.modify"],
198
+ )
199
+ )
200
+ async def trash_email(
201
+ context: ToolContext, email_id: Annotated[str, "The ID of the email to trash"]
202
+ ) -> Annotated[str, "A confirmation message with the trashed email ID and URL"]:
203
+ """
204
+ Move an email to the trash folder using the Gmail API.
205
+ """
206
+
207
+ # Set up the Gmail API client
208
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
209
+
210
+ # Trash the email
211
+ service.users().messages().trash(userId="me", id=email_id).execute()
212
+
213
+ return f"Email with ID {email_id} trashed successfully: {get_email_in_trash_url(email_id)}"
214
+
215
+
216
+ # Draft Search Tools
217
+ @tool(
218
+ requires_auth=Google(
219
+ scopes=["https://www.googleapis.com/auth/gmail.readonly"],
220
+ )
221
+ )
222
+ async def list_draft_emails(
223
+ context: ToolContext,
224
+ n_drafts: Annotated[int, "Number of draft emails to read"] = 5,
225
+ ) -> Annotated[str, "A JSON string containing a list of draft email details and their IDs"]:
226
+ """
227
+ Lists draft emails in the user's draft mailbox using the Gmail API.
228
+ """
229
+ # Set up the Gmail API client
230
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
231
+
232
+ listed_drafts = service.users().drafts().list(userId="me").execute()
233
+
234
+ if not listed_drafts:
235
+ return {"emails": []}
236
+
237
+ draft_ids = [draft["id"] for draft in listed_drafts.get("drafts", [])][:n_drafts]
238
+
239
+ emails = []
240
+ for draft_id in draft_ids:
241
+ try:
242
+ draft_data = service.users().drafts().get(userId="me", id=draft_id).execute()
243
+ draft_details = parse_draft_email(draft_data)
244
+ if draft_details:
245
+ emails.append(draft_details)
246
+ except Exception as e:
247
+ print(f"Error reading draft email {draft_id}: {e}")
248
+
249
+ return json.dumps({"emails": emails})
250
+
251
+
252
+ # Email Search Tools
253
+ @tool(
254
+ requires_auth=Google(
255
+ scopes=["https://www.googleapis.com/auth/gmail.readonly"],
256
+ )
257
+ )
258
+ async def list_emails_by_header(
259
+ context: ToolContext,
260
+ sender: Annotated[Optional[str], "The name or email address of the sender of the email"] = None,
261
+ recipient: Annotated[Optional[str], "The name or email address of the recipient"] = None,
262
+ subject: Annotated[Optional[str], "Words to find in the subject of the email"] = None,
263
+ body: Annotated[Optional[str], "Words to find in the body of the email"] = None,
264
+ date_range: Annotated[Optional[DateRange], "The date range of the email"] = None,
265
+ limit: Annotated[Optional[int], "The maximum number of emails to return"] = 25,
266
+ ) -> Annotated[
267
+ str, "A JSON string containing a list of email details matching the search criteria"
268
+ ]:
269
+ """
270
+ Search for emails by header using the Gmail API.
271
+ At least one of the following parametersMUST be provided: sender, recipient, subject, body.
272
+ """
273
+ if not any([sender, recipient, subject, body]):
274
+ raise RetryableToolError(
275
+ message="At least one of sender, recipient, subject, or body must be provided.",
276
+ developer_message="At least one of sender, recipient, subject, or body must be provided.",
277
+ )
278
+
279
+ query = build_query_string(sender, recipient, subject, body, date_range)
280
+
281
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
282
+ messages = fetch_messages(service, query, limit)
283
+
284
+ if not messages:
285
+ return json.dumps({"emails": []})
286
+
287
+ emails = process_messages(service, messages)
288
+ return json.dumps({"emails": emails})
289
+
290
+
291
+ def process_messages(service, messages):
292
+ emails = []
293
+ for msg in messages:
294
+ try:
295
+ email_data = service.users().messages().get(userId="me", id=msg["id"]).execute()
296
+ email_details = parse_email(email_data)
297
+ emails += [email_details] if email_details else []
298
+ except HttpError as e:
299
+ print(f"Error reading email {msg['id']}: {e}")
300
+ return emails
301
+
302
+
303
+ @tool(
304
+ requires_auth=Google(
305
+ scopes=["https://www.googleapis.com/auth/gmail.readonly"],
306
+ )
307
+ )
308
+ async def list_emails(
309
+ context: ToolContext,
310
+ n_emails: Annotated[int, "Number of emails to read"] = 5,
311
+ ) -> Annotated[str, "A JSON string containing a list of email details"]:
312
+ """
313
+ Read emails from a Gmail account and extract plain text content.
314
+ """
315
+ # Set up the Gmail API client
316
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
317
+
318
+ messages = service.users().messages().list(userId="me").execute().get("messages", [])
319
+
320
+ if not messages:
321
+ return {"emails": []}
322
+
323
+ emails = []
324
+ for msg in messages[:n_emails]:
325
+ try:
326
+ email_data = service.users().messages().get(userId="me", id=msg["id"]).execute()
327
+ email_details = parse_email(email_data)
328
+ if email_details:
329
+ emails.append(email_details)
330
+ except Exception as e:
331
+ print(f"Error reading email {msg['id']}: {e}")
332
+
333
+ return json.dumps({"emails": emails})
@@ -0,0 +1,294 @@
1
+ from datetime import datetime, timedelta
2
+ from enum import Enum
3
+ from zoneinfo import ZoneInfo
4
+
5
+
6
+ # ---------------------------------------------------------------------------- #
7
+ # Google Calendar Models and Enums
8
+ # ---------------------------------------------------------------------------- #
9
+ class DateRange(Enum):
10
+ TODAY = "today"
11
+ TOMORROW = "tomorrow"
12
+ THIS_WEEK = "this_week"
13
+ NEXT_WEEK = "next_week"
14
+ THIS_MONTH = "this_month"
15
+ NEXT_MONTH = "next_month"
16
+
17
+ def to_date_range(self):
18
+ today = datetime.now().date()
19
+ if self == DateRange.TODAY:
20
+ return today, today + timedelta(days=1)
21
+ elif self == DateRange.TOMORROW:
22
+ return today + timedelta(days=1), today + timedelta(days=2)
23
+ elif self == DateRange.THIS_WEEK:
24
+ start = today - timedelta(days=today.weekday())
25
+ return start, start + timedelta(days=7)
26
+ elif self == DateRange.NEXT_WEEK:
27
+ start = today + timedelta(days=7 - today.weekday())
28
+ return start, start + timedelta(days=7)
29
+ elif self == DateRange.THIS_MONTH:
30
+ start = today.replace(day=1)
31
+ next_month = start + timedelta(days=32)
32
+ end = next_month.replace(day=1)
33
+ return start, end
34
+ elif self == DateRange.NEXT_MONTH:
35
+ start = (today.replace(day=1) + timedelta(days=32)).replace(day=1)
36
+ next_month = start + timedelta(days=32)
37
+ end = next_month.replace(day=1)
38
+ return start, end
39
+
40
+ def to_datetime_range(self, time_zone_name: str | None = None) -> tuple[datetime, datetime]:
41
+ start_date, end_date = self.to_date_range()
42
+ # time_zone = ZoneInfo(time_zone_name)
43
+ start_datetime = datetime.combine(
44
+ start_date, datetime.min.time()
45
+ ) # .replace(tzinfo=time_zone)
46
+ end_datetime = datetime.combine(end_date, datetime.min.time()) # .replace(tzinfo=time_zone)
47
+ return start_datetime, end_datetime
48
+
49
+
50
+ class Day(Enum):
51
+ # TODO: THere are obvious limitations here. We should do better and support any date.
52
+ YESTERDAY = "yesterday"
53
+ TODAY = "today"
54
+ TOMORROW = "tomorrow"
55
+ THIS_SUNDAY = "this_sunday"
56
+ THIS_MONDAY = "this_monday"
57
+ THIS_TUESDAY = "this_tuesday"
58
+ THIS_WEDNESDAY = "this_wednesday"
59
+ THIS_THURSDAY = "this_thursday"
60
+ THIS_FRIDAY = "this_friday"
61
+ THIS_SATURDAY = "this_saturday"
62
+ NEXT_SUNDAY = "next_sunday"
63
+ NEXT_MONDAY = "next_monday"
64
+ NEXT_TUESDAY = "next_tuesday"
65
+ NEXT_WEDNESDAY = "next_wednesday"
66
+ NEXT_THURSDAY = "next_thursday"
67
+ NEXT_FRIDAY = "next_friday"
68
+ NEXT_SATURDAY = "next_saturday"
69
+
70
+ def to_date(self, time_zone_name: str):
71
+ time_zone = ZoneInfo(time_zone_name)
72
+ today = datetime.now(time_zone).date()
73
+ weekday = today.weekday()
74
+
75
+ if self == Day.YESTERDAY:
76
+ return today - timedelta(days=1)
77
+ elif self == Day.TODAY:
78
+ return today
79
+ elif self == Day.TOMORROW:
80
+ return today + timedelta(days=1)
81
+
82
+ day_offsets = {
83
+ Day.THIS_SUNDAY: 6,
84
+ Day.THIS_MONDAY: 0,
85
+ Day.THIS_TUESDAY: 1,
86
+ Day.THIS_WEDNESDAY: 2,
87
+ Day.THIS_THURSDAY: 3,
88
+ Day.THIS_FRIDAY: 4,
89
+ Day.THIS_SATURDAY: 5,
90
+ }
91
+
92
+ if self in day_offsets:
93
+ return today + timedelta(days=(day_offsets[self] - weekday) % 7)
94
+
95
+ next_week_offsets = {
96
+ Day.NEXT_SUNDAY: 6,
97
+ Day.NEXT_MONDAY: 0,
98
+ Day.NEXT_TUESDAY: 1,
99
+ Day.NEXT_WEDNESDAY: 2,
100
+ Day.NEXT_THURSDAY: 3,
101
+ Day.NEXT_FRIDAY: 4,
102
+ Day.NEXT_SATURDAY: 5,
103
+ }
104
+
105
+ if self in next_week_offsets:
106
+ return today + timedelta(days=(next_week_offsets[self] - weekday + 7) % 7)
107
+
108
+ raise ValueError(f"Invalid Day enum value: {self}")
109
+
110
+
111
+ class TimeSlot(Enum):
112
+ _0000 = "00:00"
113
+ _0015 = "00:15"
114
+ _0030 = "00:30"
115
+ _0045 = "00:45"
116
+ _0100 = "01:00"
117
+ _0115 = "01:15"
118
+ _0130 = "01:30"
119
+ _0145 = "01:45"
120
+ _0200 = "02:00"
121
+ _0215 = "02:15"
122
+ _0230 = "02:30"
123
+ _0245 = "02:45"
124
+ _0300 = "03:00"
125
+ _0315 = "03:15"
126
+ _0330 = "03:30"
127
+ _0345 = "03:45"
128
+ _0400 = "04:00"
129
+ _0415 = "04:15"
130
+ _0430 = "04:30"
131
+ _0445 = "04:45"
132
+ _0500 = "05:00"
133
+ _0515 = "05:15"
134
+ _0530 = "05:30"
135
+ _0545 = "05:45"
136
+ _0600 = "06:00"
137
+ _0615 = "06:15"
138
+ _0630 = "06:30"
139
+ _0645 = "06:45"
140
+ _0700 = "07:00"
141
+ _0715 = "07:15"
142
+ _0730 = "07:30"
143
+ _0745 = "07:45"
144
+ _0800 = "08:00"
145
+ _0815 = "08:15"
146
+ _0830 = "08:30"
147
+ _0845 = "08:45"
148
+ _0900 = "09:00"
149
+ _0915 = "09:15"
150
+ _0930 = "09:30"
151
+ _0945 = "09:45"
152
+ _1000 = "10:00"
153
+ _1015 = "10:15"
154
+ _1030 = "10:30"
155
+ _1045 = "10:45"
156
+ _1100 = "11:00"
157
+ _1115 = "11:15"
158
+ _1130 = "11:30"
159
+ _1145 = "11:45"
160
+ _1200 = "12:00"
161
+ _1215 = "12:15"
162
+ _1230 = "12:30"
163
+ _1245 = "12:45"
164
+ _1300 = "13:00"
165
+ _1315 = "13:15"
166
+ _1330 = "13:30"
167
+ _1345 = "13:45"
168
+ _1400 = "14:00"
169
+ _1415 = "14:15"
170
+ _1430 = "14:30"
171
+ _1445 = "14:45"
172
+ _1500 = "15:00"
173
+ _1515 = "15:15"
174
+ _1530 = "15:30"
175
+ _1545 = "15:45"
176
+ _1600 = "16:00"
177
+ _1615 = "16:15"
178
+ _1630 = "16:30"
179
+ _1645 = "16:45"
180
+ _1700 = "17:00"
181
+ _1715 = "17:15"
182
+ _1730 = "17:30"
183
+ _1745 = "17:45"
184
+ _1800 = "18:00"
185
+ _1815 = "18:15"
186
+ _1830 = "18:30"
187
+ _1845 = "18:45"
188
+ _1900 = "19:00"
189
+ _1915 = "19:15"
190
+ _1930 = "19:30"
191
+ _1945 = "19:45"
192
+ _2000 = "20:00"
193
+ _2015 = "20:15"
194
+ _2030 = "20:30"
195
+ _2045 = "20:45"
196
+ _2100 = "21:00"
197
+ _2115 = "21:15"
198
+ _2130 = "21:30"
199
+ _2145 = "21:45"
200
+ _2200 = "22:00"
201
+ _2215 = "22:15"
202
+ _2230 = "22:30"
203
+ _2245 = "22:45"
204
+ _2300 = "23:00"
205
+ _2315 = "23:15"
206
+ _2330 = "23:30"
207
+ _2345 = "23:45"
208
+
209
+ def to_time(self):
210
+ return datetime.strptime(self.value, "%H:%M").time()
211
+
212
+
213
+ class EventVisibility(Enum):
214
+ DEFAULT = "default"
215
+ PUBLIC = "public"
216
+ PRIVATE = "private"
217
+ CONFIDENTIAL = "confidential"
218
+
219
+
220
+ class EventType(Enum):
221
+ BIRTHDAY = "birthday" # Special all-day events with an annual recurrence.
222
+ DEFAULT = "default" # Regular events
223
+ FOCUS_TIME = "focusTime" # Focus time events
224
+ FROM_GMAIL = "fromGmail" # Events from Gmail
225
+ OUT_OF_OFFICE = "outOfOffice" # Out of office events
226
+ WORKING_LOCATION = "workingLocation" # Working location events
227
+
228
+
229
+ class SendUpdatesOptions(Enum):
230
+ NONE = "none" # No notifications are sent
231
+ ALL = "all" # Notifications are sent to all guests
232
+ EXTERNAL_ONLY = "externalOnly" # Notifications are sent to non-Google Calendar guests only.
233
+
234
+
235
+ # Utils for Google Drive tools
236
+ class Corpora(str, Enum):
237
+ """
238
+ Bodies of items (files/documents) to which the query applies.
239
+ Prefer 'user' or 'drive' to 'allDrives' for efficiency.
240
+ By default, corpora is set to 'user'.
241
+ """
242
+
243
+ USER = "user"
244
+ DOMAIN = "domain"
245
+ DRIVE = "drive"
246
+ ALL_DRIVES = "allDrives"
247
+
248
+
249
+ class OrderBy(str, Enum):
250
+ """
251
+ Sort keys for ordering files in Google Drive.
252
+ Each key has both ascending and descending options.
253
+ """
254
+
255
+ CREATED_TIME = "createdTime" # When the file was created (ascending)
256
+ CREATED_TIME_DESC = "createdTime desc" # When the file was created (descending)
257
+ FOLDER = "folder" # The folder ID, sorted using alphabetical ordering (ascending)
258
+ FOLDER_DESC = "folder desc" # The folder ID, sorted using alphabetical ordering (descending)
259
+ MODIFIED_BY_ME_TIME = (
260
+ "modifiedByMeTime" # The last time the file was modified by the user (ascending)
261
+ )
262
+ MODIFIED_BY_ME_TIME_DESC = (
263
+ "modifiedByMeTime desc" # The last time the file was modified by the user (descending)
264
+ )
265
+ MODIFIED_TIME = "modifiedTime" # The last time the file was modified by anyone (ascending)
266
+ MODIFIED_TIME_DESC = (
267
+ "modifiedTime desc" # The last time the file was modified by anyone (descending)
268
+ )
269
+ NAME = "name" # The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (ascending)
270
+ NAME_DESC = "name desc" # The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (descending)
271
+ NAME_NATURAL = "name_natural" # The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (ascending)
272
+ NAME_NATURAL_DESC = "name_natural desc" # The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (descending)
273
+ QUOTA_BYTES_USED = (
274
+ "quotaBytesUsed" # The number of storage quota bytes used by the file (ascending)
275
+ )
276
+ QUOTA_BYTES_USED_DESC = (
277
+ "quotaBytesUsed desc" # The number of storage quota bytes used by the file (descending)
278
+ )
279
+ RECENCY = "recency" # The most recent timestamp from the file's date-time fields (ascending)
280
+ RECENCY_DESC = (
281
+ "recency desc" # The most recent timestamp from the file's date-time fields (descending)
282
+ )
283
+ SHARED_WITH_ME_TIME = (
284
+ "sharedWithMeTime" # When the file was shared with the user, if applicable (ascending)
285
+ )
286
+ SHARED_WITH_ME_TIME_DESC = "sharedWithMeTime desc" # When the file was shared with the user, if applicable (descending)
287
+ STARRED = "starred" # Whether the user has starred the file (ascending)
288
+ STARRED_DESC = "starred desc" # Whether the user has starred the file (descending)
289
+ VIEWED_BY_ME_TIME = (
290
+ "viewedByMeTime" # The last time the file was viewed by the user (ascending)
291
+ )
292
+ VIEWED_BY_ME_TIME_DESC = (
293
+ "viewedByMeTime desc" # The last time the file was viewed by the user (descending)
294
+ )