arcade-google 0.1.6__py3-none-any.whl → 2.0.0__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,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcade_google
3
+ Version: 2.0.0
4
+ Summary: Arcade.dev LLM tools for Google Workspace
5
+ Author-email: Arcade <dev@arcade.dev>
6
+ License: Proprietary - Arcade Software License Agreement v1.0
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: arcade-tdk<3.0.0,>=2.0.0
10
+ Requires-Dist: beautifulsoup4<5.0.0,>=4.10.0
11
+ Requires-Dist: google-api-core<3.0.0,>=2.19.1
12
+ Requires-Dist: google-api-python-client<3.0.0,>=2.137.0
13
+ Requires-Dist: google-auth-httplib2<1.0.0,>=0.2.0
14
+ Requires-Dist: google-auth-oauthlib<2.0.0,>=1.2.1
15
+ Requires-Dist: google-auth<3.0.0,>=2.32.0
16
+ Requires-Dist: googleapis-common-protos<2.0.0,>=1.63.2
17
+ Provides-Extra: dev
18
+ Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.0; extra == 'dev'
19
+ Requires-Dist: arcade-serve<3.0.0,>=2.0.0; extra == 'dev'
20
+ Requires-Dist: mypy<1.6.0,>=1.5.1; extra == 'dev'
21
+ Requires-Dist: pre-commit<3.5.0,>=3.4.0; extra == 'dev'
22
+ Requires-Dist: pytest-asyncio<0.25.0,>=0.24.0; extra == 'dev'
23
+ Requires-Dist: pytest-cov<4.1.0,>=4.0.0; extra == 'dev'
24
+ Requires-Dist: pytest-mock<3.12.0,>=3.11.1; extra == 'dev'
25
+ Requires-Dist: pytest<8.4.0,>=8.3.0; extra == 'dev'
26
+ Requires-Dist: ruff<0.8.0,>=0.7.4; extra == 'dev'
27
+ Requires-Dist: tox<4.12.0,>=4.11.1; extra == 'dev'
@@ -0,0 +1,21 @@
1
+ arcade_google/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ arcade_google/constants.py,sha256=9oHLVYE90s97qpIJEpstgvsS1ZuMIyid6Y6pfGwjcuo,936
3
+ arcade_google/critics.py,sha256=ltPil4E7Ntu-1S9XpgSJVQwHBikSTyixPYNHlMeYSFM,1397
4
+ arcade_google/doc_to_html.py,sha256=6RTpzRSrazNa6AndLZhA20wgVDzZuHUqpu3WAkAsbjQ,3146
5
+ arcade_google/doc_to_markdown.py,sha256=hh9sQoJ4MiZnJGPEJgwNfJPA_mY6J9b8aAmQ-RDmLTI,2015
6
+ arcade_google/enums.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ arcade_google/exceptions.py,sha256=jcF2wipBwIxJM6RUjRXeen1lwYjRqNipCQabX078GaE,1952
8
+ arcade_google/models.py,sha256=nMmFxYBUgDgXkV2OV12e3Ah--SdBw78yZmNfZVvWydM,20269
9
+ arcade_google/utils.py,sha256=2RvLHKJ91wHb7Si4bX714i85-BD0hHWSid8ShJSVQU8,50790
10
+ arcade_google/tools/__init__.py,sha256=Hv4eth0VGmqwCKwP3Vmu0SgXHwI-PmoVXMwMjde4D8M,2211
11
+ arcade_google/tools/calendar.py,sha256=bb6wrGyGY9t-kNan5gnxTW7vfeHj2AboGISkO1TpEjE,18630
12
+ arcade_google/tools/contacts.py,sha256=4BKjiQlGVqopQzTNLnXf32G02F_EZQYq7sPPtgssbEw,3986
13
+ arcade_google/tools/docs.py,sha256=xcFNVDPOb8LWv3a90VMYnIBpB8qkfgf8siOrOJ82qac,5367
14
+ arcade_google/tools/drive.py,sha256=JR0JOSYkgKGbDXwk4pjTIJpjb3Nh-5NFb__DVKPJFKc,10819
15
+ arcade_google/tools/file_picker.py,sha256=KyJ0EFIIKws7H4-VyW-mnvV92fAy_X8mqbqQSKapT_k,2193
16
+ arcade_google/tools/gmail.py,sha256=AovOOxaYpebCsHDdhIJnP5m7luear6hG65QF04o_Db8,23014
17
+ arcade_google/tools/sheets.py,sha256=qYNPO_xwslUwEPrivgdIOQoS2gpu7Gsu9N3aLdRhJ8A,4626
18
+ arcade_google-2.0.0.dist-info/METADATA,sha256=Sp2PgKx9DFItwX2BCrU7vWWkMHlrFam9rnIKbBt3IfM,1214
19
+ arcade_google-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
20
+ arcade_google-2.0.0.dist-info/licenses/LICENSE,sha256=XjKuCk1TG4bFrY-8x79oGMmNqrS4TP7c_Zv4-TrMWQY,1063
21
+ arcade_google-2.0.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Arcade
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,296 +0,0 @@
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
- # ---------------------------------------------------------------------------- #
236
- # Google Drive Models and Enums
237
- # ---------------------------------------------------------------------------- #
238
- class Corpora(str, Enum):
239
- """
240
- Bodies of items (files/documents) to which the query applies.
241
- Prefer 'user' or 'drive' to 'allDrives' for efficiency.
242
- By default, corpora is set to 'user'.
243
- """
244
-
245
- USER = "user"
246
- DOMAIN = "domain"
247
- DRIVE = "drive"
248
- ALL_DRIVES = "allDrives"
249
-
250
-
251
- class OrderBy(str, Enum):
252
- """
253
- Sort keys for ordering files in Google Drive.
254
- Each key has both ascending and descending options.
255
- """
256
-
257
- CREATED_TIME = "createdTime" # When the file was created (ascending)
258
- CREATED_TIME_DESC = "createdTime desc" # When the file was created (descending)
259
- FOLDER = "folder" # The folder ID, sorted using alphabetical ordering (ascending)
260
- FOLDER_DESC = "folder desc" # The folder ID, sorted using alphabetical ordering (descending)
261
- MODIFIED_BY_ME_TIME = (
262
- "modifiedByMeTime" # The last time the file was modified by the user (ascending)
263
- )
264
- MODIFIED_BY_ME_TIME_DESC = (
265
- "modifiedByMeTime desc" # The last time the file was modified by the user (descending)
266
- )
267
- MODIFIED_TIME = "modifiedTime" # The last time the file was modified by anyone (ascending)
268
- MODIFIED_TIME_DESC = (
269
- "modifiedTime desc" # The last time the file was modified by anyone (descending)
270
- )
271
- NAME = "name" # The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (ascending)
272
- NAME_DESC = "name desc" # The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (descending)
273
- NAME_NATURAL = "name_natural" # The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (ascending)
274
- NAME_NATURAL_DESC = "name_natural desc" # The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (descending)
275
- QUOTA_BYTES_USED = (
276
- "quotaBytesUsed" # The number of storage quota bytes used by the file (ascending)
277
- )
278
- QUOTA_BYTES_USED_DESC = (
279
- "quotaBytesUsed desc" # The number of storage quota bytes used by the file (descending)
280
- )
281
- RECENCY = "recency" # The most recent timestamp from the file's date-time fields (ascending)
282
- RECENCY_DESC = (
283
- "recency desc" # The most recent timestamp from the file's date-time fields (descending)
284
- )
285
- SHARED_WITH_ME_TIME = (
286
- "sharedWithMeTime" # When the file was shared with the user, if applicable (ascending)
287
- )
288
- SHARED_WITH_ME_TIME_DESC = "sharedWithMeTime desc" # When the file was shared with the user, if applicable (descending)
289
- STARRED = "starred" # Whether the user has starred the file (ascending)
290
- STARRED_DESC = "starred desc" # Whether the user has starred the file (descending)
291
- VIEWED_BY_ME_TIME = (
292
- "viewedByMeTime" # The last time the file was viewed by the user (ascending)
293
- )
294
- VIEWED_BY_ME_TIME_DESC = (
295
- "viewedByMeTime desc" # The last time the file was viewed by the user (descending)
296
- )
@@ -1,282 +0,0 @@
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.get("payload", {})
86
- headers = {d["name"].lower(): d["value"] for d in payload.get("headers", [])}
87
-
88
- body_data = _get_email_body(payload)
89
-
90
- return {
91
- "id": email_data.get("id", ""),
92
- "thread_id": email_data.get("threadId", ""),
93
- "from": headers.get("from", ""),
94
- "date": headers.get("date", ""),
95
- "subject": headers.get("subject", ""),
96
- "body": _clean_email_body(body_data) if body_data else "",
97
- }
98
- except Exception as e:
99
- print(f"Error parsing email {email_data.get('id', 'unknown')}: {e}")
100
- return None
101
-
102
-
103
- def parse_draft_email(draft_email_data: dict[str, Any]) -> Optional[dict[str, str]]:
104
- """
105
- Parse draft email data and extract relevant information.
106
-
107
- Args:
108
- draft_email_data (Dict[str, Any]): Raw draft email data from Gmail API.
109
-
110
- Returns:
111
- Optional[Dict[str, str]]: Parsed draft email details or None if parsing fails.
112
- """
113
- try:
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", [])}
117
-
118
- body_data = _get_email_body(payload)
119
-
120
- return {
121
- "id": draft_email_data.get("id", ""),
122
- "thread_id": draft_email_data.get("threadId", ""),
123
- "from": headers.get("from", ""),
124
- "date": headers.get("internaldate", ""),
125
- "subject": headers.get("subject", ""),
126
- "body": _clean_email_body(body_data) if body_data else "",
127
- }
128
- except Exception as e:
129
- print(f"Error parsing draft email {draft_email_data.get('id', 'unknown')}: {e}")
130
- return None
131
-
132
-
133
- def get_draft_url(draft_id):
134
- return f"https://mail.google.com/mail/u/0/#drafts/{draft_id}"
135
-
136
-
137
- def get_sent_email_url(sent_email_id):
138
- return f"https://mail.google.com/mail/u/0/#sent/{sent_email_id}"
139
-
140
-
141
- def get_email_in_trash_url(email_id):
142
- return f"https://mail.google.com/mail/u/0/#trash/{email_id}"
143
-
144
-
145
- def _get_email_body(payload: dict[str, Any]) -> Optional[str]:
146
- """
147
- Extract email body from payload.
148
-
149
- Args:
150
- payload (Dict[str, Any]): Email payload data.
151
-
152
- Returns:
153
- Optional[str]: Decoded email body or None if not found.
154
- """
155
- if "body" in payload and payload["body"].get("data"):
156
- return urlsafe_b64decode(payload["body"]["data"]).decode()
157
-
158
- for part in payload.get("parts", []):
159
- if part.get("mimeType") == "text/plain" and "data" in part["body"]:
160
- return urlsafe_b64decode(part["body"]["data"]).decode()
161
-
162
- return None
163
-
164
-
165
- def _clean_email_body(body: str) -> str:
166
- """
167
- Remove HTML tags and clean up email body text while preserving most content.
168
-
169
- Args:
170
- body (str): The raw email body text.
171
-
172
- Returns:
173
- str: Cleaned email body text.
174
- """
175
- try:
176
- # Remove HTML tags using BeautifulSoup
177
- soup = BeautifulSoup(body, "html.parser")
178
- text = soup.get_text(separator=" ")
179
-
180
- # Clean up the text
181
- text = _clean_text(text)
182
-
183
- return text.strip()
184
- except Exception as e:
185
- print(f"Error cleaning email body: {e}")
186
- return body
187
-
188
-
189
- def _clean_text(text: str) -> str:
190
- """
191
- Clean up the text while preserving most content.
192
-
193
- Args:
194
- text (str): The input text.
195
-
196
- Returns:
197
- str: Cleaned text.
198
- """
199
- # Replace multiple newlines with a single newline
200
- text = re.sub(r"\n+", "\n", text)
201
-
202
- # Replace multiple spaces with a single space
203
- text = re.sub(r"\s+", " ", text)
204
-
205
- # Remove leading/trailing whitespace from each line
206
- text = "\n".join(line.strip() for line in text.split("\n"))
207
-
208
- return text
209
-
210
-
211
- def _update_datetime(day: Day | None, time: TimeSlot | None, time_zone: str) -> dict | None:
212
- """
213
- Update the datetime for a Google Calendar event.
214
-
215
- Args:
216
- day (Day | None): The day of the event.
217
- time (TimeSlot | None): The time of the event.
218
- time_zone (str): The time zone of the event.
219
-
220
- Returns:
221
- dict | None: The updated datetime for the event.
222
- """
223
- if day and time:
224
- dt = datetime.combine(day.to_date(time_zone), time.to_time())
225
- return {"dateTime": dt.isoformat(), "timeZone": time_zone}
226
- return None
227
-
228
-
229
- def build_query_string(sender, recipient, subject, body, date_range):
230
- """
231
- Helper function to build a query string for Gmail list_emails_by_header and search_threads tools.
232
- """
233
- query = []
234
- if sender:
235
- query.append(f"from:{sender}")
236
- if recipient:
237
- query.append(f"to:{recipient}")
238
- if subject:
239
- query.append(f"subject:{subject}")
240
- if body:
241
- query.append(body)
242
- if date_range:
243
- query.append(date_range.to_date_query())
244
- return " ".join(query)
245
-
246
-
247
- def fetch_messages(service, query_string, limit):
248
- """
249
- Helper function to fetch messages from Gmail API for the list_emails_by_header tool.
250
- """
251
- response = (
252
- service.users()
253
- .messages()
254
- .list(userId="me", q=query_string, maxResults=limit or 100)
255
- .execute()
256
- )
257
- return response.get("messages", [])
258
-
259
-
260
- def remove_none_values(params: dict) -> dict:
261
- """
262
- Remove None values from a dictionary.
263
- :param params: The dictionary to clean
264
- :return: A new dictionary with None values removed
265
- """
266
- return {k: v for k, v in params.items() if v is not None}
267
-
268
-
269
- # Drive utils
270
- def build_drive_service(token: str):
271
- """
272
- Build a Drive service object.
273
- """
274
- return build("drive", "v3", credentials=Credentials(token))
275
-
276
-
277
- # Docs utils
278
- def build_docs_service(token: str):
279
- """
280
- Build a Drive service object.
281
- """
282
- return build("docs", "v1", credentials=Credentials(token))
@@ -1,20 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: arcade_google
3
- Version: 0.1.6
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.1.6)
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)
@@ -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=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.6.dist-info/METADATA,sha256=Wt6pl-A4CN5DgC3R_1wQN6JRm3XqbaMFypUvmTyyOL4,796
10
- arcade_google-0.1.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
11
- arcade_google-0.1.6.dist-info/RECORD,,