google-api-client-wrapper 1.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.
- google_api_client_wrapper-1.0.0.dist-info/METADATA +103 -0
- google_api_client_wrapper-1.0.0.dist-info/RECORD +39 -0
- google_api_client_wrapper-1.0.0.dist-info/WHEEL +5 -0
- google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE +21 -0
- google_api_client_wrapper-1.0.0.dist-info/top_level.txt +1 -0
- google_client/__init__.py +6 -0
- google_client/services/__init__.py +13 -0
- google_client/services/calendar/__init__.py +14 -0
- google_client/services/calendar/api_service.py +454 -0
- google_client/services/calendar/constants.py +48 -0
- google_client/services/calendar/exceptions.py +35 -0
- google_client/services/calendar/query_builder.py +314 -0
- google_client/services/calendar/types.py +403 -0
- google_client/services/calendar/utils.py +338 -0
- google_client/services/drive/__init__.py +13 -0
- google_client/services/drive/api_service.py +1133 -0
- google_client/services/drive/constants.py +37 -0
- google_client/services/drive/exceptions.py +60 -0
- google_client/services/drive/query_builder.py +385 -0
- google_client/services/drive/types.py +242 -0
- google_client/services/drive/utils.py +392 -0
- google_client/services/gmail/__init__.py +16 -0
- google_client/services/gmail/api_service.py +715 -0
- google_client/services/gmail/constants.py +6 -0
- google_client/services/gmail/exceptions.py +45 -0
- google_client/services/gmail/query_builder.py +408 -0
- google_client/services/gmail/types.py +285 -0
- google_client/services/gmail/utils.py +426 -0
- google_client/services/tasks/__init__.py +12 -0
- google_client/services/tasks/api_service.py +561 -0
- google_client/services/tasks/constants.py +32 -0
- google_client/services/tasks/exceptions.py +35 -0
- google_client/services/tasks/query_builder.py +324 -0
- google_client/services/tasks/types.py +156 -0
- google_client/services/tasks/utils.py +224 -0
- google_client/user_client.py +208 -0
- google_client/utils/__init__.py +0 -0
- google_client/utils/datetime.py +144 -0
- google_client/utils/validation.py +71 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
from datetime import datetime, date, time
|
|
2
|
+
from typing import Optional, List, Dict
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from google_client.utils.datetime import convert_datetime_to_readable, current_datetime_local_timezone, \
|
|
6
|
+
convert_datetime_to_local_timezone
|
|
7
|
+
from google_client.utils.validation import is_valid_email
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Attendee:
|
|
12
|
+
"""
|
|
13
|
+
Represents an attendee of a calendar event with their email, display name, and response status.
|
|
14
|
+
Args:
|
|
15
|
+
email: The email address of the attendee.
|
|
16
|
+
display_name: The display name of the attendee (optional).
|
|
17
|
+
response_status: The response status of the attendee (optional).
|
|
18
|
+
"""
|
|
19
|
+
email: str
|
|
20
|
+
display_name: Optional[str] = None
|
|
21
|
+
response_status: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
def __post_init__(self):
|
|
24
|
+
if not self.email:
|
|
25
|
+
raise ValueError("Attendee email cannot be empty.")
|
|
26
|
+
if not is_valid_email(self.email):
|
|
27
|
+
raise ValueError("Invalid email format - email address validation failed")
|
|
28
|
+
if self.response_status and self.response_status not in ["needsAction", "declined", "tentative", "accepted"]:
|
|
29
|
+
raise ValueError(f"Invalid response status: {self.response_status}. Must be one of: needsAction, declined, tentative, accepted")
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict:
|
|
32
|
+
"""
|
|
33
|
+
Converts the Attendee instance to a dictionary representation.
|
|
34
|
+
Returns:
|
|
35
|
+
A dictionary containing the attendee data.
|
|
36
|
+
"""
|
|
37
|
+
attendee = {"email": self.email}
|
|
38
|
+
if self.display_name:
|
|
39
|
+
attendee["displayName"] = self.display_name
|
|
40
|
+
if self.response_status:
|
|
41
|
+
attendee["responseStatus"] = self.response_status
|
|
42
|
+
return attendee
|
|
43
|
+
|
|
44
|
+
def __str__(self):
|
|
45
|
+
if self.display_name:
|
|
46
|
+
return f"{self.display_name} <{self.email}>"
|
|
47
|
+
return self.email
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class CalendarEvent:
|
|
52
|
+
"""
|
|
53
|
+
Represents a calendar event with various attributes.
|
|
54
|
+
Args:
|
|
55
|
+
event_id: Unique identifier for the event.
|
|
56
|
+
summary: A brief title or summary of the event.
|
|
57
|
+
description: A detailed description of the event.
|
|
58
|
+
location: The physical or virtual location of the event.
|
|
59
|
+
start: The start time of the event as a datetime object.
|
|
60
|
+
end: The end time of the event as a datetime object.
|
|
61
|
+
html_link: A hyperlink to the event on Google Calendar.
|
|
62
|
+
attendees: A list of Attendee objects representing the people invited to the event.
|
|
63
|
+
recurrence: A list of strings defining the recurrence rules for the event in RFC 5545 format.
|
|
64
|
+
recurring_event_id: The ID of the recurring event if this event is part of a series.
|
|
65
|
+
creator: The creator of the event.
|
|
66
|
+
organizer: The organizer of the event.
|
|
67
|
+
status: The status of the event (confirmed, tentative, cancelled).
|
|
68
|
+
"""
|
|
69
|
+
event_id: Optional[str] = None
|
|
70
|
+
summary: Optional[str] = None
|
|
71
|
+
description: Optional[str] = None
|
|
72
|
+
location: Optional[str] = None
|
|
73
|
+
start: Optional[datetime] = None
|
|
74
|
+
end: Optional[datetime] = None
|
|
75
|
+
html_link: Optional[str] = None
|
|
76
|
+
attendees: List[Attendee] = field(default_factory=list)
|
|
77
|
+
recurrence: List[str] = field(default_factory=list)
|
|
78
|
+
recurring_event_id: Optional[str] = None
|
|
79
|
+
creator: Optional[str] = None
|
|
80
|
+
organizer: Optional[str] = None
|
|
81
|
+
status: Optional[str] = "confirmed"
|
|
82
|
+
|
|
83
|
+
def duration(self) -> Optional[int]:
|
|
84
|
+
"""
|
|
85
|
+
Calculate the duration of the event in minutes.
|
|
86
|
+
Returns:
|
|
87
|
+
Duration in minutes, or None if start/end times are missing.
|
|
88
|
+
"""
|
|
89
|
+
if self.start and self.end:
|
|
90
|
+
total_seconds = (self.end - self.start).total_seconds()
|
|
91
|
+
return int(total_seconds / 60)
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
def is_today(self) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Check if the event occurs today.
|
|
97
|
+
Returns:
|
|
98
|
+
True if the event is today, False otherwise.
|
|
99
|
+
"""
|
|
100
|
+
if self.start:
|
|
101
|
+
return self.start.date() == date.today()
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def is_all_day(self) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Check if the event is an all-day event.
|
|
107
|
+
Returns:
|
|
108
|
+
True if the event is all-day, False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
if not self.start or not self.end:
|
|
111
|
+
return False
|
|
112
|
+
return self.start.time() == time.min and self.end.time() == time.min and (self.end - self.start).days >= 1
|
|
113
|
+
|
|
114
|
+
def is_past(self) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Check if the event has already ended.
|
|
117
|
+
Returns:
|
|
118
|
+
True if the event is in the past, False otherwise.
|
|
119
|
+
"""
|
|
120
|
+
if self.end:
|
|
121
|
+
return self.end < current_datetime_local_timezone()
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def is_upcoming(self) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Check if the event is in the future.
|
|
127
|
+
Returns:
|
|
128
|
+
True if the event is upcoming, False otherwise.
|
|
129
|
+
"""
|
|
130
|
+
if self.start:
|
|
131
|
+
return self.start > current_datetime_local_timezone()
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def is_happening_now(self) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Check if the event is currently happening.
|
|
137
|
+
Returns:
|
|
138
|
+
True if the event is happening now, False otherwise.
|
|
139
|
+
"""
|
|
140
|
+
if not self.start or not self.end:
|
|
141
|
+
return False
|
|
142
|
+
now = current_datetime_local_timezone()
|
|
143
|
+
return self.start <= now <= self.end
|
|
144
|
+
|
|
145
|
+
def conflicts_with(self, other: "CalendarEvent") -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Check if this event conflicts with another event.
|
|
148
|
+
Args:
|
|
149
|
+
other: Another CalendarEvent to check for conflicts
|
|
150
|
+
Returns:
|
|
151
|
+
True if the events overlap in time, False otherwise.
|
|
152
|
+
"""
|
|
153
|
+
if not self.start or not self.end or not other.start or not other.end:
|
|
154
|
+
return False
|
|
155
|
+
return self.start < other.end and self.end > other.start
|
|
156
|
+
|
|
157
|
+
def get_attendee_emails(self) -> List[str]:
|
|
158
|
+
"""
|
|
159
|
+
Get a list of all attendee email addresses.
|
|
160
|
+
Returns:
|
|
161
|
+
List of email addresses.
|
|
162
|
+
"""
|
|
163
|
+
return [attendee.email for attendee in self.attendees if attendee.email]
|
|
164
|
+
|
|
165
|
+
def has_attendee(self, email: str) -> bool:
|
|
166
|
+
"""
|
|
167
|
+
Check if a specific email is in the attendee list.
|
|
168
|
+
Args:
|
|
169
|
+
email: Email address to check for
|
|
170
|
+
Returns:
|
|
171
|
+
True if the email is an attendee, False otherwise.
|
|
172
|
+
"""
|
|
173
|
+
return any(attendee.email == email for attendee in self.attendees)
|
|
174
|
+
|
|
175
|
+
def is_recurring(self) -> bool:
|
|
176
|
+
"""
|
|
177
|
+
Check if the event is part of a recurring series.
|
|
178
|
+
Returns:
|
|
179
|
+
True if the event has recurrence rules, False otherwise.
|
|
180
|
+
"""
|
|
181
|
+
return bool(self.recurrence or self.recurring_event_id)
|
|
182
|
+
|
|
183
|
+
def to_dict(self) -> dict:
|
|
184
|
+
"""
|
|
185
|
+
Convert CalendarEvent to dictionary format for Google Calendar API.
|
|
186
|
+
Returns:
|
|
187
|
+
Dictionary representation suitable for API calls.
|
|
188
|
+
"""
|
|
189
|
+
event_dict = {}
|
|
190
|
+
|
|
191
|
+
if self.event_id:
|
|
192
|
+
event_dict["id"] = self.event_id
|
|
193
|
+
if self.summary:
|
|
194
|
+
event_dict["summary"] = self.summary
|
|
195
|
+
if self.description:
|
|
196
|
+
event_dict["description"] = self.description
|
|
197
|
+
if self.location:
|
|
198
|
+
event_dict["location"] = self.location
|
|
199
|
+
if self.html_link:
|
|
200
|
+
event_dict["htmlLink"] = self.html_link
|
|
201
|
+
if self.recurrence:
|
|
202
|
+
event_dict["recurrence"] = self.recurrence
|
|
203
|
+
if self.recurring_event_id:
|
|
204
|
+
event_dict["recurringEventId"] = self.recurring_event_id
|
|
205
|
+
if self.creator:
|
|
206
|
+
event_dict["creator"] = self.creator
|
|
207
|
+
if self.organizer:
|
|
208
|
+
event_dict["organizer"] = self.organizer
|
|
209
|
+
if self.status:
|
|
210
|
+
event_dict["status"] = self.status
|
|
211
|
+
|
|
212
|
+
if self.attendees:
|
|
213
|
+
event_dict["attendees"] = [attendee.to_dict() for attendee in self.attendees]
|
|
214
|
+
|
|
215
|
+
return event_dict
|
|
216
|
+
|
|
217
|
+
def __repr__(self):
|
|
218
|
+
return (
|
|
219
|
+
f"Summary: {self.summary!r}\n"
|
|
220
|
+
f"Description: {self.description!r}\n"
|
|
221
|
+
f"Location: {self.location!r}\n"
|
|
222
|
+
f"Time: {convert_datetime_to_readable(self.start, self.end)}\n"
|
|
223
|
+
f"Link: {self.html_link!r}\n"
|
|
224
|
+
f"Attendees: {', '.join(self.get_attendee_emails())}\n"
|
|
225
|
+
f"Status: {self.status}\n"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@dataclass
|
|
230
|
+
class TimeSlot:
|
|
231
|
+
"""
|
|
232
|
+
Represents a time slot with start and end times.
|
|
233
|
+
Used for representing both busy periods and available time slots.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
start: Start datetime of the time slot
|
|
237
|
+
end: End datetime of the time slot
|
|
238
|
+
"""
|
|
239
|
+
start: datetime
|
|
240
|
+
end: datetime
|
|
241
|
+
|
|
242
|
+
def __post_init__(self):
|
|
243
|
+
if self.start >= self.end:
|
|
244
|
+
raise ValueError("Start time must be before end time")
|
|
245
|
+
|
|
246
|
+
def duration(self) -> int:
|
|
247
|
+
"""
|
|
248
|
+
Calculate the duration of the time slot in minutes.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Duration in minutes
|
|
252
|
+
"""
|
|
253
|
+
return int((self.end - self.start).total_seconds() / 60)
|
|
254
|
+
|
|
255
|
+
def overlaps_with(self, other: "TimeSlot") -> bool:
|
|
256
|
+
"""
|
|
257
|
+
Check if this time slot overlaps with another time slot.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
other: Another TimeSlot to check for overlap
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
True if the time slots overlap, False otherwise
|
|
264
|
+
"""
|
|
265
|
+
return self.start < other.end and self.end > other.start
|
|
266
|
+
|
|
267
|
+
def contains_time(self, time_point: datetime) -> bool:
|
|
268
|
+
"""
|
|
269
|
+
Check if a specific datetime falls within this time slot.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
time_point: Datetime to check
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
True if the time point is within this slot, False otherwise
|
|
276
|
+
"""
|
|
277
|
+
return self.start <= time_point < self.end
|
|
278
|
+
|
|
279
|
+
def __str__(self):
|
|
280
|
+
|
|
281
|
+
return convert_datetime_to_readable(
|
|
282
|
+
convert_datetime_to_local_timezone(self.start),
|
|
283
|
+
convert_datetime_to_local_timezone(self.end)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@dataclass
|
|
288
|
+
class FreeBusyResponse:
|
|
289
|
+
"""
|
|
290
|
+
Represents a response from a free/busy query.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
start: Start time of the query
|
|
294
|
+
end: End time of the query
|
|
295
|
+
calendars: Dictionary mapping calendar IDs to their busy periods
|
|
296
|
+
errors: Dictionary mapping calendar IDs to any errors encountered
|
|
297
|
+
"""
|
|
298
|
+
start: datetime
|
|
299
|
+
end: datetime
|
|
300
|
+
calendars: Dict[str, List[TimeSlot]] = field(default_factory=dict)
|
|
301
|
+
errors: Dict[str, str] = field(default_factory=dict)
|
|
302
|
+
|
|
303
|
+
def get_busy_periods(self, calendar_id: str = "primary") -> List[TimeSlot]:
|
|
304
|
+
"""
|
|
305
|
+
Get busy periods for a specific calendar.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
calendar_id: Calendar ID to get busy periods for
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
List of TimeSlot objects representing busy periods
|
|
312
|
+
"""
|
|
313
|
+
return self.calendars.get(calendar_id, [])
|
|
314
|
+
|
|
315
|
+
def is_time_free(self, time_point: datetime, calendar_id: str = "primary") -> bool:
|
|
316
|
+
"""
|
|
317
|
+
Check if a specific time is free in the given calendar.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
time_point: Datetime to check
|
|
321
|
+
calendar_id: Calendar ID to check
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
True if the time is free, False if busy
|
|
325
|
+
"""
|
|
326
|
+
if not (self.start <= time_point <= self.end):
|
|
327
|
+
raise ValueError("Time point is outside the queried range")
|
|
328
|
+
|
|
329
|
+
busy_periods = self.get_busy_periods(calendar_id)
|
|
330
|
+
return not any(period.contains_time(time_point) for period in busy_periods)
|
|
331
|
+
|
|
332
|
+
def is_slot_free(self, slot: TimeSlot, calendar_id: str = "primary") -> bool:
|
|
333
|
+
"""
|
|
334
|
+
Check if an entire time slot is free in the given calendar.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
slot: TimeSlot to check
|
|
338
|
+
calendar_id: Calendar ID to check
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
True if the entire slot is free, False if any part is busy
|
|
342
|
+
"""
|
|
343
|
+
busy_periods = self.get_busy_periods(calendar_id)
|
|
344
|
+
return not any(period.overlaps_with(slot) for period in busy_periods)
|
|
345
|
+
|
|
346
|
+
def get_free_slots(self, duration_minutes: int = 60, calendar_id: str = "primary") -> List[TimeSlot]:
|
|
347
|
+
"""
|
|
348
|
+
Get all free time slots of a specified duration within the queried range.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
duration_minutes: Minimum duration for free slots in minutes
|
|
352
|
+
calendar_id: Calendar ID to get free slots for
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
List of TimeSlot objects representing available time slots
|
|
356
|
+
"""
|
|
357
|
+
from ...utils.datetime import current_datetime_local_timezone
|
|
358
|
+
|
|
359
|
+
busy_periods = sorted(self.get_busy_periods(calendar_id), key=lambda x: x.start)
|
|
360
|
+
free_slots = []
|
|
361
|
+
|
|
362
|
+
# Start from the beginning of the range or current time (whichever is later)
|
|
363
|
+
current_time = max(self.start, current_datetime_local_timezone())
|
|
364
|
+
|
|
365
|
+
# Check time before first busy period
|
|
366
|
+
if busy_periods and current_time < busy_periods[0].start:
|
|
367
|
+
gap_duration = (busy_periods[0].start - current_time).total_seconds() / 60
|
|
368
|
+
if gap_duration >= duration_minutes:
|
|
369
|
+
free_slots.append(TimeSlot(current_time, busy_periods[0].start))
|
|
370
|
+
|
|
371
|
+
# Check gaps between busy periods
|
|
372
|
+
for i in range(len(busy_periods) - 1):
|
|
373
|
+
gap_start = busy_periods[i].end
|
|
374
|
+
gap_end = busy_periods[i + 1].start
|
|
375
|
+
gap_duration = (gap_end - gap_start).total_seconds() / 60
|
|
376
|
+
|
|
377
|
+
if gap_duration >= duration_minutes:
|
|
378
|
+
free_slots.append(TimeSlot(gap_start, gap_end))
|
|
379
|
+
|
|
380
|
+
# Check time after last busy period
|
|
381
|
+
if busy_periods:
|
|
382
|
+
gap_start = busy_periods[-1].end
|
|
383
|
+
if gap_start < self.end:
|
|
384
|
+
gap_duration = (self.end - gap_start).total_seconds() / 60
|
|
385
|
+
if gap_duration >= duration_minutes:
|
|
386
|
+
free_slots.append(TimeSlot(gap_start, self.end))
|
|
387
|
+
elif current_time < self.end:
|
|
388
|
+
# No busy periods at all
|
|
389
|
+
gap_duration = (self.end - current_time).total_seconds() / 60
|
|
390
|
+
if gap_duration >= duration_minutes:
|
|
391
|
+
free_slots.append(TimeSlot(current_time, self.end))
|
|
392
|
+
|
|
393
|
+
return free_slots
|
|
394
|
+
|
|
395
|
+
def has_errors(self) -> bool:
|
|
396
|
+
"""
|
|
397
|
+
Check if there were any errors in the freebusy query.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
True if there were errors, False otherwise
|
|
401
|
+
"""
|
|
402
|
+
return bool(self.errors)
|
|
403
|
+
|