Skip to content

Commit 2fd534d

Browse files
fix(google-sheets): fix auth to use .get(), add integration test, update .env.example
- Fix build_credentials to use context.auth.get("credentials", {}).get("access_token", "") - Add test_google_sheets_integration.py with GOOGLE_SHEETS_ACCESS_TOKEN skip guard - Add GOOGLE_SHEETS_ACCESS_TOKEN and GOOGLE_SHEETS_TEST_SPREADSHEET_ID to .env.example - Apply ruff format to all google-sheets files
1 parent 447ae20 commit 2fd534d

8 files changed

Lines changed: 364 additions & 84 deletions

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@
5959

6060
# -- Xero --
6161
# (uses platform OAuth — tokens are short-lived, typically not set here)
62+
63+
# -- Google Sheets --
64+
# GOOGLE_SHEETS_ACCESS_TOKEN=
65+
# GOOGLE_SHEETS_TEST_SPREADSHEET_ID=

google-sheets/google_sheets.py

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
from autohive_integrations_sdk import Integration, ExecutionContext, ActionHandler, ActionResult, ActionError
1+
from autohive_integrations_sdk import (
2+
Integration,
3+
ExecutionContext,
4+
ActionHandler,
5+
ActionResult,
6+
ActionError,
7+
)
28
from typing import Dict, Any, List
39
from googleapiclient.discovery import build
410
from googleapiclient.errors import HttpError
@@ -10,8 +16,10 @@
1016

1117

1218
def build_credentials(context: ExecutionContext) -> Credentials:
13-
access_token = context.auth["credentials"]["access_token"]
14-
return Credentials(token=access_token, token_uri="https://oauth2.googleapis.com/token") # nosec B106
19+
access_token = context.auth.get("credentials", {}).get("access_token", "")
20+
return Credentials(
21+
token=access_token, token_uri="https://oauth2.googleapis.com/token"
22+
) # nosec B106
1523

1624

1725
def build_sheets_service(context: ExecutionContext):
@@ -72,7 +80,9 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
7280
service = build_sheets_service(context)
7381
spreadsheet_id = inputs["spreadsheet_id"]
7482
include_grid = bool(inputs.get("include_grid_data", False))
75-
request = service.spreadsheets().get(spreadsheetId=spreadsheet_id, includeGridData=include_grid)
83+
request = service.spreadsheets().get(
84+
spreadsheetId=spreadsheet_id, includeGridData=include_grid
85+
)
7686
spreadsheet = request.execute()
7787
return ActionResult(data={"spreadsheet": spreadsheet}, cost_usd=0.0)
7888
except HttpError as e:
@@ -120,7 +130,10 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
120130
params["dateTimeRenderOption"] = dt_render
121131
result = service.spreadsheets().values().get(**params).execute()
122132
return ActionResult(
123-
data={"range": result.get("range", a1), "values": result.get("values", [])},
133+
data={
134+
"range": result.get("range", a1),
135+
"values": result.get("values", []),
136+
},
124137
cost_usd=0.0,
125138
)
126139
except HttpError as e:
@@ -142,7 +155,11 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
142155
if dry_run:
143156
# Validate by attempting a read of the target range to ensure spreadsheet exists
144157
service = build_sheets_service(context)
145-
_ = service.spreadsheets().get(spreadsheetId=spreadsheet_id, includeGridData=False).execute()
158+
_ = (
159+
service.spreadsheets()
160+
.get(spreadsheetId=spreadsheet_id, includeGridData=False)
161+
.execute()
162+
)
146163
# Estimate cells
147164
rows = len(values)
148165
cols = max((len(r) for r in values), default=0)
@@ -208,7 +225,9 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
208225
)
209226
.execute()
210227
)
211-
return ActionResult(data={"updates": result.get("updates", result)}, cost_usd=0.0)
228+
return ActionResult(
229+
data={"updates": result.get("updates", result)}, cost_usd=0.0
230+
)
212231
except HttpError as e:
213232
return ActionError(message=f"Google Sheets API error: {str(e)}")
214233
except Exception as e:
@@ -235,9 +254,13 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
235254
}
236255
]
237256
result = (
238-
service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": requests}).execute()
257+
service.spreadsheets()
258+
.batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": requests})
259+
.execute()
260+
)
261+
return ActionResult(
262+
data={"replies": result.get("replies", [])}, cost_usd=0.0
239263
)
240-
return ActionResult(data={"replies": result.get("replies", [])}, cost_usd=0.0)
241264
except HttpError as e:
242265
return ActionError(message=f"Google Sheets API error: {str(e)}")
243266
except Exception as e:
@@ -273,9 +296,13 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
273296
]
274297

275298
result = (
276-
service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": requests}).execute()
299+
service.spreadsheets()
300+
.batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": requests})
301+
.execute()
302+
)
303+
return ActionResult(
304+
data={"replies": result.get("replies", [])}, cost_usd=0.0
277305
)
278-
return ActionResult(data={"replies": result.get("replies", [])}, cost_usd=0.0)
279306
except HttpError as e:
280307
return ActionError(message=f"Google Sheets API error: {str(e)}")
281308
except Exception as e:
@@ -291,18 +318,26 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
291318
dry_run = bool(inputs.get("dry_run", False))
292319

293320
# Basic validation: ensure it's a list of dicts
294-
if not isinstance(requests, list) or not all(isinstance(r, dict) for r in requests):
321+
if not isinstance(requests, list) or not all(
322+
isinstance(r, dict) for r in requests
323+
):
295324
return ActionError(message="requests must be an array of objects")
296325

297326
if dry_run:
298327
# Validate by fetching spreadsheet metadata
299328
service = build_sheets_service(context)
300-
_ = service.spreadsheets().get(spreadsheetId=spreadsheet_id, includeGridData=False).execute()
329+
_ = (
330+
service.spreadsheets()
331+
.get(spreadsheetId=spreadsheet_id, includeGridData=False)
332+
.execute()
333+
)
301334
return ActionResult(data={"replies": [], "dryRun": True}, cost_usd=0.0)
302335

303336
service = build_sheets_service(context)
304337
result = (
305-
service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": requests}).execute()
338+
service.spreadsheets()
339+
.batchUpdate(spreadsheetId=spreadsheet_id, body={"requests": requests})
340+
.execute()
306341
)
307342
return ActionResult(
308343
data={"replies": result.get("replies", []), "dryRun": False},

google-sheets/tests/context.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# Add integration root and vendored dependencies to path
66
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
7-
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../dependencies")))
7+
sys.path.insert(
8+
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../dependencies"))
9+
)
810

9-
from google_sheets import google_sheets

google-sheets/tests/test_google_sheets.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def decorator(handler_class):
5757

5858
return decorator
5959

60-
async def execute_action(self, action_name: str, inputs: Dict[str, Any], context: MockExecutionContext):
60+
async def execute_action(
61+
self, action_name: str, inputs: Dict[str, Any], context: MockExecutionContext
62+
):
6163
if action_name in self._actions:
6264
handler = self._actions[action_name]()
6365
return await handler.execute(inputs, context)
@@ -119,7 +121,9 @@ async def test_list_spreadsheets_success(self, mock_build):
119121
assert result["nextPageToken"] == "next_token"
120122

121123
# Verify API calls
122-
mock_build.assert_called_once_with("drive", "v3", credentials=mock_build.call_args[1]["credentials"])
124+
mock_build.assert_called_once_with(
125+
"drive", "v3", credentials=mock_build.call_args[1]["credentials"]
126+
)
123127

124128
@patch("google_sheets.build")
125129
async def test_list_spreadsheets_with_filters(self, mock_build):
@@ -333,7 +337,9 @@ async def run_all_tests():
333337
test_instance = TestGoogleSheetsIntegration()
334338

335339
# List all test methods
336-
test_methods = [method for method in dir(test_instance) if method.startswith("test_")]
340+
test_methods = [
341+
method for method in dir(test_instance) if method.startswith("test_")
342+
]
337343

338344
print("Running Google Sheets integration tests...")
339345
print("=" * 50)

google-sheets/tests/test_google_sheets_data_unit.py

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from unittest.mock import AsyncMock, MagicMock, patch # noqa: E402
1212
from autohive_integrations_sdk.integration import ResultType # noqa: E402
1313

14-
_spec = importlib.util.spec_from_file_location("google_sheets_data_mod", os.path.join(_parent, "google_sheets.py"))
14+
_spec = importlib.util.spec_from_file_location(
15+
"google_sheets_data_mod", os.path.join(_parent, "google_sheets.py")
16+
)
1517
_mod = importlib.util.module_from_spec(_spec)
1618
sys.modules["google_sheets_data_mod"] = _mod
1719
_spec.loader.exec_module(_mod)
@@ -52,7 +54,9 @@ async def test_happy_path_returns_values(self, mock_build, mock_context):
5254
}
5355

5456
result = await google_sheets.execute_action(
55-
"sheets_read_range", {"spreadsheet_id": "sid", "range": "Sheet1!A1:B2"}, mock_context
57+
"sheets_read_range",
58+
{"spreadsheet_id": "sid", "range": "Sheet1!A1:B2"},
59+
mock_context,
5660
)
5761

5862
assert result.result.data["range"] == "Sheet1!A1:B2"
@@ -62,10 +66,14 @@ async def test_happy_path_returns_values(self, mock_build, mock_context):
6266
@patch("google_sheets_data_mod.build")
6367
async def test_empty_range_returns_empty_values(self, mock_build, mock_context):
6468
service = make_sheets_service(mock_build)
65-
service.spreadsheets().values().get().execute.return_value = {"range": "Sheet1!A1"}
69+
service.spreadsheets().values().get().execute.return_value = {
70+
"range": "Sheet1!A1"
71+
}
6672

6773
result = await google_sheets.execute_action(
68-
"sheets_read_range", {"spreadsheet_id": "sid", "range": "Sheet1!A1"}, mock_context
74+
"sheets_read_range",
75+
{"spreadsheet_id": "sid", "range": "Sheet1!A1"},
76+
mock_context,
6977
)
7078

7179
assert result.result.data["values"] == []
@@ -74,7 +82,10 @@ async def test_empty_range_returns_empty_values(self, mock_build, mock_context):
7482
@patch("google_sheets_data_mod.build")
7583
async def test_value_render_option_passed(self, mock_build, mock_context):
7684
service = make_sheets_service(mock_build)
77-
service.spreadsheets().values().get().execute.return_value = {"range": "A1", "values": []}
85+
service.spreadsheets().values().get().execute.return_value = {
86+
"range": "A1",
87+
"values": [],
88+
}
7889

7990
await google_sheets.execute_action(
8091
"sheets_read_range",
@@ -89,11 +100,18 @@ async def test_value_render_option_passed(self, mock_build, mock_context):
89100
@patch("google_sheets_data_mod.build")
90101
async def test_datetime_render_option_passed(self, mock_build, mock_context):
91102
service = make_sheets_service(mock_build)
92-
service.spreadsheets().values().get().execute.return_value = {"range": "A1", "values": []}
103+
service.spreadsheets().values().get().execute.return_value = {
104+
"range": "A1",
105+
"values": [],
106+
}
93107

94108
await google_sheets.execute_action(
95109
"sheets_read_range",
96-
{"spreadsheet_id": "sid", "range": "A1", "dateTimeRenderOption": "FORMATTED_STRING"},
110+
{
111+
"spreadsheet_id": "sid",
112+
"range": "A1",
113+
"dateTimeRenderOption": "FORMATTED_STRING",
114+
},
97115
mock_context,
98116
)
99117

@@ -109,18 +127,24 @@ async def test_http_error_returns_action_error(self, mock_build, mock_context):
109127
mock_resp = MagicMock()
110128
mock_resp.status = 400
111129
mock_resp.reason = "Bad Request"
112-
service.spreadsheets().values().get().execute.side_effect = HttpError(mock_resp, b"Bad Request")
130+
service.spreadsheets().values().get().execute.side_effect = HttpError(
131+
mock_resp, b"Bad Request"
132+
)
113133

114134
result = await google_sheets.execute_action(
115-
"sheets_read_range", {"spreadsheet_id": "sid", "range": "Bad!Range"}, mock_context
135+
"sheets_read_range",
136+
{"spreadsheet_id": "sid", "range": "Bad!Range"},
137+
mock_context,
116138
)
117139

118140
assert result.type == ResultType.ACTION_ERROR
119141
assert "Google Sheets API error" in result.result.message
120142

121143
@pytest.mark.asyncio
122144
@patch("google_sheets_data_mod.build")
123-
async def test_generic_exception_returns_action_error(self, mock_build, mock_context):
145+
async def test_generic_exception_returns_action_error(
146+
self, mock_build, mock_context
147+
):
124148
service = make_sheets_service(mock_build)
125149
service.spreadsheets().values().get().execute.side_effect = Exception("Timeout")
126150

@@ -149,7 +173,11 @@ async def test_happy_path_returns_update_info(self, mock_build, mock_context):
149173

150174
result = await google_sheets.execute_action(
151175
"sheets_write_range",
152-
{"spreadsheet_id": "sid", "range": "Sheet1!A1:B2", "values": [["Name", "Age"], ["Alice", "30"]]},
176+
{
177+
"spreadsheet_id": "sid",
178+
"range": "Sheet1!A1:B2",
179+
"values": [["Name", "Age"], ["Alice", "30"]],
180+
},
153181
mock_context,
154182
)
155183

@@ -159,13 +187,20 @@ async def test_happy_path_returns_update_info(self, mock_build, mock_context):
159187

160188
@pytest.mark.asyncio
161189
@patch("google_sheets_data_mod.build")
162-
async def test_dry_run_returns_estimate_without_write(self, mock_build, mock_context):
190+
async def test_dry_run_returns_estimate_without_write(
191+
self, mock_build, mock_context
192+
):
163193
service = make_sheets_service(mock_build)
164194
service.spreadsheets().get().execute.return_value = {"spreadsheetId": "sid"}
165195

166196
result = await google_sheets.execute_action(
167197
"sheets_write_range",
168-
{"spreadsheet_id": "sid", "range": "A1", "values": [["a", "b"], ["c", "d"]], "dry_run": True},
198+
{
199+
"spreadsheet_id": "sid",
200+
"range": "A1",
201+
"values": [["a", "b"], ["c", "d"]],
202+
"dry_run": True,
203+
},
169204
mock_context,
170205
)
171206

@@ -200,7 +235,9 @@ async def test_http_error_returns_action_error(self, mock_build, mock_context):
200235
mock_resp = MagicMock()
201236
mock_resp.status = 403
202237
mock_resp.reason = "Forbidden"
203-
service.spreadsheets().values().update().execute.side_effect = HttpError(mock_resp, b"Forbidden")
238+
service.spreadsheets().values().update().execute.side_effect = HttpError(
239+
mock_resp, b"Forbidden"
240+
)
204241

205242
result = await google_sheets.execute_action(
206243
"sheets_write_range",
@@ -213,9 +250,13 @@ async def test_http_error_returns_action_error(self, mock_build, mock_context):
213250

214251
@pytest.mark.asyncio
215252
@patch("google_sheets_data_mod.build")
216-
async def test_generic_exception_returns_action_error(self, mock_build, mock_context):
253+
async def test_generic_exception_returns_action_error(
254+
self, mock_build, mock_context
255+
):
217256
service = make_sheets_service(mock_build)
218-
service.spreadsheets().values().update().execute.side_effect = Exception("Write failed")
257+
service.spreadsheets().values().update().execute.side_effect = Exception(
258+
"Write failed"
259+
)
219260

220261
result = await google_sheets.execute_action(
221262
"sheets_write_range",
@@ -241,7 +282,11 @@ async def test_happy_path_returns_updates(self, mock_build, mock_context):
241282

242283
result = await google_sheets.execute_action(
243284
"sheets_append_rows",
244-
{"spreadsheet_id": "sid", "range": "Sheet1", "rows": [["a", "b"], ["c", "d"], ["e", "f"]]},
285+
{
286+
"spreadsheet_id": "sid",
287+
"range": "Sheet1",
288+
"rows": [["a", "b"], ["c", "d"], ["e", "f"]],
289+
},
245290
mock_context,
246291
)
247292

@@ -286,7 +331,9 @@ async def test_http_error_returns_action_error(self, mock_build, mock_context):
286331
mock_resp = MagicMock()
287332
mock_resp.status = 429
288333
mock_resp.reason = "Too Many Requests"
289-
service.spreadsheets().values().append().execute.side_effect = HttpError(mock_resp, b"Rate limited")
334+
service.spreadsheets().values().append().execute.side_effect = HttpError(
335+
mock_resp, b"Rate limited"
336+
)
290337

291338
result = await google_sheets.execute_action(
292339
"sheets_append_rows",
@@ -299,9 +346,13 @@ async def test_http_error_returns_action_error(self, mock_build, mock_context):
299346

300347
@pytest.mark.asyncio
301348
@patch("google_sheets_data_mod.build")
302-
async def test_generic_exception_returns_action_error(self, mock_build, mock_context):
349+
async def test_generic_exception_returns_action_error(
350+
self, mock_build, mock_context
351+
):
303352
service = make_sheets_service(mock_build)
304-
service.spreadsheets().values().append().execute.side_effect = Exception("Append failed")
353+
service.spreadsheets().values().append().execute.side_effect = Exception(
354+
"Append failed"
355+
)
305356

306357
result = await google_sheets.execute_action(
307358
"sheets_append_rows",

0 commit comments

Comments
 (0)