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,6 @@
1
+
2
+
3
+ MAX_RESULTS_LIMIT = 2500
4
+ MAX_BODY_LENGTH = 25000000 # ~25MB Gmail limit
5
+ DEFAULT_MAX_RESULTS = 30
6
+ MAX_SUBJECT_LENGTH = 998 # RFC 2822 practical limit
@@ -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})"