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,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
|
+
]
|