gsuite-sdk 0.1.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.
- gsuite_calendar/__init__.py +13 -0
- gsuite_calendar/calendar_entity.py +31 -0
- gsuite_calendar/client.py +268 -0
- gsuite_calendar/event.py +57 -0
- gsuite_calendar/parser.py +119 -0
- gsuite_calendar/py.typed +0 -0
- gsuite_core/__init__.py +62 -0
- gsuite_core/api_utils.py +167 -0
- gsuite_core/auth/__init__.py +6 -0
- gsuite_core/auth/oauth.py +249 -0
- gsuite_core/auth/scopes.py +84 -0
- gsuite_core/config.py +73 -0
- gsuite_core/exceptions.py +125 -0
- gsuite_core/py.typed +0 -0
- gsuite_core/storage/__init__.py +13 -0
- gsuite_core/storage/base.py +65 -0
- gsuite_core/storage/secretmanager.py +141 -0
- gsuite_core/storage/sqlite.py +79 -0
- gsuite_drive/__init__.py +12 -0
- gsuite_drive/client.py +401 -0
- gsuite_drive/file.py +103 -0
- gsuite_drive/parser.py +66 -0
- gsuite_drive/py.typed +0 -0
- gsuite_gmail/__init__.py +17 -0
- gsuite_gmail/client.py +412 -0
- gsuite_gmail/label.py +56 -0
- gsuite_gmail/message.py +211 -0
- gsuite_gmail/parser.py +155 -0
- gsuite_gmail/py.typed +0 -0
- gsuite_gmail/query.py +227 -0
- gsuite_gmail/thread.py +54 -0
- gsuite_sdk-0.1.0.dist-info/METADATA +384 -0
- gsuite_sdk-0.1.0.dist-info/RECORD +42 -0
- gsuite_sdk-0.1.0.dist-info/WHEEL +5 -0
- gsuite_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- gsuite_sdk-0.1.0.dist-info/top_level.txt +5 -0
- gsuite_sheets/__init__.py +13 -0
- gsuite_sheets/client.py +375 -0
- gsuite_sheets/parser.py +76 -0
- gsuite_sheets/py.typed +0 -0
- gsuite_sheets/spreadsheet.py +97 -0
- gsuite_sheets/worksheet.py +185 -0
gsuite_drive/client.py
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""Drive client - high-level interface."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import logging
|
|
5
|
+
from typing import BinaryIO
|
|
6
|
+
|
|
7
|
+
from googleapiclient.discovery import build
|
|
8
|
+
from googleapiclient.errors import HttpError
|
|
9
|
+
from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload, MediaIoBaseUpload
|
|
10
|
+
|
|
11
|
+
from gsuite_core import GoogleAuth
|
|
12
|
+
from gsuite_drive.file import File, Folder
|
|
13
|
+
from gsuite_drive.parser import DriveParser
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Drive:
|
|
19
|
+
"""
|
|
20
|
+
High-level Google Drive client.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
auth = GoogleAuth()
|
|
24
|
+
auth.authenticate()
|
|
25
|
+
|
|
26
|
+
drive = Drive(auth)
|
|
27
|
+
|
|
28
|
+
# List files
|
|
29
|
+
for file in drive.list_files():
|
|
30
|
+
print(f"{file.name} ({file.mime_type})")
|
|
31
|
+
|
|
32
|
+
# Upload
|
|
33
|
+
drive.upload("document.pdf")
|
|
34
|
+
|
|
35
|
+
# Download
|
|
36
|
+
file = drive.get("file_id")
|
|
37
|
+
file.download("local_copy.pdf")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, auth: GoogleAuth):
|
|
41
|
+
"""
|
|
42
|
+
Initialize Drive client.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
auth: GoogleAuth instance with valid credentials
|
|
46
|
+
"""
|
|
47
|
+
self.auth = auth
|
|
48
|
+
self._service = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def service(self):
|
|
52
|
+
"""Lazy-load Drive API service."""
|
|
53
|
+
if self._service is None:
|
|
54
|
+
self._service = build("drive", "v3", credentials=self.auth.credentials)
|
|
55
|
+
return self._service
|
|
56
|
+
|
|
57
|
+
# ========== File listing ==========
|
|
58
|
+
|
|
59
|
+
def list_files(
|
|
60
|
+
self,
|
|
61
|
+
query: str | None = None,
|
|
62
|
+
parent_id: str | None = None,
|
|
63
|
+
mime_type: str | None = None,
|
|
64
|
+
max_results: int = 100,
|
|
65
|
+
order_by: str = "modifiedTime desc",
|
|
66
|
+
) -> list[File]:
|
|
67
|
+
"""
|
|
68
|
+
List files in Drive.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
query: Drive API query string
|
|
72
|
+
parent_id: Filter by parent folder ID
|
|
73
|
+
mime_type: Filter by MIME type
|
|
74
|
+
max_results: Maximum files to return
|
|
75
|
+
order_by: Sort order
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
List of File objects
|
|
79
|
+
"""
|
|
80
|
+
# Build query
|
|
81
|
+
query_parts = []
|
|
82
|
+
if query:
|
|
83
|
+
query_parts.append(query)
|
|
84
|
+
if parent_id:
|
|
85
|
+
query_parts.append(f"'{parent_id}' in parents")
|
|
86
|
+
if mime_type:
|
|
87
|
+
query_parts.append(f"mimeType='{mime_type}'")
|
|
88
|
+
|
|
89
|
+
# Don't include trashed files
|
|
90
|
+
query_parts.append("trashed=false")
|
|
91
|
+
|
|
92
|
+
full_query = " and ".join(query_parts)
|
|
93
|
+
|
|
94
|
+
response = (
|
|
95
|
+
self.service.files()
|
|
96
|
+
.list(
|
|
97
|
+
q=full_query,
|
|
98
|
+
pageSize=min(max_results, 1000),
|
|
99
|
+
orderBy=order_by,
|
|
100
|
+
fields="files(id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, webContentLink)",
|
|
101
|
+
)
|
|
102
|
+
.execute()
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
files = []
|
|
106
|
+
for item in response.get("files", []):
|
|
107
|
+
file = self._parse_file(item)
|
|
108
|
+
files.append(file)
|
|
109
|
+
|
|
110
|
+
return files
|
|
111
|
+
|
|
112
|
+
def list_folders(self, parent_id: str | None = None) -> list[Folder]:
|
|
113
|
+
"""List folders."""
|
|
114
|
+
files = self.list_files(
|
|
115
|
+
parent_id=parent_id,
|
|
116
|
+
mime_type="application/vnd.google-apps.folder",
|
|
117
|
+
)
|
|
118
|
+
return [
|
|
119
|
+
Folder(**{k: v for k, v in f.__dict__.items() if not k.startswith("_")}) for f in files
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def search(self, name: str, exact: bool = False) -> list[File]:
|
|
123
|
+
"""
|
|
124
|
+
Search files by name.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name: File name to search
|
|
128
|
+
exact: Exact match vs contains
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Matching files
|
|
132
|
+
"""
|
|
133
|
+
if exact:
|
|
134
|
+
query = f"name='{name}'"
|
|
135
|
+
else:
|
|
136
|
+
query = f"name contains '{name}'"
|
|
137
|
+
|
|
138
|
+
return self.list_files(query=query)
|
|
139
|
+
|
|
140
|
+
# ========== File operations ==========
|
|
141
|
+
|
|
142
|
+
def get(self, file_id: str) -> File | None:
|
|
143
|
+
"""Get a file by ID."""
|
|
144
|
+
try:
|
|
145
|
+
item = (
|
|
146
|
+
self.service.files()
|
|
147
|
+
.get(
|
|
148
|
+
fileId=file_id,
|
|
149
|
+
fields="id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink, webContentLink",
|
|
150
|
+
)
|
|
151
|
+
.execute()
|
|
152
|
+
)
|
|
153
|
+
return self._parse_file(item)
|
|
154
|
+
except HttpError as e:
|
|
155
|
+
if e.resp.status == 404:
|
|
156
|
+
logger.debug(f"File not found: {file_id}")
|
|
157
|
+
return None
|
|
158
|
+
logger.error(f"Error getting file {file_id}: {e}")
|
|
159
|
+
raise
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Unexpected error getting file {file_id}: {e}")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def get_content(self, file_id: str) -> bytes:
|
|
165
|
+
"""Download file content as bytes."""
|
|
166
|
+
request = self.service.files().get_media(fileId=file_id)
|
|
167
|
+
buffer = io.BytesIO()
|
|
168
|
+
downloader = MediaIoBaseDownload(buffer, request)
|
|
169
|
+
|
|
170
|
+
done = False
|
|
171
|
+
while not done:
|
|
172
|
+
_, done = downloader.next_chunk()
|
|
173
|
+
|
|
174
|
+
return buffer.getvalue()
|
|
175
|
+
|
|
176
|
+
def download(self, file_id: str, path: str) -> str:
|
|
177
|
+
"""
|
|
178
|
+
Download file to local path.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
file_id: File ID
|
|
182
|
+
path: Local path to save
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Path where file was saved
|
|
186
|
+
"""
|
|
187
|
+
content = self.get_content(file_id)
|
|
188
|
+
with open(path, "wb") as f:
|
|
189
|
+
f.write(content)
|
|
190
|
+
return path
|
|
191
|
+
|
|
192
|
+
# ========== Upload ==========
|
|
193
|
+
|
|
194
|
+
def upload(
|
|
195
|
+
self,
|
|
196
|
+
path: str,
|
|
197
|
+
name: str | None = None,
|
|
198
|
+
parent_id: str | None = None,
|
|
199
|
+
mime_type: str | None = None,
|
|
200
|
+
) -> File:
|
|
201
|
+
"""
|
|
202
|
+
Upload a file.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
path: Local file path
|
|
206
|
+
name: Name in Drive (default: local filename)
|
|
207
|
+
parent_id: Parent folder ID
|
|
208
|
+
mime_type: MIME type (auto-detected if not provided)
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Created File
|
|
212
|
+
"""
|
|
213
|
+
import os
|
|
214
|
+
|
|
215
|
+
file_name = name or os.path.basename(path)
|
|
216
|
+
|
|
217
|
+
metadata = {"name": file_name}
|
|
218
|
+
if parent_id:
|
|
219
|
+
metadata["parents"] = [parent_id]
|
|
220
|
+
|
|
221
|
+
media = MediaFileUpload(path, mimetype=mime_type, resumable=True)
|
|
222
|
+
|
|
223
|
+
created = (
|
|
224
|
+
self.service.files()
|
|
225
|
+
.create(
|
|
226
|
+
body=metadata,
|
|
227
|
+
media_body=media,
|
|
228
|
+
fields="id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink",
|
|
229
|
+
)
|
|
230
|
+
.execute()
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return self._parse_file(created)
|
|
234
|
+
|
|
235
|
+
def upload_content(
|
|
236
|
+
self,
|
|
237
|
+
content: bytes | BinaryIO,
|
|
238
|
+
name: str,
|
|
239
|
+
parent_id: str | None = None,
|
|
240
|
+
mime_type: str = "application/octet-stream",
|
|
241
|
+
) -> File:
|
|
242
|
+
"""
|
|
243
|
+
Upload content directly.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
content: File content as bytes or file-like object
|
|
247
|
+
name: Name in Drive
|
|
248
|
+
parent_id: Parent folder ID
|
|
249
|
+
mime_type: MIME type
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Created File
|
|
253
|
+
"""
|
|
254
|
+
if isinstance(content, bytes):
|
|
255
|
+
buffer = io.BytesIO(content)
|
|
256
|
+
else:
|
|
257
|
+
buffer = content
|
|
258
|
+
|
|
259
|
+
metadata = {"name": name}
|
|
260
|
+
if parent_id:
|
|
261
|
+
metadata["parents"] = [parent_id]
|
|
262
|
+
|
|
263
|
+
media = MediaIoBaseUpload(buffer, mimetype=mime_type, resumable=True)
|
|
264
|
+
|
|
265
|
+
created = (
|
|
266
|
+
self.service.files()
|
|
267
|
+
.create(
|
|
268
|
+
body=metadata,
|
|
269
|
+
media_body=media,
|
|
270
|
+
fields="id, name, mimeType, size, createdTime, modifiedTime, parents, webViewLink",
|
|
271
|
+
)
|
|
272
|
+
.execute()
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return self._parse_file(created)
|
|
276
|
+
|
|
277
|
+
# ========== Folder operations ==========
|
|
278
|
+
|
|
279
|
+
def create_folder(
|
|
280
|
+
self,
|
|
281
|
+
name: str,
|
|
282
|
+
parent_id: str | None = None,
|
|
283
|
+
) -> Folder:
|
|
284
|
+
"""
|
|
285
|
+
Create a folder.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
name: Folder name
|
|
289
|
+
parent_id: Parent folder ID
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Created Folder
|
|
293
|
+
"""
|
|
294
|
+
metadata = {
|
|
295
|
+
"name": name,
|
|
296
|
+
"mimeType": "application/vnd.google-apps.folder",
|
|
297
|
+
}
|
|
298
|
+
if parent_id:
|
|
299
|
+
metadata["parents"] = [parent_id]
|
|
300
|
+
|
|
301
|
+
created = (
|
|
302
|
+
self.service.files()
|
|
303
|
+
.create(
|
|
304
|
+
body=metadata,
|
|
305
|
+
fields="id, name, mimeType, createdTime, modifiedTime, parents, webViewLink",
|
|
306
|
+
)
|
|
307
|
+
.execute()
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
file = self._parse_file(created)
|
|
311
|
+
return Folder(**{k: v for k, v in file.__dict__.items() if not k.startswith("_")})
|
|
312
|
+
|
|
313
|
+
# ========== Delete/Trash ==========
|
|
314
|
+
|
|
315
|
+
def trash(self, file_id: str) -> bool:
|
|
316
|
+
"""Move file to trash."""
|
|
317
|
+
try:
|
|
318
|
+
self.service.files().update(
|
|
319
|
+
fileId=file_id,
|
|
320
|
+
body={"trashed": True},
|
|
321
|
+
).execute()
|
|
322
|
+
logger.info(f"Trashed file {file_id}")
|
|
323
|
+
return True
|
|
324
|
+
except HttpError as e:
|
|
325
|
+
if e.resp.status == 404:
|
|
326
|
+
logger.warning(f"File not found for trash: {file_id}")
|
|
327
|
+
else:
|
|
328
|
+
logger.error(f"Error trashing file {file_id}: {e}")
|
|
329
|
+
return False
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(f"Unexpected error trashing file {file_id}: {e}")
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
def delete(self, file_id: str) -> bool:
|
|
335
|
+
"""Permanently delete file."""
|
|
336
|
+
try:
|
|
337
|
+
self.service.files().delete(fileId=file_id).execute()
|
|
338
|
+
logger.info(f"Deleted file {file_id}")
|
|
339
|
+
return True
|
|
340
|
+
except HttpError as e:
|
|
341
|
+
if e.resp.status == 404:
|
|
342
|
+
logger.warning(f"File not found for deletion: {file_id}")
|
|
343
|
+
else:
|
|
344
|
+
logger.error(f"Error deleting file {file_id}: {e}")
|
|
345
|
+
return False
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"Unexpected error deleting file {file_id}: {e}")
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
# ========== Sharing ==========
|
|
351
|
+
|
|
352
|
+
def share(
|
|
353
|
+
self,
|
|
354
|
+
file_id: str,
|
|
355
|
+
email: str,
|
|
356
|
+
role: str = "reader",
|
|
357
|
+
notify: bool = True,
|
|
358
|
+
) -> bool:
|
|
359
|
+
"""
|
|
360
|
+
Share a file with someone.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
file_id: File ID
|
|
364
|
+
email: Email to share with
|
|
365
|
+
role: Permission role (reader, writer, commenter)
|
|
366
|
+
notify: Send notification email
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
True if successful
|
|
370
|
+
"""
|
|
371
|
+
try:
|
|
372
|
+
self.service.permissions().create(
|
|
373
|
+
fileId=file_id,
|
|
374
|
+
body={
|
|
375
|
+
"type": "user",
|
|
376
|
+
"role": role,
|
|
377
|
+
"emailAddress": email,
|
|
378
|
+
},
|
|
379
|
+
sendNotificationEmail=notify,
|
|
380
|
+
).execute()
|
|
381
|
+
logger.info(f"Shared file {file_id} with {email} ({role})")
|
|
382
|
+
return True
|
|
383
|
+
except HttpError as e:
|
|
384
|
+
if e.resp.status == 404:
|
|
385
|
+
logger.error(f"File not found for sharing: {file_id}")
|
|
386
|
+
elif e.resp.status == 400:
|
|
387
|
+
logger.error(f"Invalid share request for {file_id}: {e}")
|
|
388
|
+
else:
|
|
389
|
+
logger.error(f"Error sharing file {file_id}: {e}")
|
|
390
|
+
return False
|
|
391
|
+
except Exception as e:
|
|
392
|
+
logger.error(f"Unexpected error sharing file {file_id}: {e}")
|
|
393
|
+
return False
|
|
394
|
+
|
|
395
|
+
# ========== Parsing ==========
|
|
396
|
+
|
|
397
|
+
def _parse_file(self, data: dict) -> File:
|
|
398
|
+
"""Parse Drive API response to File object."""
|
|
399
|
+
file = DriveParser.parse_file(data)
|
|
400
|
+
file._drive = self
|
|
401
|
+
return file
|
gsuite_drive/file.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Drive File and Folder entities."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from gsuite_drive.client import Drive
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class File:
|
|
13
|
+
"""
|
|
14
|
+
Google Drive file.
|
|
15
|
+
|
|
16
|
+
Represents a file in Google Drive with methods for
|
|
17
|
+
download, update, and management.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
name: str
|
|
22
|
+
mime_type: str
|
|
23
|
+
size: int = 0
|
|
24
|
+
created_time: datetime | None = None
|
|
25
|
+
modified_time: datetime | None = None
|
|
26
|
+
parents: list[str] = field(default_factory=list)
|
|
27
|
+
web_view_link: str | None = None
|
|
28
|
+
web_content_link: str | None = None
|
|
29
|
+
|
|
30
|
+
_drive: Optional["Drive"] = field(default=None, repr=False)
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def is_folder(self) -> bool:
|
|
34
|
+
"""Check if this is a folder."""
|
|
35
|
+
return self.mime_type == "application/vnd.google-apps.folder"
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_google_doc(self) -> bool:
|
|
39
|
+
"""Check if this is a Google Docs file."""
|
|
40
|
+
return self.mime_type.startswith("application/vnd.google-apps.")
|
|
41
|
+
|
|
42
|
+
def download(self, path: str | None = None) -> str:
|
|
43
|
+
"""
|
|
44
|
+
Download file to local path.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: Local path (default: current dir with original name)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Path where file was saved
|
|
51
|
+
"""
|
|
52
|
+
if not self._drive:
|
|
53
|
+
raise RuntimeError("File not linked to Drive client")
|
|
54
|
+
return self._drive.download(self.id, path or self.name)
|
|
55
|
+
|
|
56
|
+
def get_content(self) -> bytes:
|
|
57
|
+
"""
|
|
58
|
+
Get file content as bytes.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
File content
|
|
62
|
+
"""
|
|
63
|
+
if not self._drive:
|
|
64
|
+
raise RuntimeError("File not linked to Drive client")
|
|
65
|
+
return self._drive.get_content(self.id)
|
|
66
|
+
|
|
67
|
+
def trash(self) -> "File":
|
|
68
|
+
"""Move file to trash."""
|
|
69
|
+
if self._drive:
|
|
70
|
+
self._drive.trash(self.id)
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def delete(self) -> None:
|
|
74
|
+
"""Permanently delete file."""
|
|
75
|
+
if self._drive:
|
|
76
|
+
self._drive.delete(self.id)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class Folder(File):
|
|
81
|
+
"""
|
|
82
|
+
Google Drive folder.
|
|
83
|
+
|
|
84
|
+
A folder is a special type of file.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __post_init__(self):
|
|
88
|
+
"""Ensure mime type is folder."""
|
|
89
|
+
self.mime_type = "application/vnd.google-apps.folder"
|
|
90
|
+
|
|
91
|
+
def list_files(self, recursive: bool = False) -> list[File]:
|
|
92
|
+
"""
|
|
93
|
+
List files in this folder.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
recursive: Include files in subfolders
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
List of files
|
|
100
|
+
"""
|
|
101
|
+
if not self._drive:
|
|
102
|
+
raise RuntimeError("Folder not linked to Drive client")
|
|
103
|
+
return self._drive.list_files(parent_id=self.id)
|
gsuite_drive/parser.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Drive response parsers - converts API responses to domain entities."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from gsuite_drive.file import File, Folder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DriveParser:
|
|
9
|
+
"""Parser for Drive API responses."""
|
|
10
|
+
|
|
11
|
+
@staticmethod
|
|
12
|
+
def parse_file(data: dict) -> File:
|
|
13
|
+
"""
|
|
14
|
+
Parse Drive API response to File entity.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
data: Raw API response dict
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
File entity
|
|
21
|
+
"""
|
|
22
|
+
return File(
|
|
23
|
+
id=data["id"],
|
|
24
|
+
name=data.get("name", ""),
|
|
25
|
+
mime_type=data.get("mimeType", "application/octet-stream"),
|
|
26
|
+
size=int(data.get("size", 0)),
|
|
27
|
+
created_time=DriveParser._parse_datetime(data.get("createdTime")),
|
|
28
|
+
modified_time=DriveParser._parse_datetime(data.get("modifiedTime")),
|
|
29
|
+
parents=data.get("parents", []),
|
|
30
|
+
web_view_link=data.get("webViewLink"),
|
|
31
|
+
web_content_link=data.get("webContentLink"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def parse_folder(data: dict) -> Folder:
|
|
36
|
+
"""
|
|
37
|
+
Parse Drive API response to Folder entity.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data: Raw API response dict
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Folder entity
|
|
44
|
+
"""
|
|
45
|
+
file = DriveParser.parse_file(data)
|
|
46
|
+
return Folder(
|
|
47
|
+
id=file.id,
|
|
48
|
+
name=file.name,
|
|
49
|
+
mime_type=file.mime_type,
|
|
50
|
+
size=file.size,
|
|
51
|
+
created_time=file.created_time,
|
|
52
|
+
modified_time=file.modified_time,
|
|
53
|
+
parents=file.parents,
|
|
54
|
+
web_view_link=file.web_view_link,
|
|
55
|
+
web_content_link=file.web_content_link,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _parse_datetime(dt_string: str | None) -> datetime | None:
|
|
60
|
+
"""Parse ISO datetime string to datetime object."""
|
|
61
|
+
if not dt_string:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
return datetime.fromisoformat(dt_string.replace("Z", "+00:00"))
|
|
65
|
+
except (ValueError, TypeError):
|
|
66
|
+
return None
|
gsuite_drive/py.typed
ADDED
|
File without changes
|
gsuite_gmail/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Google Suite Gmail - Simple Gmail API client."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from gsuite_gmail import query
|
|
6
|
+
from gsuite_gmail.client import Gmail
|
|
7
|
+
from gsuite_gmail.label import Label
|
|
8
|
+
from gsuite_gmail.message import Message
|
|
9
|
+
from gsuite_gmail.thread import Thread
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Gmail",
|
|
13
|
+
"Message",
|
|
14
|
+
"Thread",
|
|
15
|
+
"Label",
|
|
16
|
+
"query",
|
|
17
|
+
]
|