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_sheets/client.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""Sheets client - high-level interface."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from googleapiclient.discovery import build
|
|
7
|
+
from googleapiclient.errors import HttpError
|
|
8
|
+
|
|
9
|
+
from gsuite_core import GoogleAuth
|
|
10
|
+
from gsuite_sheets.parser import SheetsParser
|
|
11
|
+
from gsuite_sheets.spreadsheet import Spreadsheet
|
|
12
|
+
from gsuite_sheets.worksheet import Worksheet
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Sheets:
|
|
18
|
+
"""
|
|
19
|
+
High-level Google Sheets client.
|
|
20
|
+
|
|
21
|
+
Inspired by gspread's simple API.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
auth = GoogleAuth()
|
|
25
|
+
auth.authenticate()
|
|
26
|
+
|
|
27
|
+
sheets = Sheets(auth)
|
|
28
|
+
|
|
29
|
+
# Open by title (like gspread!)
|
|
30
|
+
doc = sheets.open("My Spreadsheet")
|
|
31
|
+
|
|
32
|
+
# Or by key/url
|
|
33
|
+
doc = sheets.open_by_key("abc123...")
|
|
34
|
+
doc = sheets.open_by_url("https://docs.google.com/spreadsheets/...")
|
|
35
|
+
|
|
36
|
+
# Work with worksheets
|
|
37
|
+
ws = doc.sheet1
|
|
38
|
+
data = ws.get_all_values()
|
|
39
|
+
ws.update("A1", [["Hello", "World"]])
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, auth: GoogleAuth):
|
|
43
|
+
"""
|
|
44
|
+
Initialize Sheets client.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
auth: GoogleAuth instance with valid credentials
|
|
48
|
+
"""
|
|
49
|
+
self.auth = auth
|
|
50
|
+
self._sheets_service = None
|
|
51
|
+
self._drive_service = None
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def service(self):
|
|
55
|
+
"""Lazy-load Sheets API service."""
|
|
56
|
+
if self._sheets_service is None:
|
|
57
|
+
self._sheets_service = build("sheets", "v4", credentials=self.auth.credentials)
|
|
58
|
+
return self._sheets_service
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def drive(self):
|
|
62
|
+
"""Lazy-load Drive API service (for listing/sharing)."""
|
|
63
|
+
if self._drive_service is None:
|
|
64
|
+
self._drive_service = build("drive", "v3", credentials=self.auth.credentials)
|
|
65
|
+
return self._drive_service
|
|
66
|
+
|
|
67
|
+
# ========== Opening spreadsheets (gspread-style) ==========
|
|
68
|
+
|
|
69
|
+
def open(self, title: str) -> Spreadsheet:
|
|
70
|
+
"""
|
|
71
|
+
Open a spreadsheet by title.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
title: Spreadsheet title
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Spreadsheet object
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: If not found
|
|
81
|
+
"""
|
|
82
|
+
# Search in Drive
|
|
83
|
+
response = (
|
|
84
|
+
self.drive.files()
|
|
85
|
+
.list(
|
|
86
|
+
q=f"name='{title}' and mimeType='application/vnd.google-apps.spreadsheet' and trashed=false",
|
|
87
|
+
fields="files(id, name)",
|
|
88
|
+
pageSize=1,
|
|
89
|
+
)
|
|
90
|
+
.execute()
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
files = response.get("files", [])
|
|
94
|
+
if not files:
|
|
95
|
+
raise ValueError(f"Spreadsheet not found: {title}")
|
|
96
|
+
|
|
97
|
+
return self.open_by_key(files[0]["id"])
|
|
98
|
+
|
|
99
|
+
def open_by_key(self, key: str) -> Spreadsheet:
|
|
100
|
+
"""
|
|
101
|
+
Open a spreadsheet by key (ID).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
key: Spreadsheet ID
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Spreadsheet object
|
|
108
|
+
"""
|
|
109
|
+
response = (
|
|
110
|
+
self.service.spreadsheets()
|
|
111
|
+
.get(
|
|
112
|
+
spreadsheetId=key,
|
|
113
|
+
fields="spreadsheetId,properties,sheets.properties",
|
|
114
|
+
)
|
|
115
|
+
.execute()
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return self._parse_spreadsheet(response)
|
|
119
|
+
|
|
120
|
+
def open_by_url(self, url: str) -> Spreadsheet:
|
|
121
|
+
"""
|
|
122
|
+
Open a spreadsheet by URL.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
url: Full Google Sheets URL
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Spreadsheet object
|
|
129
|
+
"""
|
|
130
|
+
# Extract key from URL
|
|
131
|
+
import re
|
|
132
|
+
|
|
133
|
+
match = re.search(r"/spreadsheets/d/([a-zA-Z0-9-_]+)", url)
|
|
134
|
+
if not match:
|
|
135
|
+
raise ValueError(f"Invalid Google Sheets URL: {url}")
|
|
136
|
+
|
|
137
|
+
return self.open_by_key(match.group(1))
|
|
138
|
+
|
|
139
|
+
# ========== Creating spreadsheets ==========
|
|
140
|
+
|
|
141
|
+
def create(self, title: str) -> Spreadsheet:
|
|
142
|
+
"""
|
|
143
|
+
Create a new spreadsheet.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
title: Spreadsheet title
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Created Spreadsheet
|
|
150
|
+
"""
|
|
151
|
+
body = {
|
|
152
|
+
"properties": {"title": title},
|
|
153
|
+
"sheets": [{"properties": {"title": "Sheet1"}}],
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
response = self.service.spreadsheets().create(body=body).execute()
|
|
157
|
+
return self._parse_spreadsheet(response)
|
|
158
|
+
|
|
159
|
+
# ========== Listing ==========
|
|
160
|
+
|
|
161
|
+
def list_spreadsheets(self, max_results: int = 100) -> list[dict]:
|
|
162
|
+
"""
|
|
163
|
+
List all spreadsheets accessible to the user.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
List of {id, name} dicts
|
|
167
|
+
"""
|
|
168
|
+
response = (
|
|
169
|
+
self.drive.files()
|
|
170
|
+
.list(
|
|
171
|
+
q="mimeType='application/vnd.google-apps.spreadsheet' and trashed=false",
|
|
172
|
+
fields="files(id, name)",
|
|
173
|
+
pageSize=max_results,
|
|
174
|
+
orderBy="modifiedTime desc",
|
|
175
|
+
)
|
|
176
|
+
.execute()
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return response.get("files", [])
|
|
180
|
+
|
|
181
|
+
# ========== Low-level operations ==========
|
|
182
|
+
|
|
183
|
+
def get_values(self, spreadsheet_id: str, range: str) -> list[list[Any]]:
|
|
184
|
+
"""Get values from a range."""
|
|
185
|
+
response = (
|
|
186
|
+
self.service.spreadsheets()
|
|
187
|
+
.values()
|
|
188
|
+
.get(
|
|
189
|
+
spreadsheetId=spreadsheet_id,
|
|
190
|
+
range=range,
|
|
191
|
+
)
|
|
192
|
+
.execute()
|
|
193
|
+
)
|
|
194
|
+
return response.get("values", [])
|
|
195
|
+
|
|
196
|
+
def update_values(
|
|
197
|
+
self,
|
|
198
|
+
spreadsheet_id: str,
|
|
199
|
+
range: str,
|
|
200
|
+
values: list[list[Any]],
|
|
201
|
+
) -> dict:
|
|
202
|
+
"""Update values in a range."""
|
|
203
|
+
return (
|
|
204
|
+
self.service.spreadsheets()
|
|
205
|
+
.values()
|
|
206
|
+
.update(
|
|
207
|
+
spreadsheetId=spreadsheet_id,
|
|
208
|
+
range=range,
|
|
209
|
+
valueInputOption="USER_ENTERED",
|
|
210
|
+
body={"values": values},
|
|
211
|
+
)
|
|
212
|
+
.execute()
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def append_values(
|
|
216
|
+
self,
|
|
217
|
+
spreadsheet_id: str,
|
|
218
|
+
range: str,
|
|
219
|
+
values: list[list[Any]],
|
|
220
|
+
) -> dict:
|
|
221
|
+
"""Append values to a range."""
|
|
222
|
+
return (
|
|
223
|
+
self.service.spreadsheets()
|
|
224
|
+
.values()
|
|
225
|
+
.append(
|
|
226
|
+
spreadsheetId=spreadsheet_id,
|
|
227
|
+
range=range,
|
|
228
|
+
valueInputOption="USER_ENTERED",
|
|
229
|
+
insertDataOption="INSERT_ROWS",
|
|
230
|
+
body={"values": values},
|
|
231
|
+
)
|
|
232
|
+
.execute()
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def clear_values(self, spreadsheet_id: str, range: str) -> dict:
|
|
236
|
+
"""Clear values from a range."""
|
|
237
|
+
return (
|
|
238
|
+
self.service.spreadsheets()
|
|
239
|
+
.values()
|
|
240
|
+
.clear(
|
|
241
|
+
spreadsheetId=spreadsheet_id,
|
|
242
|
+
range=range,
|
|
243
|
+
)
|
|
244
|
+
.execute()
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def batch_update(
|
|
248
|
+
self,
|
|
249
|
+
spreadsheet_id: str,
|
|
250
|
+
data: list[dict],
|
|
251
|
+
) -> dict:
|
|
252
|
+
"""
|
|
253
|
+
Batch update multiple ranges.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
spreadsheet_id: Spreadsheet ID
|
|
257
|
+
data: List of {range, values} dicts
|
|
258
|
+
"""
|
|
259
|
+
return (
|
|
260
|
+
self.service.spreadsheets()
|
|
261
|
+
.values()
|
|
262
|
+
.batchUpdate(
|
|
263
|
+
spreadsheetId=spreadsheet_id,
|
|
264
|
+
body={
|
|
265
|
+
"valueInputOption": "USER_ENTERED",
|
|
266
|
+
"data": [{"range": d["range"], "values": d["values"]} for d in data],
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
.execute()
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# ========== Worksheet operations ==========
|
|
273
|
+
|
|
274
|
+
def add_worksheet(
|
|
275
|
+
self,
|
|
276
|
+
spreadsheet_id: str,
|
|
277
|
+
title: str,
|
|
278
|
+
rows: int = 1000,
|
|
279
|
+
cols: int = 26,
|
|
280
|
+
) -> Worksheet:
|
|
281
|
+
"""Add a worksheet to a spreadsheet."""
|
|
282
|
+
response = (
|
|
283
|
+
self.service.spreadsheets()
|
|
284
|
+
.batchUpdate(
|
|
285
|
+
spreadsheetId=spreadsheet_id,
|
|
286
|
+
body={
|
|
287
|
+
"requests": [
|
|
288
|
+
{
|
|
289
|
+
"addSheet": {
|
|
290
|
+
"properties": {
|
|
291
|
+
"title": title,
|
|
292
|
+
"gridProperties": {
|
|
293
|
+
"rowCount": rows,
|
|
294
|
+
"columnCount": cols,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
.execute()
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return SheetsParser.parse_worksheet_from_reply(response["replies"][0]["addSheet"])
|
|
306
|
+
|
|
307
|
+
def delete_worksheet(self, spreadsheet_id: str, sheet_id: int) -> bool:
|
|
308
|
+
"""Delete a worksheet."""
|
|
309
|
+
try:
|
|
310
|
+
self.service.spreadsheets().batchUpdate(
|
|
311
|
+
spreadsheetId=spreadsheet_id,
|
|
312
|
+
body={
|
|
313
|
+
"requests": [
|
|
314
|
+
{
|
|
315
|
+
"deleteSheet": {"sheetId": sheet_id},
|
|
316
|
+
}
|
|
317
|
+
],
|
|
318
|
+
},
|
|
319
|
+
).execute()
|
|
320
|
+
logger.info(f"Deleted worksheet {sheet_id} from {spreadsheet_id}")
|
|
321
|
+
return True
|
|
322
|
+
except HttpError as e:
|
|
323
|
+
if e.resp.status == 400:
|
|
324
|
+
logger.error(f"Cannot delete worksheet {sheet_id}: {e}")
|
|
325
|
+
else:
|
|
326
|
+
logger.error(f"Error deleting worksheet {sheet_id}: {e}")
|
|
327
|
+
return False
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.error(f"Unexpected error deleting worksheet {sheet_id}: {e}")
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
# ========== Sharing ==========
|
|
333
|
+
|
|
334
|
+
def share(
|
|
335
|
+
self,
|
|
336
|
+
spreadsheet_id: str,
|
|
337
|
+
email: str,
|
|
338
|
+
role: str = "reader",
|
|
339
|
+
notify: bool = True,
|
|
340
|
+
) -> bool:
|
|
341
|
+
"""Share a spreadsheet."""
|
|
342
|
+
try:
|
|
343
|
+
self.drive.permissions().create(
|
|
344
|
+
fileId=spreadsheet_id,
|
|
345
|
+
body={
|
|
346
|
+
"type": "user",
|
|
347
|
+
"role": role,
|
|
348
|
+
"emailAddress": email,
|
|
349
|
+
},
|
|
350
|
+
sendNotificationEmail=notify,
|
|
351
|
+
).execute()
|
|
352
|
+
logger.info(f"Shared spreadsheet {spreadsheet_id} with {email} ({role})")
|
|
353
|
+
return True
|
|
354
|
+
except HttpError as e:
|
|
355
|
+
if e.resp.status == 404:
|
|
356
|
+
logger.error(f"Spreadsheet not found: {spreadsheet_id}")
|
|
357
|
+
else:
|
|
358
|
+
logger.error(f"Error sharing spreadsheet {spreadsheet_id}: {e}")
|
|
359
|
+
return False
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error(f"Unexpected error sharing spreadsheet: {e}")
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
# ========== Parsing ==========
|
|
365
|
+
|
|
366
|
+
def _parse_spreadsheet(self, data: dict) -> Spreadsheet:
|
|
367
|
+
"""Parse API response to Spreadsheet object."""
|
|
368
|
+
spreadsheet = SheetsParser.parse_spreadsheet(data)
|
|
369
|
+
|
|
370
|
+
# Link worksheets to spreadsheet and client
|
|
371
|
+
for ws in spreadsheet.worksheets:
|
|
372
|
+
ws._spreadsheet = spreadsheet
|
|
373
|
+
|
|
374
|
+
spreadsheet._sheets = self
|
|
375
|
+
return spreadsheet
|
gsuite_sheets/parser.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Sheets response parsers - converts API responses to domain entities."""
|
|
2
|
+
|
|
3
|
+
from gsuite_sheets.spreadsheet import Spreadsheet
|
|
4
|
+
from gsuite_sheets.worksheet import Worksheet
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SheetsParser:
|
|
8
|
+
"""Parser for Sheets API responses."""
|
|
9
|
+
|
|
10
|
+
@staticmethod
|
|
11
|
+
def parse_spreadsheet(data: dict) -> Spreadsheet:
|
|
12
|
+
"""
|
|
13
|
+
Parse Sheets API response to Spreadsheet entity.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
data: Raw API response dict
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Spreadsheet entity (without client reference)
|
|
20
|
+
"""
|
|
21
|
+
props = data.get("properties", {})
|
|
22
|
+
|
|
23
|
+
worksheets = [SheetsParser.parse_worksheet(sheet) for sheet in data.get("sheets", [])]
|
|
24
|
+
|
|
25
|
+
return Spreadsheet(
|
|
26
|
+
id=data["spreadsheetId"],
|
|
27
|
+
title=props.get("title", ""),
|
|
28
|
+
url=f"https://docs.google.com/spreadsheets/d/{data['spreadsheetId']}",
|
|
29
|
+
locale=props.get("locale", "en_US"),
|
|
30
|
+
time_zone=props.get("timeZone", "America/New_York"),
|
|
31
|
+
worksheets=worksheets,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def parse_worksheet(data: dict) -> Worksheet:
|
|
36
|
+
"""
|
|
37
|
+
Parse sheet data to Worksheet entity.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
data: Raw sheet dict from API (with "properties" key)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Worksheet entity
|
|
44
|
+
"""
|
|
45
|
+
props = data.get("properties", {})
|
|
46
|
+
grid_props = props.get("gridProperties", {})
|
|
47
|
+
|
|
48
|
+
return Worksheet(
|
|
49
|
+
id=props.get("sheetId", 0),
|
|
50
|
+
title=props.get("title", ""),
|
|
51
|
+
index=props.get("index", 0),
|
|
52
|
+
row_count=grid_props.get("rowCount", 1000),
|
|
53
|
+
column_count=grid_props.get("columnCount", 26),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def parse_worksheet_from_reply(data: dict) -> Worksheet:
|
|
58
|
+
"""
|
|
59
|
+
Parse worksheet from batchUpdate reply.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
data: Reply dict with "addSheet" > "properties"
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Worksheet entity
|
|
66
|
+
"""
|
|
67
|
+
props = data["properties"]
|
|
68
|
+
grid_props = props.get("gridProperties", {})
|
|
69
|
+
|
|
70
|
+
return Worksheet(
|
|
71
|
+
id=props["sheetId"],
|
|
72
|
+
title=props["title"],
|
|
73
|
+
index=props["index"],
|
|
74
|
+
row_count=grid_props.get("rowCount", 1000),
|
|
75
|
+
column_count=grid_props.get("columnCount", 26),
|
|
76
|
+
)
|
gsuite_sheets/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Spreadsheet entity."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from gsuite_sheets.client import Sheets
|
|
8
|
+
|
|
9
|
+
from gsuite_sheets.worksheet import Worksheet
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Spreadsheet:
|
|
14
|
+
"""
|
|
15
|
+
A Google Spreadsheet.
|
|
16
|
+
|
|
17
|
+
Contains multiple worksheets (tabs).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
id: str
|
|
21
|
+
title: str
|
|
22
|
+
url: str
|
|
23
|
+
locale: str = "en_US"
|
|
24
|
+
time_zone: str = "America/New_York"
|
|
25
|
+
worksheets: list[Worksheet] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
_sheets: Optional["Sheets"] = field(default=None, repr=False)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def sheet1(self) -> Worksheet | None:
|
|
31
|
+
"""Get the first worksheet (convenience property like gspread)."""
|
|
32
|
+
return self.worksheets[0] if self.worksheets else None
|
|
33
|
+
|
|
34
|
+
def worksheet(self, title: str) -> Worksheet | None:
|
|
35
|
+
"""Get worksheet by title."""
|
|
36
|
+
for ws in self.worksheets:
|
|
37
|
+
if ws.title == title:
|
|
38
|
+
return ws
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
def get_worksheet(self, index: int) -> Worksheet | None:
|
|
42
|
+
"""Get worksheet by index (0-indexed)."""
|
|
43
|
+
if 0 <= index < len(self.worksheets):
|
|
44
|
+
return self.worksheets[index]
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def add_worksheet(
|
|
48
|
+
self,
|
|
49
|
+
title: str,
|
|
50
|
+
rows: int = 1000,
|
|
51
|
+
cols: int = 26,
|
|
52
|
+
) -> Worksheet:
|
|
53
|
+
"""
|
|
54
|
+
Add a new worksheet.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
title: Worksheet title
|
|
58
|
+
rows: Number of rows
|
|
59
|
+
cols: Number of columns
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Created Worksheet
|
|
63
|
+
"""
|
|
64
|
+
if not self._sheets:
|
|
65
|
+
raise RuntimeError("Spreadsheet not linked to Sheets client")
|
|
66
|
+
|
|
67
|
+
ws = self._sheets.add_worksheet(self.id, title, rows, cols)
|
|
68
|
+
self.worksheets.append(ws)
|
|
69
|
+
return ws
|
|
70
|
+
|
|
71
|
+
def del_worksheet(self, worksheet: Worksheet) -> bool:
|
|
72
|
+
"""Delete a worksheet."""
|
|
73
|
+
if not self._sheets:
|
|
74
|
+
raise RuntimeError("Spreadsheet not linked to Sheets client")
|
|
75
|
+
|
|
76
|
+
success = self._sheets.delete_worksheet(self.id, worksheet.id)
|
|
77
|
+
if success:
|
|
78
|
+
self.worksheets = [ws for ws in self.worksheets if ws.id != worksheet.id]
|
|
79
|
+
return success
|
|
80
|
+
|
|
81
|
+
def share(
|
|
82
|
+
self,
|
|
83
|
+
email: str,
|
|
84
|
+
role: str = "reader",
|
|
85
|
+
notify: bool = True,
|
|
86
|
+
) -> bool:
|
|
87
|
+
"""
|
|
88
|
+
Share spreadsheet with someone.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
email: Email to share with
|
|
92
|
+
role: Permission role (reader, writer)
|
|
93
|
+
notify: Send notification email
|
|
94
|
+
"""
|
|
95
|
+
if not self._sheets:
|
|
96
|
+
raise RuntimeError("Spreadsheet not linked to Sheets client")
|
|
97
|
+
return self._sheets.share(self.id, email, role, notify)
|