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,45 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class GmailError(Exception):
|
|
4
|
+
"""Base exception for Gmail API errors."""
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EmailNotFoundError(GmailError):
|
|
9
|
+
"""Raised when an email message is not found."""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LabelNotFoundError(GmailError):
|
|
14
|
+
"""Raised when a Gmail label is not found."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AttachmentNotFoundError(GmailError):
|
|
19
|
+
"""Raised when an email attachment is not found."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ThreadNotFoundError(GmailError):
|
|
24
|
+
"""Raised when an email thread is not found."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GmailPermissionError(GmailError):
|
|
29
|
+
"""Raised when the user lacks permission for a Gmail operation."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GmailQuotaExceededError(GmailError):
|
|
34
|
+
"""Raised when Gmail API quota is exceeded."""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InvalidEmailFormatError(GmailError):
|
|
39
|
+
"""Raised when an email address has invalid format."""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MessageTooLargeError(GmailError):
|
|
44
|
+
"""Raised when an email message exceeds size limits."""
|
|
45
|
+
pass
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from datetime import datetime, date, timedelta
|
|
2
|
+
from typing import Optional, List, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from .api_service import EmailMessage
|
|
6
|
+
from .types import EmailThread
|
|
7
|
+
|
|
8
|
+
# Constants (imported from gmail_client)
|
|
9
|
+
MAX_RESULTS_LIMIT = 2500
|
|
10
|
+
DEFAULT_MAX_RESULTS = 30
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EmailQueryBuilder:
|
|
14
|
+
"""
|
|
15
|
+
Builder pattern for constructing Gmail queries with a fluent API.
|
|
16
|
+
Provides a clean, readable way to build complex email queries.
|
|
17
|
+
|
|
18
|
+
Example usage:
|
|
19
|
+
emails = (EmailMessage.query()
|
|
20
|
+
.limit(50)
|
|
21
|
+
.from_sender("sender@example.com")
|
|
22
|
+
.search("meeting")
|
|
23
|
+
.with_attachments()
|
|
24
|
+
.execute())
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, api_service_class):
|
|
28
|
+
self._api_service = api_service_class
|
|
29
|
+
self._max_results: Optional[int] = DEFAULT_MAX_RESULTS
|
|
30
|
+
self._query_parts: List[str] = []
|
|
31
|
+
self._include_spam_trash: bool = False
|
|
32
|
+
self._label_ids: List[str] = []
|
|
33
|
+
|
|
34
|
+
def limit(self, count: int) -> "EmailQueryBuilder":
|
|
35
|
+
"""
|
|
36
|
+
Set the maximum number of emails to retrieve.
|
|
37
|
+
Args:
|
|
38
|
+
count: Maximum number of emails (1-2500)
|
|
39
|
+
Returns:
|
|
40
|
+
Self for method chaining
|
|
41
|
+
"""
|
|
42
|
+
if count < 1 or count > MAX_RESULTS_LIMIT:
|
|
43
|
+
raise ValueError(f"Limit must be between 1 and {MAX_RESULTS_LIMIT}")
|
|
44
|
+
self._max_results = count
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
def search(self, query: str, exact_match: bool = False) -> "EmailQueryBuilder":
|
|
48
|
+
"""
|
|
49
|
+
Add a search term to the query.
|
|
50
|
+
Args:
|
|
51
|
+
query: Search term to add
|
|
52
|
+
exact_match: Boolean indicating whether to return exact matches only
|
|
53
|
+
Returns:
|
|
54
|
+
Self for method chaining
|
|
55
|
+
"""
|
|
56
|
+
if query:
|
|
57
|
+
if exact_match:
|
|
58
|
+
query = f'"{query}"'
|
|
59
|
+
self._query_parts.append(query)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def from_sender(self, email: str) -> "EmailQueryBuilder":
|
|
63
|
+
"""
|
|
64
|
+
Filter emails from a specific sender.
|
|
65
|
+
Args:
|
|
66
|
+
email: Sender email address
|
|
67
|
+
Returns:
|
|
68
|
+
Self for method chaining
|
|
69
|
+
"""
|
|
70
|
+
if email:
|
|
71
|
+
self._query_parts.append(f"from:{email}")
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
def to_recipient(self, email: str) -> "EmailQueryBuilder":
|
|
75
|
+
"""
|
|
76
|
+
Filter emails sent to a specific recipient.
|
|
77
|
+
Args:
|
|
78
|
+
email: Recipient email address
|
|
79
|
+
Returns:
|
|
80
|
+
Self for method chaining
|
|
81
|
+
"""
|
|
82
|
+
if email:
|
|
83
|
+
self._query_parts.append(f"to:{email}")
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def with_subject(self, subject: str) -> "EmailQueryBuilder":
|
|
87
|
+
"""
|
|
88
|
+
Filter emails with specific subject content.
|
|
89
|
+
Args:
|
|
90
|
+
subject: Subject content to search for
|
|
91
|
+
Returns:
|
|
92
|
+
Self for method chaining
|
|
93
|
+
"""
|
|
94
|
+
if subject:
|
|
95
|
+
self._query_parts.append(f"subject:{subject}")
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
def with_attachments(self) -> "EmailQueryBuilder":
|
|
99
|
+
"""
|
|
100
|
+
Filter emails that have attachments.
|
|
101
|
+
Returns:
|
|
102
|
+
Self for method chaining
|
|
103
|
+
"""
|
|
104
|
+
self._query_parts.append("has:attachment")
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def without_attachments(self) -> "EmailQueryBuilder":
|
|
108
|
+
"""
|
|
109
|
+
Filter emails that don't have attachments.
|
|
110
|
+
Returns:
|
|
111
|
+
Self for method chaining
|
|
112
|
+
"""
|
|
113
|
+
self._query_parts.append("-has:attachment")
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def is_read(self) -> "EmailQueryBuilder":
|
|
117
|
+
"""
|
|
118
|
+
Filter emails that are read.
|
|
119
|
+
Returns:
|
|
120
|
+
Self for method chaining
|
|
121
|
+
"""
|
|
122
|
+
self._query_parts.append("-is:unread")
|
|
123
|
+
return self
|
|
124
|
+
|
|
125
|
+
def is_unread(self) -> "EmailQueryBuilder":
|
|
126
|
+
"""
|
|
127
|
+
Filter emails that are unread.
|
|
128
|
+
Returns:
|
|
129
|
+
Self for method chaining
|
|
130
|
+
"""
|
|
131
|
+
self._query_parts.append("is:unread")
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def is_starred(self) -> "EmailQueryBuilder":
|
|
135
|
+
"""
|
|
136
|
+
Filter emails that are starred.
|
|
137
|
+
Returns:
|
|
138
|
+
Self for method chaining
|
|
139
|
+
"""
|
|
140
|
+
self._query_parts.append("is:starred")
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def is_important(self) -> "EmailQueryBuilder":
|
|
144
|
+
"""
|
|
145
|
+
Filter emails that are marked as important.
|
|
146
|
+
Returns:
|
|
147
|
+
Self for method chaining
|
|
148
|
+
"""
|
|
149
|
+
self._query_parts.append("is:important")
|
|
150
|
+
return self
|
|
151
|
+
|
|
152
|
+
def in_folder(self, folder: str) -> "EmailQueryBuilder":
|
|
153
|
+
"""
|
|
154
|
+
Filter emails in a specific folder/label.
|
|
155
|
+
Args:
|
|
156
|
+
folder: Folder/label name (inbox, sent, drafts, trash, spam, etc.)
|
|
157
|
+
Returns:
|
|
158
|
+
Self for method chaining
|
|
159
|
+
"""
|
|
160
|
+
if folder:
|
|
161
|
+
self._query_parts.append(f"in:{folder}")
|
|
162
|
+
return self
|
|
163
|
+
|
|
164
|
+
def with_label(self, label: str) -> "EmailQueryBuilder":
|
|
165
|
+
"""
|
|
166
|
+
Filter emails with a specific label.
|
|
167
|
+
Args:
|
|
168
|
+
label: Label name
|
|
169
|
+
Returns:
|
|
170
|
+
Self for method chaining
|
|
171
|
+
"""
|
|
172
|
+
if label:
|
|
173
|
+
self._query_parts.append(f"label:{label}")
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def in_date_range(self, start_date: date, end_date: date) -> "EmailQueryBuilder":
|
|
177
|
+
"""
|
|
178
|
+
Filter emails within a specific date range.
|
|
179
|
+
Args:
|
|
180
|
+
start_date: Start of date range
|
|
181
|
+
end_date: End of date range
|
|
182
|
+
Returns:
|
|
183
|
+
Self for method chaining
|
|
184
|
+
"""
|
|
185
|
+
if start_date > end_date:
|
|
186
|
+
raise ValueError("Start date must be before end date")
|
|
187
|
+
|
|
188
|
+
start_str = start_date.strftime("%Y/%m/%d")
|
|
189
|
+
end_str = end_date.strftime("%Y/%m/%d")
|
|
190
|
+
self._query_parts.append(f"after:{start_str}")
|
|
191
|
+
self._query_parts.append(f"before:{end_str}")
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def after_date(self, date_obj: date) -> "EmailQueryBuilder":
|
|
195
|
+
"""
|
|
196
|
+
Filter emails after a specific date.
|
|
197
|
+
Args:
|
|
198
|
+
date_obj: Date to filter after
|
|
199
|
+
Returns:
|
|
200
|
+
Self for method chaining
|
|
201
|
+
"""
|
|
202
|
+
date_str = date_obj.strftime("%Y/%m/%d")
|
|
203
|
+
self._query_parts.append(f"after:{date_str}")
|
|
204
|
+
return self
|
|
205
|
+
|
|
206
|
+
def before_date(self, date_obj: date) -> "EmailQueryBuilder":
|
|
207
|
+
"""
|
|
208
|
+
Filter emails before a specific date.
|
|
209
|
+
Args:
|
|
210
|
+
date_obj: Date to filter before
|
|
211
|
+
Returns:
|
|
212
|
+
Self for method chaining
|
|
213
|
+
"""
|
|
214
|
+
date_str = date_obj.strftime("%Y/%m/%d")
|
|
215
|
+
self._query_parts.append(f"before:{date_str}")
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
def today(self) -> "EmailQueryBuilder":
|
|
219
|
+
"""
|
|
220
|
+
Filter emails from today only.
|
|
221
|
+
Returns:
|
|
222
|
+
Self for method chaining
|
|
223
|
+
"""
|
|
224
|
+
today = datetime.now().date()
|
|
225
|
+
today_str = today.strftime("%Y/%m/%d")
|
|
226
|
+
self._query_parts.append(f"after:{today_str}")
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
def yesterday(self) -> "EmailQueryBuilder":
|
|
230
|
+
"""
|
|
231
|
+
Filter emails from yesterday only.
|
|
232
|
+
Returns:
|
|
233
|
+
Self for method chaining
|
|
234
|
+
"""
|
|
235
|
+
yesterday = datetime.now().date() - timedelta(days=1)
|
|
236
|
+
today = datetime.now().date()
|
|
237
|
+
yesterday_str = yesterday.strftime("%Y/%m/%d")
|
|
238
|
+
today_str = today.strftime("%Y/%m/%d")
|
|
239
|
+
self._query_parts.append(f"after:{yesterday_str}")
|
|
240
|
+
self._query_parts.append(f"before:{today_str}")
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def last_days(self, days: int) -> "EmailQueryBuilder":
|
|
244
|
+
"""
|
|
245
|
+
Filter emails from the last N days.
|
|
246
|
+
Args:
|
|
247
|
+
days: Number of days back to search
|
|
248
|
+
Returns:
|
|
249
|
+
Self for method chaining
|
|
250
|
+
"""
|
|
251
|
+
if days < 1:
|
|
252
|
+
raise ValueError("Days must be positive")
|
|
253
|
+
|
|
254
|
+
start_date = datetime.now() - timedelta(days=days)
|
|
255
|
+
start_str = start_date.strftime("%Y/%m/%d")
|
|
256
|
+
self._query_parts.append(f"after:{start_str}")
|
|
257
|
+
return self
|
|
258
|
+
|
|
259
|
+
def this_week(self) -> "EmailQueryBuilder":
|
|
260
|
+
"""
|
|
261
|
+
Filter emails from this week.
|
|
262
|
+
Returns:
|
|
263
|
+
Self for method chaining
|
|
264
|
+
"""
|
|
265
|
+
days_since_monday = date.weekday(date.today() ) + 1 # Monday is 0, so we add 1 to include today
|
|
266
|
+
return self.last_days(days_since_monday)
|
|
267
|
+
|
|
268
|
+
def this_month(self) -> "EmailQueryBuilder":
|
|
269
|
+
"""
|
|
270
|
+
Filter emails from this month.
|
|
271
|
+
Returns:
|
|
272
|
+
Self for method chaining
|
|
273
|
+
"""
|
|
274
|
+
days_since_month_started = date.today().day # Days in current month
|
|
275
|
+
return self.last_days(days_since_month_started)
|
|
276
|
+
|
|
277
|
+
def larger_than(self, size_mb: int) -> "EmailQueryBuilder":
|
|
278
|
+
"""
|
|
279
|
+
Filter emails larger than specified size.
|
|
280
|
+
Args:
|
|
281
|
+
size_mb: Size in megabytes
|
|
282
|
+
Returns:
|
|
283
|
+
Self for method chaining
|
|
284
|
+
"""
|
|
285
|
+
if size_mb < 1:
|
|
286
|
+
raise ValueError("Size must be positive")
|
|
287
|
+
self._query_parts.append(f"larger:{size_mb}M")
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
def smaller_than(self, size_mb: int) -> "EmailQueryBuilder":
|
|
291
|
+
"""
|
|
292
|
+
Filter emails smaller than specified size.
|
|
293
|
+
Args:
|
|
294
|
+
size_mb: Size in megabytes
|
|
295
|
+
Returns:
|
|
296
|
+
Self for method chaining
|
|
297
|
+
"""
|
|
298
|
+
if size_mb < 1:
|
|
299
|
+
raise ValueError("Size must be positive")
|
|
300
|
+
self._query_parts.append(f"smaller:{size_mb}M")
|
|
301
|
+
return self
|
|
302
|
+
|
|
303
|
+
def include_spam_trash(self, include: bool = True) -> "EmailQueryBuilder":
|
|
304
|
+
"""
|
|
305
|
+
Include or exclude spam and trash emails.
|
|
306
|
+
Args:
|
|
307
|
+
include: Whether to include spam and trash
|
|
308
|
+
Returns:
|
|
309
|
+
Self for method chaining
|
|
310
|
+
"""
|
|
311
|
+
self._include_spam_trash = include
|
|
312
|
+
return self
|
|
313
|
+
|
|
314
|
+
def with_label_ids(self, label_ids: List[str]) -> "EmailQueryBuilder":
|
|
315
|
+
"""
|
|
316
|
+
Filter emails with specific label IDs.
|
|
317
|
+
Args:
|
|
318
|
+
label_ids: List of label IDs
|
|
319
|
+
Returns:
|
|
320
|
+
Self for method chaining
|
|
321
|
+
"""
|
|
322
|
+
self._label_ids.extend(label_ids)
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
def execute(self) -> List["EmailMessage"]:
|
|
326
|
+
"""
|
|
327
|
+
Execute the query and return the results.
|
|
328
|
+
Returns:
|
|
329
|
+
List of EmailMessage objects matching the query
|
|
330
|
+
"""
|
|
331
|
+
query_string = " ".join(self._query_parts) if self._query_parts else None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# Use the service layer implementation instead of dataclass methods
|
|
336
|
+
emails = self._api_service.list_emails(
|
|
337
|
+
max_results=self._max_results,
|
|
338
|
+
query=query_string,
|
|
339
|
+
include_spam_trash=self._include_spam_trash,
|
|
340
|
+
label_ids=self._label_ids if self._label_ids else None
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return emails
|
|
344
|
+
|
|
345
|
+
def count(self) -> int:
|
|
346
|
+
"""
|
|
347
|
+
Get the count of emails matching the query without retrieving them.
|
|
348
|
+
Returns:
|
|
349
|
+
Number of emails matching the query
|
|
350
|
+
"""
|
|
351
|
+
# Set limit to 1 to minimize data transfer
|
|
352
|
+
original_limit = self._max_results
|
|
353
|
+
self._max_results = 1
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
# Execute the query to get total count from Gmail API
|
|
357
|
+
# Note: Gmail API doesn't provide direct count, so we estimate
|
|
358
|
+
results = self.execute()
|
|
359
|
+
return len(results) # This is just the returned count, not total
|
|
360
|
+
finally:
|
|
361
|
+
self._max_results = original_limit
|
|
362
|
+
|
|
363
|
+
def first(self) -> Optional["EmailMessage"]:
|
|
364
|
+
"""
|
|
365
|
+
Get the first email matching the query.
|
|
366
|
+
Returns:
|
|
367
|
+
First EmailMessage object or None if no matches
|
|
368
|
+
"""
|
|
369
|
+
original_limit = self._max_results
|
|
370
|
+
self._max_results = 1
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
results = self.execute()
|
|
374
|
+
return results[0] if results else None
|
|
375
|
+
finally:
|
|
376
|
+
self._max_results = original_limit
|
|
377
|
+
|
|
378
|
+
def exists(self) -> bool:
|
|
379
|
+
"""
|
|
380
|
+
Check if any emails match the query.
|
|
381
|
+
Returns:
|
|
382
|
+
True if at least one email matches, False otherwise
|
|
383
|
+
"""
|
|
384
|
+
return self.first() is not None
|
|
385
|
+
|
|
386
|
+
def get_threads(self) -> List["EmailThread"]:
|
|
387
|
+
"""
|
|
388
|
+
Execute the query and return threads instead of individual messages.
|
|
389
|
+
Returns:
|
|
390
|
+
List of EmailThread objects matching the query
|
|
391
|
+
"""
|
|
392
|
+
query_string = " ".join(self._query_parts) if self._query_parts else None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
# Use the service layer implementation to get threads
|
|
397
|
+
threads = self._api_service.list_threads(
|
|
398
|
+
max_results=self._max_results,
|
|
399
|
+
query=query_string,
|
|
400
|
+
include_spam_trash=self._include_spam_trash,
|
|
401
|
+
label_ids=self._label_ids if self._label_ids else None
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
return threads
|
|
405
|
+
|
|
406
|
+
def __repr__(self):
|
|
407
|
+
query_string = " ".join(self._query_parts) if self._query_parts else "None"
|
|
408
|
+
return f"EmailQueryBuilder(query='{query_string}', limit={self._max_results})"
|