From 29b65fd1208b4a0d4922df570bbfa42c048af038 Mon Sep 17 00:00:00 2001 From: Kai Koenig Date: Tue, 17 Mar 2026 18:23:14 +1300 Subject: [PATCH 1/4] Return full FetchResponse from context.fetch() (#12) Add FetchResponse dataclass (status, headers, data) and update ExecutionContext.fetch() to return it instead of the raw parsed body. Export FetchResponse from the package. --- src/autohive_integrations_sdk/__init__.py | 3 +- src/autohive_integrations_sdk/integration.py | 35 ++++++++++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/autohive_integrations_sdk/__init__.py b/src/autohive_integrations_sdk/__init__.py index 0d34b59..79c35be 100644 --- a/src/autohive_integrations_sdk/__init__.py +++ b/src/autohive_integrations_sdk/__init__.py @@ -4,5 +4,6 @@ # Re-export classes from integration module from autohive_integrations_sdk.integration import ( Integration, ExecutionContext, ActionHandler, PollingTriggerHandler, ConnectedAccountHandler, - ConnectedAccountInfo, ValidationError, ActionResult, IntegrationResult, ResultType + ConnectedAccountInfo, ValidationError, ActionResult, IntegrationResult, ResultType, + FetchResponse ) \ No newline at end of file diff --git a/src/autohive_integrations_sdk/integration.py b/src/autohive_integrations_sdk/integration.py index 2d1b490..3152e3d 100644 --- a/src/autohive_integrations_sdk/integration.py +++ b/src/autohive_integrations_sdk/integration.py @@ -128,6 +128,24 @@ def __init__(self, retry_after: int, *args, **kwargs): super().__init__(*args, **kwargs) # ---- Result Classes ---- +@dataclass +class FetchResponse: + """Response object returned by ``ExecutionContext.fetch()``. + + Wraps the full HTTP response so callers can inspect status codes and + headers in addition to the parsed body. + + Attributes: + status: HTTP status code (e.g. ``200``, ``201``). + headers: Response headers as a plain ``dict``. + data: Parsed JSON (``dict``/``list``) when the response is + ``application/json``, otherwise the raw response text. + ``None`` for empty 200/201/204 responses. + """ + status: int + headers: Dict[str, str] + data: Any + @dataclass class ActionResult: """Result returned by action handlers. @@ -356,7 +374,7 @@ async def fetch( content_type: Optional[str] = None, timeout: Optional[int] = None, retry_count: int = 0 - ) -> Any: + ) -> FetchResponse: """Make an HTTP request with automatic retries and error handling. For **platform OAuth** integrations (``auth_type == "PlatformOauth2"``), @@ -382,9 +400,8 @@ async def fetch( retry_count: Internal — current retry attempt number. Returns: - Parsed JSON (``dict``/``list``) when the response is - ``application/json``, otherwise the raw response text. - Returns ``None`` for empty 200/201/204 responses. + A ``FetchResponse`` containing the HTTP status code, response + headers, and parsed body data. Raises: RateLimitError: On HTTP 429 with the ``Retry-After`` value. @@ -462,16 +479,22 @@ async def fetch( else: result = await response.text() if not result and response.status in {200, 201, 204}: - return None + result = None except Exception as e: self.logger.error(f"Error parsing response: {e}") result = await response.text() + response_headers = dict(response.headers) + if not response.ok: print(f"HTTP error encountered. Status: {response.status}. Result: {result}") raise HTTPError(response.status, str(result), result) - return result + return FetchResponse( + status=response.status, + headers=response_headers, + data=result, + ) except RateLimitError: raise From 7da1c0ca11d6867eaefc02464bcb13d1a447d720 Mon Sep 17 00:00:00 2001 From: Kai Koenig Date: Wed, 18 Mar 2026 11:43:40 +1300 Subject: [PATCH 2/4] Update docs and samples for FetchResponse (#12) Update all code examples, docstrings, and prose to use response.data instead of the raw response body, reflecting the new FetchResponse return type from context.fetch(). --- docs/manual/billing.md | 6 +++--- docs/manual/building_your_first_integration.md | 9 ++++++--- docs/manual/connected_account.md | 9 ++++++--- docs/manual/integration_structure.md | 2 +- docs/manual/patterns.md | 16 ++++++++-------- samples/api-fetch/api_fetch.py | 6 +++--- samples/template/my_integration.py | 2 +- src/autohive_integrations_sdk/integration.py | 8 ++++---- 8 files changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/manual/billing.md b/docs/manual/billing.md index 0bdd028..245be78 100644 --- a/docs/manual/billing.md +++ b/docs/manual/billing.md @@ -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 ) ``` @@ -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 ) ``` diff --git a/docs/manual/building_your_first_integration.md b/docs/manual/building_your_first_integration.md index 27846e3..2b6b181 100644 --- a/docs/manual/building_your_first_integration.md +++ b/docs/manual/building_your_first_integration.md @@ -223,7 +223,7 @@ class GetItemsAction(ActionHandler): params={"limit": limit} ) - items = response.get("data", []) + items = response.data.get("data", []) return ActionResult( data={ @@ -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: @@ -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 @@ -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( @@ -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( @@ -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 diff --git a/docs/manual/connected_account.md b/docs/manual/connected_account.md index 93af152..eab7661 100644 --- a/docs/manual/connected_account.md +++ b/docs/manual/connected_account.md @@ -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 [] @@ -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 [] @@ -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"), diff --git a/docs/manual/integration_structure.md b/docs/manual/integration_structure.md index 6523f4e..99b911e 100644 --- a/docs/manual/integration_structure.md +++ b/docs/manual/integration_structure.md @@ -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 diff --git a/docs/manual/patterns.md b/docs/manual/patterns.md index de564aa..7794a73 100644 --- a/docs/manual/patterns.md +++ b/docs/manual/patterns.md @@ -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 @@ -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: @@ -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), }) ``` @@ -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 @@ -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) diff --git a/samples/api-fetch/api_fetch.py b/samples/api-fetch/api_fetch.py index 1ffedee..8991686 100644 --- a/samples/api-fetch/api_fetch.py +++ b/samples/api-fetch/api_fetch.py @@ -23,7 +23,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> Ac response = await context.fetch(url) return ActionResult( - data=response, + data=response.data, cost_usd=0.01 ) @@ -46,7 +46,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> Ac response = await context.fetch(url) return ActionResult( - data=response, + data=response.data, cost_usd=0.01 ) @@ -65,6 +65,6 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> Ac ) return ActionResult( - data=response, + data=response.data, cost_usd=0.01 ) diff --git a/samples/template/my_integration.py b/samples/template/my_integration.py index d46e201..69a80c2 100644 --- a/samples/template/my_integration.py +++ b/samples/template/my_integration.py @@ -25,4 +25,4 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> Ac headers={"Authorization": f"Bearer {api_key}"} ) - return ActionResult(data=response) + return ActionResult(data=response.data) diff --git a/src/autohive_integrations_sdk/integration.py b/src/autohive_integrations_sdk/integration.py index 3152e3d..5bfe693 100644 --- a/src/autohive_integrations_sdk/integration.py +++ b/src/autohive_integrations_sdk/integration.py @@ -18,8 +18,8 @@ @integration.action("my_action") class MyAction(ActionHandler): async def execute(self, inputs, context): - data = await context.fetch("https://api.example.com/resource") - return ActionResult(data=data) + response = await context.fetch("https://api.example.com/resource") + return ActionResult(data=response.data) """ # Standard Library Imports @@ -260,7 +260,7 @@ class ActionHandler(ABC): @integration.action("get_user") class GetUser(ActionHandler): async def execute(self, inputs, context): - user = await context.fetch(f"https://api.example.com/users/{inputs['id']}") + user = (await context.fetch(f"https://api.example.com/users/{inputs['id']}")).data return ActionResult(data=user) """ @abstractmethod @@ -296,7 +296,7 @@ class ConnectedAccountHandler(ABC): @integration.connected_account() class MyAccountHandler(ConnectedAccountHandler): async def get_account_info(self, context): - me = await context.fetch("https://api.example.com/me") + me = (await context.fetch("https://api.example.com/me")).data return ConnectedAccountInfo( email=me["email"], first_name=me["first_name"], From c6a54ad518c985954bbf9b2dfc40a464303ce1c5 Mon Sep 17 00:00:00 2001 From: Kai Koenig Date: Wed, 18 Mar 2026 12:09:56 +1300 Subject: [PATCH 3/4] Add migration plan for FetchResponse rollout (#12) --- docs/plans/12.md | 142 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 docs/plans/12.md diff --git a/docs/plans/12.md b/docs/plans/12.md new file mode 100644 index 0000000..f90863a --- /dev/null +++ b/docs/plans/12.md @@ -0,0 +1,142 @@ +# 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 code hits + +Repo: `Autohive-AI/autohive-integrations` + +### Code (Python) — all need `response` → `response.data` +- [ ] `api-call/api_call.py` +- [ ] `asana/asana.py` +- [ ] `bitly/bitly.py` +- [ ] `box/box.py` +- [ ] `calendly/calendly.py` +- [ ] `clickup/clickup.py` +- [ ] `facebook/actions/comments.py` +- [ ] `facebook/actions/messages.py` +- [ ] `facebook/actions/pages.py` +- [ ] `facebook/actions/posts.py` +- [ ] `facebook/helpers.py` +- [ ] `fathom/fathom.py` +- [ ] `freshdesk/freshdesk.py` +- [ ] `github/github.py` +- [ ] `gitlab/gitlab.py` +- [ ] `google-calendar/google_calendar.py` +- [ ] `google-docs/google_docs.py` +- [ ] `google-looker/google_looker.py` +- [ ] `google-sheets/google_sheets.py` +- [ ] `humanitix/actions/events.py` +- [ ] `humanitix/actions/orders.py` +- [ ] `humanitix/helpers.py` +- [ ] `instagram/helpers.py` +- [ ] `instagram/actions/*.py` +- [ ] `jira/jira.py` +- [ ] `linkedin/linkedin.py` +- [ ] `mailchimp/mailchimp.py` +- [ ] `notion/notion.py` +- [ ] `pipedrive/pipedrive.py` +- [ ] `reddit/reddit.py` +- [ ] `tiktok/tiktok.py` +- [ ] `toggl/toggl.py` +- [ ] `trello/trello.py` +- [ ] `typeform/typeform.py` +- [ ] `X/x.py` +- [ ] `youtube/youtube.py` +- [ ] `zoho/zoho.py` +- [ ] `zoom/zoom.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) +``` From 4e707ba07589135fe7973453bd6743bdfbd8e120 Mon Sep 17 00:00:00 2001 From: Kai Koenig Date: Wed, 18 Mar 2026 12:14:05 +1300 Subject: [PATCH 4/4] Complete migration plan with full file inventory (#12) Add 43 missing production files and 13 test files to the autohive-integrations audit list. --- docs/plans/12.md | 70 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 6 deletions(-) diff --git a/docs/plans/12.md b/docs/plans/12.md index f90863a..80ebd00 100644 --- a/docs/plans/12.md +++ b/docs/plans/12.md @@ -25,50 +25,108 @@ All changes committed on branch `issue-12/fetch-response`. --- -## autohive-integrations (public) — 94 code hits +## autohive-integrations (public) — 94 file hits Repo: `Autohive-AI/autohive-integrations` -### Code (Python) — all need `response` → `response.data` +### 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/messages.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-sheets/google_sheets.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/actions/*.py` -- [ ] `jira/jira.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`