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