arcade-google 0.1.1__py3-none-any.whl → 0.1.2__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.
@@ -1,5 +1,4 @@
1
1
  import base64
2
- import json
3
2
  from email.message import EmailMessage
4
3
  from email.mime.text import MIMEText
5
4
  from typing import Annotated, Optional
@@ -20,6 +19,7 @@ from arcade_google.tools.utils import (
20
19
  get_sent_email_url,
21
20
  parse_draft_email,
22
21
  parse_email,
22
+ remove_none_values,
23
23
  )
24
24
 
25
25
 
@@ -36,7 +36,7 @@ async def send_email(
36
36
  recipient: Annotated[str, "The recipient of the email"],
37
37
  cc: Annotated[Optional[list[str]], "CC recipients of the email"] = None,
38
38
  bcc: Annotated[Optional[list[str]], "BCC recipients of the email"] = None,
39
- ) -> Annotated[str, "A confirmation message with the sent email ID and URL"]:
39
+ ) -> Annotated[dict, "A dictionary containing the sent email details"]:
40
40
  """
41
41
  Send an email using the Gmail API.
42
42
  """
@@ -61,7 +61,10 @@ async def send_email(
61
61
 
62
62
  # Send the email
63
63
  sent_message = service.users().messages().send(userId="me", body=email).execute()
64
- return f"Email with ID {sent_message['id']} sent: {get_sent_email_url(sent_message['id'])}"
64
+
65
+ email = parse_email(sent_message)
66
+ email["url"] = get_sent_email_url(sent_message["id"])
67
+ return email
65
68
 
66
69
 
67
70
  @tool(
@@ -71,7 +74,7 @@ async def send_email(
71
74
  )
72
75
  async def send_draft_email(
73
76
  context: ToolContext, email_id: Annotated[str, "The ID of the draft to send"]
74
- ) -> Annotated[str, "A confirmation message with the sent email ID and URL"]:
77
+ ) -> Annotated[dict, "A dictionary containing the sent email details"]:
75
78
  """
76
79
  Send a draft email using the Gmail API.
77
80
  """
@@ -82,10 +85,9 @@ async def send_draft_email(
82
85
  # Send the draft email
83
86
  sent_message = service.users().drafts().send(userId="me", body={"id": email_id}).execute()
84
87
 
85
- # Construct the URL to the sent email
86
- return (
87
- f"Draft email with ID {sent_message['id']} sent: {get_sent_email_url(sent_message['id'])}"
88
- )
88
+ email = parse_email(sent_message)
89
+ email["url"] = get_sent_email_url(sent_message["id"])
90
+ return email
89
91
 
90
92
 
91
93
  # Draft Management Tools
@@ -101,7 +103,7 @@ async def write_draft_email(
101
103
  recipient: Annotated[str, "The recipient of the draft email"],
102
104
  cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
103
105
  bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
104
- ) -> Annotated[str, "A confirmation message with the draft email ID and URL"]:
106
+ ) -> Annotated[dict, "A dictionary containing the created draft email details"]:
105
107
  """
106
108
  Compose a new email draft using the Gmail API.
107
109
  """
@@ -123,9 +125,9 @@ async def write_draft_email(
123
125
  draft = {"message": {"raw": raw_message}}
124
126
 
125
127
  draft_message = service.users().drafts().create(userId="me", body=draft).execute()
126
- return (
127
- f"Draft email with ID {draft_message['id']} created: {get_draft_url(draft_message['id'])}"
128
- )
128
+ email = parse_draft_email(draft_message)
129
+ email["url"] = get_draft_url(draft_message["id"])
130
+ return email
129
131
 
130
132
 
131
133
  @tool(
@@ -141,7 +143,7 @@ async def update_draft_email(
141
143
  recipient: Annotated[str, "The recipient of the draft email"],
142
144
  cc: Annotated[Optional[list[str]], "CC recipients of the draft email"] = None,
143
145
  bcc: Annotated[Optional[list[str]], "BCC recipients of the draft email"] = None,
144
- ) -> Annotated[str, "A confirmation message with the updated draft email ID and URL"]:
146
+ ) -> Annotated[dict, "A dictionary containing the updated draft email details"]:
145
147
  """
146
148
  Update an existing email draft using the Gmail API.
147
149
  """
@@ -166,7 +168,10 @@ async def update_draft_email(
166
168
  updated_draft_message = (
167
169
  service.users().drafts().update(userId="me", id=draft_email_id, body=draft).execute()
168
170
  )
169
- return f"Draft email with ID {updated_draft_message['id']} updated: {get_draft_url(updated_draft_message['id'])}"
171
+
172
+ email = parse_draft_email(updated_draft_message)
173
+ email["url"] = get_draft_url(updated_draft_message["id"])
174
+ return email
170
175
 
171
176
 
172
177
  @tool(
@@ -198,7 +203,7 @@ async def delete_draft_email(
198
203
  )
199
204
  async def trash_email(
200
205
  context: ToolContext, email_id: Annotated[str, "The ID of the email to trash"]
201
- ) -> Annotated[str, "A confirmation message with the trashed email ID and URL"]:
206
+ ) -> Annotated[dict, "A dictionary containing the trashed email details"]:
202
207
  """
203
208
  Move an email to the trash folder using the Gmail API.
204
209
  """
@@ -207,9 +212,11 @@ async def trash_email(
207
212
  service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
208
213
 
209
214
  # Trash the email
210
- service.users().messages().trash(userId="me", id=email_id).execute()
215
+ trashed_email = service.users().messages().trash(userId="me", id=email_id).execute()
211
216
 
212
- return f"Email with ID {email_id} trashed successfully: {get_email_in_trash_url(email_id)}"
217
+ email = parse_email(trashed_email)
218
+ email["url"] = get_email_in_trash_url(trashed_email["id"])
219
+ return email
213
220
 
214
221
 
215
222
  # Draft Search Tools
@@ -221,7 +228,7 @@ async def trash_email(
221
228
  async def list_draft_emails(
222
229
  context: ToolContext,
223
230
  n_drafts: Annotated[int, "Number of draft emails to read"] = 5,
224
- ) -> Annotated[str, "A JSON string containing a list of draft email details and their IDs"]:
231
+ ) -> Annotated[dict, "A dictionary containing a list of draft email details"]:
225
232
  """
226
233
  Lists draft emails in the user's draft mailbox using the Gmail API.
227
234
  """
@@ -245,7 +252,7 @@ async def list_draft_emails(
245
252
  except Exception as e:
246
253
  print(f"Error reading draft email {draft_id}: {e}")
247
254
 
248
- return json.dumps({"emails": emails})
255
+ return {"emails": emails}
249
256
 
250
257
 
251
258
  # Email Search Tools
@@ -263,11 +270,11 @@ async def list_emails_by_header(
263
270
  date_range: Annotated[Optional[DateRange], "The date range of the email"] = None,
264
271
  limit: Annotated[Optional[int], "The maximum number of emails to return"] = 25,
265
272
  ) -> Annotated[
266
- str, "A JSON string containing a list of email details matching the search criteria"
273
+ dict, "A dictionary containing a list of email details matching the search criteria"
267
274
  ]:
268
275
  """
269
276
  Search for emails by header using the Gmail API.
270
- At least one of the following parametersMUST be provided: sender, recipient, subject, body.
277
+ At least one of the following parameters MUST be provided: sender, recipient, subject, body.
271
278
  """
272
279
  if not any([sender, recipient, subject, body]):
273
280
  raise RetryableToolError(
@@ -281,10 +288,10 @@ async def list_emails_by_header(
281
288
  messages = fetch_messages(service, query, limit)
282
289
 
283
290
  if not messages:
284
- return json.dumps({"emails": []})
291
+ return {"emails": []}
285
292
 
286
293
  emails = process_messages(service, messages)
287
- return json.dumps({"emails": emails})
294
+ return {"emails": emails}
288
295
 
289
296
 
290
297
  def process_messages(service, messages):
@@ -307,7 +314,7 @@ def process_messages(service, messages):
307
314
  async def list_emails(
308
315
  context: ToolContext,
309
316
  n_emails: Annotated[int, "Number of emails to read"] = 5,
310
- ) -> Annotated[str, "A JSON string containing a list of email details"]:
317
+ ) -> Annotated[dict, "A dictionary containing a list of email details"]:
311
318
  """
312
319
  Read emails from a Gmail account and extract plain text content.
313
320
  """
@@ -329,4 +336,109 @@ async def list_emails(
329
336
  except Exception as e:
330
337
  print(f"Error reading email {msg['id']}: {e}")
331
338
 
332
- return json.dumps({"emails": emails})
339
+ return {"emails": emails}
340
+
341
+
342
+ @tool(
343
+ requires_auth=Google(
344
+ scopes=["https://www.googleapis.com/auth/gmail.readonly"],
345
+ )
346
+ )
347
+ async def search_threads(
348
+ context: ToolContext,
349
+ page_token: Annotated[
350
+ Optional[str], "Page token to retrieve a specific page of results in the list"
351
+ ] = None,
352
+ max_results: Annotated[int, "The maximum number of threads to return"] = 10,
353
+ include_spam_trash: Annotated[bool, "Whether to include spam and trash in the results"] = False,
354
+ label_ids: Annotated[Optional[list[str]], "The IDs of labels to filter by"] = None,
355
+ sender: Annotated[Optional[str], "The name or email address of the sender of the email"] = None,
356
+ recipient: Annotated[Optional[str], "The name or email address of the recipient"] = None,
357
+ subject: Annotated[Optional[str], "Words to find in the subject of the email"] = None,
358
+ body: Annotated[Optional[str], "Words to find in the body of the email"] = None,
359
+ date_range: Annotated[Optional[DateRange], "The date range of the email"] = None,
360
+ ) -> Annotated[dict, "A dictionary containing a list of thread details"]:
361
+ """Search for threads in the user's mailbox"""
362
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
363
+
364
+ query = (
365
+ build_query_string(sender, recipient, subject, body, date_range)
366
+ if any([sender, recipient, subject, body, date_range])
367
+ else None
368
+ )
369
+
370
+ params = {
371
+ "userId": "me",
372
+ "maxResults": min(max_results, 500),
373
+ "pageToken": page_token,
374
+ "includeSpamTrash": include_spam_trash,
375
+ "labelIds": label_ids,
376
+ "q": query,
377
+ }
378
+ params = remove_none_values(params)
379
+
380
+ threads = []
381
+ next_page_token = None
382
+ # Paginate through thread pages until we have the desired number of threads
383
+ while len(threads) < max_results:
384
+ response = service.users().threads().list(**params).execute()
385
+
386
+ threads.extend(response.get("threads", []))
387
+ next_page_token = response.get("nextPageToken")
388
+
389
+ if not next_page_token:
390
+ break
391
+
392
+ params["pageToken"] = next_page_token
393
+ params["maxResults"] = min(max_results - len(threads), 500)
394
+
395
+ return {
396
+ "threads": threads,
397
+ "num_threads": len(threads),
398
+ "next_page_token": next_page_token,
399
+ }
400
+
401
+
402
+ @tool(
403
+ requires_auth=Google(
404
+ scopes=["https://www.googleapis.com/auth/gmail.readonly"],
405
+ )
406
+ )
407
+ async def list_threads(
408
+ context: ToolContext,
409
+ page_token: Annotated[
410
+ Optional[str], "Page token to retrieve a specific page of results in the list"
411
+ ] = None,
412
+ max_results: Annotated[int, "The maximum number of threads to return"] = 10,
413
+ include_spam_trash: Annotated[bool, "Whether to include spam and trash in the results"] = False,
414
+ ) -> Annotated[dict, "A dictionary containing a list of thread details"]:
415
+ """List threads in the user's mailbox."""
416
+ return await search_threads(context, page_token, max_results, include_spam_trash)
417
+
418
+
419
+ @tool(
420
+ requires_auth=Google(
421
+ scopes=["https://www.googleapis.com/auth/gmail.readonly"],
422
+ )
423
+ )
424
+ async def get_thread(
425
+ context: ToolContext,
426
+ thread_id: Annotated[str, "The ID of the thread to retrieve"],
427
+ metadata_headers: Annotated[
428
+ Optional[list[str]], "When given and format is METADATA, only include headers specified."
429
+ ] = None,
430
+ ) -> Annotated[dict, "A dictionary containing the thread details"]:
431
+ """Get the specified thread by ID."""
432
+ params = {
433
+ "userId": "me",
434
+ "id": thread_id,
435
+ "format": "full",
436
+ "metadataHeaders": metadata_headers,
437
+ }
438
+ params = remove_none_values(params)
439
+
440
+ service = build("gmail", "v1", credentials=Credentials(context.authorization.token))
441
+ thread = service.users().threads().get(**params).execute()
442
+ thread["messages"] = [parse_email(message) for message in thread.get("messages", [])]
443
+
444
+ return thread
@@ -232,7 +232,9 @@ class SendUpdatesOptions(Enum):
232
232
  EXTERNAL_ONLY = "externalOnly" # Notifications are sent to non-Google Calendar guests only.
233
233
 
234
234
 
235
- # Utils for Google Drive tools
235
+ # ---------------------------------------------------------------------------- #
236
+ # Google Drive Models and Enums
237
+ # ---------------------------------------------------------------------------- #
236
238
  class Corpora(str, Enum):
237
239
  """
238
240
  Bodies of items (files/documents) to which the query applies.
@@ -82,16 +82,17 @@ def parse_email(email_data: dict[str, Any]) -> Optional[dict[str, str]]:
82
82
  Optional[Dict[str, str]]: Parsed email details or None if parsing fails.
83
83
  """
84
84
  try:
85
- payload = email_data["payload"]
86
- headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
85
+ payload = email_data.get("payload", {})
86
+ headers = {d["name"].lower(): d["value"] for d in payload.get("headers", [])}
87
87
 
88
88
  body_data = _get_email_body(payload)
89
89
 
90
90
  return {
91
91
  "id": email_data.get("id", ""),
92
+ "thread_id": email_data.get("threadId", ""),
92
93
  "from": headers.get("from", ""),
93
94
  "date": headers.get("date", ""),
94
- "subject": headers.get("subject", "No subject"),
95
+ "subject": headers.get("subject", ""),
95
96
  "body": _clean_email_body(body_data) if body_data else "",
96
97
  }
97
98
  except Exception as e:
@@ -110,17 +111,18 @@ def parse_draft_email(draft_email_data: dict[str, Any]) -> Optional[dict[str, st
110
111
  Optional[Dict[str, str]]: Parsed draft email details or None if parsing fails.
111
112
  """
112
113
  try:
113
- message = draft_email_data["message"]
114
- payload = message["payload"]
115
- headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
114
+ message = draft_email_data.get("message", {})
115
+ payload = message.get("payload", {})
116
+ headers = {d["name"].lower(): d["value"] for d in payload.get("headers", [])}
116
117
 
117
118
  body_data = _get_email_body(payload)
118
119
 
119
120
  return {
120
121
  "id": draft_email_data.get("id", ""),
122
+ "thread_id": draft_email_data.get("threadId", ""),
121
123
  "from": headers.get("from", ""),
122
124
  "date": headers.get("internaldate", ""),
123
- "subject": headers.get("subject", "No subject"),
125
+ "subject": headers.get("subject", ""),
124
126
  "body": _clean_email_body(body_data) if body_data else "",
125
127
  }
126
128
  except Exception as e:
@@ -226,7 +228,7 @@ def _update_datetime(day: Day | None, time: TimeSlot | None, time_zone: str) ->
226
228
 
227
229
  def build_query_string(sender, recipient, subject, body, date_range):
228
230
  """
229
- Helper function to build a query string for Gmail list_emails_by_header tool.
231
+ Helper function to build a query string for Gmail list_emails_by_header and search_threads tools.
230
232
  """
231
233
  query = []
232
234
  if sender:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: arcade_google
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Arcade tools for the entire google suite
5
5
  Author: Arcade AI
6
6
  Author-email: dev@arcade-ai.com
@@ -10,7 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
10
10
  Classifier: Programming Language :: Python :: 3.11
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
- Requires-Dist: arcade-ai (==0.1.1)
13
+ Requires-Dist: arcade-ai (==0.1.2)
14
14
  Requires-Dist: beautifulsoup4 (>=4.10.0,<5.0.0)
15
15
  Requires-Dist: google-api-core (==2.19.1)
16
16
  Requires-Dist: google-api-python-client (==2.137.0)
@@ -0,0 +1,11 @@
1
+ arcade_google/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ arcade_google/tools/__init__.py,sha256=MXM0xOEislCvl1UgwJC_Nqme83KjkQsaiUj4VKP0Q9s,49
3
+ arcade_google/tools/calendar.py,sha256=kYK0Ff3gKnen3tXgOEIHemrnBqGsfHjJlbbYCXw_qxk,11751
4
+ arcade_google/tools/docs.py,sha256=n3SpfuGx4x6RH-uVc_h2_LgRJgTU11fFexSUYjM88Ps,5045
5
+ arcade_google/tools/drive.py,sha256=U7I8DrcMKV72JEowaKH6h-4ruBEAvgJNbjq9cepvLag,3213
6
+ arcade_google/tools/gmail.py,sha256=onmGkKy-S7kJAO4zgpbzwiLB_Z1hPy_mJHUNqNMoxRk,15249
7
+ arcade_google/tools/models.py,sha256=hKZIbkL0a3ddVFoV_NxiwUhNxVxXdlKK2K44TkCGnfA,9755
8
+ arcade_google/tools/utils.py,sha256=9gJil5G0ufX9OpkFfEqzWKb5AFI9HPm-SveVyGlOVro,8515
9
+ arcade_google-0.1.2.dist-info/METADATA,sha256=qRI1cabjnyqDLS-xH7iGXpp6AzTA7Wb7AWNTgwsN8Xc,796
10
+ arcade_google-0.1.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
11
+ arcade_google-0.1.2.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- arcade_google/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- arcade_google/tools/__init__.py,sha256=MXM0xOEislCvl1UgwJC_Nqme83KjkQsaiUj4VKP0Q9s,49
3
- arcade_google/tools/calendar.py,sha256=kYK0Ff3gKnen3tXgOEIHemrnBqGsfHjJlbbYCXw_qxk,11751
4
- arcade_google/tools/docs.py,sha256=n3SpfuGx4x6RH-uVc_h2_LgRJgTU11fFexSUYjM88Ps,5045
5
- arcade_google/tools/drive.py,sha256=U7I8DrcMKV72JEowaKH6h-4ruBEAvgJNbjq9cepvLag,3213
6
- arcade_google/tools/gmail.py,sha256=9hBZ1r79LTagFbFZazjrcyr9UPnTBHmSDiGdInd52Fo,11392
7
- arcade_google/tools/models.py,sha256=um8mKisitNPimCx1HYX9spnKp1UzBWfRF4H2ducGn-8,9592
8
- arcade_google/tools/utils.py,sha256=jO9RpRm8N_NRhnGtpHDLKf_he1u2-3r8iSqR5JxaPfU,8355
9
- arcade_google-0.1.1.dist-info/METADATA,sha256=5nIDKbgtpcUS_BY-3kUG_g5W6Y1MB4mY83tO2ZVOa3M,796
10
- arcade_google-0.1.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
11
- arcade_google-0.1.1.dist-info/RECORD,,