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,338 @@
1
+ from datetime import datetime
2
+ from typing import Optional, List, Dict, Any
3
+
4
+ from .types import CalendarEvent, Attendee, TimeSlot, FreeBusyResponse
5
+ from .constants import (
6
+ MAX_SUMMARY_LENGTH, MAX_DESCRIPTION_LENGTH, MAX_LOCATION_LENGTH,
7
+ VALID_EVENT_STATUSES, VALID_RESPONSE_STATUSES
8
+ )
9
+ from ...utils.datetime import convert_datetime_to_iso
10
+
11
+
12
+ # Import from shared utilities
13
+ from ...utils.validation import is_valid_email, validate_text_field, sanitize_header_value
14
+
15
+
16
+ def validate_datetime_range(start: Optional[datetime], end: Optional[datetime]) -> None:
17
+ """Validates that start time is before end time."""
18
+ if start and end and start >= end:
19
+ raise ValueError("Event start time must be before end time")
20
+
21
+
22
+ def parse_datetime_from_api(datetime_data: Dict[str, Any]) -> Optional[datetime]:
23
+ """
24
+ Parse datetime from Google Calendar API response.
25
+
26
+ Args:
27
+ datetime_data: Dictionary containing dateTime or date fields
28
+
29
+ Returns:
30
+ Parsed datetime object or None if parsing fails
31
+ """
32
+ if not datetime_data:
33
+ return None
34
+
35
+ try:
36
+ if datetime_data.get("dateTime"):
37
+ # Handle timezone-aware datetime
38
+ dt_str = datetime_data["dateTime"]
39
+ if dt_str.endswith("Z"):
40
+ dt_str = dt_str.replace("Z", "+00:00")
41
+ return datetime.fromisoformat(dt_str)
42
+ elif datetime_data.get("date"):
43
+ # Handle all-day events (date only)
44
+ return datetime.strptime(datetime_data["date"], "%Y-%m-%d")
45
+ except (ValueError, TypeError):
46
+ pass
47
+
48
+ return None
49
+
50
+
51
+ def parse_attendees_from_api(attendees_data: List[Dict[str, Any]]) -> List[Attendee]:
52
+ """
53
+ Parse attendees from Google Calendar API response.
54
+
55
+ Args:
56
+ attendees_data: List of attendee dictionaries from API
57
+
58
+ Returns:
59
+ List of Attendee objects
60
+ """
61
+ attendees = []
62
+
63
+ for attendee_data in attendees_data:
64
+ email = attendee_data.get("email")
65
+ if email and is_valid_email(email):
66
+ try:
67
+ response_status = attendee_data.get("responseStatus")
68
+ if response_status and response_status not in VALID_RESPONSE_STATUSES:
69
+ response_status = None
70
+
71
+ attendees.append(Attendee(
72
+ email=email,
73
+ display_name=attendee_data.get("displayName"),
74
+ response_status=response_status
75
+ ))
76
+ except ValueError:
77
+ pass
78
+
79
+ return attendees
80
+
81
+
82
+ def from_google_event(google_event: Dict[str, Any]) -> CalendarEvent:
83
+ """
84
+ Create a CalendarEvent instance from a Google Calendar API response.
85
+
86
+ Args:
87
+ google_event: Dictionary containing event data from Google Calendar API
88
+
89
+ Returns:
90
+ CalendarEvent instance populated with the data from the dictionary
91
+ """
92
+ try:
93
+ # Parse basic fields
94
+ event_id = google_event.get("id")
95
+ summary = google_event.get("summary", "").strip()
96
+ description = google_event.get("description", "").strip() if google_event.get("description") else None
97
+ location = google_event.get("location", "").strip() if google_event.get("location") else None
98
+ html_link = google_event.get("htmlLink")
99
+
100
+ # Parse datetimes
101
+ start = parse_datetime_from_api(google_event.get("start", {}))
102
+ end = parse_datetime_from_api(google_event.get("end", {}))
103
+
104
+ # Parse attendees
105
+ attendees_data = google_event.get("attendees", [])
106
+ attendees = parse_attendees_from_api(attendees_data)
107
+
108
+ # Parse recurrence
109
+ recurrence = google_event.get("recurrence", [])
110
+ recurring_event_id = google_event.get("recurringEventId")
111
+
112
+ # Parse creator and organizer
113
+ creator_data = google_event.get("creator", {})
114
+ creator = creator_data.get("email") if creator_data else None
115
+
116
+ organizer_data = google_event.get("organizer", {})
117
+ organizer = organizer_data.get("email") if organizer_data else None
118
+
119
+ # Parse status
120
+ status = google_event.get("status", "confirmed")
121
+ if status not in VALID_EVENT_STATUSES:
122
+ status = "confirmed"
123
+
124
+ # Create and return the event
125
+ event = CalendarEvent(
126
+ event_id=event_id,
127
+ summary=summary,
128
+ description=description,
129
+ location=location,
130
+ start=start,
131
+ end=end,
132
+ html_link=html_link,
133
+ attendees=attendees,
134
+ recurrence=recurrence,
135
+ recurring_event_id=recurring_event_id,
136
+ creator=creator,
137
+ organizer=organizer,
138
+ status=status
139
+ )
140
+
141
+ return event
142
+
143
+ except Exception:
144
+ raise ValueError("Invalid event data - failed to parse calendar event")
145
+
146
+
147
+ 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
155
+ ) -> Dict[str, Any]:
156
+ """
157
+ Create event body dictionary for Google Calendar API.
158
+
159
+ Args:
160
+ start: Event start datetime
161
+ end: Event end datetime
162
+ summary: Event summary/title
163
+ description: Event description
164
+ location: Event location
165
+ attendees: List of attendees
166
+ recurrence: List of recurrence rules
167
+
168
+ Returns:
169
+ Dictionary suitable for Calendar API requests
170
+
171
+ Raises:
172
+ ValueError: If required fields are invalid
173
+ """
174
+ if not start or not end:
175
+ raise ValueError("Event must have both start and end times")
176
+ if start >= end:
177
+ raise ValueError("Event start time must be before end time")
178
+
179
+ # Validate text fields
180
+ validate_text_field(summary, MAX_SUMMARY_LENGTH, "summary")
181
+ validate_text_field(description, MAX_DESCRIPTION_LENGTH, "description")
182
+ validate_text_field(location, MAX_LOCATION_LENGTH, "location")
183
+
184
+ # Build event body
185
+ event_body = {
186
+ 'summary': summary or "New Event",
187
+ 'start': {'dateTime': convert_datetime_to_iso(start)},
188
+ 'end': {'dateTime': convert_datetime_to_iso(end)}
189
+ }
190
+
191
+ # Add optional fields
192
+ if description:
193
+ event_body['description'] = sanitize_header_value(description)
194
+ if location:
195
+ event_body['location'] = sanitize_header_value(location)
196
+ if attendees:
197
+ event_body['attendees'] = [attendee.to_dict() for attendee in attendees]
198
+ if recurrence:
199
+ event_body['recurrence'] = recurrence
200
+
201
+ return event_body
202
+
203
+
204
+ def parse_freebusy_response(freebusy_data: Dict[str, Any]) -> FreeBusyResponse:
205
+ """
206
+ Parse a freebusy response from Google Calendar API.
207
+
208
+ Args:
209
+ freebusy_data: Dictionary containing freebusy response from API
210
+
211
+ Returns:
212
+ FreeBusyResponse object with parsed data
213
+
214
+ Raises:
215
+ ValueError: If the response data is invalid
216
+ """
217
+ if not freebusy_data:
218
+ raise ValueError("Empty freebusy response data")
219
+
220
+ try:
221
+ # Parse time range
222
+ time_min = freebusy_data.get("timeMin")
223
+ time_max = freebusy_data.get("timeMax")
224
+
225
+ if not time_min or not time_max:
226
+ raise ValueError("Missing timeMin or timeMax in freebusy response")
227
+
228
+ # Parse start and end times
229
+ start = datetime.fromisoformat(time_min.replace('Z', '+00:00'))
230
+ end = datetime.fromisoformat(time_max.replace('Z', '+00:00'))
231
+
232
+ # Parse calendar busy periods
233
+ calendars = {}
234
+ calendars_data = freebusy_data.get("calendars", {})
235
+
236
+ for calendar_id, calendar_data in calendars_data.items():
237
+ busy_periods = []
238
+ busy_data = calendar_data.get("busy", [])
239
+
240
+ for busy_period in busy_data:
241
+ period_start_str = busy_period.get("start")
242
+ period_end_str = busy_period.get("end")
243
+
244
+ if period_start_str and period_end_str:
245
+ try:
246
+ period_start = datetime.fromisoformat(period_start_str.replace('Z', '+00:00'))
247
+ period_end = datetime.fromisoformat(period_end_str.replace('Z', '+00:00'))
248
+ busy_periods.append(TimeSlot(period_start, period_end))
249
+ except (ValueError, TypeError):
250
+ continue
251
+
252
+ calendars[calendar_id] = busy_periods
253
+
254
+ # Parse errors
255
+ errors = {}
256
+ errors_data = freebusy_data.get("errors", {})
257
+
258
+ for calendar_id, error_data in errors_data.items():
259
+ if isinstance(error_data, list) and error_data:
260
+ error_reason = error_data[0].get("reason", "Unknown error")
261
+ errors[calendar_id] = error_reason
262
+ elif isinstance(error_data, str):
263
+ errors[calendar_id] = error_data
264
+
265
+ return FreeBusyResponse(
266
+ start=start,
267
+ end=end,
268
+ calendars=calendars,
269
+ errors=errors
270
+ )
271
+
272
+ except Exception as e:
273
+ raise ValueError(f"Failed to parse freebusy response: {str(e)}")
274
+
275
+
276
+ def merge_overlapping_time_slots(time_slots: List[TimeSlot]) -> List[TimeSlot]:
277
+ """
278
+ Merge overlapping time slots into consolidated periods.
279
+
280
+ Args:
281
+ time_slots: List of TimeSlot objects that may overlap
282
+
283
+ Returns:
284
+ List of merged TimeSlot objects with no overlaps
285
+ """
286
+ if not time_slots:
287
+ return []
288
+
289
+ # Sort by start time
290
+ sorted_slots = sorted(time_slots, key=lambda x: x.start)
291
+ merged = [sorted_slots[0]]
292
+
293
+ for current in sorted_slots[1:]:
294
+ last_merged = merged[-1]
295
+
296
+ # Check if current slot overlaps with the last merged slot
297
+ if current.start <= last_merged.end:
298
+ # Merge by extending the end time if necessary
299
+ if current.end > last_merged.end:
300
+ merged[-1] = TimeSlot(last_merged.start, current.end)
301
+ else:
302
+ # No overlap, add as new slot
303
+ merged.append(current)
304
+
305
+ return merged
306
+
307
+
308
+ def validate_freebusy_request(
309
+ start: datetime,
310
+ end: datetime,
311
+ calendar_ids: List[str]
312
+ ) -> None:
313
+ """
314
+ Validate parameters for a freebusy request.
315
+
316
+ Args:
317
+ start: Start datetime for the query
318
+ end: End datetime for the query
319
+ calendar_ids: List of calendar IDs to query
320
+
321
+ Raises:
322
+ ValueError: If any parameter is invalid
323
+ """
324
+ from .constants import MAX_FREEBUSY_DAYS_RANGE, MAX_CALENDARS_PER_FREEBUSY_QUERY
325
+
326
+ if start >= end:
327
+ raise ValueError("Start time must be before end time")
328
+
329
+ # Check maximum time range (Google's API limit)
330
+ days_diff = (end - start).days
331
+ if days_diff > MAX_FREEBUSY_DAYS_RANGE:
332
+ raise ValueError(f"Time range cannot exceed {MAX_FREEBUSY_DAYS_RANGE} days")
333
+
334
+ if not calendar_ids:
335
+ raise ValueError("At least one calendar ID must be specified")
336
+
337
+ if len(calendar_ids) > MAX_CALENDARS_PER_FREEBUSY_QUERY:
338
+ raise ValueError(f"Cannot query more than {MAX_CALENDARS_PER_FREEBUSY_QUERY} calendars at once")
@@ -0,0 +1,13 @@
1
+ """Drive client module for Google API integration."""
2
+
3
+ from .api_service import DriveApiService
4
+ from .types import DriveFile, DriveFolder, Permission
5
+ from .query_builder import DriveQueryBuilder
6
+
7
+ __all__ = [
8
+ "DriveApiService",
9
+ "DriveFile",
10
+ "DriveFolder",
11
+ "Permission",
12
+ "DriveQueryBuilder",
13
+ ]