tha-google-runner 0.1.0__tar.gz

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,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12"]
14
+
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v4
20
+ with:
21
+ version: "latest"
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ run: uv python install ${{ matrix.python-version }}
25
+
26
+ - name: Install dependencies
27
+ run: uv sync --extra dev --python ${{ matrix.python-version }}
28
+
29
+ - name: Lint
30
+ run: uv run ruff check src/ tests/
31
+
32
+ - name: Test
33
+ run: uv run pytest
34
+
35
+ - name: Type check
36
+ run: uv run mypy src/
@@ -0,0 +1,53 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v4
15
+ - name: Build
16
+ run: uv build
17
+ - name: Upload dist
18
+ uses: actions/upload-artifact@v4
19
+ with:
20
+ name: dist
21
+ path: dist/
22
+
23
+ publish-testpypi:
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment: testpypi
27
+ permissions:
28
+ id-token: write
29
+ steps:
30
+ - name: Download dist
31
+ uses: actions/download-artifact@v4
32
+ with:
33
+ name: dist
34
+ path: dist/
35
+ - name: Publish to TestPyPI
36
+ uses: pypa/gh-action-pypi-publish@release/v1
37
+ with:
38
+ repository-url: https://test.pypi.org/legacy/
39
+
40
+ publish-pypi:
41
+ needs: publish-testpypi
42
+ runs-on: ubuntu-latest
43
+ environment: pypi
44
+ permissions:
45
+ id-token: write
46
+ steps:
47
+ - name: Download dist
48
+ uses: actions/download-artifact@v4
49
+ with:
50
+ name: dist
51
+ path: dist/
52
+ - name: Publish to PyPI
53
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,38 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ *.egg
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # uv
18
+ .uv/
19
+
20
+ # mypy
21
+ .mypy_cache/
22
+
23
+ # pytest
24
+ .pytest_cache/
25
+ .coverage
26
+ htmlcov/
27
+
28
+ # ruff
29
+ .ruff_cache/
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # Google auth tokens -- never commit these
36
+ token.json
37
+ client_secrets.json
38
+ *_secrets.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nathan Wright
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.4
2
+ Name: tha-google-runner
3
+ Version: 0.1.0
4
+ Summary: A Tabular Helper API library that wraps Google Sheets with a typed, consistent interface built on gspread.
5
+ Project-URL: Homepage, https://github.com/tha-guy-nate/tha-google-runner
6
+ Project-URL: Issues, https://github.com/tha-guy-nate/tha-google-runner/issues
7
+ Author: Nate Wright
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: google,gspread,helper,sheets,tabular
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Utilities
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: google-auth-oauthlib>=1.0
22
+ Requires-Dist: google-auth>=2.0
23
+ Requires-Dist: gspread>=6.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.10; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: ruff>=0.5; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # tha-google-runner
31
+
32
+ [![CI](https://github.com/tha-guy-nate/tha-google-runner/actions/workflows/ci.yml/badge.svg)](https://github.com/tha-guy-nate/tha-google-runner/actions/workflows/ci.yml)
33
+
34
+ A Tabular Helper API library that wraps Google Sheets with a typed, consistent interface built on gspread.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install tha-google-runner
40
+ ```
41
+
42
+ ## Authentication setup
43
+
44
+ `tha-google-runner` uses your **personal Google account** — not a service account. There are two ways to authenticate. Option 1 is recommended if you have the Google Cloud SDK installed.
45
+
46
+ > **Cost note:** This package is free and open source. The Google APIs it uses (Google Sheets API, Google Drive API) are also free for normal scripting workloads — Google provides a generous free tier (300 reads/min, 60 writes/min) that the vast majority of users will never exceed. Google Cloud Console may ask for a credit card when you first create a project to verify your identity, but **Google does not charge you** for the APIs used here. Any billing questions are between you and Google — not this package.
47
+
48
+ ### Option 1 — Application Default Credentials (ADC)
49
+
50
+ This is the zero-config path. Run once in your terminal:
51
+
52
+ ```bash
53
+ gcloud auth application-default login
54
+ ```
55
+
56
+ A browser window opens, you sign in with your Google account, and credentials are saved to your machine. After that, `ThaSheets()` works with no arguments.
57
+
58
+ > Don't have `gcloud`? Install the [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) — it's a standalone CLI tool, roughly similar in spirit to the AWS CLI or the Azure CLI. It is not heavy and not venv-specific; install it once at the system level and every Python project on your machine can use ADC. Or skip it entirely and use Option 2.
59
+
60
+ ### Option 2 — OAuth2 client secrets
61
+
62
+ Use this if you don't have `gcloud` or prefer not to install it.
63
+
64
+ **Step 1 — Create a Google Cloud project**
65
+
66
+ 1. Go to [console.cloud.google.com](https://console.cloud.google.com/)
67
+ 2. Click the project dropdown → **New Project** → give it any name → **Create**
68
+
69
+ **Step 2 — Enable the required APIs**
70
+
71
+ In your new project, go to **APIs & Services** → **Enable APIs and Services** and enable both:
72
+ - **Google Sheets API**
73
+ - **Google Drive API**
74
+
75
+ **Step 3 — Create OAuth2 credentials**
76
+
77
+ 1. Go to **APIs & Services** → **Credentials** → **Create Credentials** → **OAuth 2.0 Client ID**
78
+ 2. If prompted, configure the **OAuth consent screen** first:
79
+ - User type: **External** → fill in app name and your email → save
80
+ 3. Application type: **Desktop app** → give it a name → **Create**
81
+ 4. Click **Download JSON** and save the file (e.g., `client_secrets.json`)
82
+
83
+ **Step 4 — Use the credentials file**
84
+
85
+ ```python
86
+ sheets = ThaSheets(credentials_file="client_secrets.json")
87
+ ```
88
+
89
+ On the **first run**, a browser window opens for you to grant access. After that, the token is cached at `~/.config/tha-google-runner/token.json` and no browser is needed.
90
+
91
+ ---
92
+
93
+ ## Quick start
94
+
95
+ ```python
96
+ from tha_google_runner import ThaSheets
97
+
98
+ sheets = ThaSheets() # uses ADC; or pass credentials_file="client_secrets.json"
99
+
100
+ # Read all rows (first row is headers)
101
+ rows = sheets.read(spreadsheet_id="your-spreadsheet-id")
102
+
103
+ # Append new rows (writes headers automatically if the sheet is empty)
104
+ sheets.append_rows(
105
+ [{"name": "Alice", "score": 95}, {"name": "Bob", "score": 82}],
106
+ spreadsheet_id="your-spreadsheet-id",
107
+ )
108
+
109
+ # Append using raw lists — header row auto-detected and dropped if it matches the sheet
110
+ sheets.append_rows(
111
+ [["name", "score"], ["Alice", 95]],
112
+ spreadsheet_id="your-spreadsheet-id",
113
+ )
114
+
115
+ # Overwrite the entire sheet
116
+ sheets.update_rows(
117
+ [{"name": "Alice", "score": 95}],
118
+ spreadsheet_id="your-spreadsheet-id",
119
+ )
120
+
121
+ # Upsert by key — inserts new rows, updates existing ones
122
+ sheets.upsert_rows(
123
+ [{"id": "1", "name": "Alice", "score": 99}],
124
+ key="id",
125
+ spreadsheet_id="your-spreadsheet-id",
126
+ )
127
+
128
+ # Create a new spreadsheet and get its ID
129
+ spreadsheet_id = sheets.create("My Report", rows=[{"col": "val"}])
130
+
131
+ # Clear a sheet
132
+ sheets.clear(spreadsheet_id="your-spreadsheet-id")
133
+ ```
134
+
135
+ > **Finding your spreadsheet ID:** It's the long string in the URL between `/d/` and `/edit`.
136
+ > `https://docs.google.com/spreadsheets/d/<spreadsheet-id>/edit`
137
+ >
138
+ > You can also pass `url=` instead of `spreadsheet_id=` to any method and the ID will be extracted automatically.
139
+
140
+ ---
141
+
142
+ ## Row input formats
143
+
144
+ All write methods (`append_rows`, `update_rows`, `upsert_rows`, `create`, `add_sheet`) accept either format:
145
+
146
+ **`list[dict]`** — keys are column headers:
147
+ ```python
148
+ [{"name": "Alice", "score": 95}, {"name": "Bob", "score": 82}]
149
+ ```
150
+
151
+ **`list[list]`** — raw rows with automatic header detection:
152
+ ```python
153
+ [["name", "score"], ["Alice", 95], ["Bob", 82]]
154
+ ```
155
+
156
+ Header detection for `list[list]` input:
157
+
158
+ | Sheet state | First row matches existing headers? | Result |
159
+ |---|---|---|
160
+ | Has data | Yes | Header row dropped, rest appended as data |
161
+ | Has data | No | All rows treated as data |
162
+ | Empty / being replaced | — | First row always becomes headers |
163
+
164
+ ---
165
+
166
+ ## API
167
+
168
+ ### `ThaSheets(*, credentials_file=None, token_file=None)`
169
+
170
+ ```python
171
+ ThaSheets(
172
+ credentials_file: str | None = None, # path to client_secrets.json; None uses ADC
173
+ token_file: str | None = None, # override token cache path (OAuth2 only)
174
+ )
175
+ ```
176
+
177
+ The Google client is built lazily on first use and cached for the lifetime of the instance.
178
+ After any write, `sheets.rows` is set to the data rows that were written (as `list[dict]`).
179
+
180
+ ---
181
+
182
+ ### `read(*, spreadsheet_id=None, url=None, sheet_name=None) -> list[dict]`
183
+
184
+ Read all rows. The first row is treated as headers; each subsequent row becomes a `dict`.
185
+
186
+ ```python
187
+ rows = sheets.read(spreadsheet_id="spreadsheet-id")
188
+ rows = sheets.read(url="https://docs.google.com/spreadsheets/d/.../edit")
189
+ rows = sheets.read(spreadsheet_id="spreadsheet-id", sheet_name="Q1 Data")
190
+ ```
191
+
192
+ ---
193
+
194
+ ### `append_rows(rows, *, spreadsheet_id=None, url=None, sheet_name=None) -> int`
195
+
196
+ Append rows to an existing sheet. Returns the number of rows appended.
197
+
198
+ - If the sheet is empty, the headers are written first.
199
+ - Missing keys in a row are filled with `""`.
200
+
201
+ ```python
202
+ count = sheets.append_rows(
203
+ [{"name": "Alice", "score": 95}],
204
+ spreadsheet_id="spreadsheet-id",
205
+ )
206
+ ```
207
+
208
+ ---
209
+
210
+ ### `update_rows(rows, *, spreadsheet_id=None, url=None, sheet_name=None) -> int`
211
+
212
+ Overwrite all data in a sheet. Clears the sheet first, then writes headers + rows. Returns the number of rows written. Passing an empty list clears the sheet and returns `0`.
213
+
214
+ ```python
215
+ count = sheets.update_rows(
216
+ [{"name": "Alice", "score": 95}],
217
+ spreadsheet_id="spreadsheet-id",
218
+ )
219
+ ```
220
+
221
+ ---
222
+
223
+ ### `upsert_rows(rows, *, key, spreadsheet_id=None, url=None, sheet_name=None, on_conflict="update_all") -> int`
224
+
225
+ Insert new rows and update existing ones matched by key. Returns the number of rows upserted.
226
+
227
+ - `key` — column name (str) or list of column names for composite keys
228
+ - New columns in incoming rows are appended to the sheet automatically
229
+ - `on_conflict` controls what happens when multiple existing rows match the same key:
230
+ - `"update_all"` (default) — update every matching row
231
+ - `"update_first"` — update only the first match
232
+ - `"update_last"` — update only the last match
233
+ - `"skip"` — leave duplicates untouched
234
+ - `"raise"` — raise `GoogleError`
235
+
236
+ ```python
237
+ count = sheets.upsert_rows(
238
+ [{"id": "1", "name": "Alice", "score": 99}],
239
+ key="id",
240
+ spreadsheet_id="spreadsheet-id",
241
+ )
242
+
243
+ # Composite key
244
+ count = sheets.upsert_rows(rows, key=["year", "month"], spreadsheet_id="spreadsheet-id")
245
+ ```
246
+
247
+ ---
248
+
249
+ ### `create(title, *, rows=None, sheet_name="Sheet1") -> str`
250
+
251
+ Create a new spreadsheet. Returns the new spreadsheet's ID.
252
+
253
+ ```python
254
+ sid = sheets.create("My Report")
255
+ sid = sheets.create("My Report", rows=[{"col": "val"}], sheet_name="Data")
256
+ ```
257
+
258
+ ---
259
+
260
+ ### `delete(*, spreadsheet_id=None, url=None) -> None`
261
+
262
+ Permanently delete a spreadsheet.
263
+
264
+ ```python
265
+ sheets.delete(spreadsheet_id="spreadsheet-id")
266
+ ```
267
+
268
+ ---
269
+
270
+ ### `list_sheets(*, spreadsheet_id=None, url=None) -> list[str]`
271
+
272
+ Return the names of all worksheets in a spreadsheet.
273
+
274
+ ```python
275
+ names = sheets.list_sheets(spreadsheet_id="spreadsheet-id")
276
+ # ["Sheet1", "Q1 Data", "Archive"]
277
+ ```
278
+
279
+ ---
280
+
281
+ ### `add_sheet(sheet_name, *, spreadsheet_id=None, url=None, rows=None) -> None`
282
+
283
+ Add a new worksheet to an existing spreadsheet. Optionally write initial rows.
284
+
285
+ ```python
286
+ sheets.add_sheet("Q2 Data", spreadsheet_id="spreadsheet-id")
287
+ sheets.add_sheet("Q2 Data", spreadsheet_id="spreadsheet-id", rows=[{"col": "val"}])
288
+ ```
289
+
290
+ ---
291
+
292
+ ### `delete_sheet(sheet_name, *, spreadsheet_id=None, url=None) -> None`
293
+
294
+ Delete a worksheet from a spreadsheet.
295
+
296
+ ```python
297
+ sheets.delete_sheet("Archive", spreadsheet_id="spreadsheet-id")
298
+ ```
299
+
300
+ ---
301
+
302
+ ### `share(email, *, spreadsheet_id=None, url=None, role="reader") -> None`
303
+
304
+ Share a spreadsheet with a user. `role` can be `"reader"`, `"writer"`, or `"owner"`.
305
+
306
+ ```python
307
+ sheets.share("colleague@example.com", spreadsheet_id="spreadsheet-id", role="writer")
308
+ ```
309
+
310
+ ---
311
+
312
+ ### `clear(*, spreadsheet_id=None, url=None, sheet_name=None) -> None`
313
+
314
+ Clear all data in a sheet. Resets `sheets.rows` to `[]`.
315
+
316
+ ```python
317
+ sheets.clear(spreadsheet_id="spreadsheet-id")
318
+ sheets.clear(spreadsheet_id="spreadsheet-id", sheet_name="Archive")
319
+ ```
320
+
321
+ ---
322
+
323
+ ## License
324
+
325
+ MIT