diff --git a/CHANGELOG.md b/CHANGELOG.md index a0ac993..70c6e48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.15.4] - 2026-06-30 +### Added + +- **`client.twitter.tweets.advanced_search()` / `advanced_search_all()`** — aliases for `search()` / `search_all()` that match the `/advanced_search` REST endpoint name, so the endpoint-named call no longer raises `AttributeError`. (SCR-52) + ### Fixed - **Auto-pagination no longer repeats the first page** (`*_all` iterators, e.g. `client.twitter.tweets.search_all`). The shared `paginate()` helper now stops when the backend returns the same cursor it was given, instead of re-fetching the page it just yielded — previously a non-advancing cursor from the API could cause a repeat-page loop. (SCR-52) diff --git a/src/scrapebadger/twitter/tweets.py b/src/scrapebadger/twitter/tweets.py index be683b9..a10283d 100644 --- a/src/scrapebadger/twitter/tweets.py +++ b/src/scrapebadger/twitter/tweets.py @@ -404,6 +404,36 @@ async def search_all( ): yield tweet + async def advanced_search( + self, + query: str, + *, + query_type: QueryType = QueryType.TOP, + count: int | None = None, + cursor: str | None = None, + ) -> PaginatedResponse[Tweet]: + """Alias for :meth:`search`, matching the ``/advanced_search`` endpoint name.""" + return await self.search(query, query_type=query_type, count=count, cursor=cursor) + + async def advanced_search_all( + self, + query: str, + *, + query_type: QueryType = QueryType.TOP, + count: int | None = None, + max_pages: int | None = None, + max_items: int | None = None, + ) -> AsyncIterator[Tweet]: + """Alias for :meth:`search_all`, matching the ``/advanced_search`` endpoint name.""" + async for tweet in self.search_all( + query, + query_type=query_type, + count=count, + max_pages=max_pages, + max_items=max_items, + ): + yield tweet + async def get_user_tweets( self, username: str, diff --git a/tests/test_twitter_tweets_aliases.py b/tests/test_twitter_tweets_aliases.py new file mode 100644 index 0000000..a5cc7ae --- /dev/null +++ b/tests/test_twitter_tweets_aliases.py @@ -0,0 +1,47 @@ +"""SCR-52: advanced_search/_all aliases on TweetsClient. + +Customers reached for ``client.twitter.tweets.advanced_search(...)`` (the REST +endpoint name) and hit ``AttributeError``. These thin aliases delegate to +``search`` / ``search_all`` so the endpoint-named call works. +""" + +from __future__ import annotations + +from typing import Any +from unittest.mock import AsyncMock + +import pytest + +from scrapebadger._internal.client import BaseClient +from scrapebadger._internal.config import ClientConfig +from scrapebadger.twitter.tweets import TweetsClient + + +@pytest.fixture +def tweets_client() -> TweetsClient: + return TweetsClient(BaseClient(ClientConfig(api_key="test_key", max_retries=1))) + + +async def test_advanced_search_delegates_to_search(tweets_client: TweetsClient) -> None: + page: dict[str, Any] = {"data": [{"id": "1"}], "next_cursor": None} + tweets_client._client.get = AsyncMock(return_value=page) # type: ignore[method-assign] + + result = await tweets_client.advanced_search("python") + + assert [t.id for t in result.data] == ["1"] + assert tweets_client._client.get.call_args[0][0] == "/v1/twitter/tweets/advanced_search" + + +async def test_advanced_search_all_delegates_to_search_all( + tweets_client: TweetsClient, +) -> None: + page: dict[str, Any] = {"data": [{"id": "1"}, {"id": "2"}]} + headers: dict[str, str] = {} + tweets_client._client.get_with_headers = AsyncMock(return_value=(page, headers)) # type: ignore[method-assign] + + ids = [t.id async for t in tweets_client.advanced_search_all("python")] + + assert ids == ["1", "2"] + assert tweets_client._client.get_with_headers.call_args[0][0] == ( + "/v1/twitter/tweets/advanced_search" + ) diff --git a/uv.lock b/uv.lock index 27e8f0f..a3ad213 100644 --- a/uv.lock +++ b/uv.lock @@ -752,7 +752,7 @@ wheels = [ [[package]] name = "scrapebadger" -version = "0.15.3" +version = "0.15.4" source = { editable = "." } dependencies = [ { name = "httpx" },