Skip to content

Commit ff1d164

Browse files
feat(instagram): upgrade SDK to 2.0.0, fix helpers import, add pytest unit + integration tests (#327)
* feat(instagram): upgrade SDK to 2.0.0, fix helpers import, add pytest unit + integration tests - Fix ModuleNotFoundError: add sys.path.insert so actions/ subpackage can resolve helpers - Use explicit config path in Integration.load() to avoid CWD-relative resolution - Bump autohive-integrations-sdk to ~=2.0.0; update all response.data accesses - Replace old context.py/test_instagram.py with conftest.py, 57 unit tests, and live integration tests * fix(instagram): resolve ruff lint and formatting errors * fix(instagram): fix ruff formatting, use inputs[] for required media_type fields, revert __init__.py to simple imports * fix(instagram): apply ruff formatting to integration test file * fix(instagram): guard against duplicate Integration instance when loaded by file path * fix(instagram): add total_count to get_comments output_schema --------- Co-authored-by: Shubhank <72601061+Sagsgit@users.noreply.github.com>
1 parent 438d0e1 commit ff1d164

14 files changed

Lines changed: 1359 additions & 841 deletions

instagram/actions/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
"""
2-
Instagram integration actions.
3-
4-
Importing this module registers all actions with the instagram integration instance.
5-
"""
6-
71
from . import account
82
from . import media
93
from . import comments

instagram/actions/account.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
"""
2-
Instagram Account actions - Account information retrieval.
3-
"""
4-
51
from autohive_integrations_sdk import ActionHandler, ActionResult, ExecutionContext
62
from typing import Dict, Any
73

@@ -11,14 +7,7 @@
117

128
@instagram.action("get_account")
139
class GetAccountAction(ActionHandler):
14-
"""
15-
Retrieve Instagram Business/Creator account details.
16-
17-
Returns profile information including username, bio, follower counts,
18-
and other account metadata.
19-
"""
20-
21-
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
10+
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> ActionResult:
2211
fields = ",".join(
2312
[
2413
"id",
@@ -32,19 +21,18 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
3221
"website",
3322
]
3423
)
35-
3624
response = await context.fetch(f"{INSTAGRAM_GRAPH_API_BASE}/me", method="GET", params={"fields": fields})
37-
25+
data = response.data
3826
return ActionResult(
3927
data={
40-
"id": response.get("id", ""),
41-
"username": response.get("username", ""),
42-
"name": response.get("name", ""),
43-
"biography": response.get("biography", ""),
44-
"followers_count": response.get("followers_count", 0),
45-
"following_count": response.get("follows_count", 0),
46-
"media_count": response.get("media_count", 0),
47-
"profile_picture_url": response.get("profile_picture_url", ""),
48-
"website": response.get("website", ""),
28+
"id": data.get("id", ""),
29+
"username": data.get("username", ""),
30+
"name": data.get("name", ""),
31+
"biography": data.get("biography", ""),
32+
"followers_count": data.get("followers_count", 0),
33+
"following_count": data.get("follows_count", 0),
34+
"media_count": data.get("media_count", 0),
35+
"profile_picture_url": data.get("profile_picture_url", ""),
36+
"website": data.get("website", ""),
4937
}
5038
)

instagram/actions/comments.py

Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
"""
2-
Instagram Comments actions - Read, reply, hide, and delete comments.
3-
"""
4-
51
from autohive_integrations_sdk import ActionHandler, ActionResult, ExecutionContext
62
from typing import Dict, Any
73

@@ -10,12 +6,9 @@
106

117

128
def _build_comment_response(comment: Dict[str, Any]) -> Dict[str, Any]:
13-
"""Normalize a comment object from the Graph API into a consistent response format."""
149
replies_data = comment.get("replies", {}).get("data", [])
1510
from_data = comment.get("from", {})
16-
1711
username = comment.get("username") or from_data.get("username", "")
18-
1912
return {
2013
"id": comment.get("id", ""),
2114
"text": comment.get("text", ""),
@@ -38,14 +31,7 @@ def _build_comment_response(comment: Dict[str, Any]) -> Dict[str, Any]:
3831

3932
@instagram.action("get_comments")
4033
class GetCommentsAction(ActionHandler):
41-
"""
42-
Retrieve comments on an Instagram media object.
43-
44-
Returns comment text, author username, timestamps, and like counts.
45-
Also includes nested replies.
46-
"""
47-
48-
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
34+
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> ActionResult:
4935
media_id = inputs["media_id"]
5036
limit = min(inputs.get("limit", 25), 100)
5137
after_cursor = inputs.get("after_cursor")
@@ -54,34 +40,33 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
5440
"id,text,timestamp,username,like_count,from{id,username},"
5541
"replies{id,text,timestamp,username,from{id,username}}"
5642
)
57-
5843
params = {"fields": fields, "limit": limit}
5944
if after_cursor:
6045
params["after"] = after_cursor
6146

62-
response = await context.fetch(f"{INSTAGRAM_GRAPH_API_BASE}/{media_id}/comments", method="GET", params=params)
63-
64-
comments = [_build_comment_response(c) for c in response.get("data", [])]
65-
66-
paging = response.get("paging", {})
47+
response = await context.fetch(
48+
f"{INSTAGRAM_GRAPH_API_BASE}/{media_id}/comments",
49+
method="GET",
50+
params=params,
51+
)
52+
data = response.data
53+
comments = [_build_comment_response(c) for c in data.get("data", [])]
54+
paging = data.get("paging", {})
6755
cursors = paging.get("cursors", {})
6856
next_cursor = cursors.get("after") if paging.get("next") else None
6957

70-
return ActionResult(data={"comments": comments, "total_count": len(comments), "next_cursor": next_cursor})
58+
return ActionResult(
59+
data={
60+
"comments": comments,
61+
"total_count": len(comments),
62+
"next_cursor": next_cursor,
63+
}
64+
)
7165

7266

7367
@instagram.action("manage_comment")
7468
class ManageCommentAction(ActionHandler):
75-
"""
76-
Interact with comments on Instagram media.
77-
78-
Supported actions:
79-
- reply: Post a reply to the comment
80-
- hide: Hide the comment from public view
81-
- unhide: Make a hidden comment visible again
82-
"""
83-
84-
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
69+
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> ActionResult:
8570
comment_id = inputs["comment_id"]
8671
action = inputs["action"]
8772
message = inputs.get("message")
@@ -91,14 +76,19 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
9176

9277
if action == "reply":
9378
response = await context.fetch(
94-
f"{INSTAGRAM_GRAPH_API_BASE}/{comment_id}/replies", method="POST", data={"message": message}
79+
f"{INSTAGRAM_GRAPH_API_BASE}/{comment_id}/replies",
80+
method="POST",
81+
data={"message": message},
9582
)
96-
return ActionResult(data={"success": True, "action_taken": "reply", "reply_id": response.get("id", "")})
83+
reply_id = response.data.get("id", "")
84+
return ActionResult(data={"success": True, "action_taken": "reply", "reply_id": reply_id})
9785

9886
elif action in ("hide", "unhide"):
9987
hide_value = action == "hide"
10088
await context.fetch(
101-
f"{INSTAGRAM_GRAPH_API_BASE}/{comment_id}", method="POST", data={"hide": str(hide_value).lower()}
89+
f"{INSTAGRAM_GRAPH_API_BASE}/{comment_id}",
90+
method="POST",
91+
data={"hide": str(hide_value).lower()},
10292
)
10393
return ActionResult(data={"success": True, "action_taken": action, "is_hidden": hide_value})
10494

@@ -107,16 +97,7 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
10797

10898
@instagram.action("delete_comment")
10999
class DeleteCommentAction(ActionHandler):
110-
"""
111-
Permanently delete a comment from Instagram media.
112-
113-
This action cannot be undone. You can only delete comments on
114-
media you own or comments that others have left on your media.
115-
"""
116-
117-
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
100+
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> ActionResult:
118101
comment_id = inputs["comment_id"]
119-
120102
await context.fetch(f"{INSTAGRAM_GRAPH_API_BASE}/{comment_id}", method="DELETE")
121-
122103
return ActionResult(data={"success": True, "deleted_comment_id": comment_id})

instagram/actions/insights.py

Lines changed: 29 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
"""
2-
Instagram Insights actions - Analytics for accounts and media.
3-
"""
4-
51
from autohive_integrations_sdk import ActionHandler, ActionResult, ExecutionContext
62
from typing import Dict, Any
73

@@ -11,27 +7,6 @@
117

128
@instagram.action("get_insights")
139
class GetInsightsAction(ActionHandler):
14-
"""
15-
Retrieve advanced analytics for an Instagram account or specific media.
16-
17-
Use this for detailed performance metrics beyond basic like/comment
18-
counts. For simple engagement counts, use Get Posts instead.
19-
20-
Account insights include:
21-
- Reach and impressions
22-
- Profile views
23-
- Follower demographics
24-
- Content interactions (likes, comments, shares, saves)
25-
26-
Media insights include:
27-
- Reach and impressions
28-
- Saves and shares
29-
- Video views and watch time (for Reels)
30-
- Story navigation and profile activity
31-
32-
Note: Story insights are only available for 24 hours after posting.
33-
"""
34-
3510
DEFAULT_ACCOUNT_METRICS = [
3611
"reach",
3712
"profile_views",
@@ -43,9 +18,15 @@ class GetInsightsAction(ActionHandler):
4318
"saves",
4419
"follows_and_unfollows",
4520
]
46-
47-
DEFAULT_MEDIA_METRICS_FEED = ["reach", "likes", "comments", "shares", "saved", "total_interactions", "views"]
48-
21+
DEFAULT_MEDIA_METRICS_FEED = [
22+
"reach",
23+
"likes",
24+
"comments",
25+
"shares",
26+
"saved",
27+
"total_interactions",
28+
"views",
29+
]
4930
DEFAULT_MEDIA_METRICS_REELS = [
5031
"reach",
5132
"likes",
@@ -57,10 +38,16 @@ class GetInsightsAction(ActionHandler):
5738
"ig_reels_avg_watch_time",
5839
"ig_reels_video_view_total_time",
5940
]
41+
DEFAULT_MEDIA_METRICS_STORY = [
42+
"reach",
43+
"views",
44+
"navigation",
45+
"profile_activity",
46+
"shares",
47+
"follows",
48+
]
6049

61-
DEFAULT_MEDIA_METRICS_STORY = ["reach", "views", "navigation", "profile_activity", "shares", "follows"]
62-
63-
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
50+
async def execute(self, inputs: Dict[str, Any], context: ExecutionContext) -> ActionResult:
6451
target_type = inputs["target_type"]
6552
target_id = inputs.get("target_id")
6653
custom_metrics = inputs.get("metrics")
@@ -69,36 +56,34 @@ async def execute(self, inputs: Dict[str, Any], context: ExecutionContext):
6956
if target_type == "account":
7057
account_id = await get_instagram_account_id(context)
7158
metrics = custom_metrics or self.DEFAULT_ACCOUNT_METRICS
72-
73-
params = {"metric": ",".join(metrics), "period": period, "metric_type": "total_value"}
74-
59+
params = {
60+
"metric": ",".join(metrics),
61+
"period": period,
62+
"metric_type": "total_value",
63+
}
7564
endpoint = f"{INSTAGRAM_GRAPH_API_BASE}/{account_id}/insights"
76-
7765
else:
7866
if not target_id:
7967
raise Exception("target_id is required for media insights")
80-
81-
media_response = await context.fetch(
82-
f"{INSTAGRAM_GRAPH_API_BASE}/{target_id}", method="GET", params={"fields": "media_product_type"}
68+
type_r = await context.fetch(
69+
f"{INSTAGRAM_GRAPH_API_BASE}/{target_id}",
70+
method="GET",
71+
params={"fields": "media_product_type"},
8372
)
84-
media_product_type = media_response.get("media_product_type", "FEED")
85-
73+
media_product_type = type_r.data.get("media_product_type", "FEED")
8674
if media_product_type == "REELS":
8775
metrics = custom_metrics or self.DEFAULT_MEDIA_METRICS_REELS
8876
elif media_product_type == "STORY":
8977
metrics = custom_metrics or self.DEFAULT_MEDIA_METRICS_STORY
9078
else:
9179
metrics = custom_metrics or self.DEFAULT_MEDIA_METRICS_FEED
92-
9380
params = {"metric": ",".join(metrics)}
9481
endpoint = f"{INSTAGRAM_GRAPH_API_BASE}/{target_id}/insights"
9582

9683
response = await context.fetch(endpoint, method="GET", params=params)
97-
9884
metrics_data = {}
99-
for item in response.get("data", []):
85+
for item in response.data.get("data", []):
10086
metric_name = item.get("name", "")
101-
10287
total_value = item.get("total_value", {})
10388
if total_value:
10489
metrics_data[metric_name] = total_value.get("value", total_value)

0 commit comments

Comments
 (0)