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.
Files changed (44) hide show
  1. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/PKG-INFO +1 -1
  2. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/PKG-INFO +1 -1
  3. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/types.py +47 -41
  4. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/utils.py +69 -68
  5. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/types.py +21 -7
  6. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/pyproject.toml +1 -1
  7. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/LICENSE +0 -0
  8. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/README.md +0 -0
  9. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/SOURCES.txt +0 -0
  10. {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
  11. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_api_client_wrapper.egg-info/requires.txt +0 -0
  12. {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
  13. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/__init__.py +0 -0
  14. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/__init__.py +0 -0
  15. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/__init__.py +0 -0
  16. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/api_service.py +0 -0
  17. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/constants.py +0 -0
  18. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/exceptions.py +0 -0
  19. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/calendar/query_builder.py +0 -0
  20. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/__init__.py +0 -0
  21. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/api_service.py +0 -0
  22. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/constants.py +0 -0
  23. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/exceptions.py +0 -0
  24. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/query_builder.py +0 -0
  25. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/types.py +0 -0
  26. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/drive/utils.py +0 -0
  27. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/__init__.py +0 -0
  28. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/api_service.py +0 -0
  29. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/constants.py +0 -0
  30. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/exceptions.py +0 -0
  31. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/query_builder.py +0 -0
  32. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/gmail/utils.py +0 -0
  33. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/__init__.py +0 -0
  34. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/api_service.py +0 -0
  35. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/constants.py +0 -0
  36. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/exceptions.py +0 -0
  37. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/query_builder.py +0 -0
  38. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/types.py +0 -0
  39. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/services/tasks/utils.py +0 -0
  40. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/user_client.py +0 -0
  41. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/__init__.py +0 -0
  42. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/datetime.py +0 -0
  43. {google_api_client_wrapper-1.1.2 → google_api_client_wrapper-1.1.4}/google_client/utils/validation.py +0 -0
  44. {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.2
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.2
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, description="A list of Attendee objects representing the people invited to the event")
54
- recurrence: List[str] = Field(default_factory=list, description="A list of strings defining the recurrence rules for the event in RFC 5545 format")
55
- recurring_event_id: Optional[str] = Field(None, description="The ID of the recurring event if this event is part of a series")
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, description="Dictionary mapping calendar IDs to their busy periods")
266
- errors: Dict[str, str] = Field(default_factory=dict, description="Dictionary mapping calendar IDs to any errors encountered")
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
- start: datetime,
149
- end: datetime,
150
- summary: str = None,
151
- description: str = None,
152
- location: str = None,
153
- attendees: List[Attendee] = None,
154
- recurrence: List[str] = None
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
- start: datetime,
310
- end: datetime,
311
- calendar_ids: List[str]
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, description="List of EmailMessage objects in this thread")
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, description="List of recipient email addresses (To field)")
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, description="List of BCC recipient email addresses")
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.2"
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"}]