google-api-client-wrapper 1.1.2__tar.gz → 1.1.4__tar.gz
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.
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/PKG-INFO +1 -1
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/PKG-INFO +1 -1
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/types.py +47 -41
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/utils.py +69 -68
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/types.py +21 -7
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/pyproject.toml +1 -1
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/LICENSE +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/README.md +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/SOURCES.txt +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/dependency_links.txt +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/requires.txt +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/top_level.txt +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/__init__.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/__init__.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/__init__.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/api_service.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/constants.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/exceptions.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/query_builder.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/__init__.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/api_service.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/constants.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/exceptions.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/query_builder.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/types.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/utils.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/__init__.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/api_service.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/constants.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/exceptions.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/query_builder.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/utils.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/__init__.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/api_service.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/constants.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/exceptions.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/query_builder.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/types.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/utils.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/user_client.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/__init__.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/datetime.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/validation.py +0 -0
- {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-api-client-wrapper
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: A comprehensive Python wrapper for Google APIs, providing clean and intuitive access to Gmail, Google Drive, Google Calendar, and Google Tasks services.
|
|
5
5
|
Author-email: Dagmawi Molla <dagmawishewadeg@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-api-client-wrapper
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
4
4
|
Summary: A comprehensive Python wrapper for Google APIs, providing clean and intuitive access to Gmail, Google Drive, Google Calendar, and Google Tasks services.
|
|
5
5
|
Author-email: Dagmawi Molla <dagmawishewadeg@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -50,9 +50,12 @@ class CalendarEvent(BaseModel):
|
|
|
50
50
|
start: Optional[datetime] = Field(None, description="The start time of the event as a datetime object")
|
|
51
51
|
end: Optional[datetime] = Field(None, description="The end time of the event as a datetime object")
|
|
52
52
|
html_link: Optional[str] = Field(None, description="A hyperlink to the event on Google Calendar")
|
|
53
|
-
attendees: List[Attendee] = Field(default_factory=list,
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
attendees: List[Attendee] = Field(default_factory=list,
|
|
54
|
+
description="A list of Attendee objects representing the people invited to the event")
|
|
55
|
+
recurrence: List[str] = Field(default_factory=list,
|
|
56
|
+
description="A list of strings defining the recurrence rules for the event in RFC 5545 format")
|
|
57
|
+
recurring_event_id: Optional[str] = Field(None,
|
|
58
|
+
description="The ID of the recurring event if this event is part of a series")
|
|
56
59
|
creator: Optional[str] = Field(None, description="The creator of the event")
|
|
57
60
|
organizer: Optional[str] = Field(None, description="The organizer of the event")
|
|
58
61
|
status: Optional[str] = Field("confirmed", description="The status of the event (confirmed, tentative, cancelled)")
|
|
@@ -164,7 +167,7 @@ class CalendarEvent(BaseModel):
|
|
|
164
167
|
Dictionary representation suitable for API calls.
|
|
165
168
|
"""
|
|
166
169
|
event_dict = {}
|
|
167
|
-
|
|
170
|
+
|
|
168
171
|
if self.event_id:
|
|
169
172
|
event_dict["id"] = self.event_id
|
|
170
173
|
if self.summary:
|
|
@@ -173,6 +176,8 @@ class CalendarEvent(BaseModel):
|
|
|
173
176
|
event_dict["description"] = self.description
|
|
174
177
|
if self.location:
|
|
175
178
|
event_dict["location"] = self.location
|
|
179
|
+
if self.start:
|
|
180
|
+
event_dict["time"] = convert_datetime_to_readable(self.start, self.end)
|
|
176
181
|
if self.html_link:
|
|
177
182
|
event_dict["htmlLink"] = self.html_link
|
|
178
183
|
if self.recurrence:
|
|
@@ -185,10 +190,10 @@ class CalendarEvent(BaseModel):
|
|
|
185
190
|
event_dict["organizer"] = self.organizer
|
|
186
191
|
if self.status:
|
|
187
192
|
event_dict["status"] = self.status
|
|
188
|
-
|
|
193
|
+
|
|
189
194
|
if self.attendees:
|
|
190
195
|
event_dict["attendees"] = [attendee.to_dict() for attendee in self.attendees]
|
|
191
|
-
|
|
196
|
+
|
|
192
197
|
return event_dict
|
|
193
198
|
|
|
194
199
|
def __repr__(self):
|
|
@@ -210,46 +215,45 @@ class TimeSlot(BaseModel):
|
|
|
210
215
|
"""
|
|
211
216
|
start: datetime = Field(..., description="Start datetime of the time slot")
|
|
212
217
|
end: datetime = Field(..., description="End datetime of the time slot")
|
|
213
|
-
|
|
218
|
+
|
|
214
219
|
def model_post_init(self, __context__: Any):
|
|
215
220
|
if self.start >= self.end:
|
|
216
221
|
raise ValueError("Start time must be before end time")
|
|
217
|
-
|
|
222
|
+
|
|
218
223
|
def duration(self) -> int:
|
|
219
224
|
"""
|
|
220
225
|
Calculate the duration of the time slot in minutes.
|
|
221
|
-
|
|
226
|
+
|
|
222
227
|
Returns:
|
|
223
228
|
Duration in minutes
|
|
224
229
|
"""
|
|
225
230
|
return int((self.end - self.start).total_seconds() / 60)
|
|
226
|
-
|
|
231
|
+
|
|
227
232
|
def overlaps_with(self, other: "TimeSlot") -> bool:
|
|
228
233
|
"""
|
|
229
234
|
Check if this time slot overlaps with another time slot.
|
|
230
|
-
|
|
235
|
+
|
|
231
236
|
Args:
|
|
232
237
|
other: Another TimeSlot to check for overlap
|
|
233
|
-
|
|
238
|
+
|
|
234
239
|
Returns:
|
|
235
240
|
True if the time slots overlap, False otherwise
|
|
236
241
|
"""
|
|
237
242
|
return self.start < other.end and self.end > other.start
|
|
238
|
-
|
|
243
|
+
|
|
239
244
|
def contains_time(self, time_point: datetime) -> bool:
|
|
240
245
|
"""
|
|
241
246
|
Check if a specific datetime falls within this time slot.
|
|
242
|
-
|
|
247
|
+
|
|
243
248
|
Args:
|
|
244
249
|
time_point: Datetime to check
|
|
245
|
-
|
|
250
|
+
|
|
246
251
|
Returns:
|
|
247
252
|
True if the time point is within this slot, False otherwise
|
|
248
253
|
"""
|
|
249
254
|
return self.start <= time_point < self.end
|
|
250
|
-
|
|
251
|
-
def __str__(self):
|
|
252
255
|
|
|
256
|
+
def __str__(self):
|
|
253
257
|
return convert_datetime_to_readable(
|
|
254
258
|
convert_datetime_to_local_timezone(self.start),
|
|
255
259
|
convert_datetime_to_local_timezone(self.end)
|
|
@@ -262,86 +266,88 @@ class FreeBusyResponse(BaseModel):
|
|
|
262
266
|
"""
|
|
263
267
|
start: datetime = Field(..., description="Start time of the query")
|
|
264
268
|
end: datetime = Field(..., description="End time of the query")
|
|
265
|
-
calendars: Dict[str, List[TimeSlot]] = Field(default_factory=dict,
|
|
266
|
-
|
|
267
|
-
|
|
269
|
+
calendars: Dict[str, List[TimeSlot]] = Field(default_factory=dict,
|
|
270
|
+
description="Dictionary mapping calendar IDs to their busy periods")
|
|
271
|
+
errors: Dict[str, str] = Field(default_factory=dict,
|
|
272
|
+
description="Dictionary mapping calendar IDs to any errors encountered")
|
|
273
|
+
|
|
268
274
|
def get_busy_periods(self, calendar_id: str = "primary") -> List[TimeSlot]:
|
|
269
275
|
"""
|
|
270
276
|
Get busy periods for a specific calendar.
|
|
271
|
-
|
|
277
|
+
|
|
272
278
|
Args:
|
|
273
279
|
calendar_id: Calendar ID to get busy periods for
|
|
274
|
-
|
|
280
|
+
|
|
275
281
|
Returns:
|
|
276
282
|
List of TimeSlot objects representing busy periods
|
|
277
283
|
"""
|
|
278
284
|
return self.calendars.get(calendar_id, [])
|
|
279
|
-
|
|
285
|
+
|
|
280
286
|
def is_time_free(self, time_point: datetime, calendar_id: str = "primary") -> bool:
|
|
281
287
|
"""
|
|
282
288
|
Check if a specific time is free in the given calendar.
|
|
283
|
-
|
|
289
|
+
|
|
284
290
|
Args:
|
|
285
291
|
time_point: Datetime to check
|
|
286
292
|
calendar_id: Calendar ID to check
|
|
287
|
-
|
|
293
|
+
|
|
288
294
|
Returns:
|
|
289
295
|
True if the time is free, False if busy
|
|
290
296
|
"""
|
|
291
297
|
if not (self.start <= time_point <= self.end):
|
|
292
298
|
raise ValueError("Time point is outside the queried range")
|
|
293
|
-
|
|
299
|
+
|
|
294
300
|
busy_periods = self.get_busy_periods(calendar_id)
|
|
295
301
|
return not any(period.contains_time(time_point) for period in busy_periods)
|
|
296
|
-
|
|
302
|
+
|
|
297
303
|
def is_slot_free(self, slot: TimeSlot, calendar_id: str = "primary") -> bool:
|
|
298
304
|
"""
|
|
299
305
|
Check if an entire time slot is free in the given calendar.
|
|
300
|
-
|
|
306
|
+
|
|
301
307
|
Args:
|
|
302
308
|
slot: TimeSlot to check
|
|
303
309
|
calendar_id: Calendar ID to check
|
|
304
|
-
|
|
310
|
+
|
|
305
311
|
Returns:
|
|
306
312
|
True if the entire slot is free, False if any part is busy
|
|
307
313
|
"""
|
|
308
314
|
busy_periods = self.get_busy_periods(calendar_id)
|
|
309
315
|
return not any(period.overlaps_with(slot) for period in busy_periods)
|
|
310
|
-
|
|
316
|
+
|
|
311
317
|
def get_free_slots(self, duration_minutes: int = 60, calendar_id: str = "primary") -> List[TimeSlot]:
|
|
312
318
|
"""
|
|
313
319
|
Get all free time slots of a specified duration within the queried range.
|
|
314
|
-
|
|
320
|
+
|
|
315
321
|
Args:
|
|
316
322
|
duration_minutes: Minimum duration for free slots in minutes
|
|
317
323
|
calendar_id: Calendar ID to get free slots for
|
|
318
|
-
|
|
324
|
+
|
|
319
325
|
Returns:
|
|
320
326
|
List of TimeSlot objects representing available time slots
|
|
321
327
|
"""
|
|
322
328
|
from ...utils.datetime import current_datetime_local_timezone
|
|
323
|
-
|
|
329
|
+
|
|
324
330
|
busy_periods = sorted(self.get_busy_periods(calendar_id), key=lambda x: x.start)
|
|
325
331
|
free_slots = []
|
|
326
|
-
|
|
332
|
+
|
|
327
333
|
# Start from the beginning of the range or current time (whichever is later)
|
|
328
334
|
current_time = max(self.start, current_datetime_local_timezone())
|
|
329
|
-
|
|
335
|
+
|
|
330
336
|
# Check time before first busy period
|
|
331
337
|
if busy_periods and current_time < busy_periods[0].start:
|
|
332
338
|
gap_duration = (busy_periods[0].start - current_time).total_seconds() / 60
|
|
333
339
|
if gap_duration >= duration_minutes:
|
|
334
340
|
free_slots.append(TimeSlot(start=current_time, end=busy_periods[0].start))
|
|
335
|
-
|
|
341
|
+
|
|
336
342
|
# Check gaps between busy periods
|
|
337
343
|
for i in range(len(busy_periods) - 1):
|
|
338
344
|
gap_start = busy_periods[i].end
|
|
339
345
|
gap_end = busy_periods[i + 1].start
|
|
340
346
|
gap_duration = (gap_end - gap_start).total_seconds() / 60
|
|
341
|
-
|
|
347
|
+
|
|
342
348
|
if gap_duration >= duration_minutes:
|
|
343
349
|
free_slots.append(TimeSlot(start=gap_start, end=gap_end))
|
|
344
|
-
|
|
350
|
+
|
|
345
351
|
# Check time after last busy period
|
|
346
352
|
if busy_periods:
|
|
347
353
|
gap_start = busy_periods[-1].end
|
|
@@ -354,13 +360,13 @@ class FreeBusyResponse(BaseModel):
|
|
|
354
360
|
gap_duration = (self.end - current_time).total_seconds() / 60
|
|
355
361
|
if gap_duration >= duration_minutes:
|
|
356
362
|
free_slots.append(TimeSlot(start=current_time, end=self.end))
|
|
357
|
-
|
|
363
|
+
|
|
358
364
|
return free_slots
|
|
359
|
-
|
|
365
|
+
|
|
360
366
|
def has_errors(self) -> bool:
|
|
361
367
|
"""
|
|
362
368
|
Check if there were any errors in the freebusy query.
|
|
363
|
-
|
|
369
|
+
|
|
364
370
|
Returns:
|
|
365
371
|
True if there were errors, False otherwise
|
|
366
372
|
"""
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from typing import Optional, List, Dict, Any
|
|
3
3
|
|
|
4
|
+
import tzlocal
|
|
5
|
+
|
|
4
6
|
from .types import CalendarEvent, Attendee, TimeSlot, FreeBusyResponse
|
|
5
7
|
from .constants import (
|
|
6
8
|
MAX_SUMMARY_LENGTH, MAX_DESCRIPTION_LENGTH, MAX_LOCATION_LENGTH,
|
|
@@ -8,7 +10,6 @@ from .constants import (
|
|
|
8
10
|
)
|
|
9
11
|
from ...utils.datetime import convert_datetime_to_iso
|
|
10
12
|
|
|
11
|
-
|
|
12
13
|
# Import from shared utilities
|
|
13
14
|
from ...utils.validation import is_valid_email, validate_text_field, sanitize_header_value
|
|
14
15
|
|
|
@@ -22,16 +23,16 @@ def validate_datetime_range(start: Optional[datetime], end: Optional[datetime])
|
|
|
22
23
|
def parse_datetime_from_api(datetime_data: Dict[str, Any]) -> Optional[datetime]:
|
|
23
24
|
"""
|
|
24
25
|
Parse datetime from Google Calendar API response.
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
Args:
|
|
27
28
|
datetime_data: Dictionary containing dateTime or date fields
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
Returns:
|
|
30
31
|
Parsed datetime object or None if parsing fails
|
|
31
32
|
"""
|
|
32
33
|
if not datetime_data:
|
|
33
34
|
return None
|
|
34
|
-
|
|
35
|
+
|
|
35
36
|
try:
|
|
36
37
|
if datetime_data.get("dateTime"):
|
|
37
38
|
# Handle timezone-aware datetime
|
|
@@ -44,22 +45,22 @@ def parse_datetime_from_api(datetime_data: Dict[str, Any]) -> Optional[datetime]
|
|
|
44
45
|
return datetime.strptime(datetime_data["date"], "%Y-%m-%d")
|
|
45
46
|
except (ValueError, TypeError):
|
|
46
47
|
pass
|
|
47
|
-
|
|
48
|
+
|
|
48
49
|
return None
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def parse_attendees_from_api(attendees_data: List[Dict[str, Any]]) -> List[Attendee]:
|
|
52
53
|
"""
|
|
53
54
|
Parse attendees from Google Calendar API response.
|
|
54
|
-
|
|
55
|
+
|
|
55
56
|
Args:
|
|
56
57
|
attendees_data: List of attendee dictionaries from API
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
Returns:
|
|
59
60
|
List of Attendee objects
|
|
60
61
|
"""
|
|
61
62
|
attendees = []
|
|
62
|
-
|
|
63
|
+
|
|
63
64
|
for attendee_data in attendees_data:
|
|
64
65
|
email = attendee_data.get("email")
|
|
65
66
|
if email and is_valid_email(email):
|
|
@@ -67,7 +68,7 @@ def parse_attendees_from_api(attendees_data: List[Dict[str, Any]]) -> List[Atten
|
|
|
67
68
|
response_status = attendee_data.get("responseStatus")
|
|
68
69
|
if response_status and response_status not in VALID_RESPONSE_STATUSES:
|
|
69
70
|
response_status = None
|
|
70
|
-
|
|
71
|
+
|
|
71
72
|
attendees.append(Attendee(
|
|
72
73
|
email=email,
|
|
73
74
|
display_name=attendee_data.get("displayName"),
|
|
@@ -75,17 +76,17 @@ def parse_attendees_from_api(attendees_data: List[Dict[str, Any]]) -> List[Atten
|
|
|
75
76
|
))
|
|
76
77
|
except ValueError:
|
|
77
78
|
pass
|
|
78
|
-
|
|
79
|
+
|
|
79
80
|
return attendees
|
|
80
81
|
|
|
81
82
|
|
|
82
83
|
def from_google_event(google_event: Dict[str, Any]) -> CalendarEvent:
|
|
83
84
|
"""
|
|
84
85
|
Create a CalendarEvent instance from a Google Calendar API response.
|
|
85
|
-
|
|
86
|
+
|
|
86
87
|
Args:
|
|
87
88
|
google_event: Dictionary containing event data from Google Calendar API
|
|
88
|
-
|
|
89
|
+
|
|
89
90
|
Returns:
|
|
90
91
|
CalendarEvent instance populated with the data from the dictionary
|
|
91
92
|
"""
|
|
@@ -96,31 +97,31 @@ def from_google_event(google_event: Dict[str, Any]) -> CalendarEvent:
|
|
|
96
97
|
description = google_event.get("description", "").strip() if google_event.get("description") else None
|
|
97
98
|
location = google_event.get("location", "").strip() if google_event.get("location") else None
|
|
98
99
|
html_link = google_event.get("htmlLink")
|
|
99
|
-
|
|
100
|
+
|
|
100
101
|
# Parse datetimes
|
|
101
102
|
start = parse_datetime_from_api(google_event.get("start", {}))
|
|
102
103
|
end = parse_datetime_from_api(google_event.get("end", {}))
|
|
103
|
-
|
|
104
|
+
|
|
104
105
|
# Parse attendees
|
|
105
106
|
attendees_data = google_event.get("attendees", [])
|
|
106
107
|
attendees = parse_attendees_from_api(attendees_data)
|
|
107
|
-
|
|
108
|
+
|
|
108
109
|
# Parse recurrence
|
|
109
110
|
recurrence = google_event.get("recurrence", [])
|
|
110
111
|
recurring_event_id = google_event.get("recurringEventId")
|
|
111
|
-
|
|
112
|
+
|
|
112
113
|
# Parse creator and organizer
|
|
113
114
|
creator_data = google_event.get("creator", {})
|
|
114
115
|
creator = creator_data.get("email") if creator_data else None
|
|
115
|
-
|
|
116
|
+
|
|
116
117
|
organizer_data = google_event.get("organizer", {})
|
|
117
118
|
organizer = organizer_data.get("email") if organizer_data else None
|
|
118
|
-
|
|
119
|
+
|
|
119
120
|
# Parse status
|
|
120
121
|
status = google_event.get("status", "confirmed")
|
|
121
122
|
if status not in VALID_EVENT_STATUSES:
|
|
122
123
|
status = "confirmed"
|
|
123
|
-
|
|
124
|
+
|
|
124
125
|
# Create and return the event
|
|
125
126
|
event = CalendarEvent(
|
|
126
127
|
event_id=event_id,
|
|
@@ -137,37 +138,37 @@ def from_google_event(google_event: Dict[str, Any]) -> CalendarEvent:
|
|
|
137
138
|
organizer=organizer,
|
|
138
139
|
status=status
|
|
139
140
|
)
|
|
140
|
-
|
|
141
|
+
|
|
141
142
|
return event
|
|
142
|
-
|
|
143
|
+
|
|
143
144
|
except Exception:
|
|
144
145
|
raise ValueError("Invalid event data - failed to parse calendar event")
|
|
145
146
|
|
|
146
147
|
|
|
147
148
|
def create_event_body(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
start: datetime,
|
|
150
|
+
end: datetime,
|
|
151
|
+
summary: str = None,
|
|
152
|
+
description: str = None,
|
|
153
|
+
location: str = None,
|
|
154
|
+
attendees: List[Attendee] = None,
|
|
155
|
+
recurrence: List[str] = None
|
|
155
156
|
) -> Dict[str, Any]:
|
|
156
157
|
"""
|
|
157
158
|
Create event body dictionary for Google Calendar API.
|
|
158
|
-
|
|
159
|
+
|
|
159
160
|
Args:
|
|
160
161
|
start: Event start datetime
|
|
161
|
-
end: Event end datetime
|
|
162
|
+
end: Event end datetime
|
|
162
163
|
summary: Event summary/title
|
|
163
164
|
description: Event description
|
|
164
165
|
location: Event location
|
|
165
166
|
attendees: List of attendees
|
|
166
167
|
recurrence: List of recurrence rules
|
|
167
|
-
|
|
168
|
+
|
|
168
169
|
Returns:
|
|
169
170
|
Dictionary suitable for Calendar API requests
|
|
170
|
-
|
|
171
|
+
|
|
171
172
|
Raises:
|
|
172
173
|
ValueError: If required fields are invalid
|
|
173
174
|
"""
|
|
@@ -175,19 +176,19 @@ def create_event_body(
|
|
|
175
176
|
raise ValueError("Event must have both start and end times")
|
|
176
177
|
if start >= end:
|
|
177
178
|
raise ValueError("Event start time must be before end time")
|
|
178
|
-
|
|
179
|
+
|
|
179
180
|
# Validate text fields
|
|
180
181
|
validate_text_field(summary, MAX_SUMMARY_LENGTH, "summary")
|
|
181
182
|
validate_text_field(description, MAX_DESCRIPTION_LENGTH, "description")
|
|
182
183
|
validate_text_field(location, MAX_LOCATION_LENGTH, "location")
|
|
183
|
-
|
|
184
|
+
|
|
184
185
|
# Build event body
|
|
185
186
|
event_body = {
|
|
186
187
|
'summary': summary or "New Event",
|
|
187
|
-
'start': {'dateTime': convert_datetime_to_iso(start)},
|
|
188
|
-
'end': {'dateTime': convert_datetime_to_iso(end)}
|
|
188
|
+
'start': {'dateTime': convert_datetime_to_iso(start), 'timeZone': tzlocal.get_localzone_name()},
|
|
189
|
+
'end': {'dateTime': convert_datetime_to_iso(end), 'timeZone': tzlocal.get_localzone_name()}
|
|
189
190
|
}
|
|
190
|
-
|
|
191
|
+
|
|
191
192
|
# Add optional fields
|
|
192
193
|
if description:
|
|
193
194
|
event_body['description'] = sanitize_header_value(description)
|
|
@@ -197,50 +198,50 @@ def create_event_body(
|
|
|
197
198
|
event_body['attendees'] = [attendee.to_dict() for attendee in attendees]
|
|
198
199
|
if recurrence:
|
|
199
200
|
event_body['recurrence'] = recurrence
|
|
200
|
-
|
|
201
|
+
|
|
201
202
|
return event_body
|
|
202
203
|
|
|
203
204
|
|
|
204
205
|
def parse_freebusy_response(freebusy_data: Dict[str, Any]) -> FreeBusyResponse:
|
|
205
206
|
"""
|
|
206
207
|
Parse a freebusy response from Google Calendar API.
|
|
207
|
-
|
|
208
|
+
|
|
208
209
|
Args:
|
|
209
210
|
freebusy_data: Dictionary containing freebusy response from API
|
|
210
|
-
|
|
211
|
+
|
|
211
212
|
Returns:
|
|
212
213
|
FreeBusyResponse object with parsed data
|
|
213
|
-
|
|
214
|
+
|
|
214
215
|
Raises:
|
|
215
216
|
ValueError: If the response data is invalid
|
|
216
217
|
"""
|
|
217
218
|
if not freebusy_data:
|
|
218
219
|
raise ValueError("Empty freebusy response data")
|
|
219
|
-
|
|
220
|
+
|
|
220
221
|
try:
|
|
221
222
|
# Parse time range
|
|
222
223
|
time_min = freebusy_data.get("timeMin")
|
|
223
224
|
time_max = freebusy_data.get("timeMax")
|
|
224
|
-
|
|
225
|
+
|
|
225
226
|
if not time_min or not time_max:
|
|
226
227
|
raise ValueError("Missing timeMin or timeMax in freebusy response")
|
|
227
|
-
|
|
228
|
+
|
|
228
229
|
# Parse start and end times
|
|
229
230
|
start = datetime.fromisoformat(time_min.replace('Z', '+00:00'))
|
|
230
231
|
end = datetime.fromisoformat(time_max.replace('Z', '+00:00'))
|
|
231
|
-
|
|
232
|
+
|
|
232
233
|
# Parse calendar busy periods
|
|
233
234
|
calendars = {}
|
|
234
235
|
calendars_data = freebusy_data.get("calendars", {})
|
|
235
|
-
|
|
236
|
+
|
|
236
237
|
for calendar_id, calendar_data in calendars_data.items():
|
|
237
238
|
busy_periods = []
|
|
238
239
|
busy_data = calendar_data.get("busy", [])
|
|
239
|
-
|
|
240
|
+
|
|
240
241
|
for busy_period in busy_data:
|
|
241
242
|
period_start_str = busy_period.get("start")
|
|
242
243
|
period_end_str = busy_period.get("end")
|
|
243
|
-
|
|
244
|
+
|
|
244
245
|
if period_start_str and period_end_str:
|
|
245
246
|
try:
|
|
246
247
|
period_start = datetime.fromisoformat(period_start_str.replace('Z', '+00:00'))
|
|
@@ -248,27 +249,27 @@ def parse_freebusy_response(freebusy_data: Dict[str, Any]) -> FreeBusyResponse:
|
|
|
248
249
|
busy_periods.append(TimeSlot(start=period_start, end=period_end))
|
|
249
250
|
except (ValueError, TypeError):
|
|
250
251
|
continue
|
|
251
|
-
|
|
252
|
+
|
|
252
253
|
calendars[calendar_id] = busy_periods
|
|
253
|
-
|
|
254
|
+
|
|
254
255
|
# Parse errors
|
|
255
256
|
errors = {}
|
|
256
257
|
errors_data = freebusy_data.get("errors", {})
|
|
257
|
-
|
|
258
|
+
|
|
258
259
|
for calendar_id, error_data in errors_data.items():
|
|
259
260
|
if isinstance(error_data, list) and error_data:
|
|
260
261
|
error_reason = error_data[0].get("reason", "Unknown error")
|
|
261
262
|
errors[calendar_id] = error_reason
|
|
262
263
|
elif isinstance(error_data, str):
|
|
263
264
|
errors[calendar_id] = error_data
|
|
264
|
-
|
|
265
|
+
|
|
265
266
|
return FreeBusyResponse(
|
|
266
267
|
start=start,
|
|
267
268
|
end=end,
|
|
268
269
|
calendars=calendars,
|
|
269
270
|
errors=errors
|
|
270
271
|
)
|
|
271
|
-
|
|
272
|
+
|
|
272
273
|
except Exception as e:
|
|
273
274
|
raise ValueError(f"Failed to parse freebusy response: {str(e)}")
|
|
274
275
|
|
|
@@ -276,23 +277,23 @@ def parse_freebusy_response(freebusy_data: Dict[str, Any]) -> FreeBusyResponse:
|
|
|
276
277
|
def merge_overlapping_time_slots(time_slots: List[TimeSlot]) -> List[TimeSlot]:
|
|
277
278
|
"""
|
|
278
279
|
Merge overlapping time slots into consolidated periods.
|
|
279
|
-
|
|
280
|
+
|
|
280
281
|
Args:
|
|
281
282
|
time_slots: List of TimeSlot objects that may overlap
|
|
282
|
-
|
|
283
|
+
|
|
283
284
|
Returns:
|
|
284
285
|
List of merged TimeSlot objects with no overlaps
|
|
285
286
|
"""
|
|
286
287
|
if not time_slots:
|
|
287
288
|
return []
|
|
288
|
-
|
|
289
|
+
|
|
289
290
|
# Sort by start time
|
|
290
291
|
sorted_slots = sorted(time_slots, key=lambda x: x.start)
|
|
291
292
|
merged = [sorted_slots[0]]
|
|
292
|
-
|
|
293
|
+
|
|
293
294
|
for current in sorted_slots[1:]:
|
|
294
295
|
last_merged = merged[-1]
|
|
295
|
-
|
|
296
|
+
|
|
296
297
|
# Check if current slot overlaps with the last merged slot
|
|
297
298
|
if current.start <= last_merged.end:
|
|
298
299
|
# Merge by extending the end time if necessary
|
|
@@ -301,38 +302,38 @@ def merge_overlapping_time_slots(time_slots: List[TimeSlot]) -> List[TimeSlot]:
|
|
|
301
302
|
else:
|
|
302
303
|
# No overlap, add as new slot
|
|
303
304
|
merged.append(current)
|
|
304
|
-
|
|
305
|
+
|
|
305
306
|
return merged
|
|
306
307
|
|
|
307
308
|
|
|
308
309
|
def validate_freebusy_request(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
310
|
+
start: datetime,
|
|
311
|
+
end: datetime,
|
|
312
|
+
calendar_ids: List[str]
|
|
312
313
|
) -> None:
|
|
313
314
|
"""
|
|
314
315
|
Validate parameters for a freebusy request.
|
|
315
|
-
|
|
316
|
+
|
|
316
317
|
Args:
|
|
317
318
|
start: Start datetime for the query
|
|
318
319
|
end: End datetime for the query
|
|
319
320
|
calendar_ids: List of calendar IDs to query
|
|
320
|
-
|
|
321
|
+
|
|
321
322
|
Raises:
|
|
322
323
|
ValueError: If any parameter is invalid
|
|
323
324
|
"""
|
|
324
325
|
from .constants import MAX_FREEBUSY_DAYS_RANGE, MAX_CALENDARS_PER_FREEBUSY_QUERY
|
|
325
|
-
|
|
326
|
+
|
|
326
327
|
if start >= end:
|
|
327
328
|
raise ValueError("Start time must be before end time")
|
|
328
|
-
|
|
329
|
+
|
|
329
330
|
# Check maximum time range (Google's API limit)
|
|
330
331
|
days_diff = (end - start).days
|
|
331
332
|
if days_diff > MAX_FREEBUSY_DAYS_RANGE:
|
|
332
333
|
raise ValueError(f"Time range cannot exceed {MAX_FREEBUSY_DAYS_RANGE} days")
|
|
333
|
-
|
|
334
|
+
|
|
334
335
|
if not calendar_ids:
|
|
335
336
|
raise ValueError("At least one calendar ID must be specified")
|
|
336
|
-
|
|
337
|
+
|
|
337
338
|
if len(calendar_ids) > MAX_CALENDARS_PER_FREEBUSY_QUERY:
|
|
338
339
|
raise ValueError(f"Cannot query more than {MAX_CALENDARS_PER_FREEBUSY_QUERY} calendars at once")
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
from typing import Optional, List
|
|
3
2
|
from datetime import datetime
|
|
4
3
|
from pydantic import BaseModel, Field
|
|
@@ -14,7 +13,6 @@ class EmailAddress(BaseModel):
|
|
|
14
13
|
email: str = Field(..., description="The email address")
|
|
15
14
|
name: Optional[str] = Field(None, description="The display name")
|
|
16
15
|
|
|
17
|
-
|
|
18
16
|
def to_dict(self) -> dict:
|
|
19
17
|
"""
|
|
20
18
|
Converts the EmailAddress instance to a dictionary representation.
|
|
@@ -42,7 +40,6 @@ class EmailAttachment(BaseModel):
|
|
|
42
40
|
attachment_id: str = Field(..., description="The unique identifier for the attachment in Gmail")
|
|
43
41
|
message_id: str = Field(..., description="The message id of the message the attachment is attached to")
|
|
44
42
|
|
|
45
|
-
|
|
46
43
|
def to_dict(self) -> dict:
|
|
47
44
|
"""
|
|
48
45
|
Converts the EmailAttachment instance to a dictionary representation.
|
|
@@ -87,7 +84,8 @@ class EmailThread(BaseModel):
|
|
|
87
84
|
Represents a Gmail thread containing multiple related messages.
|
|
88
85
|
"""
|
|
89
86
|
thread_id: Optional[str] = Field(None, description="Unique identifier for the thread")
|
|
90
|
-
messages: List["EmailMessage"] = Field(default_factory=list,
|
|
87
|
+
messages: List["EmailMessage"] = Field(default_factory=list,
|
|
88
|
+
description="List of EmailMessage objects in this thread")
|
|
91
89
|
snippet: Optional[str] = Field(None, description="A short snippet of the thread content")
|
|
92
90
|
history_id: Optional[str] = Field(None, description="The history ID of the thread")
|
|
93
91
|
|
|
@@ -129,7 +127,7 @@ class EmailThread(BaseModel):
|
|
|
129
127
|
participants.add((message.sender.email, message.sender.name))
|
|
130
128
|
for recipient in message.recipients + message.cc_recipients + message.bcc_recipients:
|
|
131
129
|
participants.add((recipient.email, recipient.name))
|
|
132
|
-
|
|
130
|
+
|
|
133
131
|
return [EmailAddress(email=email, name=name) for email, name in participants]
|
|
134
132
|
|
|
135
133
|
def __repr__(self):
|
|
@@ -159,9 +157,11 @@ class EmailMessage(BaseModel):
|
|
|
159
157
|
attachments: List[EmailAttachment] = Field(default_factory=list, description="List of attachments in the email")
|
|
160
158
|
|
|
161
159
|
sender: Optional[EmailAddress] = Field(None, description="The sender's email address information")
|
|
162
|
-
recipients: List[EmailAddress] = Field(default_factory=list,
|
|
160
|
+
recipients: List[EmailAddress] = Field(default_factory=list,
|
|
161
|
+
description="List of recipient email addresses (To field)")
|
|
163
162
|
cc_recipients: List[EmailAddress] = Field(default_factory=list, description="List of CC recipient email addresses")
|
|
164
|
-
bcc_recipients: List[EmailAddress] = Field(default_factory=list,
|
|
163
|
+
bcc_recipients: List[EmailAddress] = Field(default_factory=list,
|
|
164
|
+
description="List of BCC recipient email addresses")
|
|
165
165
|
|
|
166
166
|
date_time: Optional[datetime] = Field(None, description="When the message was sent or received")
|
|
167
167
|
|
|
@@ -238,6 +238,20 @@ class EmailMessage(BaseModel):
|
|
|
238
238
|
"""
|
|
239
239
|
return label in self.labels
|
|
240
240
|
|
|
241
|
+
def to_dict(self):
|
|
242
|
+
return {
|
|
243
|
+
"message_id": self.message_id,
|
|
244
|
+
"thread_id": self.thread_id,
|
|
245
|
+
"sender": self.sender.email,
|
|
246
|
+
"recipients": [recipient.email for recipient in self.recipients],
|
|
247
|
+
"date_time": convert_datetime_to_readable(self.date_time),
|
|
248
|
+
"subject": self.subject,
|
|
249
|
+
"labels": self.labels,
|
|
250
|
+
"snippet": self.snippet.encode("ascii", "ignore").decode("ascii"),
|
|
251
|
+
"body": self.get_plain_text_content().encode("ascii", "ignore").decode("ascii"),
|
|
252
|
+
"attachments": [attachment.to_dict() for attachment in self.attachments],
|
|
253
|
+
}
|
|
254
|
+
|
|
241
255
|
def __repr__(self):
|
|
242
256
|
return (
|
|
243
257
|
f"Subject: {self.subject!r}\n"
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "google-api-client-wrapper"
|
|
9
|
-
version = "1.1.
|
|
9
|
+
version = "1.1.4"
|
|
10
10
|
description = "A comprehensive Python wrapper for Google APIs, providing clean and intuitive access to Gmail, Google Drive, Google Calendar, and Google Tasks services."
|
|
11
11
|
readme = {file = "README.md", content-type = "text/markdown"}
|
|
12
12
|
authors = [{name = "Dagmawi Molla", email = "dagmawishewadeg@gmail.com"}]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/user_client.py
RENAMED
|
File without changes
|
{google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/__init__.py
RENAMED
|
File without changes
|
{google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/datetime.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|