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,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()