Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/manual/billing.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class CallApiAction(ActionHandler):
response = await context.fetch(url, headers={"Authorization": f"Bearer {api_key}"})

return ActionResult(
data={"result": response},
data={"result": response.data},
cost_usd=0.05
)
```
Expand Down Expand Up @@ -93,11 +93,11 @@ class GenerateContentAction(ActionHandler):
)

# Calculate cost based on usage returned by the API
tokens_used = response.get("usage", {}).get("total_tokens", 0)
tokens_used = response.data.get("usage", {}).get("total_tokens", 0)
cost = tokens_used * 0.00001 # $0.01 per 1000 tokens

return ActionResult(
data={"content": response["result"]},
data={"content": response.data["result"]},
cost_usd=cost
)
```
Expand Down
9 changes: 6 additions & 3 deletions docs/manual/building_your_first_integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ class GetItemsAction(ActionHandler):
params={"limit": limit}
)

items = response.get("data", [])
items = response.data.get("data", [])

return ActionResult(
data={
Expand Down Expand Up @@ -254,7 +254,7 @@ class GetItemsAction(ActionHandler):
**`ActionHandler`** — Base class for all action handlers. You must implement the `async def execute()` method.

**`ExecutionContext`** — Provided to every handler. Gives you:
- `context.fetch(url, ...)` — Make HTTP requests with automatic auth handling
- `context.fetch(url, ...)` — Make HTTP requests with automatic auth handling; returns a `FetchResponse` with `.status`, `.headers`, and `.data` attributes
- `context.auth` — Access authentication credentials

**`ActionResult`** — The required return type for all action handlers. Contains:
Expand All @@ -263,7 +263,7 @@ class GetItemsAction(ActionHandler):

### Making HTTP Requests

Use `context.fetch()` for all HTTP calls. It handles authentication headers, retries, timeouts, and response parsing automatically.
Use `context.fetch()` for all HTTP calls. It handles authentication headers, retries, timeouts, and response parsing automatically. It returns a `FetchResponse` object — access the parsed body via `.data`, the HTTP status via `.status`, and response headers via `.headers`.

```python
# GET with query parameters
Expand All @@ -272,6 +272,7 @@ response = await context.fetch(
method="GET",
params={"limit": 10, "status": "active"}
)
items = response.data # parsed response body

# POST with JSON body
response = await context.fetch(
Expand All @@ -280,6 +281,7 @@ response = await context.fetch(
headers={"Content-Type": "application/json"},
json={"name": "New Item", "status": "active"}
)
new_item = response.data

# POST with form-encoded body
response = await context.fetch(
Expand All @@ -288,6 +290,7 @@ response = await context.fetch(
headers={"Content-Type": "application/x-www-form-urlencoded"},
data={"name": "New Item"}
)
result = response.data
```

### Handling Inputs
Expand Down
9 changes: 6 additions & 3 deletions docs/manual/connected_account.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,13 @@ class MyConnectedAccountHandler(ConnectedAccountHandler):
# Fetch user info from the API
# For platform OAuth, context.fetch() auto-injects the Authorization header.
# For custom auth, pass headers manually using context.auth.get("credentials", {}).get("api_key")
user_data = await context.fetch(
response = await context.fetch(
"https://api.example.com/user",
method="GET"
)

# Return ConnectedAccountInfo with available fields
user_data = response.data
name = user_data.get("name", "")
name_parts = name.split(maxsplit=1) if name else []

Expand Down Expand Up @@ -138,13 +139,14 @@ class GithubConnectedAccountHandler(ConnectedAccountHandler):
async def get_account_info(self, context: ExecutionContext) -> ConnectedAccountInfo:
"""Fetch GitHub user information"""
# context.fetch() auto-injects the Authorization header for platform OAuth
user_data = await context.fetch(
response = await context.fetch(
"https://api.github.com/user",
method="GET",
headers={"Accept": "application/vnd.github.v3+json"}
)

# Parse name into first/last
user_data = response.data
name = user_data.get("name", "")
name_parts = name.split(maxsplit=1) if name else []

Expand Down Expand Up @@ -173,11 +175,12 @@ class LinkedInConnectedAccountHandler(ConnectedAccountHandler):
async def get_account_info(self, context: ExecutionContext) -> ConnectedAccountInfo:
"""Fetch LinkedIn user information"""
# context.fetch() auto-injects the Authorization header for platform OAuth
user_data = await context.fetch(
response = await context.fetch(
"https://api.linkedin.com/v2/userinfo",
method="GET"
)

user_data = response.data
return ConnectedAccountInfo(
email=user_data.get("email"),
first_name=user_data.get("given_name"),
Expand Down
2 changes: 1 addition & 1 deletion docs/manual/integration_structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ Required fields:
Optional fields:
- `scopes`: array of OAuth scopes to request

With platform auth, `context.fetch()` automatically injects the `Authorization` header.
With platform auth, `context.fetch()` automatically injects the `Authorization` header and returns a `FetchResponse` object (with `.status`, `.headers`, and `.data` attributes).

#### No Auth

Expand Down
16 changes: 8 additions & 8 deletions docs/manual/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ListItemsAction(ActionHandler):
params=params
)

items = response if isinstance(response, list) else response.get("data", [])
items = response.data if isinstance(response.data, list) else response.data.get("data", [])
if not items:
break

Expand Down Expand Up @@ -62,10 +62,10 @@ class ListProjectsAction(ActionHandler):
params=params
)

data = response.get("data", [])
data = response.data.get("data", [])
all_projects.extend(data)

next_page = response.get("next_page")
next_page = response.data.get("next_page")
if next_page and next_page.get("offset"):
offset = next_page["offset"]
else:
Expand Down Expand Up @@ -93,9 +93,9 @@ class ListVideosAction(ActionHandler):
response = await context.fetch(f"{BASE_URL}/videos", method="POST", json=request_body)

return ActionResult(data={
"videos": response.get("videos", []),
"cursor": response.get("cursor"),
"has_more": response.get("has_more", False),
"videos": response.data.get("videos", []),
"cursor": response.data.get("cursor"),
"has_more": response.data.get("has_more", False),
})
```

Expand All @@ -120,7 +120,7 @@ BASE_URL = f"https://api.example.com/{API_VERSION}"
async def get_account_id(context: ExecutionContext) -> str:
"""Fetch the authenticated user's account ID."""
response = await context.fetch(f"{BASE_URL}/me", method="GET")
account_id = response.get("id")
account_id = response.data.get("id")
if not account_id:
raise Exception("Failed to retrieve account ID")
return account_id
Expand Down Expand Up @@ -164,7 +164,7 @@ class ExampleAPI:

while True:
response = await context.fetch(url, params=params, headers=headers)
items = response if isinstance(response, list) else []
items = response.data if isinstance(response.data, list) else []
if not items:
break
all_items.extend(items)
Expand Down
200 changes: 200 additions & 0 deletions docs/plans/12.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this plan file belong in this repo? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair question, but I think it's useful - I'm a big fan of information in the repo and given that - while 12 is completed in here - there's follow-up work I want to audit against at some point when this change goes into a release, I think it's a good place. Happy to remove in the future.

The other options are to put it into a discussion thread, a new GH issue or in our internal Notion, but none of those are ideal either.

Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Issue #12 — FetchResponse Migration Plan

> `context.fetch()` now returns a `FetchResponse` (with `.status`, `.headers`, `.data`)
> instead of the raw parsed body. This is a **breaking change**.

## SDK repo (integrations-sdk) — ✅ Done

All changes committed on branch `issue-12/fetch-response`.

### Implementation
- [x] `src/autohive_integrations_sdk/integration.py` — `FetchResponse` dataclass + updated `fetch()` return type
- [x] `src/autohive_integrations_sdk/__init__.py` — export `FetchResponse`

### Docs & samples
- [x] `docs/manual/billing.md`
- [x] `docs/manual/building_your_first_integration.md`
- [x] `docs/manual/connected_account.md`
- [x] `docs/manual/integration_structure.md`
- [x] `docs/manual/patterns.md`
- [x] `samples/api-fetch/api_fetch.py`
- [x] `samples/template/my_integration.py`

### Not updated (generated)
- `docs/apidocs/` — regenerate after merge with `pdoc`

---

## autohive-integrations (public) — 94 file hits

Repo: `Autohive-AI/autohive-integrations`

### Code (Python) — 81 production files, all need `response` → `response.data`
- [ ] `api-call/api_call.py`
- [ ] `app-business-reviews/app_business_reviews.py`
- [ ] `asana/asana.py`
- [ ] `bigquery/bigquery.py`
- [ ] `bitly/bitly.py`
- [ ] `box/box.py`
- [ ] `calendly/calendly.py`
- [ ] `canva/canva.py`
- [ ] `circle/circle.py`
- [ ] `clickup/clickup.py`
- [ ] `coda/coda.py`
- [ ] `companies-register/companies_register.py`
- [ ] `dropbox/dropbox.py`
- [ ] `elevenlabs/elevenlabs.py`
- [ ] `eventbrite/eventbrite.py`
- [ ] `facebook/actions/comments.py`
- [ ] `facebook/actions/insights.py`
- [ ] `facebook/actions/pages.py`
- [ ] `facebook/actions/posts.py`
- [ ] `facebook/helpers.py`
- [ ] `fathom/fathom.py`
- [ ] `float/float.py`
- [ ] `freshdesk/freshdesk.py`
- [ ] `front/front.py`
- [ ] `github/github.py`
- [ ] `gitlab/gitlab.py`
- [ ] `gong/gong.py`
- [ ] `google-calendar/google_calendar.py`
- [ ] `google-docs/google_docs.py`
- [ ] `google-looker/google_looker.py`
- [ ] `google-tasks/google_tasks.py`
- [ ] `hackernews/hackernews.py`
- [ ] `harvest/harvest.py`
- [ ] `heartbeat/heartbeat.py`
- [ ] `heygen/heygen.py`
- [ ] `humanitix/actions/checkin.py`
- [ ] `humanitix/actions/events.py`
- [ ] `humanitix/actions/orders.py`
- [ ] `humanitix/actions/tags.py`
- [ ] `humanitix/actions/tickets.py`
- [ ] `humanitix/helpers.py`
- [ ] `instagram/actions/account.py`
- [ ] `instagram/actions/comments.py`
- [ ] `instagram/actions/insights.py`
- [ ] `instagram/actions/media.py`
- [ ] `instagram/helpers.py`
- [ ] `instagram/instagram.py`
- [ ] `linkedin/linkedin.py`
- [ ] `linkedin-ads/linkedin_ads.py`
- [ ] `mailchimp/mailchimp.py`
- [ ] `microsoft-excel/microsoft_excel.py`
- [ ] `microsoft-planner/microsoft_planner.py`
- [ ] `microsoft-powerpoint/microsoft_powerpoint.py`
- [ ] `microsoft-word/microsoft_word.py`
- [ ] `microsoft365/microsoft365.py`
- [ ] `monday-com/monday_com.py`
- [ ] `netlify/netlify.py`
- [ ] `notion/notion.py`
- [ ] `nzbn/nzbn.py`
- [ ] `pipedrive/pipedrive.py`
- [ ] `powerbi/powerbi.py`
- [ ] `productboard/productboard.py`
- [ ] `reddit/reddit.py`
- [ ] `retail-express/retail_express.py`
- [ ] `rss-reader-feedparser-ah-fetch/rss_reader.py`
- [ ] `shopify-admin/shopify_admin.py`
- [ ] `shopify-customer/shopify_customer.py`
- [ ] `shopify-storefront/shopify_storefront.py`
- [ ] `stripe/stripe.py`
- [ ] `tiktok/tiktok.py`
- [ ] `toggl/toggl.py`
- [ ] `trello/trello.py`
- [ ] `typeform/typeform.py`
- [ ] `uber/uber.py`
- [ ] `webcal/webcal.py`
- [ ] `whatsapp/whatsapp.py`
- [ ] `X/x.py`
- [ ] `xero/xero.py`
- [ ] `youtube/youtube.py`
- [ ] `zoho/zoho.py`
- [ ] `zoom/zoom.py`

### Tests (Python) — 13 test files, mock return values need to return `FetchResponse`
- [ ] `linkedin-ads/tests/test_linkedin_ads.py`
- [ ] `microsoft-planner/tests/test_microsoft_planner.py`
- [ ] `microsoft-powerpoint/tests/test_microsoft_powerpoint.py`
- [ ] `microsoft-word/tests/test_microsoft_word.py`
- [ ] `microsoft365/tests/test_microsoft365_integration.py`
- [ ] `monday-com/tests/test_monday_com.py`
- [ ] `notion/tests/test_notion_integration.py`
- [ ] `powerbi/tests/test_powerbi_integration.py`
- [ ] `productboard/tests/test_productboard.py`
- [ ] `shopify-customer/tests/test_unit.py`
- [ ] `uber/tests/test_uber.py`
- [ ] `xero/tests/test_xero.py`
- [ ] `zoom/tests/run_test.py`

### Docs (Markdown)
- [ ] `README.md`
- [ ] `api-call/README.md`

### Dependency
- [ ] Bump `autohive-integrations-sdk` version in each integration's `requirements.txt`

---

## integrations (private) — 5 code hits

Repo: `Autohive-AI/integrations`

### Code (Python)
- [ ] `discord/discord.py`
- [ ] `hubspot/hubspot.py`
- [ ] `perplexity/perplexity.py`
- [ ] `slack/slack.py`
- [ ] `telegram/telegram.py`

### Dependency
- [ ] Bump `autohive-integrations-sdk` version in each integration's `requirements.txt`

---

## Autohive (private/platform) — 2 code hits

Repo: `Autohive-AI/Autohive`

### Code (Python)
- [ ] `integrations/github/github_integration.py`
- [ ] `integrations/slack/slack.py`

### Dependency
- [ ] Bump `autohive-integrations-sdk` version in `requirements.txt`

---

## autohive-docs (private) — 1 doc hit

Repo: `Autohive-AI/autohive-docs`

### Docs (Markdown)
- [ ] `content/docs/integrations/api-call.md`

---

## Release sequence

1. Merge & release SDK (`integrations-sdk`) with new minor version
2. Update `autohive-integrations` (public) — bulk PR for all integrations + docs
3. Update `integrations` (private) — PR for 5 integrations
4. Update `Autohive` (private) — PR for 2 integration files
5. Update `autohive-docs` (private) — PR for api-call docs page
6. Regenerate `docs/apidocs/` in SDK repo

## Migration pattern

Every call site follows the same mechanical change:

```python
# Before
response = await context.fetch(url)
data = response.get("key") # or response["key"], etc.
return ActionResult(data=response)

# After
response = await context.fetch(url)
data = response.data.get("key") # access body via .data
return ActionResult(data=response.data)
```
Loading