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.
- arcade_google/__init__.py +0 -0
- arcade_google/tools/__init__.py +1 -0
- arcade_google/tools/calendar.py +307 -0
- arcade_google/tools/docs.py +151 -0
- arcade_google/tools/drive.py +80 -0
- arcade_google/tools/gmail.py +333 -0
- arcade_google/tools/models.py +294 -0
- arcade_google/tools/utils.py +280 -0
- arcade_google-0.0.13.dist-info/METADATA +20 -0
- arcade_google-0.0.13.dist-info/RECORD +11 -0
- arcade_google-0.0.13.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from base64 import urlsafe_b64decode
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
from bs4 import BeautifulSoup
|
|
9
|
+
from google.oauth2.credentials import Credentials
|
|
10
|
+
from googleapiclient.discovery import build
|
|
11
|
+
|
|
12
|
+
from arcade_google.tools.models import Day, TimeSlot
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_datetime(datetime_str: str, time_zone: str) -> datetime:
|
|
16
|
+
"""
|
|
17
|
+
Parse a datetime string in ISO 8601 format and ensure it is timezone-aware.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
datetime_str (str): The datetime string to parse. Expected format: 'YYYY-MM-DDTHH:MM:SS'.
|
|
21
|
+
time_zone (str): The timezone to apply if the datetime string is naive.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
datetime: A timezone-aware datetime object.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If the datetime string is not in the correct format.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
dt = datetime.fromisoformat(datetime_str)
|
|
31
|
+
if dt.tzinfo is None:
|
|
32
|
+
dt = dt.replace(tzinfo=ZoneInfo(time_zone))
|
|
33
|
+
except ValueError as e:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Invalid datetime format: '{datetime_str}'. Expected ISO 8601 format, e.g., '2024-12-31T15:30:00'."
|
|
36
|
+
) from e
|
|
37
|
+
return dt
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DateRange(Enum):
|
|
41
|
+
TODAY = "today"
|
|
42
|
+
YESTERDAY = "yesterday"
|
|
43
|
+
LAST_7_DAYS = "last_7_days"
|
|
44
|
+
LAST_30_DAYS = "last_30_days"
|
|
45
|
+
THIS_MONTH = "this_month"
|
|
46
|
+
LAST_MONTH = "last_month"
|
|
47
|
+
THIS_YEAR = "this_year"
|
|
48
|
+
|
|
49
|
+
def to_date_query(self):
|
|
50
|
+
today = datetime.now()
|
|
51
|
+
result = "after:"
|
|
52
|
+
comparison_date = today
|
|
53
|
+
|
|
54
|
+
if self == DateRange.YESTERDAY:
|
|
55
|
+
comparison_date = today - timedelta(days=1)
|
|
56
|
+
elif self == DateRange.LAST_7_DAYS:
|
|
57
|
+
comparison_date = today - timedelta(days=7)
|
|
58
|
+
elif self == DateRange.LAST_30_DAYS:
|
|
59
|
+
comparison_date = today - timedelta(days=30)
|
|
60
|
+
elif self == DateRange.THIS_MONTH:
|
|
61
|
+
comparison_date = today.replace(day=1)
|
|
62
|
+
elif self == DateRange.LAST_MONTH:
|
|
63
|
+
comparison_date = (today.replace(day=1) - timedelta(days=1)).replace(day=1)
|
|
64
|
+
elif self == DateRange.THIS_YEAR:
|
|
65
|
+
comparison_date = today.replace(month=1, day=1)
|
|
66
|
+
elif self == DateRange.LAST_MONTH:
|
|
67
|
+
comparison_date = (today.replace(month=1, day=1) - timedelta(days=1)).replace(
|
|
68
|
+
month=1, day=1
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return result + comparison_date.strftime("%Y/%m/%d")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_email(email_data: dict[str, Any]) -> Optional[dict[str, str]]:
|
|
75
|
+
"""
|
|
76
|
+
Parse email data and extract relevant information.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
email_data (Dict[str, Any]): Raw email data from Gmail API.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Optional[Dict[str, str]]: Parsed email details or None if parsing fails.
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
payload = email_data["payload"]
|
|
86
|
+
headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
|
|
87
|
+
|
|
88
|
+
body_data = _get_email_body(payload)
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
"id": email_data.get("id", ""),
|
|
92
|
+
"from": headers.get("from", ""),
|
|
93
|
+
"date": headers.get("date", ""),
|
|
94
|
+
"subject": headers.get("subject", "No subject"),
|
|
95
|
+
"body": _clean_email_body(body_data) if body_data else "",
|
|
96
|
+
}
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f"Error parsing email {email_data.get('id', 'unknown')}: {e}")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_draft_email(draft_email_data: dict[str, Any]) -> Optional[dict[str, str]]:
|
|
103
|
+
"""
|
|
104
|
+
Parse draft email data and extract relevant information.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
draft_email_data (Dict[str, Any]): Raw draft email data from Gmail API.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Optional[Dict[str, str]]: Parsed draft email details or None if parsing fails.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
message = draft_email_data["message"]
|
|
114
|
+
payload = message["payload"]
|
|
115
|
+
headers = {d["name"].lower(): d["value"] for d in payload["headers"]}
|
|
116
|
+
|
|
117
|
+
body_data = _get_email_body(payload)
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
"id": draft_email_data.get("id", ""),
|
|
121
|
+
"from": headers.get("from", ""),
|
|
122
|
+
"date": headers.get("internaldate", ""),
|
|
123
|
+
"subject": headers.get("subject", "No subject"),
|
|
124
|
+
"body": _clean_email_body(body_data) if body_data else "",
|
|
125
|
+
}
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f"Error parsing draft email {draft_email_data.get('id', 'unknown')}: {e}")
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_draft_url(draft_id):
|
|
132
|
+
return f"https://mail.google.com/mail/u/0/#drafts/{draft_id}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_sent_email_url(sent_email_id):
|
|
136
|
+
return f"https://mail.google.com/mail/u/0/#sent/{sent_email_id}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_email_in_trash_url(email_id):
|
|
140
|
+
return f"https://mail.google.com/mail/u/0/#trash/{email_id}"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _get_email_body(payload: dict[str, Any]) -> Optional[str]:
|
|
144
|
+
"""
|
|
145
|
+
Extract email body from payload.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
payload (Dict[str, Any]): Email payload data.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Optional[str]: Decoded email body or None if not found.
|
|
152
|
+
"""
|
|
153
|
+
if "body" in payload and payload["body"].get("data"):
|
|
154
|
+
return urlsafe_b64decode(payload["body"]["data"]).decode()
|
|
155
|
+
|
|
156
|
+
for part in payload.get("parts", []):
|
|
157
|
+
if part.get("mimeType") == "text/plain" and "data" in part["body"]:
|
|
158
|
+
return urlsafe_b64decode(part["body"]["data"]).decode()
|
|
159
|
+
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _clean_email_body(body: str) -> str:
|
|
164
|
+
"""
|
|
165
|
+
Remove HTML tags and clean up email body text while preserving most content.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
body (str): The raw email body text.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
str: Cleaned email body text.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
# Remove HTML tags using BeautifulSoup
|
|
175
|
+
soup = BeautifulSoup(body, "html.parser")
|
|
176
|
+
text = soup.get_text(separator=" ")
|
|
177
|
+
|
|
178
|
+
# Clean up the text
|
|
179
|
+
text = _clean_text(text)
|
|
180
|
+
|
|
181
|
+
return text.strip()
|
|
182
|
+
except Exception as e:
|
|
183
|
+
print(f"Error cleaning email body: {e}")
|
|
184
|
+
return body
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _clean_text(text: str) -> str:
|
|
188
|
+
"""
|
|
189
|
+
Clean up the text while preserving most content.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
text (str): The input text.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
str: Cleaned text.
|
|
196
|
+
"""
|
|
197
|
+
# Replace multiple newlines with a single newline
|
|
198
|
+
text = re.sub(r"\n+", "\n", text)
|
|
199
|
+
|
|
200
|
+
# Replace multiple spaces with a single space
|
|
201
|
+
text = re.sub(r"\s+", " ", text)
|
|
202
|
+
|
|
203
|
+
# Remove leading/trailing whitespace from each line
|
|
204
|
+
text = "\n".join(line.strip() for line in text.split("\n"))
|
|
205
|
+
|
|
206
|
+
return text
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _update_datetime(day: Day | None, time: TimeSlot | None, time_zone: str) -> dict | None:
|
|
210
|
+
"""
|
|
211
|
+
Update the datetime for a Google Calendar event.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
day (Day | None): The day of the event.
|
|
215
|
+
time (TimeSlot | None): The time of the event.
|
|
216
|
+
time_zone (str): The time zone of the event.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
dict | None: The updated datetime for the event.
|
|
220
|
+
"""
|
|
221
|
+
if day and time:
|
|
222
|
+
dt = datetime.combine(day.to_date(time_zone), time.to_time())
|
|
223
|
+
return {"dateTime": dt.isoformat(), "timeZone": time_zone}
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def build_query_string(sender, recipient, subject, body, date_range):
|
|
228
|
+
"""
|
|
229
|
+
Helper function to build a query string for Gmail list_emails_by_header tool.
|
|
230
|
+
"""
|
|
231
|
+
query = []
|
|
232
|
+
if sender:
|
|
233
|
+
query.append(f"from:{sender}")
|
|
234
|
+
if recipient:
|
|
235
|
+
query.append(f"to:{recipient}")
|
|
236
|
+
if subject:
|
|
237
|
+
query.append(f"subject:{subject}")
|
|
238
|
+
if body:
|
|
239
|
+
query.append(body)
|
|
240
|
+
if date_range:
|
|
241
|
+
query.append(date_range.to_date_query())
|
|
242
|
+
return " ".join(query)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def fetch_messages(service, query_string, limit):
|
|
246
|
+
"""
|
|
247
|
+
Helper function to fetch messages from Gmail API for the list_emails_by_header tool.
|
|
248
|
+
"""
|
|
249
|
+
response = (
|
|
250
|
+
service.users()
|
|
251
|
+
.messages()
|
|
252
|
+
.list(userId="me", q=query_string, maxResults=limit or 100)
|
|
253
|
+
.execute()
|
|
254
|
+
)
|
|
255
|
+
return response.get("messages", [])
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def remove_none_values(params: dict) -> dict:
|
|
259
|
+
"""
|
|
260
|
+
Remove None values from a dictionary.
|
|
261
|
+
:param params: The dictionary to clean
|
|
262
|
+
:return: A new dictionary with None values removed
|
|
263
|
+
"""
|
|
264
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# Drive utils
|
|
268
|
+
def build_drive_service(token: str):
|
|
269
|
+
"""
|
|
270
|
+
Build a Drive service object.
|
|
271
|
+
"""
|
|
272
|
+
return build("drive", "v3", credentials=Credentials(token))
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Docs utils
|
|
276
|
+
def build_docs_service(token: str):
|
|
277
|
+
"""
|
|
278
|
+
Build a Drive service object.
|
|
279
|
+
"""
|
|
280
|
+
return build("docs", "v1", credentials=Credentials(token))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: arcade_google
|
|
3
|
+
Version: 0.0.13
|
|
4
|
+
Summary: Arcade tools for the entire google suite
|
|
5
|
+
Author: Arcade AI
|
|
6
|
+
Author-email: dev@arcade-ai.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Dist: arcade-ai (==0.0.13)
|
|
14
|
+
Requires-Dist: beautifulsoup4 (>=4.10.0,<5.0.0)
|
|
15
|
+
Requires-Dist: google-api-core (==2.19.1)
|
|
16
|
+
Requires-Dist: google-api-python-client (==2.137.0)
|
|
17
|
+
Requires-Dist: google-auth (==2.32.0)
|
|
18
|
+
Requires-Dist: google-auth-httplib2 (==0.2.0)
|
|
19
|
+
Requires-Dist: google-auth-oauthlib (==1.2.1)
|
|
20
|
+
Requires-Dist: googleapis-common-protos (==1.63.2)
|
|
@@ -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=YuMEapqYOiPU59hsXyVy70E-6i5Jt2xMCRRkuamEh1c,11782
|
|
4
|
+
arcade_google/tools/docs.py,sha256=uu3-Nc0T0WQ8fzbqETjNBxkdy5FEZjt71Slu5NAgPRU,5075
|
|
5
|
+
arcade_google/tools/drive.py,sha256=mVMaToUOgxXyjP96t2o1X72PEtD7628J5wBS9F6hcjg,3243
|
|
6
|
+
arcade_google/tools/gmail.py,sha256=TtC_6S7-KVg9N2c7Xq8l1uYKhD5HUWYcDmZZce9613k,11421
|
|
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.0.13.dist-info/METADATA,sha256=vKRnnhTXAOrEsrYy_6XUkmq3Q6XAH6a-76qlzImUZJM,798
|
|
10
|
+
arcade_google-0.0.13.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
11
|
+
arcade_google-0.0.13.dist-info/RECORD,,
|