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,242 @@
1
+ from datetime import datetime
2
+ from typing import Optional, List
3
+ from dataclasses import dataclass, field
4
+
5
+ from google_client.services.drive.constants import GOOGLE_DOCS_MIME_TYPE, GOOGLE_SHEETS_MIME_TYPE, \
6
+ GOOGLE_SLIDES_MIME_TYPE
7
+
8
+
9
+ @dataclass
10
+ class Permission:
11
+ """
12
+ Represents a permission for a Drive file or folder.
13
+ Args:
14
+ permission_id: The unique identifier for this permission.
15
+ type: The type of permission (user, group, domain, anyone).
16
+ role: The role of the permission (reader, writer, commenter, owner).
17
+ email_address: The email address for user/group permissions.
18
+ domain: The domain name for domain permissions.
19
+ display_name: Display name of the person/group.
20
+ deleted: Whether this permission has been deleted.
21
+ """
22
+ permission_id: str
23
+ type: Optional[str] = None
24
+ role: Optional[str] = None
25
+ email_address: Optional[str] = None
26
+ domain: Optional[str] = None
27
+ display_name: Optional[str] = None
28
+ deleted: bool = False
29
+
30
+ def to_dict(self) -> dict:
31
+ """
32
+ Converts the Permission instance to a dictionary representation.
33
+ Returns:
34
+ A dictionary containing the permission data.
35
+ """
36
+ result = {}
37
+ if self.permission_id:
38
+ result["id"] = self.permission_id
39
+ if self.type:
40
+ result["type"] = self.type
41
+ if self.role:
42
+ result["role"] = self.role
43
+ if self.email_address:
44
+ result["emailAddress"] = self.email_address
45
+ if self.domain:
46
+ result["domain"] = self.domain
47
+ if self.display_name:
48
+ result["displayName"] = self.display_name
49
+ if self.deleted:
50
+ result["deleted"] = self.deleted
51
+ return result
52
+
53
+ def __str__(self):
54
+ if self.email_address:
55
+ return f"{self.display_name or self.email_address} ({self.role})"
56
+ elif self.domain:
57
+ return f"Domain: {self.domain} ({self.role})"
58
+ else:
59
+ return f"{self.type} ({self.role})"
60
+
61
+
62
+ @dataclass
63
+ class DriveItem:
64
+ """
65
+ Base class for items in Google Drive (files and folders).
66
+ """
67
+ item_id: str
68
+ name: Optional[str] = None
69
+ created_time: Optional[datetime] = None
70
+ modified_time: Optional[datetime] = None
71
+ parent_ids: List[str] = field(default_factory=list)
72
+ web_view_link: Optional[str] = None
73
+ owners: List[str] = field(default_factory=list)
74
+ permissions: List[Permission] = field(default_factory=list)
75
+ description: Optional[str] = None
76
+ starred: bool = False
77
+ trashed: bool = False
78
+ shared: bool = False
79
+
80
+ def get_parent_folder_id(self) -> Optional[str]:
81
+ """
82
+ Get the first parent folder ID.
83
+ Returns:
84
+ The first parent folder ID, or None if no parents.
85
+ """
86
+ return self.parent_ids[0] if self.parent_ids else None
87
+
88
+ def has_parent(self) -> bool:
89
+ """
90
+ Check if this item has a parent folder.
91
+ Returns:
92
+ True if item has at least one parent folder.
93
+ """
94
+ return bool(self.parent_ids)
95
+
96
+ def get_all_parent_ids(self) -> List[str]:
97
+ """
98
+ Get all parent folder IDs.
99
+ Returns:
100
+ List of all parent folder IDs.
101
+ """
102
+ return self.parent_ids.copy()
103
+
104
+ def is_in_folder(self, folder_id: str) -> bool:
105
+ """
106
+ Check if this item is in a specific parent folder.
107
+ Args:
108
+ folder_id: ID of the folder to check
109
+ Returns:
110
+ True if this item is in the specified folder.
111
+ """
112
+ return folder_id in self.parent_ids
113
+
114
+ def to_dict(self) -> dict:
115
+ """
116
+ Converts the DriveItem instance to a dictionary representation.
117
+ Returns:
118
+ A dictionary containing the item data.
119
+ """
120
+ result = {
121
+ "id": self.item_id,
122
+ "name": self.name,
123
+ "createdTime": self.created_time.isoformat() + "Z" if self.created_time else None,
124
+ "modifiedTime": self.modified_time.isoformat() + "Z" if self.modified_time else None,
125
+ "parents": self.parent_ids,
126
+ "webViewLink": self.web_view_link,
127
+ "description": self.description,
128
+ "permissions": [p.to_dict() for p in self.permissions],
129
+ "starred": self.starred,
130
+ "trashed": self.trashed,
131
+ "shared": self.shared,
132
+ }
133
+ return result
134
+
135
+
136
+ @dataclass
137
+ class DriveFile(DriveItem):
138
+ """
139
+ Represents a file in Google Drive.
140
+ """
141
+ mime_type: Optional[str] = None
142
+ size: Optional[int] = None
143
+ web_content_link: Optional[str] = None
144
+ original_filename: Optional[str] = None
145
+ file_extension: Optional[str] = None
146
+ md5_checksum: Optional[str] = None
147
+
148
+ @property
149
+ def file_id(self):
150
+ return self.item_id
151
+
152
+ def is_google_doc(self) -> bool:
153
+ """
154
+ Check if this file is a Google Workspace document.
155
+ Returns:
156
+ True if the file is a Google Workspace document.
157
+ """
158
+ google_mime_types = [
159
+ GOOGLE_DOCS_MIME_TYPE,
160
+ GOOGLE_SHEETS_MIME_TYPE,
161
+ GOOGLE_SLIDES_MIME_TYPE,
162
+ "application/vnd.google-apps.drawing",
163
+ "application/vnd.google-apps.form",
164
+ ]
165
+ return self.mime_type in google_mime_types
166
+
167
+ def human_readable_size(self) -> str:
168
+ """
169
+ Get human-readable file size.
170
+ Returns:
171
+ Size in human-readable format (e.g., "1.2 MB").
172
+ """
173
+ if self.size is None:
174
+ return "Unknown"
175
+
176
+ if self.size == 0:
177
+ return "0 B"
178
+
179
+ size = self.size
180
+ units = ["B", "KB", "MB", "GB", "TB"]
181
+ unit_index = 0
182
+
183
+ while size >= 1024 and unit_index < len(units) - 1:
184
+ size /= 1024
185
+ unit_index += 1
186
+
187
+ return f"{size:.1f} {units[unit_index]}"
188
+
189
+ def to_dict(self) -> dict:
190
+ """
191
+ Converts the DriveFile instance to a dictionary representation.
192
+ Returns:
193
+ A dictionary containing the file data.
194
+ """
195
+ result = super().to_dict()
196
+ result.update({
197
+ "mimeType": self.mime_type,
198
+ "size": str(self.size) if self.size is not None else None,
199
+ "webContentLink": self.web_content_link,
200
+ })
201
+ return result
202
+
203
+ def __str__(self):
204
+ size_str = f"({self.human_readable_size()})"
205
+ return f"{self.name} {size_str}"
206
+
207
+ def __repr__(self):
208
+ return f"DriveFile(id={self.item_id!r}, name={self.name!r}, mime_type={self.mime_type!r})"
209
+
210
+
211
+ @dataclass
212
+ class DriveFolder(DriveItem):
213
+ """
214
+ Represents a folder in Google Drive.
215
+ """
216
+ @property
217
+ def folder_id(self):
218
+ return self.item_id
219
+
220
+ @property
221
+ def parents(self):
222
+ return self.parent_ids
223
+
224
+ @parents.setter
225
+ def parents(self, value):
226
+ self.parent_ids = value
227
+
228
+ def to_dict(self) -> dict:
229
+ """
230
+ Converts the DriveFolder instance to a dictionary representation.
231
+ Returns:
232
+ A dictionary containing the folder data.
233
+ """
234
+ result = super().to_dict()
235
+ result["mimeType"] = "application/vnd.google-apps.folder"
236
+ return result
237
+
238
+ def __str__(self):
239
+ return f"[Folder] {self.name}"
240
+
241
+ def __repr__(self):
242
+ return f"DriveFolder(id={self.item_id!r}, name={self.name!r})"
@@ -0,0 +1,392 @@
1
+ from datetime import datetime
2
+ from typing import Optional, Dict, Any, List, Union
3
+ import mimetypes
4
+ import os
5
+
6
+ from .types import DriveFile, DriveFolder, Permission
7
+ from .constants import FOLDER_MIME_TYPE, GOOGLE_DOCS_MIME_TYPE, MICROSOFT_WORD_MIME_TYPE, GOOGLE_SHEETS_MIME_TYPE, \
8
+ MICROSOFT_EXCEL_MIME_TYPE, GOOGLE_SLIDES_MIME_TYPE, MICROSOFT_POWERPOINT_MIME_TYPE
9
+
10
+
11
+ def convert_mime_type_to_downloadable(mime_type: str) -> str:
12
+ mime_type_conversion = {
13
+ GOOGLE_DOCS_MIME_TYPE: MICROSOFT_WORD_MIME_TYPE,
14
+ GOOGLE_SHEETS_MIME_TYPE: MICROSOFT_EXCEL_MIME_TYPE,
15
+ GOOGLE_SLIDES_MIME_TYPE: MICROSOFT_POWERPOINT_MIME_TYPE
16
+ }
17
+
18
+ return mime_type_conversion.get(mime_type)
19
+
20
+ def convert_api_file_to_drive_file(api_file: Dict[str, Any]) -> DriveFile:
21
+ """
22
+ Convert a file resource from the Drive API to a DriveFile object.
23
+
24
+ Args:
25
+ api_file: File resource dictionary from Drive API
26
+
27
+ Returns:
28
+ DriveFile object
29
+ """
30
+ # Parse datetime fields
31
+ created_time = None
32
+ if api_file.get("createdTime"):
33
+ created_time = datetime.fromisoformat(api_file["createdTime"].replace("Z", "+00:00"))
34
+
35
+ modified_time = None
36
+ if api_file.get("modifiedTime"):
37
+ modified_time = datetime.fromisoformat(api_file["modifiedTime"].replace("Z", "+00:00"))
38
+
39
+ # Parse size (API returns it as string)
40
+ size = None
41
+ if api_file.get("size"):
42
+ try:
43
+ size = int(api_file["size"])
44
+ except (ValueError, TypeError):
45
+ size = None
46
+
47
+ # Parse permissions
48
+ permissions = []
49
+ if api_file.get("permissions"):
50
+ for perm_data in api_file["permissions"]:
51
+ permissions.append(convert_api_permission_to_permission(perm_data))
52
+
53
+ # Extract owners
54
+ owners = []
55
+ if api_file.get("owners"):
56
+ owners = [owner.get("emailAddress", owner.get("displayName", "Unknown"))
57
+ for owner in api_file["owners"]]
58
+
59
+ return DriveFile(
60
+ item_id=api_file.get("id"),
61
+ name=api_file.get("name"),
62
+ mime_type=api_file.get("mimeType"),
63
+ size=size,
64
+ created_time=created_time,
65
+ modified_time=modified_time,
66
+ parent_ids=api_file.get("parents", []),
67
+ web_view_link=api_file.get("webViewLink"),
68
+ web_content_link=api_file.get("webContentLink"),
69
+ owners=owners,
70
+ permissions=permissions,
71
+ description=api_file.get("description"),
72
+ starred=api_file.get("starred", False),
73
+ trashed=api_file.get("trashed", False),
74
+ shared=api_file.get("shared", False),
75
+ original_filename=api_file.get("originalFilename"),
76
+ file_extension=api_file.get("fileExtension"),
77
+ md5_checksum=api_file.get("md5Checksum"),
78
+ )
79
+
80
+
81
+ def convert_api_file_to_drive_folder(api_file: Dict[str, Any]) -> DriveFolder:
82
+ """
83
+ Convert a folder resource from the Drive API to a DriveFolder object.
84
+
85
+ Args:
86
+ api_file: File resource dictionary from Drive API (must be a folder)
87
+
88
+ Returns:
89
+ DriveFolder object
90
+ """
91
+ # Parse datetime fields
92
+ created_time = None
93
+ if api_file.get("createdTime"):
94
+ created_time = datetime.fromisoformat(api_file["createdTime"].replace("Z", "+00:00"))
95
+
96
+ modified_time = None
97
+ if api_file.get("modifiedTime"):
98
+ modified_time = datetime.fromisoformat(api_file["modifiedTime"].replace("Z", "+00:00"))
99
+
100
+ # Parse permissions
101
+ permissions = []
102
+ if api_file.get("permissions"):
103
+ for perm_data in api_file["permissions"]:
104
+ permissions.append(convert_api_permission_to_permission(perm_data))
105
+
106
+ # Extract owners
107
+ owners = []
108
+ if api_file.get("owners"):
109
+ owners = [owner.get("emailAddress", owner.get("displayName", "Unknown"))
110
+ for owner in api_file["owners"]]
111
+
112
+ return DriveFolder(
113
+ item_id=api_file.get("id"),
114
+ name=api_file.get("name"),
115
+ created_time=created_time,
116
+ modified_time=modified_time,
117
+ parent_ids=api_file.get("parents", []),
118
+ web_view_link=api_file.get("webViewLink"),
119
+ owners=owners,
120
+ permissions=permissions,
121
+ description=api_file.get("description"),
122
+ starred=api_file.get("starred", False),
123
+ trashed=api_file.get("trashed", False),
124
+ shared=api_file.get("shared", False),
125
+ )
126
+
127
+
128
+ def convert_api_file_to_correct_type(api_file: Dict[str, Any]) -> Union[DriveFile, DriveFolder]:
129
+ """
130
+ Convert a file/folder resource from the Drive API to the correct type.
131
+
132
+ Args:
133
+ api_file: File resource dictionary from Drive API
134
+
135
+ Returns:
136
+ DriveFile or DriveFolder object based on MIME type
137
+ """
138
+ mime_type = api_file.get("mimeType")
139
+
140
+ if mime_type == FOLDER_MIME_TYPE:
141
+ return convert_api_file_to_drive_folder(api_file)
142
+ else:
143
+ return convert_api_file_to_drive_file(api_file)
144
+
145
+
146
+ def convert_api_permission_to_permission(api_permission: Dict[str, Any]) -> Permission:
147
+ """
148
+ Convert a permission resource from the Drive API to a Permission object.
149
+
150
+ Args:
151
+ api_permission: Permission resource dictionary from Drive API
152
+
153
+ Returns:
154
+ Permission object
155
+ """
156
+ return Permission(
157
+ permission_id=api_permission.get("id"),
158
+ type=api_permission.get("type"),
159
+ role=api_permission.get("role"),
160
+ email_address=api_permission.get("emailAddress"),
161
+ domain=api_permission.get("domain"),
162
+ display_name=api_permission.get("displayName"),
163
+ deleted=api_permission.get("deleted", False),
164
+ )
165
+
166
+
167
+ def guess_mime_type(file_path: str) -> str:
168
+ """
169
+ Guess the MIME type of a file based on its extension.
170
+
171
+ Args:
172
+ file_path: Path to the file
173
+
174
+ Returns:
175
+ MIME type string
176
+ """
177
+ mime_type, _ = mimetypes.guess_type(file_path)
178
+ return mime_type or "application/octet-stream"
179
+
180
+
181
+ def guess_extension(mime_type: str) -> Optional[str]:
182
+ """
183
+ Guess the extension of a file based on its MIME type.
184
+ Args:
185
+ mime_type: The MIME type of the file
186
+
187
+ Returns:
188
+ Extension string. None if MIME type is unknown
189
+ """
190
+ if mime_type in [GOOGLE_DOCS_MIME_TYPE, GOOGLE_SHEETS_MIME_TYPE, GOOGLE_SLIDES_MIME_TYPE]:
191
+ return mimetypes.guess_extension(convert_mime_type_to_downloadable(mime_type))
192
+
193
+ return mimetypes.guess_extension(mime_type)
194
+
195
+
196
+ def build_file_metadata(
197
+ name: str,
198
+ parents: Optional[List[str]] = None,
199
+ description: Optional[str] = None,
200
+ **kwargs
201
+ ) -> Dict[str, Any]:
202
+ """
203
+ Build file metadata dictionary for Drive API operations.
204
+
205
+ Args:
206
+ name: File name
207
+ parents: List of parent folder IDs
208
+ description: File description
209
+ **kwargs: Additional metadata fields
210
+
211
+ Returns:
212
+ Metadata dictionary
213
+ """
214
+ metadata = {"name": name}
215
+
216
+ if parents:
217
+ metadata["parents"] = parents
218
+
219
+ if description:
220
+ metadata["description"] = description
221
+
222
+ # Add any additional metadata
223
+ metadata.update(kwargs)
224
+
225
+ return metadata
226
+
227
+
228
+ def sanitize_filename(filename: str) -> str:
229
+ """
230
+ Sanitize a filename to be safe for Drive upload.
231
+
232
+ Args:
233
+ filename: Original filename
234
+
235
+ Returns:
236
+ Sanitized filename
237
+ """
238
+ # Characters that are problematic in Drive
239
+ invalid_chars = ['<', '>', ':', '"', '|', '?', '*', '/', '\\']
240
+
241
+ sanitized = filename
242
+ for char in invalid_chars:
243
+ sanitized = sanitized.replace(char, '_')
244
+
245
+ # Remove leading/trailing whitespace and dots
246
+ sanitized = sanitized.strip('. ')
247
+
248
+ # Ensure filename is not empty
249
+ if not sanitized:
250
+ sanitized = "untitled"
251
+
252
+ return sanitized
253
+
254
+
255
+ def format_file_size(size_bytes: Optional[int]) -> str:
256
+ """
257
+ Format file size in bytes to human-readable format.
258
+
259
+ Args:
260
+ size_bytes: Size in bytes
261
+
262
+ Returns:
263
+ Human-readable size string
264
+ """
265
+ if size_bytes is None:
266
+ return "Unknown"
267
+
268
+ if size_bytes == 0:
269
+ return "0 B"
270
+
271
+ units = ["B", "KB", "MB", "GB", "TB", "PB"]
272
+ size = float(size_bytes)
273
+ unit_index = 0
274
+
275
+ while size >= 1024 and unit_index < len(units) - 1:
276
+ size /= 1024
277
+ unit_index += 1
278
+
279
+ return f"{size:.1f} {units[unit_index]}"
280
+
281
+
282
+ def is_folder_mime_type(mime_type: str) -> bool:
283
+ """
284
+ Check if a MIME type represents a folder.
285
+
286
+ Args:
287
+ mime_type: MIME type string
288
+
289
+ Returns:
290
+ True if the MIME type is for a folder
291
+ """
292
+ return mime_type == FOLDER_MIME_TYPE
293
+
294
+
295
+ def build_search_query(*query_parts: str) -> str:
296
+ """
297
+ Build a Drive search query from multiple parts.
298
+
299
+ Args:
300
+ *query_parts: Query parts to combine
301
+
302
+ Returns:
303
+ Combined search query
304
+ """
305
+ # Filter out empty query parts
306
+ valid_parts = [part.strip() for part in query_parts if part and part.strip()]
307
+
308
+ if not valid_parts:
309
+ return ""
310
+
311
+ # Join with AND operator
312
+ return " and ".join(f"({part})" for part in valid_parts)
313
+
314
+
315
+ def extract_file_id_from_url(url: str) -> Optional[str]:
316
+ """
317
+ Extract file ID from a Google Drive URL.
318
+
319
+ Args:
320
+ url: Google Drive URL
321
+
322
+ Returns:
323
+ File ID if found, None otherwise
324
+ """
325
+ # Common Drive URL patterns
326
+ patterns = [
327
+ r"/file/d/([a-zA-Z0-9-_]+)", # /file/d/FILE_ID/view or /file/d/FILE_ID/edit
328
+ r"/folders/([a-zA-Z0-9-_]+)", # /folders/FOLDER_ID
329
+ r"id=([a-zA-Z0-9-_]+)", # ?id=FILE_ID
330
+ ]
331
+
332
+ import re
333
+ for pattern in patterns:
334
+ match = re.search(pattern, url)
335
+ if match:
336
+ return match.group(1)
337
+
338
+ return None
339
+
340
+
341
+ def parse_folder_path(path: str) -> List[str]:
342
+ """
343
+ Parse a folder path into individual folder names.
344
+
345
+ Args:
346
+ path: Folder path like "/Documents/Projects" or "Documents/Projects"
347
+
348
+ Returns:
349
+ List of folder names
350
+ """
351
+ if not path:
352
+ return []
353
+
354
+ # Remove leading/trailing slashes and split
355
+ path = path.strip('/')
356
+ if not path:
357
+ return []
358
+
359
+ return [name.strip() for name in path.split('/') if name.strip()]
360
+
361
+
362
+ def build_folder_path(folder_names: List[str]) -> str:
363
+ """
364
+ Build a folder path from a list of folder names.
365
+
366
+ Args:
367
+ folder_names: List of folder names
368
+
369
+ Returns:
370
+ Folder path string
371
+ """
372
+ if not folder_names:
373
+ return "/"
374
+
375
+ return "/" + "/".join(folder_names)
376
+
377
+
378
+ def normalize_folder_path(path: str) -> str:
379
+ """
380
+ Normalize a folder path by removing extra slashes and whitespace.
381
+
382
+ Args:
383
+ path: Raw folder path
384
+
385
+ Returns:
386
+ Normalized folder path
387
+ """
388
+ if not path:
389
+ return "/"
390
+
391
+ folder_names = parse_folder_path(path)
392
+ return build_folder_path(folder_names)
@@ -0,0 +1,16 @@
1
+ """Gmail client module for Google API integration."""
2
+
3
+ from .api_service import GmailApiService
4
+ from .query_builder import EmailQueryBuilder
5
+ from .types import EmailMessage, EmailAddress, EmailAttachment, Label, EmailThread
6
+
7
+ __all__ = [
8
+ "EmailMessage",
9
+ "EmailAddress",
10
+ "EmailAttachment",
11
+ "Label",
12
+ "EmailThread",
13
+ "EmailQueryBuilder",
14
+ "GmailApiService",
15
+
16
+ ]