Skip to content
Merged
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
77 changes: 61 additions & 16 deletions src/late/resources/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,45 @@ def get(self, account_id: str) -> AccountGetResponse:
data = self._client._get(self._path(account_id))
return AccountGetResponse.model_validate(data)

# Signature must mirror _GeneratedAccountsResource.get_follower_stats; this
# override exists ONLY to return a typed FollowerStatsResponse instead of a
# raw dict. Keep the params in sync on every OpenAPI regen, dropping them is
# what caused the TypeError in GET-820. account_ids is a comma-separated
# string (the MCP wrapper and the REST endpoint both use that shape); a list
# is also accepted for backwards compatibility and joined into that shape.
def get_follower_stats(
self,
*,
account_ids: list[str] | None = None,
account_ids: str | list[str] | None = None,
profile_id: str | None = None,
from_date: str | None = None,
to_date: str | None = None,
granularity: str | None = None,
) -> FollowerStatsResponse:
"""
Get follower statistics for accounts.

Requires analytics add-on.
Get follower statistics for accounts. Requires analytics add-on.

Args:
account_ids: Optional list of account IDs to filter
account_ids: Account IDs to filter, as a comma-separated string or a
list of IDs (optional, defaults to all)
profile_id: Filter by profile ID
from_date: Start date YYYY-MM-DD (server defaults to 30 days ago)
to_date: End date YYYY-MM-DD (server defaults to today)
granularity: Aggregation level ('daily' | 'weekly' | 'monthly')

Returns:
FollowerStatsResponse with 'stats' attribute
FollowerStatsResponse with 'accounts', 'dateRange', 'aggregation'
"""
params = None
if account_ids:
params = {"accountIds": ",".join(account_ids)}
data = self._client._get(self._path("follower-stats"), params=params)
if isinstance(account_ids, list):
account_ids = ",".join(account_ids)
params = self._build_params(
account_ids=account_ids,
profile_id=profile_id,
from_date=from_date,
to_date=to_date,
granularity=granularity,
)
data = self._client._get(self._path("follower-stats"), params=params or None)
return FollowerStatsResponse.model_validate(data)

# -------------------------------------------------------------------------
Expand All @@ -109,14 +128,40 @@ async def aget(self, account_id: str) -> AccountGetResponse:
data = await self._client._aget(self._path(account_id))
return AccountGetResponse.model_validate(data)

# Same signature/params as the sync version; only the return wrapping differs.
# See GET-820 note on get_follower_stats above.
async def aget_follower_stats(
self,
*,
account_ids: list[str] | None = None,
account_ids: str | list[str] | None = None,
profile_id: str | None = None,
from_date: str | None = None,
to_date: str | None = None,
granularity: str | None = None,
) -> FollowerStatsResponse:
"""Get follower statistics asynchronously."""
params = None
if account_ids:
params = {"accountIds": ",".join(account_ids)}
data = await self._client._aget(self._path("follower-stats"), params=params)
"""Get follower statistics asynchronously.

Args:
account_ids: Account IDs to filter, as a comma-separated string or a
list of IDs (optional, defaults to all)
profile_id: Filter by profile ID
from_date: Start date YYYY-MM-DD
to_date: End date YYYY-MM-DD
granularity: Aggregation level ('daily' | 'weekly' | 'monthly')

Returns:
FollowerStatsResponse with 'accounts', 'dateRange', 'aggregation'
"""
if isinstance(account_ids, list):
account_ids = ",".join(account_ids)
params = self._build_params(
account_ids=account_ids,
profile_id=profile_id,
from_date=from_date,
to_date=to_date,
granularity=granularity,
)
data = await self._client._aget(
self._path("follower-stats"), params=params or None
)
return FollowerStatsResponse.model_validate(data)
37 changes: 28 additions & 9 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,16 +460,10 @@ def test_get_account(self, client: Late, mock_account: dict) -> None:

@respx.mock
def test_get_follower_stats(self, client: Late) -> None:
"""Test getting follower statistics."""
"""Test getting follower statistics. account_ids as a list is still
accepted for backwards compatibility and joined into a comma string."""
route = respx.get("https://api.test.com/v1/accounts/follower-stats").mock(
return_value=httpx.Response(
200,
json={
"stats": [
{"accountId": "acc_123", "followersCount": 1000, "change": 50}
]
},
)
return_value=httpx.Response(200, json={"accounts": []})
)

client.accounts.get_follower_stats(account_ids=["acc_123", "acc_456"])
Expand All @@ -482,6 +476,31 @@ def test_get_follower_stats(self, client: Late) -> None:
assert "acc_123" in url_str
assert "acc_456" in url_str

@respx.mock
def test_get_follower_stats_forwards_all_params(self, client: Late) -> None:
"""Regression for GET-820: the manual override dropped profile_id/
from_date/to_date/granularity, raising TypeError. All five params must
reach the endpoint as camelCase query params."""
route = respx.get("https://api.test.com/v1/accounts/follower-stats").mock(
return_value=httpx.Response(200, json={"accounts": []})
)

client.accounts.get_follower_stats(
account_ids="acc_123,acc_456",
profile_id="prof_1",
from_date="2026-01-01",
to_date="2026-01-31",
granularity="weekly",
)

assert route.called
url_str = str(route.calls[0].request.url)
assert "accountIds=acc_123" in url_str
assert "profileId=prof_1" in url_str
assert "fromDate=2026-01-01" in url_str
assert "toDate=2026-01-31" in url_str
assert "granularity=weekly" in url_str


# =============================================================================
# Media Resource Tests
Expand Down
Loading