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,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: google-api-client-wrapper
3
+ Version: 1.0.0
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
+ Author-email: Dagmawi Molla <dagmawishewadeg@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Dagmawi Molla
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/dsmolla/google-api-client-wrapper
29
+ Project-URL: Documentation, https://github.com/dsmolla/google-api-wrapper/blob/main/README.md
30
+ Project-URL: Repository, https://github.com/dsmolla/google-api-client-wrapper
31
+ Keywords: google-api,gmail,google-drive,google-calendar,google-tasks,api-wrapper,python-wrapper
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
37
+ Requires-Python: >=3.9
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: google-api-python-client>=2.163.0
41
+ Requires-Dist: google-auth>=2.28.1
42
+ Requires-Dist: google-auth-httplib2
43
+ Requires-Dist: google-auth-oauthlib>=1.2.1
44
+ Requires-Dist: tzlocal>=5.3.1
45
+ Requires-Dist: html2text>=2025.4.15
46
+ Dynamic: license-file
47
+
48
+ # Google API Client
49
+
50
+ A comprehensive Python wrapper for Google APIs, providing clean and intuitive access to Gmail, Google Drive, Google Calendar, and Google Tasks services.
51
+
52
+ ## Features
53
+
54
+ - **Gmail Service**: Send, receive, search, and manage emails
55
+ - **Google Drive Service**: Upload, download, and manage files and folders
56
+ - **Google Calendar Service**: Create, update, and manage calendar events
57
+ - **Google Tasks Service**: Manage tasks and task lists
58
+ - **OAuth2 Authentication**: Secure authentication flow
59
+ - **Query Builders**: Intuitive query building for each service
60
+ - **Multi-User Authentication**: Supports multiple users to be authenticated
61
+ - **Dataclass Models**: Uses Python dataclasses for clean, type-safe data structures (GmailMessage, EmailAddress, Task, Event, etc.)
62
+
63
+ ## Installation
64
+
65
+
66
+ ## Quick Start
67
+
68
+ ```python
69
+ from google_client.user_client import UserClient
70
+ from datetime import datetime, timedelta
71
+
72
+ # Authenticate User
73
+ user_1 = UserClient.from_file("< user_1_token_path >", "< app_credentials_path >")
74
+ user_2, user_2_token = UserClient.from_credentials_info(
75
+ "< user_2_token_dict >": 1,
76
+ "< app_credentials_dict >": 2,
77
+ "< scopes_list >,
78
+ )
79
+
80
+ # List User 1's emails
81
+ user_1_emails = user_1.gmail.list_emails()
82
+
83
+ # Create a calendar event for User 2
84
+ user_2_event = user_2.calendar.create_event(datetime.now(), datetime.now() + timedelta(hours=1))
85
+ ```
86
+
87
+ ## Package Documentation
88
+
89
+ Each service has detailed documentation with examples and API reference:
90
+
91
+ - **[Gmail Service](google_client/services/gmail/README.md)** - Email management and operations
92
+ - **[Google Drive Service](google_client/services/drive/README.md)** - File and folder management
93
+ - **[Google Calendar Service](google_client/services/calendar/README.md)** - Calendar and event management
94
+ - **[Google Tasks Service](google_client/services/tasks/README.md)** - Task and task list management
95
+
96
+ ## Links
97
+ - **[Google Auth & Tokens](https://developers.google.com/identity/protocols/oauth2/web-server#python)**
98
+
99
+ ---
100
+ See individual package documentation for detailed usage examples and API references.
101
+
102
+ For more information look at the official Google API Documentation for each service
103
+
@@ -0,0 +1,39 @@
1
+ google_api_client_wrapper-1.0.0.dist-info/licenses/LICENSE,sha256=Rctsw-Efg0OoOLOVl4vLNXopCB61er5H5aF1GXPqaR8,1091
2
+ google_client/__init__.py,sha256=h6aGXG3HRmhov2wdymfrjMSvbjwXlpnjqf2rHBULT7c,59
3
+ google_client/user_client.py,sha256=2ib-5MzX2Wp6Ant9FURxeEsLAqUQRSeAr8fmuhqMPIg,6939
4
+ google_client/services/__init__.py,sha256=BTDc288hVabLRpdYLjzko-zV5efHMARC4SBfqFf6pcU,212
5
+ google_client/services/calendar/__init__.py,sha256=VZl9TOXAKNvDTBJX_cdNGSj0dT044UjeP51eCcm5P08,373
6
+ google_client/services/calendar/api_service.py,sha256=5EtDk4ZiGTPfuEnDLTuirqK4bZ9H1VPMHsq3BSB6CAw,17271
7
+ google_client/services/calendar/constants.py,sha256=nb9onECh3NpMDjbaEZrY_rz57Nd8fQhzDKgB2FtAPNI,1235
8
+ google_client/services/calendar/exceptions.py,sha256=WbWRf1dwICNnEQSblUU88i5IKfgGDBYJdE63MaqE_X4,824
9
+ google_client/services/calendar/query_builder.py,sha256=qMUNzq1_RrlAyL3lZd_ms9b0Q6ZzjphoU8qitmHAhZU,10741
10
+ google_client/services/calendar/types.py,sha256=lDodMsfsnkfB-QekYQXNZJSGA1BZhIQln92ZDIZM6Ts,14877
11
+ google_client/services/calendar/utils.py,sha256=0X4_yGakkllak51FMJNAEyW1CEG4IGMe_B86inFrxN4,11741
12
+ google_client/services/drive/__init__.py,sha256=vC2xPcj9LwDYrlJnkDDbVhV_lZrpTBBCSGZPXwb_7GA,323
13
+ google_client/services/drive/api_service.py,sha256=ay5oPzfOfudH0utuLPJi2fwi670RqBH3pTbdpB15n-8,42362
14
+ google_client/services/drive/constants.py,sha256=F1vw9xDM90KtRsEBnGdQfEczZvlQ4q8oy2sSYU2p5sE,1458
15
+ google_client/services/drive/exceptions.py,sha256=sPWSIkN5ME__YLqP5zUUvHr0bN7JovYyNLWSdle-WZ4,1259
16
+ google_client/services/drive/query_builder.py,sha256=YvMLGH8KrJdF9Syn0JkUSK1RDHdRZtGGTQCPrn0jH84,13293
17
+ google_client/services/drive/types.py,sha256=uElqEP2s2XSoU1ST9SLw_4rNPvLVQ98JjcemqdWy0Pg,7655
18
+ google_client/services/drive/utils.py,sha256=DlfZ2-0gIl8V4MvdJ9TZWNPWaPfFjtev9YvJsZUD5tA,11318
19
+ google_client/services/gmail/__init__.py,sha256=2KmwgFFCICBNWqzRUIAHhw91ECert1EUcRxr1k8dSqs,398
20
+ google_client/services/gmail/api_service.py,sha256=4Nx7ZKwOsi6SgsOybFUHymjTZ2ikRxP4lZyoamv0Ruw,23586
21
+ google_client/services/gmail/constants.py,sha256=HlSelKC2LQ69qsGy8kDuE3oM9MY26QOj7fLnfKyK1bQ,157
22
+ google_client/services/gmail/exceptions.py,sha256=Y6ryrLgE1rb29-DqigTRVNJpbN9bY7jeIY-1vDqDE6M,994
23
+ google_client/services/gmail/query_builder.py,sha256=T2DgV9qEdyC_4bmLuSp01nVdBQIxCngrVSB4GHczTZs,13423
24
+ google_client/services/gmail/types.py,sha256=mSYCKEYEX-yIcx0ZG6JBv890V-6xU2E68wWSeF-pAeA,9468
25
+ google_client/services/gmail/utils.py,sha256=5fgIwAAt1X8Mj1Lkw-ySYs1ikw8jCV_Ok_MKQE-sZuk,15740
26
+ google_client/services/tasks/__init__.py,sha256=-49orKlF1Dv8-StGLXypFvrdd212eEVGQEqlpr4_RhI,251
27
+ google_client/services/tasks/api_service.py,sha256=yXiXjggdXbHjxNrzZW9vvWgv0JjVA4lSIiQnZ0QQMdg,20067
28
+ google_client/services/tasks/constants.py,sha256=iG7Pgwan-xvCi7dvrOGnxhqJ9GGrGrUapM1x638HYXY,803
29
+ google_client/services/tasks/exceptions.py,sha256=twOONDyslz0sl8T8QeqmC0pySf3vxLgsL1r7Y4gvBPk,790
30
+ google_client/services/tasks/query_builder.py,sha256=X2SlPwZAqhi_FAs1JBVjAhzb1g3H4_uop7BjRTB9g5U,11544
31
+ google_client/services/tasks/types.py,sha256=6G8-cIF2tiOqTQ9a9SDOHdL4PKHmoSj1ODqMTpZl8Jo,5184
32
+ google_client/services/tasks/utils.py,sha256=jXHaN0P3-SeQGzgUmErloiz6hF5JO1tyfbvd0dVA4Fw,6952
33
+ google_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ google_client/utils/datetime.py,sha256=tCnCPLRjkC2jgwBXAY0wcXnjOBNIJJpJLQVD_nFJijY,4860
35
+ google_client/utils/validation.py,sha256=fBvxyrjGwrGWF0GFRNXkJChBmdkWZl51Ir7u4h2JfWc,2177
36
+ google_api_client_wrapper-1.0.0.dist-info/METADATA,sha256=fGscJdLRVpclvKNhDkKltq_i52o-pUthmMkjinqKoRk,4746
37
+ google_api_client_wrapper-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ google_api_client_wrapper-1.0.0.dist-info/top_level.txt,sha256=8R7Ea6TH-HnXwFDVa1U0t2zjTQxPO4mr8kNbBaslVnA,14
39
+ google_api_client_wrapper-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dagmawi Molla
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ google_client
@@ -0,0 +1,6 @@
1
+
2
+ from . import services
3
+
4
+ __all__ = [
5
+ "services",
6
+ ]
@@ -0,0 +1,13 @@
1
+ """Google API services for various services."""
2
+
3
+ from . import calendar
4
+ from . import gmail
5
+ from . import drive
6
+ from . import tasks
7
+
8
+ __all__ = [
9
+ "calendar",
10
+ "gmail",
11
+ "drive",
12
+ "tasks"
13
+ ]
@@ -0,0 +1,14 @@
1
+ """Calendar client module for Google API integration."""
2
+
3
+ from .api_service import CalendarApiService
4
+ from .types import CalendarEvent, Attendee, TimeSlot, FreeBusyResponse
5
+ from .query_builder import EventQueryBuilder
6
+
7
+ __all__ = [
8
+ "CalendarApiService",
9
+ "CalendarEvent",
10
+ "Attendee",
11
+ "TimeSlot",
12
+ "FreeBusyResponse",
13
+ "EventQueryBuilder",
14
+ ]
@@ -0,0 +1,454 @@
1
+ from datetime import datetime, timedelta
2
+ from typing import Optional, List, Any, Dict
3
+
4
+ from googleapiclient.errors import HttpError
5
+
6
+ from ...utils.datetime import convert_datetime_to_iso, today_start
7
+ from .types import CalendarEvent, Attendee, FreeBusyResponse, TimeSlot
8
+ from . import utils
9
+ from .constants import DEFAULT_MAX_RESULTS, MAX_RESULTS_LIMIT, DEFAULT_CALENDAR_ID, DEFAULT_FREEBUSY_DURATION_MINUTES
10
+ from .exceptions import (
11
+ CalendarError, CalendarPermissionError, EventNotFoundError,
12
+ CalendarNotFoundError, EventConflictError, InvalidEventDataError
13
+ )
14
+
15
+
16
+ class CalendarApiService:
17
+ """
18
+ Service layer for Calendar API operations.
19
+ Contains all Calendar API functionality that was removed from dataclasses.
20
+ """
21
+
22
+ def __init__(self, service: Any):
23
+ """
24
+ Initialize Calendar service.
25
+
26
+ Args:
27
+ service: The Calendar API service instance
28
+ """
29
+ self._service = service
30
+
31
+ def query(self):
32
+ """
33
+ Create a new EventQueryBuilder for building complex event queries with a fluent API.
34
+
35
+ Returns:
36
+ EventQueryBuilder instance for method chaining
37
+
38
+ Example:
39
+ events = (user.calendar.query()
40
+ .limit(50)
41
+ .today()
42
+ .search("meeting")
43
+ .with_location()
44
+ .execute())
45
+ """
46
+ from .query_builder import EventQueryBuilder
47
+ return EventQueryBuilder(self)
48
+
49
+ def list_events(
50
+ self,
51
+ max_results: Optional[int] = DEFAULT_MAX_RESULTS,
52
+ start: Optional[datetime] = today_start(),
53
+ end: Optional[datetime] = None,
54
+ query: Optional[str] = None,
55
+ calendar_id: str = DEFAULT_CALENDAR_ID,
56
+ single_events: bool = True,
57
+ order_by: str = 'startTime'
58
+ ) -> List[CalendarEvent]:
59
+ """
60
+ Fetches a list of events from Google Calendar with optional filtering.
61
+
62
+ Args:
63
+ max_results: Maximum number of events to retrieve. Defaults to 100.
64
+ start: Start time for events (inclusive). Defaults to today.
65
+ end: End time for events (exclusive). Defaults to 30 days from start date
66
+ query: Text search query string.
67
+ calendar_id: Calendar ID to query (default: 'primary').
68
+ single_events: Whether to expand recurring events into instances.
69
+ order_by: How to order the events ('startTime' or 'updated').
70
+
71
+ Returns:
72
+ A list of CalendarEvent objects representing the events found.
73
+ If no events are found, an empty list is returned.
74
+ """
75
+ # Input validation
76
+ if max_results and (max_results < 1 or max_results > MAX_RESULTS_LIMIT):
77
+ raise ValueError(f"max_results must be between 1 and {MAX_RESULTS_LIMIT}")
78
+
79
+ if not end:
80
+ end = start + timedelta(days=30)
81
+
82
+ try:
83
+ # Build request parameters
84
+ request_params = {
85
+ 'calendarId': calendar_id,
86
+ 'maxResults': max_results,
87
+ 'singleEvents': single_events,
88
+ }
89
+
90
+ if order_by and single_events:
91
+ request_params['orderBy'] = order_by
92
+
93
+ # Add time range filters
94
+ if start:
95
+ request_params['timeMin'] = convert_datetime_to_iso(start)
96
+ if end:
97
+ request_params['timeMax'] = convert_datetime_to_iso(end)
98
+ if query:
99
+ request_params['q'] = query
100
+
101
+ # Make API call
102
+ result = self._service.events().list(**request_params).execute()
103
+ events_data = result.get('items', [])
104
+
105
+
106
+ # Parse events
107
+ calendar_events = []
108
+ for event_data in events_data:
109
+ try:
110
+ calendar_events.append(utils.from_google_event(event_data))
111
+ except Exception as e:
112
+ pass
113
+
114
+ return calendar_events
115
+
116
+ except HttpError as e:
117
+ if e.resp.status == 403:
118
+ raise CalendarPermissionError(f"Permission denied")
119
+ elif e.resp.status == 404:
120
+ raise CalendarNotFoundError(f"Calendar not found")
121
+ else:
122
+ raise CalendarError(f"Calendar API error listing events")
123
+ except Exception as e:
124
+ raise CalendarError(f"Unexpected error listing events")
125
+
126
+ def get_event(self, event_id: str, calendar_id: str = DEFAULT_CALENDAR_ID) -> CalendarEvent:
127
+ """
128
+ Retrieves a specific event from Google Calendar using its unique identifier.
129
+
130
+ Args:
131
+ event_id: The unique identifier of the event to be retrieved.
132
+ calendar_id: Calendar ID containing the event (default: 'primary').
133
+
134
+ Returns:
135
+ A CalendarEvent object representing the event with the specified ID.
136
+ """
137
+
138
+ try:
139
+ event_data = self._service.events().get(
140
+ calendarId=calendar_id,
141
+ eventId=event_id
142
+ ).execute()
143
+
144
+ return utils.from_google_event(event_data)
145
+
146
+ except HttpError as e:
147
+ if e.resp.status == 404:
148
+ raise EventNotFoundError(f"Event not found")
149
+ elif e.resp.status == 403:
150
+ raise CalendarPermissionError(f"Permission denied accessing event")
151
+ else:
152
+ raise CalendarError(f"Calendar API error getting event")
153
+ except Exception as e:
154
+ raise CalendarError(f"Unexpected error getting event")
155
+
156
+ def create_event(
157
+ self,
158
+ start: datetime,
159
+ end: datetime,
160
+ summary: str = None,
161
+ description: str = None,
162
+ location: str = None,
163
+ attendees: List[Attendee] = None,
164
+ recurrence: List[str] = None,
165
+ calendar_id: str = DEFAULT_CALENDAR_ID
166
+ ) -> CalendarEvent:
167
+ """
168
+ Creates a new calendar event.
169
+
170
+ Args:
171
+ start: Event start datetime.
172
+ end: Event end datetime.
173
+ summary: Brief title or summary of the event.
174
+ description: Detailed description of the event.
175
+ location: Physical or virtual location of the event.
176
+ attendees: List of Attendee objects for invited people.
177
+ recurrence: List of recurrence rules in RFC 5545 format.
178
+ calendar_id: Calendar ID to create event in (default: 'primary').
179
+
180
+ Returns:
181
+ A CalendarEvent object representing the created event.
182
+ """
183
+ # Create event preparation
184
+
185
+ try:
186
+ # Create event body using utils
187
+ event_body = utils.create_event_body(
188
+ start=start,
189
+ end=end,
190
+ summary=summary,
191
+ description=description,
192
+ location=location,
193
+ attendees=attendees,
194
+ recurrence=recurrence
195
+ )
196
+
197
+ # Make API call
198
+ created_event = self._service.events().insert(
199
+ calendarId=calendar_id,
200
+ body=event_body
201
+ ).execute()
202
+
203
+ calendar_event = utils.from_google_event(created_event)
204
+ return calendar_event
205
+
206
+ except HttpError as e:
207
+ if e.resp.status == 403:
208
+ raise CalendarPermissionError(f"Permission denied creating event")
209
+ elif e.resp.status == 409:
210
+ raise EventConflictError(f"Event conflict")
211
+ else:
212
+ raise CalendarError(f"Calendar API error creating event")
213
+ except ValueError as e:
214
+ raise InvalidEventDataError(f"Invalid event data")
215
+ except Exception as e:
216
+ raise CalendarError(f"Unexpected error creating event")
217
+
218
+ def update_event(
219
+ self,
220
+ event: CalendarEvent,
221
+ calendar_id: str = DEFAULT_CALENDAR_ID
222
+ ) -> CalendarEvent:
223
+ """
224
+ Updates an existing calendar event.
225
+
226
+ Args:
227
+ event: CalendarEvent object with updated data.
228
+ calendar_id: Calendar ID containing the event (default: 'primary').
229
+
230
+ Returns:
231
+ A CalendarEvent object representing the updated event.
232
+ """
233
+
234
+ try:
235
+ # Convert event to API format
236
+ event_body = event.to_dict()
237
+
238
+ # Remove fields that shouldn't be updated
239
+ fields_to_remove = ['id', 'htmlLink', 'recurringEventId']
240
+ for field in fields_to_remove:
241
+ event_body.pop(field, None)
242
+
243
+ # Add datetime fields if they exist
244
+ if event.start and event.end:
245
+ event_body['start'] = {'dateTime': convert_datetime_to_iso(event.start)}
246
+ event_body['end'] = {'dateTime': convert_datetime_to_iso(event.end)}
247
+
248
+ # Make API call
249
+ updated_event = self._service.events().update(
250
+ calendarId=calendar_id,
251
+ eventId=event.event_id,
252
+ body=event_body
253
+ ).execute()
254
+
255
+ updated_calendar_event = utils.from_google_event(updated_event)
256
+ return updated_calendar_event
257
+
258
+ except HttpError as e:
259
+ if e.resp.status == 404:
260
+ raise EventNotFoundError(f"Event not found")
261
+ elif e.resp.status == 403:
262
+ raise CalendarPermissionError(f"Permission denied updating event")
263
+ elif e.resp.status == 409:
264
+ raise EventConflictError(f"Event conflict during update")
265
+ else:
266
+ raise CalendarError(f"Calendar API error updating event")
267
+ except ValueError as e:
268
+ raise InvalidEventDataError(f"Invalid event data")
269
+ except Exception as e:
270
+ raise CalendarError(f"Unexpected error updating event")
271
+
272
+ def delete_event(
273
+ self,
274
+ event: CalendarEvent,
275
+ calendar_id: str = DEFAULT_CALENDAR_ID
276
+ ) -> bool:
277
+ """
278
+ Deletes a calendar event.
279
+
280
+ Args:
281
+ event: The Calendar event to delete.
282
+ calendar_id: Calendar ID containing the event (default: 'primary').
283
+
284
+ Returns:
285
+ True if the operation was successful, False otherwise.
286
+ """
287
+
288
+ try:
289
+ self._service.events().delete(
290
+ calendarId=calendar_id,
291
+ eventId=event.event_id
292
+ ).execute()
293
+
294
+ return True
295
+
296
+ except HttpError as e:
297
+ if e.resp.status == 404:
298
+ raise EventNotFoundError(f"Event not found")
299
+ elif e.resp.status == 403:
300
+ raise CalendarPermissionError(f"Permission denied deleting event")
301
+ else:
302
+ raise CalendarError(f"Calendar API error deleting event")
303
+ except Exception as e:
304
+ raise CalendarError(f"Unexpected error deleting event")
305
+
306
+ def batch_get_events(self, event_ids: List[str], calendar_id: str = DEFAULT_CALENDAR_ID) -> List[CalendarEvent]:
307
+ """
308
+ Retrieves multiple events by their IDs.
309
+
310
+ Args:
311
+ event_ids: List of event IDs to retrieve.
312
+ calendar_id: Calendar ID containing the events (default: 'primary').
313
+
314
+ Returns:
315
+ List of CalendarEvent objects.
316
+ """
317
+
318
+ calendar_events = []
319
+ for event_id in event_ids:
320
+ try:
321
+ calendar_events.append(self.get_event(event_id, calendar_id))
322
+ except Exception as e:
323
+ pass
324
+
325
+ return calendar_events
326
+
327
+ def batch_create_events(self, events_data: List[Dict[str, Any]], calendar_id: str = DEFAULT_CALENDAR_ID) -> List[CalendarEvent]:
328
+ """
329
+ Creates multiple events.
330
+
331
+ Args:
332
+ events_data: List of dictionaries containing event parameters.
333
+ calendar_id: Calendar ID to create events in (default: 'primary').
334
+
335
+ Returns:
336
+ List of created CalendarEvent objects.
337
+ """
338
+
339
+ created_events = []
340
+ for event_data in events_data:
341
+ try:
342
+ created_events.append(self.create_event(calendar_id=calendar_id, **event_data))
343
+ except Exception as e:
344
+ pass
345
+
346
+ return created_events
347
+
348
+ def get_freebusy(
349
+ self,
350
+ start: datetime,
351
+ end: datetime,
352
+ calendar_ids: Optional[List[str]] = None,
353
+ ) -> FreeBusyResponse:
354
+ """
355
+ Query free/busy information for specified calendars and time range.
356
+
357
+ Args:
358
+ start: Start datetime for the query
359
+ end: End datetime for the query
360
+ calendar_ids: List of calendar IDs to query (defaults to primary calendar)
361
+
362
+ Returns:
363
+ FreeBusyResponse object containing availability information
364
+
365
+ Raises:
366
+ CalendarError: If the API request fails
367
+ ValueError: If the parameters are invalid
368
+ """
369
+ if calendar_ids is None:
370
+ calendar_ids = [DEFAULT_CALENDAR_ID]
371
+
372
+ # Validate the request parameters
373
+ utils.validate_freebusy_request(start, end, calendar_ids)
374
+
375
+ try:
376
+ # Make the API call
377
+ request_body = {
378
+ "timeMin": convert_datetime_to_iso(start),
379
+ "timeMax": convert_datetime_to_iso(end),
380
+ "items": [{"id": cal_id} for cal_id in calendar_ids]
381
+ }
382
+
383
+ result = self._service.freebusy().query(body=request_body).execute()
384
+
385
+ # Parse and return the response
386
+ return utils.parse_freebusy_response(result)
387
+
388
+ except HttpError as e:
389
+ if e.resp.status == 403:
390
+ raise CalendarPermissionError("Permission denied for freebusy query")
391
+ elif e.resp.status == 404:
392
+ raise CalendarNotFoundError("One or more calendars not found")
393
+ else:
394
+ raise CalendarError(f"Calendar API error during freebusy query")
395
+ except ValueError as e:
396
+ raise ValueError(f"Invalid freebusy request")
397
+ except Exception as e:
398
+ raise CalendarError(f"Unexpected error during freebusy query")
399
+
400
+ def find_free_slots(
401
+ self,
402
+ start: datetime,
403
+ end: datetime,
404
+ duration_minutes: int = DEFAULT_FREEBUSY_DURATION_MINUTES,
405
+ calendar_ids: Optional[List[str]] = None
406
+ ) -> List[TimeSlot]:
407
+ """
408
+ Find all available time slots of a specified duration within a time range.
409
+
410
+ Args:
411
+ start: Start datetime for the search
412
+ end: End datetime for the search
413
+ duration_minutes: Minimum duration for free slots in minutes
414
+ calendar_ids: List of calendar IDs to check (defaults to primary calendar)
415
+
416
+ Returns:
417
+ List of TimeSlot objects representing available time slots
418
+
419
+ Raises:
420
+ CalendarError: If the API request fails
421
+ ValueError: If the parameters are invalid
422
+ """
423
+ from .constants import MIN_TIME_SLOT_DURATION_MINUTES, MAX_TIME_SLOT_DURATION_MINUTES
424
+
425
+ if duration_minutes < MIN_TIME_SLOT_DURATION_MINUTES:
426
+ raise ValueError(f"Duration must be at least {MIN_TIME_SLOT_DURATION_MINUTES} minutes")
427
+ if duration_minutes > MAX_TIME_SLOT_DURATION_MINUTES:
428
+ raise ValueError(f"Duration cannot exceed {MAX_TIME_SLOT_DURATION_MINUTES} minutes")
429
+
430
+ # Get freebusy information
431
+ freebusy_response = self.get_freebusy(start, end, calendar_ids)
432
+
433
+ # If multiple calendars, we need to find slots that are free in ALL calendars
434
+ if len(calendar_ids or [DEFAULT_CALENDAR_ID]) == 1:
435
+ calendar_id = calendar_ids[0] if calendar_ids else DEFAULT_CALENDAR_ID
436
+ return freebusy_response.get_free_slots(duration_minutes, calendar_id)
437
+ else:
438
+ # For multiple calendars, collect all busy periods from all calendars
439
+ all_busy_periods = []
440
+ for calendar_id in (calendar_ids or [DEFAULT_CALENDAR_ID]):
441
+ all_busy_periods.extend(freebusy_response.get_busy_periods(calendar_id))
442
+
443
+ # Merge overlapping busy periods
444
+ merged_busy = utils.merge_overlapping_time_slots(all_busy_periods)
445
+
446
+ # Create a temporary response with merged busy periods for the primary calendar
447
+ temp_response = FreeBusyResponse(
448
+ start=start,
449
+ end=end,
450
+ calendars={DEFAULT_CALENDAR_ID: merged_busy}
451
+ )
452
+
453
+ return temp_response.get_free_slots(duration_minutes, DEFAULT_CALENDAR_ID)
454
+