diff --git a/src/late/resources/accounts.py b/src/late/resources/accounts.py index 38094b2..77747f3 100644 --- a/src/late/resources/accounts.py +++ b/src/late/resources/accounts.py @@ -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) # ------------------------------------------------------------------------- @@ -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) diff --git a/tests/test_integration.py b/tests/test_integration.py index a832d56..d75e0a2 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -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"]) @@ -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