From ef1ae2ba72930b9243fd239dbc7e25675c9c6c97 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:30:19 +0000 Subject: [PATCH 1/2] feat(api): align reply extraction contracts Regenerate every SDK from the exact production OpenAPI contract. --- .stats.yml | 4 +- api.md | 6 +- src/x_twitter_scraper/resources/x/accounts.py | 73 ++++---- .../resources/x/communities/communities.py | 12 +- .../resources/x/communities/tweets.py | 24 +-- src/x_twitter_scraper/resources/x/lists.py | 12 +- .../resources/x/tweets/tweets.py | 89 +++++----- .../resources/x/users/users.py | 60 +++---- src/x_twitter_scraper/types/__init__.py | 19 ++ .../types/shared/content_disclosure.py | 3 - .../types/shared/embedded_tweet.py | 163 +++++++++++++++++- .../types/shared/paginated_tweets.py | 12 +- .../types/shared/search_tweet.py | 155 ++++++++++++++++- .../types/shared/tweet_media.py | 80 ++++++++- .../types/shared/user_profile.py | 79 ++++++++- .../types/support/ticket_list_response.py | 17 +- .../types/support/ticket_retrieve_response.py | 22 +-- .../types/support/ticket_update_response.py | 6 +- .../types/trend_list_response.py | 11 ++ src/x_twitter_scraper/types/x/__init__.py | 2 + ...nt_connection_attempt_retrieve_response.py | 16 +- ...nt_connection_challenge_submit_response.py | 2 +- .../types/x/account_create_response.py | 68 ++++++++ .../types/x/account_reauth_response.py | 2 +- .../tweet_list_by_community_params.py | 8 +- .../types/x/communities/tweet_list_params.py | 8 +- .../x/community_retrieve_info_response.py | 6 - .../x/community_retrieve_search_params.py | 8 +- .../types/x/list_retrieve_tweets_params.py | 8 +- src/x_twitter_scraper/types/x/tweet_author.py | 4 +- src/x_twitter_scraper/types/x/tweet_detail.py | 155 ++++++++++++++++- .../types/x/tweet_get_quotes_params.py | 8 +- .../types/x/tweet_get_replies_params.py | 22 ++- .../types/x/tweet_get_replies_response.py | 151 ++++++++++++++++ .../types/x/tweet_get_thread_params.py | 8 +- .../types/x/tweet_retrieve_response.py | 8 +- .../types/x/user_retrieve_likes_params.py | 8 +- .../types/x/user_retrieve_media_params.py | 8 +- .../types/x/user_retrieve_mentions_params.py | 8 +- .../types/x/user_retrieve_replies_params.py | 8 +- .../types/x/user_retrieve_tweets_params.py | 8 +- .../types/x_get_article_response.py | 2 - .../types/x_get_trends_response.py | 11 ++ tests/api_resources/x/test_accounts.py | 13 +- tests/api_resources/x/test_tweets.py | 21 ++- 45 files changed, 1131 insertions(+), 287 deletions(-) create mode 100644 src/x_twitter_scraper/types/x/account_create_response.py create mode 100644 src/x_twitter_scraper/types/x/tweet_get_replies_response.py diff --git a/.stats.yml b/.stats.yml index 722a8b3..ec9dedf 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 123 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik/x-twitter-scraper-323137ebb1ee9824cd9cfb63a325c0839deb81f17244e42e8154d6567e7af954.yml -openapi_spec_hash: 48da9c2747c1e7c474dd514d39c28c49 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik/x-twitter-scraper-5f26f3694399b4ac5a4431c66f7d27c4024f9d0dcd899702de5fde1150e28330.yml +openapi_spec_hash: 362e24e622f9a74ded8b38018e50460c config_hash: dab26921eb273c16e5570c0485c9ac2c diff --git a/api.md b/api.md index 7f630d5..b652b9b 100644 --- a/api.md +++ b/api.md @@ -276,6 +276,7 @@ from x_twitter_scraper.types.x import ( TweetCreateResponse, TweetRetrieveResponse, TweetDeleteResponse, + TweetGetRepliesResponse, ) ``` @@ -287,7 +288,7 @@ Methods: - client.x.tweets.delete(id, \*\*params) -> TweetDeleteResponse - client.x.tweets.get_favoriters(id, \*\*params) -> PaginatedUsers - client.x.tweets.get_quotes(id, \*\*params) -> PaginatedTweets -- client.x.tweets.get_replies(id, \*\*params) -> PaginatedTweets +- client.x.tweets.get_replies(id, \*\*params) -> TweetGetRepliesResponse - client.x.tweets.get_retweeters(id, \*\*params) -> PaginatedUsers - client.x.tweets.get_thread(id, \*\*params) -> PaginatedTweets - client.x.tweets.search(\*\*params) -> PaginatedTweets @@ -460,6 +461,7 @@ Types: from x_twitter_scraper.types.x import ( XAccount, XAccountDetail, + AccountCreateResponse, AccountListResponse, AccountDeleteResponse, AccountBulkRetryResponse, @@ -469,7 +471,7 @@ from x_twitter_scraper.types.x import ( Methods: -- client.x.accounts.create(\*\*params) -> object +- client.x.accounts.create(\*\*params) -> AccountCreateResponse - client.x.accounts.retrieve(id) -> XAccountDetail - client.x.accounts.list() -> AccountListResponse - client.x.accounts.delete(id) -> AccountDeleteResponse diff --git a/src/x_twitter_scraper/resources/x/accounts.py b/src/x_twitter_scraper/resources/x/accounts.py index e2c7a34..0776c04 100644 --- a/src/x_twitter_scraper/resources/x/accounts.py +++ b/src/x_twitter_scraper/resources/x/accounts.py @@ -6,6 +6,8 @@ from __future__ import annotations +from typing import Any, cast + import httpx from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given @@ -22,6 +24,7 @@ from ..._base_client import make_request_options from ...types.x.x_account_detail import XAccountDetail from ...types.x.account_list_response import AccountListResponse +from ...types.x.account_create_response import AccountCreateResponse from ...types.x.account_delete_response import AccountDeleteResponse from ...types.x.account_reauth_response import AccountReauthResponse from ...types.x.account_bulk_retry_response import AccountBulkRetryResponse @@ -64,7 +67,7 @@ def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> AccountCreateResponse: """ Connect X account @@ -85,21 +88,26 @@ def create( timeout: Override the client-level default timeout for this request, in seconds """ - return self._post( - "/x/accounts", - body=maybe_transform( - { - "email": email, - "password": password, - "totp_secret": totp_secret, - "username": username, - }, - account_create_params.AccountCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + return cast( + AccountCreateResponse, + self._post( + "/x/accounts", + body=maybe_transform( + { + "email": email, + "password": password, + "totp_secret": totp_secret, + "username": username, + }, + account_create_params.AccountCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AccountCreateResponse + ), # Union types cannot be passed in as arguments in the type system ), - cast_to=object, ) def retrieve( @@ -295,7 +303,7 @@ async def create( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> object: + ) -> AccountCreateResponse: """ Connect X account @@ -316,21 +324,26 @@ async def create( timeout: Override the client-level default timeout for this request, in seconds """ - return await self._post( - "/x/accounts", - body=await async_maybe_transform( - { - "email": email, - "password": password, - "totp_secret": totp_secret, - "username": username, - }, - account_create_params.AccountCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + return cast( + AccountCreateResponse, + await self._post( + "/x/accounts", + body=await async_maybe_transform( + { + "email": email, + "password": password, + "totp_secret": totp_secret, + "username": username, + }, + account_create_params.AccountCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AccountCreateResponse + ), # Union types cannot be passed in as arguments in the type system ), - cast_to=object, ) async def retrieve( diff --git a/src/x_twitter_scraper/resources/x/communities/communities.py b/src/x_twitter_scraper/resources/x/communities/communities.py index 390741c..60ee7f8 100644 --- a/src/x_twitter_scraper/resources/x/communities/communities.py +++ b/src/x_twitter_scraper/resources/x/communities/communities.py @@ -332,10 +332,8 @@ def retrieve_search( cursor: Pagination cursor for community search - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. query_type: Sort order (Latest or Top) @@ -649,10 +647,8 @@ async def retrieve_search( cursor: Pagination cursor for community search - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. query_type: Sort order (Latest or Top) diff --git a/src/x_twitter_scraper/resources/x/communities/tweets.py b/src/x_twitter_scraper/resources/x/communities/tweets.py index 751b6e9..8922783 100644 --- a/src/x_twitter_scraper/resources/x/communities/tweets.py +++ b/src/x_twitter_scraper/resources/x/communities/tweets.py @@ -74,10 +74,8 @@ def list( cursor: Pagination cursor for community results - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. query_type: Sort order for community results (Latest or Top) @@ -130,10 +128,8 @@ def list_by_community( Args: cursor: Pagination cursor for community tweets - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. extra_headers: Send extra headers @@ -212,10 +208,8 @@ async def list( cursor: Pagination cursor for community results - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. query_type: Sort order for community results (Latest or Top) @@ -268,10 +262,8 @@ async def list_by_community( Args: cursor: Pagination cursor for community tweets - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. extra_headers: Send extra headers diff --git a/src/x_twitter_scraper/resources/x/lists.py b/src/x_twitter_scraper/resources/x/lists.py index f1b7eaf..a3454d3 100644 --- a/src/x_twitter_scraper/resources/x/lists.py +++ b/src/x_twitter_scraper/resources/x/lists.py @@ -173,10 +173,8 @@ def retrieve_tweets( include_replies: Include replies (default false) - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. since_time: Unix timestamp - filter after @@ -362,10 +360,8 @@ async def retrieve_tweets( include_replies: Include replies (default false) - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. since_time: Unix timestamp - filter after diff --git a/src/x_twitter_scraper/resources/x/tweets/tweets.py b/src/x_twitter_scraper/resources/x/tweets/tweets.py index 3037620..3f4cc10 100644 --- a/src/x_twitter_scraper/resources/x/tweets/tweets.py +++ b/src/x_twitter_scraper/resources/x/tweets/tweets.py @@ -55,6 +55,7 @@ from ....types.x.tweet_create_response import TweetCreateResponse from ....types.x.tweet_delete_response import TweetDeleteResponse from ....types.x.tweet_retrieve_response import TweetRetrieveResponse +from ....types.x.tweet_get_replies_response import TweetGetRepliesResponse __all__ = ["TweetsResource", "AsyncTweetsResource"] @@ -390,10 +391,8 @@ def get_quotes( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -490,12 +489,14 @@ def get_replies( hashtags: str | Omit = omit, in_reply_to_tweet_id: str | Omit = omit, language: str | Omit = omit, + limit: int | Omit = omit, media_type: Literal["images", "videos", "gifs", "media", "links", "none"] | Omit = omit, mentioning: str | Omit = omit, min_faves: int | Omit = omit, min_quotes: int | Omit = omit, min_replies: int | Omit = omit, min_retweets: int | Omit = omit, + mode: Literal["complete"] | Omit = omit, page_size: int | Omit = omit, quotes: Literal["include", "exclude", "only"] | Omit = omit, quotes_of_tweet_id: str | Omit = omit, @@ -515,14 +516,13 @@ def get_replies( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> PaginatedTweets: - """Returns visible replies. + ) -> TweetGetRepliesResponse: + """Returns direct replies. - For an unfiltered first page, Xquik compares a terminal - page with the post's reported reply count. If the page is visibly incomplete, - the endpoint returns 424 `replies_incomplete` instead of presenting partial - coverage as complete. Use tweet search with a `conversation_id:{id}` query as - the broader fallback. + Complete mode merges available timeline views, supported + rankings, every forward cursor module, labeled hidden-content branches, + exact-parent time partitions scaled to the reported reply count, and search. It + separates nested replies and returns 424 below 80% coverage. Args: any_words: Words or quoted phrases where any one can match. Separate with spaces, commas, @@ -546,6 +546,10 @@ def get_replies( language: Language code filter, e.g. en or tr. + limit: With mode=complete, maximum combined direct and nested reply rows (1-25000). + Without complete mode, this is the deprecated pageSize alias and uses the normal + 1-100 page range. + media_type: Filter by media type. mentioning: Filter tweets mentioning a username. @@ -558,10 +562,11 @@ def get_replies( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + mode: Set complete for maximum-coverage collection. Complete mode accepts only limit. + Remove cursor, pageSize, count, time ranges, and tweet filters. + + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -617,12 +622,14 @@ def get_replies( "hashtags": hashtags, "in_reply_to_tweet_id": in_reply_to_tweet_id, "language": language, + "limit": limit, "media_type": media_type, "mentioning": mentioning, "min_faves": min_faves, "min_quotes": min_quotes, "min_replies": min_replies, "min_retweets": min_retweets, + "mode": mode, "page_size": page_size, "quotes": quotes, "quotes_of_tweet_id": quotes_of_tweet_id, @@ -640,7 +647,7 @@ def get_replies( tweet_get_replies_params.TweetGetRepliesParams, ), ), - cast_to=PaginatedTweets, + cast_to=TweetGetRepliesResponse, ) def get_retweeters( @@ -714,10 +721,8 @@ def get_thread( Args: cursor: Pagination cursor for thread tweets - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. extra_headers: Send extra headers @@ -1270,10 +1275,8 @@ async def get_quotes( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -1370,12 +1373,14 @@ async def get_replies( hashtags: str | Omit = omit, in_reply_to_tweet_id: str | Omit = omit, language: str | Omit = omit, + limit: int | Omit = omit, media_type: Literal["images", "videos", "gifs", "media", "links", "none"] | Omit = omit, mentioning: str | Omit = omit, min_faves: int | Omit = omit, min_quotes: int | Omit = omit, min_replies: int | Omit = omit, min_retweets: int | Omit = omit, + mode: Literal["complete"] | Omit = omit, page_size: int | Omit = omit, quotes: Literal["include", "exclude", "only"] | Omit = omit, quotes_of_tweet_id: str | Omit = omit, @@ -1395,14 +1400,13 @@ async def get_replies( extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> PaginatedTweets: - """Returns visible replies. + ) -> TweetGetRepliesResponse: + """Returns direct replies. - For an unfiltered first page, Xquik compares a terminal - page with the post's reported reply count. If the page is visibly incomplete, - the endpoint returns 424 `replies_incomplete` instead of presenting partial - coverage as complete. Use tweet search with a `conversation_id:{id}` query as - the broader fallback. + Complete mode merges available timeline views, supported + rankings, every forward cursor module, labeled hidden-content branches, + exact-parent time partitions scaled to the reported reply count, and search. It + separates nested replies and returns 424 below 80% coverage. Args: any_words: Words or quoted phrases where any one can match. Separate with spaces, commas, @@ -1426,6 +1430,10 @@ async def get_replies( language: Language code filter, e.g. en or tr. + limit: With mode=complete, maximum combined direct and nested reply rows (1-25000). + Without complete mode, this is the deprecated pageSize alias and uses the normal + 1-100 page range. + media_type: Filter by media type. mentioning: Filter tweets mentioning a username. @@ -1438,10 +1446,11 @@ async def get_replies( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + mode: Set complete for maximum-coverage collection. Complete mode accepts only limit. + Remove cursor, pageSize, count, time ranges, and tweet filters. + + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -1497,12 +1506,14 @@ async def get_replies( "hashtags": hashtags, "in_reply_to_tweet_id": in_reply_to_tweet_id, "language": language, + "limit": limit, "media_type": media_type, "mentioning": mentioning, "min_faves": min_faves, "min_quotes": min_quotes, "min_replies": min_replies, "min_retweets": min_retweets, + "mode": mode, "page_size": page_size, "quotes": quotes, "quotes_of_tweet_id": quotes_of_tweet_id, @@ -1520,7 +1531,7 @@ async def get_replies( tweet_get_replies_params.TweetGetRepliesParams, ), ), - cast_to=PaginatedTweets, + cast_to=TweetGetRepliesResponse, ) async def get_retweeters( @@ -1594,10 +1605,8 @@ async def get_thread( Args: cursor: Pagination cursor for thread tweets - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. extra_headers: Send extra headers diff --git a/src/x_twitter_scraper/resources/x/users/users.py b/src/x_twitter_scraper/resources/x/users/users.py index b235156..c32eb4c 100644 --- a/src/x_twitter_scraper/resources/x/users/users.py +++ b/src/x_twitter_scraper/resources/x/users/users.py @@ -438,10 +438,8 @@ def retrieve_likes( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -592,10 +590,8 @@ def retrieve_media( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -748,10 +744,8 @@ def retrieve_mentions( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -911,10 +905,8 @@ def retrieve_replies( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -1118,10 +1110,8 @@ def retrieve_tweets( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -1638,10 +1628,8 @@ async def retrieve_likes( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -1792,10 +1780,8 @@ async def retrieve_media( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -1948,10 +1934,8 @@ async def retrieve_mentions( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -2111,10 +2095,8 @@ async def retrieve_replies( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. @@ -2318,10 +2300,8 @@ async def retrieve_tweets( min_retweets: Minimum retweets threshold. - page_size: Maximum items requested from this page (1-100, default 20). The response can - contain fewer items because the source returned fewer, filters removed items, or - remaining credits cover fewer results. Keep requesting next_cursor while - has_next_page is true, even when a page is empty. The deprecated limit and count + page_size: Maximum page items (1-100, default 20). Source, filters, or credits can reduce + results. Continue while has_next_page is true. Deprecated limit and count aliases remain accepted. quotes: Quote mode. diff --git a/src/x_twitter_scraper/types/__init__.py b/src/x_twitter_scraper/types/__init__.py index a9d5574..26495ac 100644 --- a/src/x_twitter_scraper/types/__init__.py +++ b/src/x_twitter_scraper/types/__init__.py @@ -6,6 +6,8 @@ from __future__ import annotations +from . import x, shared +from .. import _compat from .draft import Draft as Draft from .event import Event as Event from .shared import ( @@ -111,3 +113,20 @@ from .radar_retrieve_trending_topics_response import ( RadarRetrieveTrendingTopicsResponse as RadarRetrieveTrendingTopicsResponse, ) + +# Rebuild cyclical models only after all modules are imported. +# This ensures that, when building the deferred (due to cyclical references) model schema, +# Pydantic can resolve the necessary references. +# See: https://github.com/pydantic/pydantic/issues/11250 for more context. +if _compat.PYDANTIC_V1: + x.tweet_detail.TweetDetail.update_forward_refs() # type: ignore + x.tweet_retrieve_response.TweetRetrieveResponse.update_forward_refs() # type: ignore + shared.embedded_tweet.EmbeddedTweet.update_forward_refs() # type: ignore + shared.paginated_tweets.PaginatedTweets.update_forward_refs() # type: ignore + shared.search_tweet.SearchTweet.update_forward_refs() # type: ignore +else: + x.tweet_detail.TweetDetail.model_rebuild(_parent_namespace_depth=0) + x.tweet_retrieve_response.TweetRetrieveResponse.model_rebuild(_parent_namespace_depth=0) + shared.embedded_tweet.EmbeddedTweet.model_rebuild(_parent_namespace_depth=0) + shared.paginated_tweets.PaginatedTweets.model_rebuild(_parent_namespace_depth=0) + shared.search_tweet.SearchTweet.model_rebuild(_parent_namespace_depth=0) diff --git a/src/x_twitter_scraper/types/shared/content_disclosure.py b/src/x_twitter_scraper/types/shared/content_disclosure.py index 0010340..d3dfd04 100644 --- a/src/x_twitter_scraper/types/shared/content_disclosure.py +++ b/src/x_twitter_scraper/types/shared/content_disclosure.py @@ -19,9 +19,6 @@ class Advertising(BaseModel): class AIGenerated(BaseModel): - can_edit: Optional[bool] = FieldInfo(alias="canEdit", default=None) - """Whether the disclosure can be edited on X.""" - detection_source: Optional[str] = FieldInfo(alias="detectionSource", default=None) """Source of the AI-generated media disclosure.""" diff --git a/src/x_twitter_scraper/types/shared/embedded_tweet.py b/src/x_twitter_scraper/types/shared/embedded_tweet.py index 3a00162..e30366a 100644 --- a/src/x_twitter_scraper/types/shared/embedded_tweet.py +++ b/src/x_twitter_scraper/types/shared/embedded_tweet.py @@ -4,6 +4,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from __future__ import annotations + from typing import Dict, List, Optional from pydantic import Field as FieldInfo @@ -13,7 +15,123 @@ from .user_profile import UserProfile from .content_disclosure import ContentDisclosure -__all__ = ["EmbeddedTweet"] +__all__ = [ + "EmbeddedTweet", + "Article", + "Card", + "CommunityNote", + "Edit", + "NoteTweet", + "NoteTweetRichtextTag", + "Place", + "PreviousCounts", +] + + +class Article(BaseModel): + """Article metadata attached to a tweet.""" + + id: Optional[str] = None + + cover_media_url: Optional[str] = FieldInfo(alias="coverMediaUrl", default=None) + + preview_text: Optional[str] = FieldInfo(alias="previewText", default=None) + + title: Optional[str] = None + + +class Card(BaseModel): + """Public card metadata attached to a tweet.""" + + id: Optional[str] = None + + binding_values: Optional[Dict[str, object]] = FieldInfo(alias="bindingValues", default=None) + + name: Optional[str] = None + + url: Optional[str] = None + + +class CommunityNote(BaseModel): + """Community Note presentation metadata returned by X.""" + + id: Optional[str] = None + + destination_url: Optional[str] = FieldInfo(alias="destinationUrl", default=None) + + footer: Optional[str] = None + + short_title: Optional[str] = FieldInfo(alias="shortTitle", default=None) + + subtitle: Optional[str] = None + + title: Optional[str] = None + + visual_style: Optional[str] = FieldInfo(alias="visualStyle", default=None) + + +class Edit(BaseModel): + """Edit history metadata returned by X.""" + + editable_until_msecs: Optional[str] = FieldInfo(alias="editableUntilMsecs", default=None) + + edit_tweet_ids: Optional[List[str]] = FieldInfo(alias="editTweetIds", default=None) + + +class NoteTweetRichtextTag(BaseModel): + from_index: int = FieldInfo(alias="fromIndex") + + to_index: int = FieldInfo(alias="toIndex") + + types: List[str] + + +class NoteTweet(BaseModel): + """Complete Note Tweet content and rich-text metadata.""" + + text: str + + id: Optional[str] = None + + entities: Optional[Dict[str, object]] = None + + is_expandable: Optional[bool] = FieldInfo(alias="isExpandable", default=None) + + richtext_tags: Optional[List[NoteTweetRichtextTag]] = FieldInfo(alias="richtextTags", default=None) + + +class Place(BaseModel): + """Public place metadata attached to a tweet.""" + + id: Optional[str] = None + + bounding_box: Optional[Dict[str, object]] = FieldInfo(alias="boundingBox", default=None) + + country: Optional[str] = None + + country_code: Optional[str] = FieldInfo(alias="countryCode", default=None) + + full_name: Optional[str] = FieldInfo(alias="fullName", default=None) + + name: Optional[str] = None + + place_type: Optional[str] = FieldInfo(alias="placeType", default=None) + + url: Optional[str] = None + + +class PreviousCounts(BaseModel): + """Engagement counts retained from a prior tweet edit.""" + + bookmark_count: Optional[int] = FieldInfo(alias="bookmarkCount", default=None) + + like_count: Optional[int] = FieldInfo(alias="likeCount", default=None) + + quote_count: Optional[int] = FieldInfo(alias="quoteCount", default=None) + + reply_count: Optional[int] = FieldInfo(alias="replyCount", default=None) + + retweet_count: Optional[int] = FieldInfo(alias="retweetCount", default=None) class EmbeddedTweet(BaseModel): @@ -38,9 +156,18 @@ class EmbeddedTweet(BaseModel): view_count: int = FieldInfo(alias="viewCount") + article: Optional[Article] = None + """Article metadata attached to a tweet.""" + author: Optional[UserProfile] = None """X user profile with bio, follower counts, and verification status.""" + card: Optional[Card] = None + """Public card metadata attached to a tweet.""" + + community_note: Optional[CommunityNote] = FieldInfo(alias="communityNote", default=None) + """Community Note presentation metadata returned by X.""" + content_disclosure: Optional[ContentDisclosure] = FieldInfo(alias="contentDisclosure", default=None) """ Content disclosure metadata shown by X when a tweet is labeled as paid @@ -53,6 +180,9 @@ class EmbeddedTweet(BaseModel): display_text_range: Optional[List[int]] = FieldInfo(alias="displayTextRange", default=None) + edit: Optional[Edit] = None + """Edit history metadata returned by X.""" + entities: Optional[Dict[str, object]] = None in_reply_to_id: Optional[str] = FieldInfo(alias="inReplyToId", default=None) @@ -69,12 +199,43 @@ class EmbeddedTweet(BaseModel): is_reply: Optional[bool] = FieldInfo(alias="isReply", default=None) + is_translatable: Optional[bool] = FieldInfo(alias="isTranslatable", default=None) + lang: Optional[str] = None media: Optional[List[TweetMedia]] = None + note_tweet: Optional[NoteTweet] = FieldInfo(alias="noteTweet", default=None) + """Complete Note Tweet content and rich-text metadata.""" + + place: Optional[Place] = None + """Public place metadata attached to a tweet.""" + + possibly_sensitive: Optional[bool] = FieldInfo(alias="possiblySensitive", default=None) + + previous_counts: Optional[PreviousCounts] = FieldInfo(alias="previousCounts", default=None) + """Engagement counts retained from a prior tweet edit.""" + + quoted_tweet: Optional["EmbeddedTweet"] = None + """Quoted or retweeted tweet context. + + Every object includes id, text, and engagement metrics. A zero metric can mean X + did not report the count. Author, media, and conversation fields appear when + available. + """ + + retweeted_tweet: Optional["EmbeddedTweet"] = None + """Quoted or retweeted tweet context. + + Every object includes id, text, and engagement metrics. A zero metric can mean X + did not report the count. Author, media, and conversation fields appear when + available. + """ + source: Optional[str] = None type: Optional[str] = None url: Optional[str] = None + + view_state: Optional[str] = FieldInfo(alias="viewState", default=None) diff --git a/src/x_twitter_scraper/types/shared/paginated_tweets.py b/src/x_twitter_scraper/types/shared/paginated_tweets.py index b94f94d..4ad997a 100644 --- a/src/x_twitter_scraper/types/shared/paginated_tweets.py +++ b/src/x_twitter_scraper/types/shared/paginated_tweets.py @@ -4,22 +4,26 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from __future__ import annotations + from typing import List from ..._models import BaseModel -from .search_tweet import SearchTweet __all__ = ["PaginatedTweets"] class PaginatedTweets(BaseModel): - """Paginated tweet results. + """Paginated tweets. - The item count can be lower than pageSize when the source returns fewer tweets, filters remove tweets, or remaining credits cover fewer results. Follow next_cursor while has_next_page is true. An empty page can still have has_next_page true after filtering. Zero affordable results returns 402 insufficient_credits. + Source visibility, filters, or remaining credits can reduce results. An empty filtered page can still have has_next_page true. Follow next_cursor while has_next_page is true. Zero affordable results returns 402 insufficient_credits. """ has_next_page: bool next_cursor: str - tweets: List[SearchTweet] + tweets: List["SearchTweet"] + + +from .search_tweet import SearchTweet diff --git a/src/x_twitter_scraper/types/shared/search_tweet.py b/src/x_twitter_scraper/types/shared/search_tweet.py index 91dfdd6..1ed7a5a 100644 --- a/src/x_twitter_scraper/types/shared/search_tweet.py +++ b/src/x_twitter_scraper/types/shared/search_tweet.py @@ -4,6 +4,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from __future__ import annotations + from typing import Dict, List, Optional from pydantic import Field as FieldInfo @@ -11,10 +13,125 @@ from ..._models import BaseModel from .tweet_media import TweetMedia from .user_profile import UserProfile -from .embedded_tweet import EmbeddedTweet from .content_disclosure import ContentDisclosure -__all__ = ["SearchTweet"] +__all__ = [ + "SearchTweet", + "Article", + "Card", + "CommunityNote", + "Edit", + "NoteTweet", + "NoteTweetRichtextTag", + "Place", + "PreviousCounts", +] + + +class Article(BaseModel): + """Article metadata attached to a tweet.""" + + id: Optional[str] = None + + cover_media_url: Optional[str] = FieldInfo(alias="coverMediaUrl", default=None) + + preview_text: Optional[str] = FieldInfo(alias="previewText", default=None) + + title: Optional[str] = None + + +class Card(BaseModel): + """Public card metadata attached to a tweet.""" + + id: Optional[str] = None + + binding_values: Optional[Dict[str, object]] = FieldInfo(alias="bindingValues", default=None) + + name: Optional[str] = None + + url: Optional[str] = None + + +class CommunityNote(BaseModel): + """Community Note presentation metadata returned by X.""" + + id: Optional[str] = None + + destination_url: Optional[str] = FieldInfo(alias="destinationUrl", default=None) + + footer: Optional[str] = None + + short_title: Optional[str] = FieldInfo(alias="shortTitle", default=None) + + subtitle: Optional[str] = None + + title: Optional[str] = None + + visual_style: Optional[str] = FieldInfo(alias="visualStyle", default=None) + + +class Edit(BaseModel): + """Edit history metadata returned by X.""" + + editable_until_msecs: Optional[str] = FieldInfo(alias="editableUntilMsecs", default=None) + + edit_tweet_ids: Optional[List[str]] = FieldInfo(alias="editTweetIds", default=None) + + +class NoteTweetRichtextTag(BaseModel): + from_index: int = FieldInfo(alias="fromIndex") + + to_index: int = FieldInfo(alias="toIndex") + + types: List[str] + + +class NoteTweet(BaseModel): + """Complete Note Tweet content and rich-text metadata.""" + + text: str + + id: Optional[str] = None + + entities: Optional[Dict[str, object]] = None + + is_expandable: Optional[bool] = FieldInfo(alias="isExpandable", default=None) + + richtext_tags: Optional[List[NoteTweetRichtextTag]] = FieldInfo(alias="richtextTags", default=None) + + +class Place(BaseModel): + """Public place metadata attached to a tweet.""" + + id: Optional[str] = None + + bounding_box: Optional[Dict[str, object]] = FieldInfo(alias="boundingBox", default=None) + + country: Optional[str] = None + + country_code: Optional[str] = FieldInfo(alias="countryCode", default=None) + + full_name: Optional[str] = FieldInfo(alias="fullName", default=None) + + name: Optional[str] = None + + place_type: Optional[str] = FieldInfo(alias="placeType", default=None) + + url: Optional[str] = None + + +class PreviousCounts(BaseModel): + """Engagement counts retained from a prior tweet edit.""" + + bookmark_count: Optional[int] = FieldInfo(alias="bookmarkCount", default=None) + + like_count: Optional[int] = FieldInfo(alias="likeCount", default=None) + + quote_count: Optional[int] = FieldInfo(alias="quoteCount", default=None) + + reply_count: Optional[int] = FieldInfo(alias="replyCount", default=None) + + retweet_count: Optional[int] = FieldInfo(alias="retweetCount", default=None) class SearchTweet(BaseModel): @@ -39,9 +156,18 @@ class SearchTweet(BaseModel): view_count: int = FieldInfo(alias="viewCount") + article: Optional[Article] = None + """Article metadata attached to a tweet.""" + author: Optional[UserProfile] = None """X user profile with bio, follower counts, and verification status.""" + card: Optional[Card] = None + """Public card metadata attached to a tweet.""" + + community_note: Optional[CommunityNote] = FieldInfo(alias="communityNote", default=None) + """Community Note presentation metadata returned by X.""" + content_disclosure: Optional[ContentDisclosure] = FieldInfo(alias="contentDisclosure", default=None) """ Content disclosure metadata shown by X when a tweet is labeled as paid @@ -56,6 +182,9 @@ class SearchTweet(BaseModel): display_text_range: Optional[List[int]] = FieldInfo(alias="displayTextRange", default=None) """Rendered text's start and end offsets.""" + edit: Optional[Edit] = None + """Edit history metadata returned by X.""" + entities: Optional[Dict[str, object]] = None """ Parsed search-result entities including URLs, mentions, hashtags, and media @@ -83,13 +212,26 @@ class SearchTweet(BaseModel): is_reply: Optional[bool] = FieldInfo(alias="isReply", default=None) """True when this search result is a reply""" + is_translatable: Optional[bool] = FieldInfo(alias="isTranslatable", default=None) + lang: Optional[str] = None """Search result language code.""" media: Optional[List[TweetMedia]] = None """Search-result media attachments, omitted when no media is present""" - quoted_tweet: Optional[EmbeddedTweet] = None + note_tweet: Optional[NoteTweet] = FieldInfo(alias="noteTweet", default=None) + """Complete Note Tweet content and rich-text metadata.""" + + place: Optional[Place] = None + """Public place metadata attached to a tweet.""" + + possibly_sensitive: Optional[bool] = FieldInfo(alias="possiblySensitive", default=None) + + previous_counts: Optional[PreviousCounts] = FieldInfo(alias="previousCounts", default=None) + """Engagement counts retained from a prior tweet edit.""" + + quoted_tweet: Optional["EmbeddedTweet"] = None """Quoted or retweeted tweet context. Every object includes id, text, and engagement metrics. A zero metric can mean X @@ -97,7 +239,7 @@ class SearchTweet(BaseModel): available. """ - retweeted_tweet: Optional[EmbeddedTweet] = None + retweeted_tweet: Optional["EmbeddedTweet"] = None """Quoted or retweeted tweet context. Every object includes id, text, and engagement metrics. A zero metric can mean X @@ -112,3 +254,8 @@ class SearchTweet(BaseModel): url: Optional[str] = None """Search result permalink.""" + + view_state: Optional[str] = FieldInfo(alias="viewState", default=None) + + +from .embedded_tweet import EmbeddedTweet diff --git a/src/x_twitter_scraper/types/shared/tweet_media.py b/src/x_twitter_scraper/types/shared/tweet_media.py index 2b35fdf..4c306a7 100644 --- a/src/x_twitter_scraper/types/shared/tweet_media.py +++ b/src/x_twitter_scraper/types/shared/tweet_media.py @@ -4,14 +4,42 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import Dict, List, Optional from typing_extensions import Literal from pydantic import Field as FieldInfo from ..._models import BaseModel -__all__ = ["TweetMedia", "VideoVariant"] +__all__ = ["TweetMedia", "FaceRect", "FocusRect", "Sizes", "VideoVariant"] + + +class FaceRect(BaseModel): + h: int + + w: int + + x: int + + y: int + + +class FocusRect(BaseModel): + h: int + + w: int + + x: int + + y: int + + +class Sizes(BaseModel): + h: int + + resize: str + + w: int class VideoVariant(BaseModel): @@ -33,5 +61,53 @@ class TweetMedia(BaseModel): url: str """X media link from the tweet""" + id: Optional[str] = None + """X media entity ID.""" + + allow_download: Optional[bool] = FieldInfo(alias="allowDownload", default=None) + """Whether X permits direct media download.""" + + alt_text: Optional[str] = FieldInfo(alias="altText", default=None) + """Accessibility text supplied for the media.""" + + aspect_ratio: Optional[List[int]] = FieldInfo(alias="aspectRatio", default=None) + """Video aspect ratio as width and height.""" + + availability_status: Optional[str] = FieldInfo(alias="availabilityStatus", default=None) + """Media availability state reported by X.""" + + display_url: Optional[str] = FieldInfo(alias="displayUrl", default=None) + """Display-friendly media URL reported by X.""" + + duration_millis: Optional[int] = FieldInfo(alias="durationMillis", default=None) + """Video duration in milliseconds.""" + + expanded_url: Optional[str] = FieldInfo(alias="expandedUrl", default=None) + """Expanded X media URL.""" + + face_rects: Optional[Dict[str, List[FaceRect]]] = FieldInfo(alias="faceRects", default=None) + """Face-aware crop rectangles grouped by media size.""" + + focus_rects: Optional[List[FocusRect]] = FieldInfo(alias="focusRects", default=None) + """Suggested image crops reported by X.""" + + height: Optional[int] = None + """Original media height.""" + + indices: Optional[List[int]] = None + """Media entity offsets in the tweet text.""" + + media_key: Optional[str] = FieldInfo(alias="mediaKey", default=None) + """Stable X media key.""" + + monetizable: Optional[bool] = None + """Whether X reports the media as monetizable.""" + + sizes: Optional[Dict[str, Sizes]] = None + """Named media renditions and resize modes.""" + video_variants: Optional[List[VideoVariant]] = FieldInfo(alias="videoVariants", default=None) """Available video encodings, ordered as returned""" + + width: Optional[int] = None + """Original media width.""" diff --git a/src/x_twitter_scraper/types/shared/user_profile.py b/src/x_twitter_scraper/types/shared/user_profile.py index d863bcc..2686cd9 100644 --- a/src/x_twitter_scraper/types/shared/user_profile.py +++ b/src/x_twitter_scraper/types/shared/user_profile.py @@ -10,7 +10,41 @@ from ..._models import BaseModel -__all__ = ["UserProfile"] +__all__ = ["UserProfile", "AffiliatesHighlightedLabel", "HighlightsInfo", "IdentityVerification"] + + +class AffiliatesHighlightedLabel(BaseModel): + """Organization affiliation label shown on an X profile.""" + + badge_url: Optional[str] = FieldInfo(alias="badgeUrl", default=None) + + description: Optional[str] = None + + url: Optional[str] = None + + url_type: Optional[str] = FieldInfo(alias="urlType", default=None) + + user_label_display_type: Optional[str] = FieldInfo(alias="userLabelDisplayType", default=None) + + user_label_type: Optional[str] = FieldInfo(alias="userLabelType", default=None) + + +class HighlightsInfo(BaseModel): + """Profile highlight availability and count metadata.""" + + can_highlight_tweets: Optional[bool] = FieldInfo(alias="canHighlightTweets", default=None) + + highlighted_tweets: Optional[str] = FieldInfo(alias="highlightedTweets", default=None) + + +class IdentityVerification(BaseModel): + """Identity verification metadata displayed by X.""" + + description: Optional[str] = None + + is_identity_verified: Optional[bool] = FieldInfo(alias="isIdentityVerified", default=None) + + verified_since_msec: Optional[str] = FieldInfo(alias="verifiedSinceMsec", default=None) class UserProfile(BaseModel): @@ -22,9 +56,14 @@ class UserProfile(BaseModel): username: str + affiliates_highlighted_label: Optional[AffiliatesHighlightedLabel] = FieldInfo( + alias="affiliatesHighlightedLabel", default=None + ) + """Organization affiliation label shown on an X profile.""" + automated_by: Optional[str] = FieldInfo(alias="automatedBy", default=None) - can_dm: Optional[bool] = FieldInfo(alias="canDm", default=None) + business_account_affiliates_count: Optional[int] = FieldInfo(alias="businessAccountAffiliatesCount", default=None) community_role: Optional[str] = FieldInfo(alias="communityRole", default=None) """Community role when returned by community member reads""" @@ -33,6 +72,8 @@ class UserProfile(BaseModel): created_at: Optional[str] = FieldInfo(alias="createdAt", default=None) + creator_subscriptions_count: Optional[int] = FieldInfo(alias="creatorSubscriptionsCount", default=None) + description: Optional[str] = None favourites_count: Optional[int] = FieldInfo(alias="favouritesCount", default=None) @@ -43,11 +84,25 @@ class UserProfile(BaseModel): has_custom_timelines: Optional[bool] = FieldInfo(alias="hasCustomTimelines", default=None) + has_graduated_access: Optional[bool] = FieldInfo(alias="hasGraduatedAccess", default=None) + + has_hidden_subscriptions_on_profile: Optional[bool] = FieldInfo( + alias="hasHiddenSubscriptionsOnProfile", default=None + ) + + highlights_info: Optional[HighlightsInfo] = FieldInfo(alias="highlightsInfo", default=None) + """Profile highlight availability and count metadata.""" + + identity_verification: Optional[IdentityVerification] = FieldInfo(alias="identityVerification", default=None) + """Identity verification metadata displayed by X.""" + is_automated: Optional[bool] = FieldInfo(alias="isAutomated", default=None) is_blue_verified: Optional[bool] = FieldInfo(alias="isBlueVerified", default=None) """Whether X shows a blue verification badge""" + is_profile_translatable: Optional[bool] = FieldInfo(alias="isProfileTranslatable", default=None) + is_translator: Optional[bool] = FieldInfo(alias="isTranslator", default=None) is_verified: Optional[bool] = FieldInfo(alias="isVerified", default=None) @@ -57,6 +112,8 @@ class UserProfile(BaseModel): media_count: Optional[int] = FieldInfo(alias="mediaCount", default=None) + parody_commentary_fan_label: Optional[str] = FieldInfo(alias="parodyCommentaryFanLabel", default=None) + pinned_tweet_ids: Optional[List[str]] = FieldInfo(alias="pinnedTweetIds", default=None) possibly_sensitive: Optional[bool] = FieldInfo(alias="possiblySensitive", default=None) @@ -67,13 +124,25 @@ class UserProfile(BaseModel): profile_banner_url: Optional[str] = FieldInfo(alias="profileBannerUrl", default=None) """Original X profile banner field when available""" + profile_description_language: Optional[str] = FieldInfo(alias="profileDescriptionLanguage", default=None) + + profile_image_shape: Optional[str] = FieldInfo(alias="profileImageShape", default=None) + + profile_interstitial_type: Optional[str] = FieldInfo(alias="profileInterstitialType", default=None) + profile_picture: Optional[str] = FieldInfo(alias="profilePicture", default=None) + profile_sort_enabled: Optional[bool] = FieldInfo(alias="profileSortEnabled", default=None) + + profile_translator_type: Optional[str] = FieldInfo(alias="profileTranslatorType", default=None) + protected: Optional[bool] = None """Whether the profile protects its posts""" statuses_count: Optional[int] = FieldInfo(alias="statusesCount", default=None) + super_follow_eligible: Optional[bool] = FieldInfo(alias="superFollowEligible", default=None) + unavailable: Optional[bool] = None unavailable_reason: Optional[str] = FieldInfo(alias="unavailableReason", default=None) @@ -84,10 +153,4 @@ class UserProfile(BaseModel): verified_type: Optional[str] = FieldInfo(alias="verifiedType", default=None) - viewer_followed_by: Optional[bool] = FieldInfo(alias="viewerFollowedBy", default=None) - """Whether this profile follows the authenticated viewer""" - - viewer_following: Optional[bool] = FieldInfo(alias="viewerFollowing", default=None) - """Whether the authenticated viewer follows this profile""" - withheld_in_countries: Optional[List[str]] = FieldInfo(alias="withheldInCountries", default=None) diff --git a/src/x_twitter_scraper/types/support/ticket_list_response.py b/src/x_twitter_scraper/types/support/ticket_list_response.py index 44c4b55..0a80685 100644 --- a/src/x_twitter_scraper/types/support/ticket_list_response.py +++ b/src/x_twitter_scraper/types/support/ticket_list_response.py @@ -4,8 +4,9 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import List from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -15,18 +16,18 @@ class Ticket(BaseModel): - created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) + created_at: datetime = FieldInfo(alias="createdAt") - message_count: Optional[int] = FieldInfo(alias="messageCount", default=None) + message_count: int = FieldInfo(alias="messageCount") - public_id: Optional[str] = FieldInfo(alias="publicId", default=None) + public_id: str = FieldInfo(alias="publicId") - status: Optional[str] = None + status: Literal["open", "in_progress", "resolved", "closed"] - subject: Optional[str] = None + subject: str - updated_at: Optional[datetime] = FieldInfo(alias="updatedAt", default=None) + updated_at: datetime = FieldInfo(alias="updatedAt") class TicketListResponse(BaseModel): - tickets: Optional[List[Ticket]] = None + tickets: List[Ticket] diff --git a/src/x_twitter_scraper/types/support/ticket_retrieve_response.py b/src/x_twitter_scraper/types/support/ticket_retrieve_response.py index eaff19b..274d150 100644 --- a/src/x_twitter_scraper/types/support/ticket_retrieve_response.py +++ b/src/x_twitter_scraper/types/support/ticket_retrieve_response.py @@ -4,7 +4,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List, Optional +from typing import List from datetime import datetime from typing_extensions import Literal @@ -39,24 +39,24 @@ class MessageAttachment(BaseModel): class Message(BaseModel): - attachments: Optional[List[MessageAttachment]] = None + attachments: List[MessageAttachment] - body: Optional[str] = None + body: str - created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) + created_at: datetime = FieldInfo(alias="createdAt") - sender: Optional[str] = None + sender: Literal["user", "support", "system"] class TicketRetrieveResponse(BaseModel): - created_at: Optional[datetime] = FieldInfo(alias="createdAt", default=None) + created_at: datetime = FieldInfo(alias="createdAt") - messages: Optional[List[Message]] = None + messages: List[Message] - public_id: Optional[str] = FieldInfo(alias="publicId", default=None) + public_id: str = FieldInfo(alias="publicId") - status: Optional[str] = None + status: Literal["open", "in_progress", "resolved", "closed"] - subject: Optional[str] = None + subject: str - updated_at: Optional[datetime] = FieldInfo(alias="updatedAt", default=None) + updated_at: datetime = FieldInfo(alias="updatedAt") diff --git a/src/x_twitter_scraper/types/support/ticket_update_response.py b/src/x_twitter_scraper/types/support/ticket_update_response.py index 59d6a5a..a7ebc97 100644 --- a/src/x_twitter_scraper/types/support/ticket_update_response.py +++ b/src/x_twitter_scraper/types/support/ticket_update_response.py @@ -4,7 +4,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -14,6 +14,6 @@ class TicketUpdateResponse(BaseModel): - public_id: Optional[str] = FieldInfo(alias="publicId", default=None) + public_id: str = FieldInfo(alias="publicId") - status: Optional[str] = None + status: Literal["open", "resolved", "closed"] diff --git a/src/x_twitter_scraper/types/trend_list_response.py b/src/x_twitter_scraper/types/trend_list_response.py index f5b4e6f..917d36a 100644 --- a/src/x_twitter_scraper/types/trend_list_response.py +++ b/src/x_twitter_scraper/types/trend_list_response.py @@ -6,6 +6,8 @@ from typing import List, Optional +from pydantic import Field as FieldInfo + from .._models import BaseModel __all__ = ["TrendListResponse", "Trend"] @@ -16,10 +18,19 @@ class Trend(BaseModel): description: Optional[str] = None + promoted_content: Optional[str] = FieldInfo(alias="promotedContent", default=None) + """Promotion identifier from X. Null for organic trends.""" + query: Optional[str] = None rank: Optional[int] = None + tweet_volume: Optional[int] = FieldInfo(alias="tweetVolume", default=None) + """Approximate public post volume when X supplies it.""" + + url: Optional[str] = None + """X search URL for the trend.""" + class TrendListResponse(BaseModel): total: int diff --git a/src/x_twitter_scraper/types/x/__init__.py b/src/x_twitter_scraper/types/x/__init__.py index a2de5e3..dfb924b 100644 --- a/src/x_twitter_scraper/types/x/__init__.py +++ b/src/x_twitter_scraper/types/x/__init__.py @@ -27,6 +27,7 @@ from .profile_update_params import ProfileUpdateParams as ProfileUpdateParams from .tweet_create_response import TweetCreateResponse as TweetCreateResponse from .tweet_delete_response import TweetDeleteResponse as TweetDeleteResponse +from .account_create_response import AccountCreateResponse as AccountCreateResponse from .account_delete_response import AccountDeleteResponse as AccountDeleteResponse from .account_reauth_response import AccountReauthResponse as AccountReauthResponse from .community_create_params import CommunityCreateParams as CommunityCreateParams @@ -41,6 +42,7 @@ from .community_create_response import CommunityCreateResponse as CommunityCreateResponse from .community_delete_response import CommunityDeleteResponse as CommunityDeleteResponse from .dm_retrieve_history_params import DmRetrieveHistoryParams as DmRetrieveHistoryParams +from .tweet_get_replies_response import TweetGetRepliesResponse as TweetGetRepliesResponse from .user_retrieve_batch_params import UserRetrieveBatchParams as UserRetrieveBatchParams from .user_retrieve_likes_params import UserRetrieveLikesParams as UserRetrieveLikesParams from .user_retrieve_media_params import UserRetrieveMediaParams as UserRetrieveMediaParams diff --git a/src/x_twitter_scraper/types/x/account_connection_attempt_retrieve_response.py b/src/x_twitter_scraper/types/x/account_connection_attempt_retrieve_response.py index 7f8592f..10a169d 100644 --- a/src/x_twitter_scraper/types/x/account_connection_attempt_retrieve_response.py +++ b/src/x_twitter_scraper/types/x/account_connection_attempt_retrieve_response.py @@ -2,10 +2,11 @@ from typing import Union, Optional from datetime import datetime -from typing_extensions import Literal, TypeAlias +from typing_extensions import Literal, Annotated, TypeAlias from pydantic import Field as FieldInfo +from ..._utils import PropertyInfo from ..._models import BaseModel __all__ = [ @@ -74,9 +75,12 @@ class XAccountConnectionChallenge(BaseModel): username: str -AccountConnectionAttemptRetrieveResponse: TypeAlias = Union[ - XAccountConnectionAttemptPending, - XAccountConnectionAttemptSuccess, - XAccountConnectionAttemptFailed, - XAccountConnectionChallenge, +AccountConnectionAttemptRetrieveResponse: TypeAlias = Annotated[ + Union[ + XAccountConnectionAttemptPending, + XAccountConnectionAttemptSuccess, + XAccountConnectionAttemptFailed, + XAccountConnectionChallenge, + ], + PropertyInfo(discriminator="status"), ] diff --git a/src/x_twitter_scraper/types/x/account_connection_challenge_submit_response.py b/src/x_twitter_scraper/types/x/account_connection_challenge_submit_response.py index d131559..695537a 100644 --- a/src/x_twitter_scraper/types/x/account_connection_challenge_submit_response.py +++ b/src/x_twitter_scraper/types/x/account_connection_challenge_submit_response.py @@ -23,7 +23,7 @@ class AccountConnectionChallengeSubmitResponse(BaseModel): health: Literal["healthy", "locked", "needsReauth", "recovering", "suspended", "temporaryIssue"] - status: str + status: Literal["active"] x_user_id: str = FieldInfo(alias="xUserId") diff --git a/src/x_twitter_scraper/types/x/account_create_response.py b/src/x_twitter_scraper/types/x/account_create_response.py new file mode 100644 index 0000000..96651ae --- /dev/null +++ b/src/x_twitter_scraper/types/x/account_create_response.py @@ -0,0 +1,68 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = [ + "AccountCreateResponse", + "SanitizedXAccount", + "XAccountConnectionAttemptPending", + "XAccountConnectionChallenge", +] + + +class SanitizedXAccount(BaseModel): + """Sanitized X account summary returned by connect and reauth.""" + + id: str + + created_at: datetime = FieldInfo(alias="createdAt") + + health: Literal["healthy", "locked", "needsReauth", "recovering", "suspended", "temporaryIssue"] + + status: Literal["active"] + + x_user_id: str = FieldInfo(alias="xUserId") + + x_username: str = FieldInfo(alias="xUsername") + + +class XAccountConnectionAttemptPending(BaseModel): + """The connection is still in progress.""" + + id: str + + object: Literal["x_account_connection_attempt"] + + poll_after_ms: int = FieldInfo(alias="pollAfterMs") + + status: Literal["pending"] + + +class XAccountConnectionChallenge(BaseModel): + """Resumable account connection challenge. + + Submit the email code to finish the same connection attempt. + """ + + id: str + + expires_at: datetime = FieldInfo(alias="expiresAt") + + message: str + + object: Literal["x_account_connection_challenge"] + + status: Literal["requires_email_code"] + + username: str + + +AccountCreateResponse: TypeAlias = Union[ + SanitizedXAccount, XAccountConnectionAttemptPending, XAccountConnectionChallenge +] diff --git a/src/x_twitter_scraper/types/x/account_reauth_response.py b/src/x_twitter_scraper/types/x/account_reauth_response.py index 9e1d481..f52b1c5 100644 --- a/src/x_twitter_scraper/types/x/account_reauth_response.py +++ b/src/x_twitter_scraper/types/x/account_reauth_response.py @@ -23,7 +23,7 @@ class AccountReauthResponse(BaseModel): health: Literal["healthy", "locked", "needsReauth", "recovering", "suspended", "temporaryIssue"] - status: str + status: Literal["active"] x_user_id: str = FieldInfo(alias="xUserId") diff --git a/src/x_twitter_scraper/types/x/communities/tweet_list_by_community_params.py b/src/x_twitter_scraper/types/x/communities/tweet_list_by_community_params.py index a5b683d..8bc6fe9 100644 --- a/src/x_twitter_scraper/types/x/communities/tweet_list_by_community_params.py +++ b/src/x_twitter_scraper/types/x/communities/tweet_list_by_community_params.py @@ -18,10 +18,8 @@ class TweetListByCommunityParams(TypedDict, total=False): """Pagination cursor for community tweets""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ diff --git a/src/x_twitter_scraper/types/x/communities/tweet_list_params.py b/src/x_twitter_scraper/types/x/communities/tweet_list_params.py index 8ea8138..792a790 100644 --- a/src/x_twitter_scraper/types/x/communities/tweet_list_params.py +++ b/src/x_twitter_scraper/types/x/communities/tweet_list_params.py @@ -24,12 +24,10 @@ class TweetListParams(TypedDict, total=False): """Pagination cursor for community results""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ query_type: Annotated[Literal["Latest", "Top"], PropertyInfo(alias="queryType")] diff --git a/src/x_twitter_scraper/types/x/community_retrieve_info_response.py b/src/x_twitter_scraper/types/x/community_retrieve_info_response.py index e77004a..84afb73 100644 --- a/src/x_twitter_scraper/types/x/community_retrieve_info_response.py +++ b/src/x_twitter_scraper/types/x/community_retrieve_info_response.py @@ -57,9 +57,6 @@ class Community(BaseModel): invites_policy: Optional[str] = None """Invitation policy""" - is_member: Optional[bool] = None - """Whether the authenticated viewer is a member""" - is_nsfw: Optional[bool] = None """Whether the community is marked sensitive""" @@ -78,9 +75,6 @@ class Community(BaseModel): primary_topic: Optional[CommunityPrimaryTopic] = None """Primary topic""" - role: Optional[str] = None - """Authenticated viewer's community role""" - rules: Optional[List[CommunityRule]] = None """Community rules""" diff --git a/src/x_twitter_scraper/types/x/community_retrieve_search_params.py b/src/x_twitter_scraper/types/x/community_retrieve_search_params.py index bccf151..a3edeff 100644 --- a/src/x_twitter_scraper/types/x/community_retrieve_search_params.py +++ b/src/x_twitter_scraper/types/x/community_retrieve_search_params.py @@ -24,12 +24,10 @@ class CommunityRetrieveSearchParams(TypedDict, total=False): """Pagination cursor for community search""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ query_type: Annotated[Literal["Latest", "Top"], PropertyInfo(alias="queryType")] diff --git a/src/x_twitter_scraper/types/x/list_retrieve_tweets_params.py b/src/x_twitter_scraper/types/x/list_retrieve_tweets_params.py index b53749c..d283cd2 100644 --- a/src/x_twitter_scraper/types/x/list_retrieve_tweets_params.py +++ b/src/x_twitter_scraper/types/x/list_retrieve_tweets_params.py @@ -21,12 +21,10 @@ class ListRetrieveTweetsParams(TypedDict, total=False): """Include replies (default false)""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ since_time: Annotated[str, PropertyInfo(alias="sinceTime")] diff --git a/src/x_twitter_scraper/types/x/tweet_author.py b/src/x_twitter_scraper/types/x/tweet_author.py index 7785a1f..4654a6f 100644 --- a/src/x_twitter_scraper/types/x/tweet_author.py +++ b/src/x_twitter_scraper/types/x/tweet_author.py @@ -15,4 +15,6 @@ class TweetAuthor(UserProfile): The lookup route always includes follower count and verification state. Other profile fields appear when available. """ - pass + followers: int # type: ignore + + verified: bool # type: ignore diff --git a/src/x_twitter_scraper/types/x/tweet_detail.py b/src/x_twitter_scraper/types/x/tweet_detail.py index 1dd674b..ba05a82 100644 --- a/src/x_twitter_scraper/types/x/tweet_detail.py +++ b/src/x_twitter_scraper/types/x/tweet_detail.py @@ -4,6 +4,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from __future__ import annotations + from typing import Dict, List, Optional from pydantic import Field as FieldInfo @@ -11,10 +13,125 @@ from ..._models import BaseModel from .tweet_author import TweetAuthor from ..shared.tweet_media import TweetMedia -from ..shared.embedded_tweet import EmbeddedTweet from ..shared.content_disclosure import ContentDisclosure -__all__ = ["TweetDetail"] +__all__ = [ + "TweetDetail", + "Article", + "Card", + "CommunityNote", + "Edit", + "NoteTweet", + "NoteTweetRichtextTag", + "Place", + "PreviousCounts", +] + + +class Article(BaseModel): + """Article metadata attached to a tweet.""" + + id: Optional[str] = None + + cover_media_url: Optional[str] = FieldInfo(alias="coverMediaUrl", default=None) + + preview_text: Optional[str] = FieldInfo(alias="previewText", default=None) + + title: Optional[str] = None + + +class Card(BaseModel): + """Public card metadata attached to a tweet.""" + + id: Optional[str] = None + + binding_values: Optional[Dict[str, object]] = FieldInfo(alias="bindingValues", default=None) + + name: Optional[str] = None + + url: Optional[str] = None + + +class CommunityNote(BaseModel): + """Community Note presentation metadata returned by X.""" + + id: Optional[str] = None + + destination_url: Optional[str] = FieldInfo(alias="destinationUrl", default=None) + + footer: Optional[str] = None + + short_title: Optional[str] = FieldInfo(alias="shortTitle", default=None) + + subtitle: Optional[str] = None + + title: Optional[str] = None + + visual_style: Optional[str] = FieldInfo(alias="visualStyle", default=None) + + +class Edit(BaseModel): + """Edit history metadata returned by X.""" + + editable_until_msecs: Optional[str] = FieldInfo(alias="editableUntilMsecs", default=None) + + edit_tweet_ids: Optional[List[str]] = FieldInfo(alias="editTweetIds", default=None) + + +class NoteTweetRichtextTag(BaseModel): + from_index: int = FieldInfo(alias="fromIndex") + + to_index: int = FieldInfo(alias="toIndex") + + types: List[str] + + +class NoteTweet(BaseModel): + """Complete Note Tweet content and rich-text metadata.""" + + text: str + + id: Optional[str] = None + + entities: Optional[Dict[str, object]] = None + + is_expandable: Optional[bool] = FieldInfo(alias="isExpandable", default=None) + + richtext_tags: Optional[List[NoteTweetRichtextTag]] = FieldInfo(alias="richtextTags", default=None) + + +class Place(BaseModel): + """Public place metadata attached to a tweet.""" + + id: Optional[str] = None + + bounding_box: Optional[Dict[str, object]] = FieldInfo(alias="boundingBox", default=None) + + country: Optional[str] = None + + country_code: Optional[str] = FieldInfo(alias="countryCode", default=None) + + full_name: Optional[str] = FieldInfo(alias="fullName", default=None) + + name: Optional[str] = None + + place_type: Optional[str] = FieldInfo(alias="placeType", default=None) + + url: Optional[str] = None + + +class PreviousCounts(BaseModel): + """Engagement counts retained from a prior tweet edit.""" + + bookmark_count: Optional[int] = FieldInfo(alias="bookmarkCount", default=None) + + like_count: Optional[int] = FieldInfo(alias="likeCount", default=None) + + quote_count: Optional[int] = FieldInfo(alias="quoteCount", default=None) + + reply_count: Optional[int] = FieldInfo(alias="replyCount", default=None) + + retweet_count: Optional[int] = FieldInfo(alias="retweetCount", default=None) class TweetDetail(BaseModel): @@ -39,6 +156,9 @@ class TweetDetail(BaseModel): view_count: int = FieldInfo(alias="viewCount") + article: Optional[Article] = None + """Article metadata attached to a tweet.""" + author: Optional[TweetAuthor] = None """Tweet author profile. @@ -46,6 +166,12 @@ class TweetDetail(BaseModel): profile fields appear when available. """ + card: Optional[Card] = None + """Public card metadata attached to a tweet.""" + + community_note: Optional[CommunityNote] = FieldInfo(alias="communityNote", default=None) + """Community Note presentation metadata returned by X.""" + content_disclosure: Optional[ContentDisclosure] = FieldInfo(alias="contentDisclosure", default=None) """ Content disclosure metadata shown by X when a tweet is labeled as paid @@ -60,6 +186,9 @@ class TweetDetail(BaseModel): display_text_range: Optional[List[int]] = FieldInfo(alias="displayTextRange", default=None) """Start and end offsets for rendered tweet text""" + edit: Optional[Edit] = None + """Edit history metadata returned by X.""" + entities: Optional[Dict[str, object]] = None """Parsed entities from the tweet text (URLs, mentions, hashtags, media)""" @@ -84,13 +213,26 @@ class TweetDetail(BaseModel): is_reply: Optional[bool] = FieldInfo(alias="isReply", default=None) """Whether this tweet is a reply to another tweet""" + is_translatable: Optional[bool] = FieldInfo(alias="isTranslatable", default=None) + lang: Optional[str] = None """Tweet language code""" media: Optional[List[TweetMedia]] = None """Attached media items, omitted when the tweet has no media""" - quoted_tweet: Optional[EmbeddedTweet] = None + note_tweet: Optional[NoteTweet] = FieldInfo(alias="noteTweet", default=None) + """Complete Note Tweet content and rich-text metadata.""" + + place: Optional[Place] = None + """Public place metadata attached to a tweet.""" + + possibly_sensitive: Optional[bool] = FieldInfo(alias="possiblySensitive", default=None) + + previous_counts: Optional[PreviousCounts] = FieldInfo(alias="previousCounts", default=None) + """Engagement counts retained from a prior tweet edit.""" + + quoted_tweet: Optional["EmbeddedTweet"] = None """Quoted or retweeted tweet context. Every object includes id, text, and engagement metrics. A zero metric can mean X @@ -98,7 +240,7 @@ class TweetDetail(BaseModel): available. """ - retweeted_tweet: Optional[EmbeddedTweet] = None + retweeted_tweet: Optional["EmbeddedTweet"] = None """Quoted or retweeted tweet context. Every object includes id, text, and engagement metrics. A zero metric can mean X @@ -114,3 +256,8 @@ class TweetDetail(BaseModel): url: Optional[str] = None """Tweet permalink URL""" + + view_state: Optional[str] = FieldInfo(alias="viewState", default=None) + + +from ..shared.embedded_tweet import EmbeddedTweet diff --git a/src/x_twitter_scraper/types/x/tweet_get_quotes_params.py b/src/x_twitter_scraper/types/x/tweet_get_quotes_params.py index d18489f..64babcd 100644 --- a/src/x_twitter_scraper/types/x/tweet_get_quotes_params.py +++ b/src/x_twitter_scraper/types/x/tweet_get_quotes_params.py @@ -73,12 +73,10 @@ class TweetGetQuotesParams(TypedDict, total=False): """Minimum retweets threshold.""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ quotes: Literal["include", "exclude", "only"] diff --git a/src/x_twitter_scraper/types/x/tweet_get_replies_params.py b/src/x_twitter_scraper/types/x/tweet_get_replies_params.py index 881bf6f..f505b0d 100644 --- a/src/x_twitter_scraper/types/x/tweet_get_replies_params.py +++ b/src/x_twitter_scraper/types/x/tweet_get_replies_params.py @@ -49,6 +49,13 @@ class TweetGetRepliesParams(TypedDict, total=False): language: str """Language code filter, e.g. en or tr.""" + limit: int + """With mode=complete, maximum combined direct and nested reply rows (1-25000). + + Without complete mode, this is the deprecated pageSize alias and uses the normal + 1-100 page range. + """ + media_type: Annotated[ Literal["images", "videos", "gifs", "media", "links", "none"], PropertyInfo(alias="mediaType") ] @@ -69,13 +76,18 @@ class TweetGetRepliesParams(TypedDict, total=False): min_retweets: Annotated[int, PropertyInfo(alias="minRetweets")] """Minimum retweets threshold.""" + mode: Literal["complete"] + """Set complete for maximum-coverage collection. + + Complete mode accepts only limit. Remove cursor, pageSize, count, time ranges, + and tweet filters. + """ + page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ quotes: Literal["include", "exclude", "only"] diff --git a/src/x_twitter_scraper/types/x/tweet_get_replies_response.py b/src/x_twitter_scraper/types/x/tweet_get_replies_response.py new file mode 100644 index 0000000..5b55421 --- /dev/null +++ b/src/x_twitter_scraper/types/x/tweet_get_replies_response.py @@ -0,0 +1,151 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Literal + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel +from ..shared.paginated_tweets import PaginatedTweets + +__all__ = [ + "TweetGetRepliesResponse", + "TweetGetRepliesResponseDiagnostic", + "TweetGetRepliesResponseDiagnosticRichness", + "TweetGetRepliesResponseDiagnosticStrategiesAttempted", +] + + +class TweetGetRepliesResponseDiagnosticRichness(BaseModel): + """Field-presence counts across the collected direct replies.""" + + article: int + """Replies with article content.""" + + author: int + """Replies with author details.""" + + card: int + """Replies with card metadata.""" + + community_note: int = FieldInfo(alias="communityNote") + """Replies with community-note data.""" + + created_at: int = FieldInfo(alias="createdAt") + """Replies with a creation timestamp.""" + + engagement_counts: int = FieldInfo(alias="engagementCounts") + """Replies with engagement counts.""" + + entities: int + """Replies with entity metadata.""" + + language: int + """Replies with a language value.""" + + media: int + """Replies with media metadata.""" + + quoted_or_reposted_tweet: int = FieldInfo(alias="quotedOrRepostedTweet") + """Replies with quoted or reposted tweet data.""" + + text: int + """Replies with text.""" + + total_replies: int = FieldInfo(alias="totalReplies") + """Total unique direct replies evaluated for richness.""" + + url: int + """Replies with a canonical URL.""" + + +class TweetGetRepliesResponseDiagnosticStrategiesAttempted(BaseModel): + name: str + + new_direct_replies: int = FieldInfo(alias="newDirectReplies") + + new_nested_replies: int = FieldInfo(alias="newNestedReplies") + + pages_attempted: int = FieldInfo(alias="pagesAttempted") + + stop_reason: Literal[ + "deadline", "empty_pages", "error", "missing_cursor", "no_next_page", "page_cap", "repeated_cursor" + ] = FieldInfo(alias="stopReason") + + +class TweetGetRepliesResponseDiagnostic(BaseModel): + """Evidence for direct-reply coverage and collector behavior.""" + + complete: bool + """Whether coverage met the target without truncation.""" + + coverage_percentage: float = FieldInfo(alias="coveragePercentage") + """Unique direct replies as a percentage of the reported count.""" + + cursor_failures: int = FieldInfo(alias="cursorFailures") + """Cursor requests that failed.""" + + duplicate_count: int = FieldInfo(alias="duplicateCount") + """Duplicate tweet IDs removed across pages and strategies.""" + + empty_false_progress_pages: int = FieldInfo(alias="emptyFalseProgressPages") + """Empty pages rejected because they did not make progress.""" + + malformed_count: int = FieldInfo(alias="malformedCount") + """Malformed response items rejected.""" + + missing_response_modules_or_fields: List[str] = FieldInfo(alias="missingResponseModulesOrFields") + """Expected response modules or fields missing from X.""" + + nested_reply_count: int = FieldInfo(alias="nestedReplyCount") + """Unique nested replies kept outside direct coverage.""" + + pages_attempted: int = FieldInfo(alias="pagesAttempted") + """Total pages attempted across all strategies.""" + + recommended_fallback: str = FieldInfo(alias="recommendedFallback") + """Recommended next action when coverage is incomplete.""" + + repeated_cursor_count: int = FieldInfo(alias="repeatedCursorCount") + """Repeated cursors rejected to prevent loops.""" + + reported_reply_count: int = FieldInfo(alias="reportedReplyCount") + """Reply count reported on the source post.""" + + response_truncated: bool = FieldInfo(alias="responseTruncated") + """Whether the requested row limit truncated safe results.""" + + richness: TweetGetRepliesResponseDiagnosticRichness + """Field-presence counts across the collected direct replies.""" + + strategies_attempted: List[TweetGetRepliesResponseDiagnosticStrategiesAttempted] = FieldInfo( + alias="strategiesAttempted" + ) + """Per-strategy pagination and contribution evidence.""" + + target_direct_replies: int = FieldInfo(alias="targetDirectReplies") + """Minimum direct replies required for the coverage target.""" + + unique_direct_replies: int = FieldInfo(alias="uniqueDirectReplies") + """Unique replies whose parent ID equals the source post ID.""" + + unrelated_count: int = FieldInfo(alias="unrelatedCount") + """Tweets rejected because they belonged elsewhere.""" + + +class TweetGetRepliesResponse(PaginatedTweets): + """Reply rows. + + Complete mode also returns nested replies and coverage diagnostics. Keep nested replies separate from direct coverage. + """ + + diagnostic: Optional[TweetGetRepliesResponseDiagnostic] = None + """Evidence for direct-reply coverage and collector behavior.""" + + nested_replies: Optional[List["SearchTweet"]] = None + """Nested replies. Excluded from direct coverage.""" + + +from ..shared.search_tweet import SearchTweet diff --git a/src/x_twitter_scraper/types/x/tweet_get_thread_params.py b/src/x_twitter_scraper/types/x/tweet_get_thread_params.py index e31da7c..8d9250f 100644 --- a/src/x_twitter_scraper/types/x/tweet_get_thread_params.py +++ b/src/x_twitter_scraper/types/x/tweet_get_thread_params.py @@ -18,10 +18,8 @@ class TweetGetThreadParams(TypedDict, total=False): """Pagination cursor for thread tweets""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ diff --git a/src/x_twitter_scraper/types/x/tweet_retrieve_response.py b/src/x_twitter_scraper/types/x/tweet_retrieve_response.py index 253bc89..58fdf27 100644 --- a/src/x_twitter_scraper/types/x/tweet_retrieve_response.py +++ b/src/x_twitter_scraper/types/x/tweet_retrieve_response.py @@ -4,17 +4,18 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from __future__ import annotations + from typing import Optional from ..._models import BaseModel from .tweet_author import TweetAuthor -from .tweet_detail import TweetDetail __all__ = ["TweetRetrieveResponse"] class TweetRetrieveResponse(BaseModel): - tweet: TweetDetail + tweet: "TweetDetail" """Full tweet with text, engagement metrics, media, and metadata. A zero metric can mean X did not report the count. @@ -26,3 +27,6 @@ class TweetRetrieveResponse(BaseModel): The lookup route always includes follower count and verification state. Other profile fields appear when available. """ + + +from .tweet_detail import TweetDetail diff --git a/src/x_twitter_scraper/types/x/user_retrieve_likes_params.py b/src/x_twitter_scraper/types/x/user_retrieve_likes_params.py index 0a3f94c..255f9d9 100644 --- a/src/x_twitter_scraper/types/x/user_retrieve_likes_params.py +++ b/src/x_twitter_scraper/types/x/user_retrieve_likes_params.py @@ -70,12 +70,10 @@ class UserRetrieveLikesParams(TypedDict, total=False): """Minimum retweets threshold.""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ quotes: Literal["include", "exclude", "only"] diff --git a/src/x_twitter_scraper/types/x/user_retrieve_media_params.py b/src/x_twitter_scraper/types/x/user_retrieve_media_params.py index 053cebe..3730305 100644 --- a/src/x_twitter_scraper/types/x/user_retrieve_media_params.py +++ b/src/x_twitter_scraper/types/x/user_retrieve_media_params.py @@ -70,12 +70,10 @@ class UserRetrieveMediaParams(TypedDict, total=False): """Minimum retweets threshold.""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ quotes: Literal["include", "exclude", "only"] diff --git a/src/x_twitter_scraper/types/x/user_retrieve_mentions_params.py b/src/x_twitter_scraper/types/x/user_retrieve_mentions_params.py index 58b6e01..8731903 100644 --- a/src/x_twitter_scraper/types/x/user_retrieve_mentions_params.py +++ b/src/x_twitter_scraper/types/x/user_retrieve_mentions_params.py @@ -70,12 +70,10 @@ class UserRetrieveMentionsParams(TypedDict, total=False): """Minimum retweets threshold.""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ quotes: Literal["include", "exclude", "only"] diff --git a/src/x_twitter_scraper/types/x/user_retrieve_replies_params.py b/src/x_twitter_scraper/types/x/user_retrieve_replies_params.py index 911a040..fa091ad 100644 --- a/src/x_twitter_scraper/types/x/user_retrieve_replies_params.py +++ b/src/x_twitter_scraper/types/x/user_retrieve_replies_params.py @@ -73,12 +73,10 @@ class UserRetrieveRepliesParams(TypedDict, total=False): """Minimum retweets threshold.""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ quotes: Literal["include", "exclude", "only"] diff --git a/src/x_twitter_scraper/types/x/user_retrieve_tweets_params.py b/src/x_twitter_scraper/types/x/user_retrieve_tweets_params.py index 6f30e7b..d2faa4f 100644 --- a/src/x_twitter_scraper/types/x/user_retrieve_tweets_params.py +++ b/src/x_twitter_scraper/types/x/user_retrieve_tweets_params.py @@ -76,12 +76,10 @@ class UserRetrieveTweetsParams(TypedDict, total=False): """Minimum retweets threshold.""" page_size: Annotated[int, PropertyInfo(alias="pageSize")] - """Maximum items requested from this page (1-100, default 20). + """Maximum page items (1-100, default 20). - The response can contain fewer items because the source returned fewer, filters - removed items, or remaining credits cover fewer results. Keep requesting - next_cursor while has_next_page is true, even when a page is empty. The - deprecated limit and count aliases remain accepted. + Source, filters, or credits can reduce results. Continue while has_next_page is + true. Deprecated limit and count aliases remain accepted. """ quotes: Literal["include", "exclude", "only"] diff --git a/src/x_twitter_scraper/types/x_get_article_response.py b/src/x_twitter_scraper/types/x_get_article_response.py index b80cc71..83eaf73 100644 --- a/src/x_twitter_scraper/types/x_get_article_response.py +++ b/src/x_twitter_scraper/types/x_get_article_response.py @@ -79,8 +79,6 @@ class Author(BaseModel): username: str - can_dm: Optional[bool] = FieldInfo(alias="canDm", default=None) - created_at: Optional[str] = FieldInfo(alias="createdAt", default=None) description: Optional[str] = None diff --git a/src/x_twitter_scraper/types/x_get_trends_response.py b/src/x_twitter_scraper/types/x_get_trends_response.py index 43aa53b..66befe5 100644 --- a/src/x_twitter_scraper/types/x_get_trends_response.py +++ b/src/x_twitter_scraper/types/x_get_trends_response.py @@ -6,6 +6,8 @@ from typing import List, Optional +from pydantic import Field as FieldInfo + from .._models import BaseModel __all__ = ["XGetTrendsResponse", "Trend"] @@ -16,10 +18,19 @@ class Trend(BaseModel): description: Optional[str] = None + promoted_content: Optional[str] = FieldInfo(alias="promotedContent", default=None) + """Promotion identifier from X. Null for organic trends.""" + query: Optional[str] = None rank: Optional[int] = None + tweet_volume: Optional[int] = FieldInfo(alias="tweetVolume", default=None) + """Approximate public post volume when X supplies it.""" + + url: Optional[str] = None + """X search URL for the trend.""" + class XGetTrendsResponse(BaseModel): count: int diff --git a/tests/api_resources/x/test_accounts.py b/tests/api_resources/x/test_accounts.py index c3d34b5..878c4c8 100644 --- a/tests/api_resources/x/test_accounts.py +++ b/tests/api_resources/x/test_accounts.py @@ -16,6 +16,7 @@ from x_twitter_scraper.types.x import ( XAccountDetail, AccountListResponse, + AccountCreateResponse, AccountDeleteResponse, AccountReauthResponse, AccountBulkRetryResponse, @@ -36,7 +37,7 @@ def test_method_create(self, client: XTwitterScraper) -> None: totp_secret="", username="your_x_username", ) - assert_matches_type(object, account, path=["response"]) + assert_matches_type(AccountCreateResponse, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -51,7 +52,7 @@ def test_raw_response_create(self, client: XTwitterScraper) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = response.parse() - assert_matches_type(object, account, path=["response"]) + assert_matches_type(AccountCreateResponse, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -66,7 +67,7 @@ def test_streaming_response_create(self, client: XTwitterScraper) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = response.parse() - assert_matches_type(object, account, path=["response"]) + assert_matches_type(AccountCreateResponse, account, path=["response"]) assert cast(Any, response.is_closed) is True @@ -282,7 +283,7 @@ async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: totp_secret="", username="your_x_username", ) - assert_matches_type(object, account, path=["response"]) + assert_matches_type(AccountCreateResponse, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -297,7 +298,7 @@ async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = await response.parse() - assert_matches_type(object, account, path=["response"]) + assert_matches_type(AccountCreateResponse, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -312,7 +313,7 @@ async def test_streaming_response_create(self, async_client: AsyncXTwitterScrape assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = await response.parse() - assert_matches_type(object, account, path=["response"]) + assert_matches_type(AccountCreateResponse, account, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/x/test_tweets.py b/tests/api_resources/x/test_tweets.py index 3a3d213..6b447d9 100644 --- a/tests/api_resources/x/test_tweets.py +++ b/tests/api_resources/x/test_tweets.py @@ -18,6 +18,7 @@ TweetCreateResponse, TweetDeleteResponse, TweetRetrieveResponse, + TweetGetRepliesResponse, ) from x_twitter_scraper.types.shared import PaginatedUsers, PaginatedTweets @@ -342,7 +343,7 @@ def test_method_get_replies(self, client: XTwitterScraper) -> None: tweet = client.x.tweets.get_replies( id="id", ) - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -359,12 +360,14 @@ def test_method_get_replies_with_all_params(self, client: XTwitterScraper) -> No hashtags="hashtags", in_reply_to_tweet_id="inReplyToTweetId", language="language", + limit=1, media_type="images", mentioning="mentioning", min_faves=0, min_quotes=0, min_replies=0, min_retweets=0, + mode="complete", page_size=1, quotes="include", quotes_of_tweet_id="quotesOfTweetId", @@ -379,7 +382,7 @@ def test_method_get_replies_with_all_params(self, client: XTwitterScraper) -> No url="url", verified_only=True, ) - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -391,7 +394,7 @@ def test_raw_response_get_replies(self, client: XTwitterScraper) -> None: assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" tweet = response.parse() - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -403,7 +406,7 @@ def test_streaming_response_get_replies(self, client: XTwitterScraper) -> None: assert response.http_request.headers.get("X-Stainless-Lang") == "python" tweet = response.parse() - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) assert cast(Any, response.is_closed) is True @@ -918,7 +921,7 @@ async def test_method_get_replies(self, async_client: AsyncXTwitterScraper) -> N tweet = await async_client.x.tweets.get_replies( id="id", ) - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -935,12 +938,14 @@ async def test_method_get_replies_with_all_params(self, async_client: AsyncXTwit hashtags="hashtags", in_reply_to_tweet_id="inReplyToTweetId", language="language", + limit=1, media_type="images", mentioning="mentioning", min_faves=0, min_quotes=0, min_replies=0, min_retweets=0, + mode="complete", page_size=1, quotes="include", quotes_of_tweet_id="quotesOfTweetId", @@ -955,7 +960,7 @@ async def test_method_get_replies_with_all_params(self, async_client: AsyncXTwit url="url", verified_only=True, ) - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -967,7 +972,7 @@ async def test_raw_response_get_replies(self, async_client: AsyncXTwitterScraper assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" tweet = await response.parse() - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -979,7 +984,7 @@ async def test_streaming_response_get_replies(self, async_client: AsyncXTwitterS assert response.http_request.headers.get("X-Stainless-Lang") == "python" tweet = await response.parse() - assert_matches_type(PaginatedTweets, tweet, path=["response"]) + assert_matches_type(TweetGetRepliesResponse, tweet, path=["response"]) assert cast(Any, response.is_closed) is True From b9c37caf3608fb16346f946b2a2259bea945f78e Mon Sep 17 00:00:00 2001 From: kriptoburak Date: Fri, 31 Jul 2026 17:15:10 +0300 Subject: [PATCH 2/2] test: cover generated reply response unions Signed-off-by: kriptoburak --- src/x_twitter_scraper/types/__init__.py | 2 ++ tests/mock_api_routes.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/x_twitter_scraper/types/__init__.py b/src/x_twitter_scraper/types/__init__.py index 26495ac..59d1a62 100644 --- a/src/x_twitter_scraper/types/__init__.py +++ b/src/x_twitter_scraper/types/__init__.py @@ -119,12 +119,14 @@ # Pydantic can resolve the necessary references. # See: https://github.com/pydantic/pydantic/issues/11250 for more context. if _compat.PYDANTIC_V1: + x.tweet_get_replies_response.TweetGetRepliesResponse.update_forward_refs() # type: ignore x.tweet_detail.TweetDetail.update_forward_refs() # type: ignore x.tweet_retrieve_response.TweetRetrieveResponse.update_forward_refs() # type: ignore shared.embedded_tweet.EmbeddedTweet.update_forward_refs() # type: ignore shared.paginated_tweets.PaginatedTweets.update_forward_refs() # type: ignore shared.search_tweet.SearchTweet.update_forward_refs() # type: ignore else: + x.tweet_get_replies_response.TweetGetRepliesResponse.model_rebuild(_parent_namespace_depth=0) x.tweet_detail.TweetDetail.model_rebuild(_parent_namespace_depth=0) x.tweet_retrieve_response.TweetRetrieveResponse.model_rebuild(_parent_namespace_depth=0) shared.embedded_tweet.EmbeddedTweet.model_rebuild(_parent_namespace_depth=0) diff --git a/tests/mock_api_routes.py b/tests/mock_api_routes.py index fba94ce..f139787 100644 --- a/tests/mock_api_routes.py +++ b/tests/mock_api_routes.py @@ -82,9 +82,12 @@ def _response_type(module_name: str, name: str) -> ResponseType: response_type = getattr(module, name, None) if isinstance(response_type, type) and issubclass(response_type, BaseModel): return response_type - for variant in get_args(response_type): + pending = list(get_args(response_type)) + while pending: + variant = pending.pop(0) if isinstance(variant, type) and issubclass(variant, BaseModel): return variant + pending.extend(get_args(variant)) raise TypeError(f"{module_name}.{name} is not a response model")