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,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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any