Skip to content

Commit 5441c20

Browse files
feat(app-business-reviews): upgrade SDK to 2.0.0, add pytest unit tests
- Pin autohive-integrations-sdk~=2.0.0 in requirements.txt - Add .data to all context.fetch() return accesses - Convert raise ValueError to return ActionError(message=...) - Return ActionResult(data=..., cost_usd=0.0) from all action handlers - Bump config.json version to 2.0.0 - Add tests/conftest.py and 40-test unit suite covering all 6 actions
1 parent f40054c commit 5441c20

5 files changed

Lines changed: 792 additions & 52 deletions

File tree

app-business-reviews/app_business_reviews.py

Lines changed: 63 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from autohive_integrations_sdk import Integration, ExecutionContext, ActionHandler
1+
from autohive_integrations_sdk import Integration, ExecutionContext, ActionHandler, ActionResult, ActionError
22
from typing import Dict, Any
33

44
# Create the integration using the config.json
@@ -29,7 +29,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
2929
response = await context.fetch("https://serpapi.com/search", method="GET", params=params)
3030

3131
# Extract apps from organic results
32-
organic_results = response.get("organic_results", [])
32+
organic_results = response.data.get("organic_results", [])
3333
limit = inputs.get("num", 10)
3434
apps = []
3535

@@ -48,7 +48,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
4848
}
4949
apps.append(app)
5050

51-
return {"apps": apps, "total_results": len(apps)}
51+
return ActionResult(data={"apps": apps, "total_results": len(apps)}, cost_usd=0.0)
5252

5353

5454
@app_business_reviews.action("get_reviews_app_store")
@@ -61,7 +61,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
6161
app_name = inputs.get("app_name")
6262

6363
if not product_id and not app_name:
64-
raise ValueError("Either product_id or app_name must be provided")
64+
return ActionError(message="Either product_id or app_name must be provided")
6565

6666
# If app_name is provided but no product_id, search for the app first
6767
if app_name and not product_id:
@@ -73,16 +73,16 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
7373

7474
search_response = await context.fetch("https://serpapi.com/search", method="GET", params=search_params)
7575

76-
organic_results = search_response.get("organic_results", [])
76+
organic_results = search_response.data.get("organic_results", [])
7777
if not organic_results:
78-
raise ValueError(f"No apps found for search term: {app_name}")
78+
return ActionError(message=f"No apps found for search term: {app_name}")
7979

8080
# Get the first result's product ID
8181
first_result = organic_results[0]
8282
product_id = str(first_result.get("id"))
8383

8484
if not product_id:
85-
raise ValueError(f"Could not extract product ID for app: {app_name}")
85+
return ActionError(message=f"Could not extract product ID for app: {app_name}")
8686

8787
# Build SerpApi request parameters for reviews
8888
params = {"api_key": api_key, "engine": "apple_reviews", "product_id": product_id}
@@ -110,7 +110,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
110110
response = await context.fetch("https://serpapi.com/search", method="GET", params=params)
111111

112112
# Extract reviews data from current page
113-
page_reviews = response.get("reviews", [])
113+
page_reviews = response.data.get("reviews", [])
114114
if not page_reviews:
115115
break
116116

@@ -139,16 +139,19 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
139139
current_page += 1
140140

141141
# Check if there are more pages using pagination info
142-
pagination_info = response.get("serpapi_pagination", {})
142+
pagination_info = response.data.get("serpapi_pagination", {})
143143
if not pagination_info.get("next"):
144144
break
145145

146-
return {
147-
"reviews": all_reviews,
148-
"total_reviews": len(all_reviews),
149-
"app_name": app_title,
150-
"product_id": product_id,
151-
}
146+
return ActionResult(
147+
data={
148+
"reviews": all_reviews,
149+
"total_reviews": len(all_reviews),
150+
"app_name": app_title,
151+
"product_id": product_id,
152+
},
153+
cost_usd=0.0,
154+
)
152155

153156

154157
# ---- Google Play Store Actions ----
@@ -166,7 +169,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
166169
response = await context.fetch("https://serpapi.com/search", method="GET", params=params)
167170

168171
# Extract apps from organic results
169-
organic_results = response.get("organic_results", [])
172+
organic_results = response.data.get("organic_results", [])
170173
limit = inputs.get("limit", 10)
171174
apps = []
172175

@@ -189,7 +192,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
189192
if len(apps) >= limit:
190193
break
191194

192-
return {"apps": apps, "total_results": len(apps)}
195+
return ActionResult(data={"apps": apps, "total_results": len(apps)}, cost_usd=0.0)
193196

194197

195198
@app_business_reviews.action("get_reviews_google_play")
@@ -202,17 +205,17 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
202205
app_name = inputs.get("app_name")
203206

204207
if not product_id and not app_name:
205-
raise ValueError("Either product_id or app_name must be provided")
208+
return ActionError(message="Either product_id or app_name must be provided")
206209

207210
# If app_name is provided but no product_id, search for the app first
208211
if app_name and not product_id:
209212
search_params = {"api_key": api_key, "engine": "google_play", "store": "apps", "q": app_name}
210213

211214
search_response = await context.fetch("https://serpapi.com/search", method="GET", params=search_params)
212215

213-
organic_results = search_response.get("organic_results", [])
216+
organic_results = search_response.data.get("organic_results", [])
214217
if not organic_results:
215-
raise ValueError(f"No apps found for search term: {app_name}")
218+
return ActionError(message=f"No apps found for search term: {app_name}")
216219

217220
# Get the first result's product ID from nested items structure
218221
product_id = None
@@ -224,7 +227,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
224227
break
225228

226229
if not product_id:
227-
raise ValueError(f"Could not extract product ID for app: {app_name}")
230+
return ActionError(message=f"Could not extract product ID for app: {app_name}")
228231

229232
# Build SerpApi request parameters
230233
params = {
@@ -251,6 +254,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
251254
all_reviews = []
252255
next_page_token = None
253256
pages_fetched = 0
257+
response = None
254258

255259
# Fetch reviews with pagination
256260
while pages_fetched < max_pages:
@@ -265,7 +269,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
265269
response = await context.fetch("https://serpapi.com/search", method="GET", params=params)
266270

267271
# Extract reviews data from current page
268-
page_reviews = response.get("reviews", [])
272+
page_reviews = response.data.get("reviews", [])
269273
if not page_reviews:
270274
break
271275

@@ -285,21 +289,24 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
285289
pages_fetched += 1
286290

287291
# Check if there's a next page
288-
pagination_info = response.get("serpapi_pagination", {})
292+
pagination_info = response.data.get("serpapi_pagination", {})
289293
next_page_token = pagination_info.get("next_page_token")
290294
if not next_page_token:
291295
break
292296

293297
# Extract app information from the response
294-
app_info = response.get("product_info", {})
298+
app_info = response.data.get("product_info", {}) if response is not None else {}
295299

296-
return {
297-
"reviews": all_reviews,
298-
"total_reviews": len(all_reviews),
299-
"app_name": app_info.get("title", ""),
300-
"app_rating": app_info.get("rating") or 0.0,
301-
"product_id": product_id,
302-
}
300+
return ActionResult(
301+
data={
302+
"reviews": all_reviews,
303+
"total_reviews": len(all_reviews),
304+
"app_name": app_info.get("title", ""),
305+
"app_rating": app_info.get("rating") or 0.0,
306+
"product_id": product_id,
307+
},
308+
cost_usd=0.0,
309+
)
303310

304311

305312
# ---- Google Maps Actions ----
@@ -323,7 +330,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
323330
response = await context.fetch("https://serpapi.com/search", method="GET", params=params)
324331

325332
# Extract places from local results
326-
local_results = response.get("local_results", [])
333+
local_results = response.data.get("local_results", [])
327334
limit = inputs.get("num_results", 5)
328335
places = []
329336

@@ -340,7 +347,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
340347
}
341348
places.append(place)
342349

343-
return {"places": places, "total_results": len(places)}
350+
return ActionResult(data={"places": places, "total_results": len(places)}, cost_usd=0.0)
344351

345352

346353
@app_business_reviews.action("get_reviews_google_maps")
@@ -355,7 +362,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
355362
local_results = [] # Initialize to store search results
356363

357364
if not place_id and not data_id and not query:
358-
raise ValueError("Either place_id, data_id, or query (business name) must be provided")
365+
return ActionError(message="Either place_id, data_id, or query (business name) must be provided")
359366

360367
# If query is provided but no place_id/data_id, search for the place first
361368
if query and not place_id and not data_id:
@@ -369,25 +376,27 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
369376
# Search for the place to get place_id and data_id
370377
search_response = await context.fetch("https://serpapi.com/search", method="GET", params=search_params)
371378

372-
local_results = search_response.get("local_results", [])
379+
local_results = search_response.data.get("local_results", [])
373380
if not local_results:
374381
# Provide helpful error message
375382
suggestion = (
376383
f"No businesses found for search query: '{search_query}'. Use place_id instead. "
377384
"Visit: https://developers.google.com/maps/documentation/places/web-service/"
378385
"place-id to find Place ID manually."
379386
)
380-
raise ValueError(suggestion)
387+
return ActionError(message=suggestion)
381388

382389
# Get the first result's place_id and data_id
383390
first_result = local_results[0]
384391
place_id = first_result.get("place_id")
385392
data_id = first_result.get("data_id")
386393

387394
if not place_id and not data_id:
388-
raise ValueError(
389-
f"Could not extract place_id or data_id for business: {search_query}. "
390-
"The search returned results but they don't contain required identifiers."
395+
return ActionError(
396+
message=(
397+
f"Could not extract place_id or data_id for business: {search_query}. "
398+
"The search returned results but they don't contain required identifiers."
399+
)
391400
)
392401

393402
# Build SerpApi request parameters for reviews
@@ -399,7 +408,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
399408
elif data_id:
400409
params["data_id"] = data_id
401410
else:
402-
raise ValueError("Could not resolve place_id or data_id from the provided query")
411+
return ActionError(message="Could not resolve place_id or data_id from the provided query")
403412

404413
# Add sort parameter if provided
405414
if inputs.get("sort_by"):
@@ -411,6 +420,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
411420
all_reviews = []
412421
next_page_token = None
413422
pages_fetched = 0
423+
response = None
414424

415425
# Fetch reviews with pagination
416426
while pages_fetched < max_pages:
@@ -426,7 +436,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
426436
response = await context.fetch("https://serpapi.com/search", method="GET", params=params)
427437

428438
# Extract reviews data from current page
429-
page_reviews = response.get("reviews", [])
439+
page_reviews = response.data.get("reviews", [])
430440
if not page_reviews:
431441
break
432442

@@ -444,12 +454,12 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
444454
pages_fetched += 1
445455

446456
# Check if there's a next page
447-
next_page_token = response.get("serpapi_pagination", {}).get("next_page_token")
457+
next_page_token = response.data.get("serpapi_pagination", {}).get("next_page_token")
448458
if not next_page_token:
449459
break
450460

451461
# Extract business information from the last response
452-
place_info = response.get("place_info", {})
462+
place_info = response.data.get("place_info", {}) if response is not None else {}
453463

454464
# Use business name from search result if we searched, otherwise from place_info
455465
business_name = place_info.get("title", "")
@@ -458,10 +468,13 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
458468
if local_results:
459469
business_name = local_results[0].get("title", business_name)
460470

461-
return {
462-
"reviews": all_reviews,
463-
"total_reviews": len(all_reviews),
464-
"average_rating": place_info.get("rating") or 0.0,
465-
"business_name": business_name,
466-
"place_id": place_id or place_info.get("place_id", inputs.get("place_id", "")),
467-
}
471+
return ActionResult(
472+
data={
473+
"reviews": all_reviews,
474+
"total_reviews": len(all_reviews),
475+
"average_rating": place_info.get("rating") or 0.0,
476+
"business_name": business_name,
477+
"place_id": place_id or place_info.get("place_id", inputs.get("place_id", "")),
478+
},
479+
cost_usd=0.0,
480+
)

app-business-reviews/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "App and Business Reviews",
33
"display_name": "App and Business Reviews",
4-
"version": "1.0.0",
4+
"version": "2.0.0",
55
"description": "Access reviews from App Store, Google Play Store, and Google Maps using SerpAPI",
66
"entry_point": "app_business_reviews.py",
77
"auth": {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
autohive-integrations-sdk~=1.0.2
1+
autohive-integrations-sdk~=2.0.0
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import sys
2+
import os
3+
4+
# Allow 'from context import ...' to work when pytest runs from repo root
5+
sys.path.insert(0, os.path.dirname(__file__))

0 commit comments

Comments
 (0)