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,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
|
+
]
|