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.
@@ -0,0 +1,185 @@
1
+ """Worksheet entity."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING, Any, Optional
5
+
6
+ if TYPE_CHECKING:
7
+ from gsuite_sheets.spreadsheet import Spreadsheet
8
+
9
+
10
+ @dataclass
11
+ class Worksheet:
12
+ """
13
+ A worksheet (tab) within a spreadsheet.
14
+
15
+ Provides methods for reading and writing cell data.
16
+ """
17
+
18
+ id: int
19
+ title: str
20
+ index: int
21
+ row_count: int = 1000
22
+ column_count: int = 26
23
+
24
+ _spreadsheet: Optional["Spreadsheet"] = field(default=None, repr=False)
25
+
26
+ @property
27
+ def url(self) -> str | None:
28
+ """Get URL to this specific worksheet."""
29
+ if self._spreadsheet:
30
+ return f"{self._spreadsheet.url}#gid={self.id}"
31
+ return None
32
+
33
+ # ========== Reading ==========
34
+
35
+ def get(self, range: str = "A1") -> list[list[Any]]:
36
+ """
37
+ Get values from a range.
38
+
39
+ Args:
40
+ range: A1 notation (e.g., "A1:B10", "A:C", "1:5")
41
+
42
+ Returns:
43
+ 2D list of values
44
+ """
45
+ if not self._spreadsheet:
46
+ raise RuntimeError("Worksheet not linked to spreadsheet")
47
+ return self._spreadsheet._sheets.get_values(self._spreadsheet.id, f"{self.title}!{range}")
48
+
49
+ def get_all_values(self) -> list[list[Any]]:
50
+ """Get all values in the worksheet."""
51
+ return self.get("A1:ZZ")
52
+
53
+ def get_all_records(self, head: int = 1) -> list[dict]:
54
+ """
55
+ Get all rows as list of dicts using row as headers.
56
+
57
+ Args:
58
+ head: Row number to use as headers (1-indexed)
59
+
60
+ Returns:
61
+ List of dicts with header keys
62
+ """
63
+ values = self.get_all_values()
64
+ if len(values) < head:
65
+ return []
66
+
67
+ headers = values[head - 1]
68
+ records = []
69
+
70
+ for row in values[head:]:
71
+ # Pad row to match headers length
72
+ padded = row + [""] * (len(headers) - len(row))
73
+ record = dict(zip(headers, padded))
74
+ records.append(record)
75
+
76
+ return records
77
+
78
+ def row_values(self, row: int) -> list[Any]:
79
+ """Get all values in a row (1-indexed)."""
80
+ values = self.get(f"{row}:{row}")
81
+ return values[0] if values else []
82
+
83
+ def col_values(self, col: int) -> list[Any]:
84
+ """Get all values in a column (1-indexed)."""
85
+ col_letter = self._col_to_letter(col)
86
+ values = self.get(f"{col_letter}:{col_letter}")
87
+ return [row[0] if row else "" for row in values]
88
+
89
+ def cell(self, row: int, col: int) -> Any:
90
+ """Get a single cell value (1-indexed)."""
91
+ col_letter = self._col_to_letter(col)
92
+ values = self.get(f"{col_letter}{row}")
93
+ if values and values[0]:
94
+ return values[0][0]
95
+ return ""
96
+
97
+ # ========== Writing ==========
98
+
99
+ def update(self, range: str, values: list[list[Any]]) -> dict:
100
+ """
101
+ Update a range with values.
102
+
103
+ Args:
104
+ range: A1 notation
105
+ values: 2D list of values
106
+
107
+ Returns:
108
+ Update response
109
+ """
110
+ if not self._spreadsheet:
111
+ raise RuntimeError("Worksheet not linked to spreadsheet")
112
+ return self._spreadsheet._sheets.update_values(
113
+ self._spreadsheet.id, f"{self.title}!{range}", values
114
+ )
115
+
116
+ def update_cell(self, row: int, col: int, value: Any) -> dict:
117
+ """Update a single cell (1-indexed)."""
118
+ col_letter = self._col_to_letter(col)
119
+ return self.update(f"{col_letter}{row}", [[value]])
120
+
121
+ def append_row(self, values: list[Any]) -> dict:
122
+ """Append a row to the end of the worksheet."""
123
+ if not self._spreadsheet:
124
+ raise RuntimeError("Worksheet not linked to spreadsheet")
125
+ return self._spreadsheet._sheets.append_values(
126
+ self._spreadsheet.id, f"{self.title}!A1", [values]
127
+ )
128
+
129
+ def append_rows(self, rows: list[list[Any]]) -> dict:
130
+ """Append multiple rows."""
131
+ if not self._spreadsheet:
132
+ raise RuntimeError("Worksheet not linked to spreadsheet")
133
+ return self._spreadsheet._sheets.append_values(
134
+ self._spreadsheet.id, f"{self.title}!A1", rows
135
+ )
136
+
137
+ def clear(self, range: str | None = None) -> dict:
138
+ """
139
+ Clear values from a range or entire worksheet.
140
+
141
+ Args:
142
+ range: A1 notation (default: entire sheet)
143
+ """
144
+ if not self._spreadsheet:
145
+ raise RuntimeError("Worksheet not linked to spreadsheet")
146
+
147
+ full_range = f"{self.title}!{range}" if range else self.title
148
+ return self._spreadsheet._sheets.clear_values(self._spreadsheet.id, full_range)
149
+
150
+ # ========== Search ==========
151
+
152
+ def find(self, query: str) -> tuple[int, int] | None:
153
+ """
154
+ Find first cell containing query.
155
+
156
+ Returns:
157
+ (row, col) tuple or None
158
+ """
159
+ values = self.get_all_values()
160
+ for row_idx, row in enumerate(values):
161
+ for col_idx, cell in enumerate(row):
162
+ if str(cell) == query:
163
+ return (row_idx + 1, col_idx + 1)
164
+ return None
165
+
166
+ def findall(self, query: str) -> list[tuple[int, int]]:
167
+ """Find all cells containing query."""
168
+ values = self.get_all_values()
169
+ results = []
170
+ for row_idx, row in enumerate(values):
171
+ for col_idx, cell in enumerate(row):
172
+ if str(cell) == query:
173
+ results.append((row_idx + 1, col_idx + 1))
174
+ return results
175
+
176
+ # ========== Helpers ==========
177
+
178
+ @staticmethod
179
+ def _col_to_letter(col: int) -> str:
180
+ """Convert column number to letter (1=A, 27=AA)."""
181
+ result = ""
182
+ while col > 0:
183
+ col, remainder = divmod(col - 1, 26)
184
+ result = chr(65 + remainder) + result
185
+ return result