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.
Files changed (39) hide show
  1. google_api_client_wrapper-1.0.0.dist-info/METADATA +103 -0
  2. google_api_client_wrapper-1.0.0.dist-info/RECORD +39 -0
  3. google_api_client_wrapper-1.0.0.dist-info/WHEEL +5 -0
  4. google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE +21 -0
  5. google_api_client_wrapper-1.0.0.dist-info/top_level.txt +1 -0
  6. google_client/__init__.py +6 -0
  7. google_client/services/__init__.py +13 -0
  8. google_client/services/calendar/__init__.py +14 -0
  9. google_client/services/calendar/api_service.py +454 -0
  10. google_client/services/calendar/constants.py +48 -0
  11. google_client/services/calendar/exceptions.py +35 -0
  12. google_client/services/calendar/query_builder.py +314 -0
  13. google_client/services/calendar/types.py +403 -0
  14. google_client/services/calendar/utils.py +338 -0
  15. google_client/services/drive/__init__.py +13 -0
  16. google_client/services/drive/api_service.py +1133 -0
  17. google_client/services/drive/constants.py +37 -0
  18. google_client/services/drive/exceptions.py +60 -0
  19. google_client/services/drive/query_builder.py +385 -0
  20. google_client/services/drive/types.py +242 -0
  21. google_client/services/drive/utils.py +392 -0
  22. google_client/services/gmail/__init__.py +16 -0
  23. google_client/services/gmail/api_service.py +715 -0
  24. google_client/services/gmail/constants.py +6 -0
  25. google_client/services/gmail/exceptions.py +45 -0
  26. google_client/services/gmail/query_builder.py +408 -0
  27. google_client/services/gmail/types.py +285 -0
  28. google_client/services/gmail/utils.py +426 -0
  29. google_client/services/tasks/__init__.py +12 -0
  30. google_client/services/tasks/api_service.py +561 -0
  31. google_client/services/tasks/constants.py +32 -0
  32. google_client/services/tasks/exceptions.py +35 -0
  33. google_client/services/tasks/query_builder.py +324 -0
  34. google_client/services/tasks/types.py +156 -0
  35. google_client/services/tasks/utils.py +224 -0
  36. google_client/user_client.py +208 -0
  37. google_client/utils/__init__.py +0 -0
  38. google_client/utils/datetime.py +144 -0
  39. 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
+