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,208 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User-centric Google API Client.
|
|
3
|
+
|
|
4
|
+
This module provides a clean, user-focused API where each user gets their own
|
|
5
|
+
client instance with easy access to all Google services.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from google.auth.transport.requests import Request
|
|
11
|
+
from google.oauth2.credentials import Credentials
|
|
12
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
13
|
+
from googleapiclient.discovery import build
|
|
14
|
+
|
|
15
|
+
from . services.gmail import GmailApiService
|
|
16
|
+
from . services.calendar import CalendarApiService
|
|
17
|
+
from . services.tasks import TasksApiService
|
|
18
|
+
from . services.drive import DriveApiService
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# SCOPES = [
|
|
22
|
+
# 'https://www.googleapis.com/auth/calendar',
|
|
23
|
+
# 'https://mail.google.com/',
|
|
24
|
+
# 'https://www.googleapis.com/auth/tasks',
|
|
25
|
+
# 'https://www.googleapis.com/auth/drive'
|
|
26
|
+
# ]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UserClient:
|
|
30
|
+
"""
|
|
31
|
+
User-centric client that provides clean access to all Google APIs.
|
|
32
|
+
|
|
33
|
+
Usage Examples:
|
|
34
|
+
# Single user from file
|
|
35
|
+
user = UserClient.from_file()
|
|
36
|
+
events = user.calendar.list_events(number_of_results=10)
|
|
37
|
+
emails = user.gmail.list_emails(max_results=20)
|
|
38
|
+
tasks = user.tasks.list_tasks()
|
|
39
|
+
files = user.drive.list(max_results=10)
|
|
40
|
+
|
|
41
|
+
# Multi-user scenario
|
|
42
|
+
user_1 = UserClient.from_credentials_info(app_creds, user1_token)
|
|
43
|
+
user_2 = UserClient.from_credentials_info(app_creds, user2_token)
|
|
44
|
+
|
|
45
|
+
user_1_events = user_1.calendar.list_events()
|
|
46
|
+
user_2_events = user_2.calendar.list_events()
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, credentials: Credentials):
|
|
50
|
+
"""
|
|
51
|
+
Initialize user client with credentials.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
credentials: Google OAuth2 credentials for this user
|
|
55
|
+
"""
|
|
56
|
+
self._credentials = credentials
|
|
57
|
+
|
|
58
|
+
self._gmail_service = None
|
|
59
|
+
self._calendar_service = None
|
|
60
|
+
self._tasks_service = None
|
|
61
|
+
self._drive_service = None
|
|
62
|
+
|
|
63
|
+
self._gmail = None
|
|
64
|
+
self._calendar = None
|
|
65
|
+
self._tasks = None
|
|
66
|
+
self._drive = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def from_credentials_info(
|
|
71
|
+
cls,
|
|
72
|
+
app_credentials: dict,
|
|
73
|
+
user_token_data: dict = None,
|
|
74
|
+
scopes: list = None,
|
|
75
|
+
port: int = 8080
|
|
76
|
+
) -> tuple["UserClient", dict]:
|
|
77
|
+
"""
|
|
78
|
+
Create a UserClient from credential data.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
app_credentials: OAuth client configuration dict
|
|
82
|
+
user_token_data: Previously stored user token data dict
|
|
83
|
+
scopes: List of OAuth scopes to request
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
tuple: (UserClient instance, updated_token_data_to_store)
|
|
87
|
+
"""
|
|
88
|
+
scopes = scopes
|
|
89
|
+
creds = None
|
|
90
|
+
|
|
91
|
+
# Try to load existing credentials from memory
|
|
92
|
+
if user_token_data:
|
|
93
|
+
creds = Credentials.from_authorized_user_info(user_token_data, scopes)
|
|
94
|
+
|
|
95
|
+
if not creds or not creds.valid:
|
|
96
|
+
if creds and creds.expired and creds.refresh_token:
|
|
97
|
+
creds.refresh(Request())
|
|
98
|
+
else:
|
|
99
|
+
flow = InstalledAppFlow.from_client_config(app_credentials, scopes)
|
|
100
|
+
creds = flow.run_local_server(port=port)
|
|
101
|
+
|
|
102
|
+
# Return credentials and token data to store
|
|
103
|
+
token_data_to_store = {
|
|
104
|
+
'token': creds.token,
|
|
105
|
+
'refresh_token': creds.refresh_token,
|
|
106
|
+
'token_uri': creds.token_uri,
|
|
107
|
+
'client_id': creds.client_id,
|
|
108
|
+
'client_secret': creds.client_secret,
|
|
109
|
+
'scopes': creds.scopes
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return cls(creds), token_data_to_store
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_file(
|
|
116
|
+
cls,
|
|
117
|
+
token_path: str = None,
|
|
118
|
+
credentials_path: str = None,
|
|
119
|
+
scopes: list = None,
|
|
120
|
+
port: int = 8080
|
|
121
|
+
) -> "UserClient":
|
|
122
|
+
"""
|
|
123
|
+
Create a UserClient from credential data.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
token_path: Path to previously stored user's token file (contents of token.json)
|
|
127
|
+
credentials_path: Path to OAuth client's credential file (contents of credentials.json)
|
|
128
|
+
scopes: List of OAuth scopes to request
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A UserClient instance
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
credentials_path = credentials_path
|
|
136
|
+
scopes = scopes
|
|
137
|
+
|
|
138
|
+
creds = None
|
|
139
|
+
|
|
140
|
+
if os.path.exists(token_path):
|
|
141
|
+
creds = Credentials.from_authorized_user_file(token_path, scopes)
|
|
142
|
+
|
|
143
|
+
if not creds or not creds.valid:
|
|
144
|
+
if creds and creds.expired and creds.refresh_token:
|
|
145
|
+
creds.refresh(Request())
|
|
146
|
+
else:
|
|
147
|
+
flow = InstalledAppFlow.from_client_secrets_file(
|
|
148
|
+
credentials_path, scopes
|
|
149
|
+
)
|
|
150
|
+
creds = flow.run_local_server(port=port)
|
|
151
|
+
|
|
152
|
+
with open(token_path, "w") as token:
|
|
153
|
+
token.write(creds.to_json())
|
|
154
|
+
|
|
155
|
+
return cls(creds)
|
|
156
|
+
|
|
157
|
+
def _get_gmail_service(self):
|
|
158
|
+
"""Get or create Gmail API service for this user."""
|
|
159
|
+
if self._gmail_service is None:
|
|
160
|
+
self._gmail_service = build("gmail", "v1", credentials=self._credentials)
|
|
161
|
+
return self._gmail_service
|
|
162
|
+
|
|
163
|
+
def _get_calendar_service(self):
|
|
164
|
+
"""Get or create Calendar API service for this user."""
|
|
165
|
+
if self._calendar_service is None:
|
|
166
|
+
self._calendar_service = build("calendar", "v3", credentials=self._credentials)
|
|
167
|
+
return self._calendar_service
|
|
168
|
+
|
|
169
|
+
def _get_tasks_service(self):
|
|
170
|
+
"""Get or create Tasks API service for this user."""
|
|
171
|
+
if self._tasks_service is None:
|
|
172
|
+
self._tasks_service = build("tasks", "v1", credentials=self._credentials)
|
|
173
|
+
return self._tasks_service
|
|
174
|
+
|
|
175
|
+
def _get_drive_service(self):
|
|
176
|
+
"""Get or create Drive API service for this user."""
|
|
177
|
+
if self._drive_service is None:
|
|
178
|
+
self._drive_service = build("drive", "v3", credentials=self._credentials)
|
|
179
|
+
return self._drive_service
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def gmail(self):
|
|
183
|
+
"""Gmail service layer for this user."""
|
|
184
|
+
if self._gmail is None:
|
|
185
|
+
self._gmail = GmailApiService(self._get_gmail_service())
|
|
186
|
+
return self._gmail
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def calendar(self):
|
|
190
|
+
"""Calendar service layer for this user."""
|
|
191
|
+
if self._calendar is None:
|
|
192
|
+
self._calendar = CalendarApiService(self._get_calendar_service())
|
|
193
|
+
return self._calendar
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def tasks(self):
|
|
197
|
+
"""Tasks service layer for this user."""
|
|
198
|
+
if self._tasks is None:
|
|
199
|
+
self._tasks = TasksApiService(self._get_tasks_service())
|
|
200
|
+
return self._tasks
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def drive(self):
|
|
204
|
+
"""Drive service layer for this user."""
|
|
205
|
+
if self._drive is None:
|
|
206
|
+
self._drive = DriveApiService(self._get_drive_service())
|
|
207
|
+
return self._drive
|
|
208
|
+
|
|
File without changes
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from datetime import datetime, date, time, timedelta
|
|
2
|
+
import tzlocal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def current_datetime_local_timezone() -> datetime:
|
|
6
|
+
"""
|
|
7
|
+
Returns the current date and time in the local timezone.
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
A datetime object representing the current date and time.
|
|
11
|
+
"""
|
|
12
|
+
return datetime.now(tzlocal.get_localzone())
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def convert_datetime_to_iso(date_time: datetime) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Converts a given datetime object to a string in ISO format, adjusted
|
|
18
|
+
to the local timezone.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
date_time: The datetime object to be converted.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The ISO formatted string of the datetime in the local timezone.
|
|
25
|
+
"""
|
|
26
|
+
return date_time.astimezone(tzlocal.get_localzone()).isoformat()
|
|
27
|
+
|
|
28
|
+
def convert_datetime_to_readable(start: datetime, end: datetime = None) -> str:
|
|
29
|
+
"""
|
|
30
|
+
Converts one or two ISO datetime strings into a human-readable format.
|
|
31
|
+
This function accepts a mandatory `start` time and an optional `end` time.
|
|
32
|
+
Both inputs are expected to be in ISO format. The output will be a
|
|
33
|
+
formatted string where the time is displayed in a readable format with varying
|
|
34
|
+
detail based on the relationship between the provided `start` and `end`
|
|
35
|
+
timestamps. If only the `start` time is provided, the result will contain only
|
|
36
|
+
the formatted `start` time. If an `end` time is provided, the display will
|
|
37
|
+
depend on whether the two timestamps occur on the same day or on different days.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
start: A datetime string in ISO format representing the starting
|
|
41
|
+
time of the event.
|
|
42
|
+
end: An optional datetime string in ISO format representing the
|
|
43
|
+
ending time of the event. Default is None.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
A formatted string combining `start` and `end` times in a
|
|
47
|
+
human-readable form.
|
|
48
|
+
"""
|
|
49
|
+
start = start.strftime("%a, %b %d, %Y %I:%M%p")
|
|
50
|
+
|
|
51
|
+
if end:
|
|
52
|
+
if end.day == datetime.strptime(start, "%a, %b %d, %Y %I:%M%p").day:
|
|
53
|
+
# If start and end are on the same day
|
|
54
|
+
end = end.strftime("%I:%M%p")
|
|
55
|
+
else:
|
|
56
|
+
end = end.strftime("%a, %b %d, %Y %I:%M%p")
|
|
57
|
+
return f"{start} - {end}" if end else f"{start}"
|
|
58
|
+
|
|
59
|
+
def convert_datetime_to_local_timezone(date_time: datetime) -> datetime:
|
|
60
|
+
"""
|
|
61
|
+
Converts a given datetime object to a local-timezone-aware timezone.
|
|
62
|
+
Args:
|
|
63
|
+
date_time: The datetime object to be converted.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A datetime object representing the local-timezone-aware timezone.
|
|
67
|
+
"""
|
|
68
|
+
return datetime.astimezone(date_time, tzlocal.get_localzone())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def combine_with_timezone(date_obj: date, time_obj: time) -> datetime:
|
|
72
|
+
"""
|
|
73
|
+
Combines a date and time into a timezone-aware datetime using the local timezone.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
date_obj: The date component
|
|
77
|
+
time_obj: The time component
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
A timezone-aware datetime object in the local timezone
|
|
81
|
+
"""
|
|
82
|
+
naive_datetime = datetime.combine(date_obj, time_obj)
|
|
83
|
+
local_tz = tzlocal.get_localzone()
|
|
84
|
+
return naive_datetime.replace(tzinfo=local_tz)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def today_start() -> datetime:
|
|
88
|
+
"""
|
|
89
|
+
Returns the start of today (00:00:00) in the local timezone.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A timezone-aware datetime representing the start of today
|
|
93
|
+
"""
|
|
94
|
+
return combine_with_timezone(date.today(), time.min)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def today_end() -> datetime:
|
|
98
|
+
"""
|
|
99
|
+
Returns the end of today (23:59:59.999999) in the local timezone.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
A timezone-aware datetime representing the end of today
|
|
103
|
+
"""
|
|
104
|
+
return combine_with_timezone(date.today(), time.max)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def date_start(target_date: date) -> datetime:
|
|
108
|
+
"""
|
|
109
|
+
Returns the start of the specified date (00:00:00) in the local timezone.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
target_date: The date to get the start of
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A timezone-aware datetime representing the start of the specified date
|
|
116
|
+
"""
|
|
117
|
+
return combine_with_timezone(target_date, time.min)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def date_end(target_date: date) -> datetime:
|
|
121
|
+
"""
|
|
122
|
+
Returns the end of the specified date (23:59:59.999999) in the local timezone.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
target_date: The date to get the end of
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A timezone-aware datetime representing the end of the specified date
|
|
129
|
+
"""
|
|
130
|
+
return combine_with_timezone(target_date, time.max)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def days_from_today(days: int) -> datetime:
|
|
134
|
+
"""
|
|
135
|
+
Returns the start of a date that is N days from today in the local timezone.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
days: Number of days from today (positive for future, negative for past)
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A timezone-aware datetime representing the start of the target date
|
|
142
|
+
"""
|
|
143
|
+
target_date = date.today() + timedelta(days=days)
|
|
144
|
+
return date_start(target_date)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared validation utilities for Google API client.
|
|
3
|
+
|
|
4
|
+
This module provides common validation functions used across all services
|
|
5
|
+
to maintain consistency and reduce code duplication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_valid_email(email: str) -> bool:
|
|
13
|
+
"""
|
|
14
|
+
Validate email format using regex.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
email: Email address to validate
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
True if email format is valid, False otherwise
|
|
21
|
+
"""
|
|
22
|
+
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
23
|
+
return re.match(pattern, email) is not None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_text_field(value: Optional[str], max_length: int, field_name: str, service_prefix: str = "") -> None:
|
|
27
|
+
"""
|
|
28
|
+
Validates text field length and content.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
value: Text value to validate
|
|
32
|
+
max_length: Maximum allowed length
|
|
33
|
+
field_name: Name of the field for error messages
|
|
34
|
+
service_prefix: Service prefix for error message (e.g., "Email", "Event")
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If value exceeds maximum length
|
|
38
|
+
"""
|
|
39
|
+
if value and len(value) > max_length:
|
|
40
|
+
prefix = f"{service_prefix} " if service_prefix else ""
|
|
41
|
+
raise ValueError(f"{prefix}{field_name} cannot exceed {max_length} characters")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def sanitize_header_value(value: str) -> str:
|
|
45
|
+
"""
|
|
46
|
+
Sanitize a string value for safe use in HTTP headers.
|
|
47
|
+
|
|
48
|
+
Prevents header injection by removing control characters that could
|
|
49
|
+
be used to inject additional headers or corrupt the MIME structure.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
value: The string to sanitize
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Sanitized string safe for use in headers
|
|
56
|
+
"""
|
|
57
|
+
if not value:
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
# Remove control characters that could cause header injection
|
|
61
|
+
# This includes \r, \n, \0, and other control characters
|
|
62
|
+
sanitized = re.sub(r'[\r\n\x00-\x1f\x7f-\x9f]', '', value)
|
|
63
|
+
|
|
64
|
+
# Remove any quotes that could break the header structure
|
|
65
|
+
sanitized = sanitized.replace('"', '')
|
|
66
|
+
|
|
67
|
+
# Limit length to prevent overly long headers
|
|
68
|
+
if len(sanitized) > 255:
|
|
69
|
+
sanitized = sanitized[:255]
|
|
70
|
+
|
|
71
|
+
return sanitized.strip()
|