diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5d02000..e7ca613 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.1" + ".": "0.7.0" } diff --git a/.stats.yml b/.stats.yml index 4477ceb..722a8b3 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 122 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik/x-twitter-scraper-26de9a1d18b92c9472fcc289877ba281ae305db846b7b14065142e26c7637d72.yml -openapi_spec_hash: f90f7016413291fdcbedb986385dc3ae -config_hash: 17c84052d27489691ee6dd861cc56e24 +configured_endpoints: 123 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik/x-twitter-scraper-323137ebb1ee9824cd9cfb63a325c0839deb81f17244e42e8154d6567e7af954.yml +openapi_spec_hash: 48da9c2747c1e7c474dd514d39c28c49 +config_hash: dab26921eb273c16e5570c0485c9ac2c diff --git a/CHANGELOG.md b/CHANGELOG.md index 85fe493..aaeb01d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.7.0 (2026-07-29) + +Full Changelog: [v0.6.1...v0.7.0](https://github.com/Xquik-dev/x-twitter-scraper-python/compare/v0.6.1...v0.7.0) + +### Features + +* **api:** api update ([b63b37b](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/b63b37b882b4d227a542f0404604927912b60e99)) +* **api:** api update ([533e09c](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/533e09c3b13682cc872ad9bac273dbc9f3de00ac)) + ## 0.6.1 (2026-07-28) Full Changelog: [v0.6.0...v0.6.1](https://github.com/Xquik-dev/x-twitter-scraper-python/compare/v0.6.0...v0.6.1) diff --git a/api.md b/api.md index c0013a1..7f630d5 100644 --- a/api.md +++ b/api.md @@ -460,7 +460,6 @@ Types: from x_twitter_scraper.types.x import ( XAccount, XAccountDetail, - AccountCreateResponse, AccountListResponse, AccountDeleteResponse, AccountBulkRetryResponse, @@ -470,7 +469,7 @@ from x_twitter_scraper.types.x import ( Methods: -- client.x.accounts.create(\*\*params) -> AccountCreateResponse +- client.x.accounts.create(\*\*params) -> object - client.x.accounts.retrieve(id) -> XAccountDetail - client.x.accounts.list() -> AccountListResponse - client.x.accounts.delete(id) -> AccountDeleteResponse @@ -489,6 +488,18 @@ Methods: - client.x.account_connection_challenges.submit(id, \*\*params) -> AccountConnectionChallengeSubmitResponse +## AccountConnectionAttempts + +Types: + +```python +from x_twitter_scraper.types.x import AccountConnectionAttemptRetrieveResponse +``` + +Methods: + +- client.x.account_connection_attempts.retrieve(id) -> AccountConnectionAttemptRetrieveResponse + ## Bookmarks Types: diff --git a/pyproject.toml b/pyproject.toml index 04a8828..ff63ccb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "x_twitter_scraper" -version = "0.6.1" +version = "0.7.0" description = "Python library for the Xquik API. Not affiliated with X Corp." dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/x_twitter_scraper/_version.py b/src/x_twitter_scraper/_version.py index abef8df..d459452 100644 --- a/src/x_twitter_scraper/_version.py +++ b/src/x_twitter_scraper/_version.py @@ -5,4 +5,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "x_twitter_scraper" -__version__ = "0.6.1" # x-release-please-version +__version__ = "0.7.0" # x-release-please-version diff --git a/src/x_twitter_scraper/resources/compose.py b/src/x_twitter_scraper/resources/compose.py index cf5ddd8..37ee572 100644 --- a/src/x_twitter_scraper/resources/compose.py +++ b/src/x_twitter_scraper/resources/compose.py @@ -67,10 +67,10 @@ def create( ) -> ComposeCreateResponse: """Run one step of Xquik's three-step writing workflow. - Compose returns questions - and editorial rules. Refine returns goal-specific guidance. Score applies - deterministic text checks. It does not predict reach or expose X ranking - weights. + Compose returns questions, + editorial rules, and source-specific Radar recommendations. Refine returns + goal-specific guidance. Score applies deterministic text checks. It does not + predict reach or expose X ranking weights. Args: topic: Subject for the post. @@ -109,10 +109,10 @@ def create( ) -> ComposeCreateResponse: """Run one step of Xquik's three-step writing workflow. - Compose returns questions - and editorial rules. Refine returns goal-specific guidance. Score applies - deterministic text checks. It does not predict reach or expose X ranking - weights. + Compose returns questions, + editorial rules, and source-specific Radar recommendations. Refine returns + goal-specific guidance. Score applies deterministic text checks. It does not + predict reach or expose X ranking weights. Args: goal: Editorial goal for the guidance. @@ -154,10 +154,10 @@ def create( ) -> ComposeCreateResponse: """Run one step of Xquik's three-step writing workflow. - Compose returns questions - and editorial rules. Refine returns goal-specific guidance. Score applies - deterministic text checks. It does not predict reach or expose X ranking - weights. + Compose returns questions, + editorial rules, and source-specific Radar recommendations. Refine returns + goal-specific guidance. Score applies deterministic text checks. It does not + predict reach or expose X ranking weights. Args: draft: Full post text for deterministic editorial checks. @@ -267,10 +267,10 @@ async def create( ) -> ComposeCreateResponse: """Run one step of Xquik's three-step writing workflow. - Compose returns questions - and editorial rules. Refine returns goal-specific guidance. Score applies - deterministic text checks. It does not predict reach or expose X ranking - weights. + Compose returns questions, + editorial rules, and source-specific Radar recommendations. Refine returns + goal-specific guidance. Score applies deterministic text checks. It does not + predict reach or expose X ranking weights. Args: topic: Subject for the post. @@ -309,10 +309,10 @@ async def create( ) -> ComposeCreateResponse: """Run one step of Xquik's three-step writing workflow. - Compose returns questions - and editorial rules. Refine returns goal-specific guidance. Score applies - deterministic text checks. It does not predict reach or expose X ranking - weights. + Compose returns questions, + editorial rules, and source-specific Radar recommendations. Refine returns + goal-specific guidance. Score applies deterministic text checks. It does not + predict reach or expose X ranking weights. Args: goal: Editorial goal for the guidance. @@ -354,10 +354,10 @@ async def create( ) -> ComposeCreateResponse: """Run one step of Xquik's three-step writing workflow. - Compose returns questions - and editorial rules. Refine returns goal-specific guidance. Score applies - deterministic text checks. It does not predict reach or expose X ranking - weights. + Compose returns questions, + editorial rules, and source-specific Radar recommendations. Refine returns + goal-specific guidance. Score applies deterministic text checks. It does not + predict reach or expose X ranking weights. Args: draft: Full post text for deterministic editorial checks. diff --git a/src/x_twitter_scraper/resources/x/__init__.py b/src/x_twitter_scraper/resources/x/__init__.py index e40b088..562a792 100644 --- a/src/x_twitter_scraper/resources/x/__init__.py +++ b/src/x_twitter_scraper/resources/x/__init__.py @@ -100,6 +100,14 @@ WriteActionsResourceWithStreamingResponse, AsyncWriteActionsResourceWithStreamingResponse, ) +from .account_connection_attempts import ( + AccountConnectionAttemptsResource, + AsyncAccountConnectionAttemptsResource, + AccountConnectionAttemptsResourceWithRawResponse, + AsyncAccountConnectionAttemptsResourceWithRawResponse, + AccountConnectionAttemptsResourceWithStreamingResponse, + AsyncAccountConnectionAttemptsResourceWithStreamingResponse, +) from .account_connection_challenges import ( AccountConnectionChallengesResource, AsyncAccountConnectionChallengesResource, @@ -170,6 +178,12 @@ "AsyncAccountConnectionChallengesResourceWithRawResponse", "AccountConnectionChallengesResourceWithStreamingResponse", "AsyncAccountConnectionChallengesResourceWithStreamingResponse", + "AccountConnectionAttemptsResource", + "AsyncAccountConnectionAttemptsResource", + "AccountConnectionAttemptsResourceWithRawResponse", + "AsyncAccountConnectionAttemptsResourceWithRawResponse", + "AccountConnectionAttemptsResourceWithStreamingResponse", + "AsyncAccountConnectionAttemptsResourceWithStreamingResponse", "BookmarksResource", "AsyncBookmarksResource", "BookmarksResourceWithRawResponse", diff --git a/src/x_twitter_scraper/resources/x/account_connection_attempts.py b/src/x_twitter_scraper/resources/x/account_connection_attempts.py new file mode 100644 index 0000000..c5d31d4 --- /dev/null +++ b/src/x_twitter_scraper/resources/x/account_connection_attempts.py @@ -0,0 +1,180 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Any, cast + +import httpx + +from ..._types import Body, Query, Headers, NotGiven, not_given +from ..._utils import path_template +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..._base_client import make_request_options +from ...types.x.account_connection_attempt_retrieve_response import AccountConnectionAttemptRetrieveResponse + +__all__ = ["AccountConnectionAttemptsResource", "AsyncAccountConnectionAttemptsResource"] + + +class AccountConnectionAttemptsResource(SyncAPIResource): + """Connected X account management""" + + @cached_property + def with_raw_response(self) -> AccountConnectionAttemptsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Xquik-dev/x-twitter-scraper-python#accessing-raw-response-data-eg-headers + """ + return AccountConnectionAttemptsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccountConnectionAttemptsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Xquik-dev/x-twitter-scraper-python#with_streaming_response + """ + return AccountConnectionAttemptsResourceWithStreamingResponse(self) + + def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccountConnectionAttemptRetrieveResponse: + """ + Get X account connection status + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return cast( + AccountConnectionAttemptRetrieveResponse, + self._get( + path_template("/x/account-connection-attempts/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AccountConnectionAttemptRetrieveResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + +class AsyncAccountConnectionAttemptsResource(AsyncAPIResource): + """Connected X account management""" + + @cached_property + def with_raw_response(self) -> AsyncAccountConnectionAttemptsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Xquik-dev/x-twitter-scraper-python#accessing-raw-response-data-eg-headers + """ + return AsyncAccountConnectionAttemptsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccountConnectionAttemptsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Xquik-dev/x-twitter-scraper-python#with_streaming_response + """ + return AsyncAccountConnectionAttemptsResourceWithStreamingResponse(self) + + async def retrieve( + self, + id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AccountConnectionAttemptRetrieveResponse: + """ + Get X account connection status + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not id: + raise ValueError(f"Expected a non-empty value for `id` but received {id!r}") + return cast( + AccountConnectionAttemptRetrieveResponse, + await self._get( + path_template("/x/account-connection-attempts/{id}", id=id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=cast( + Any, AccountConnectionAttemptRetrieveResponse + ), # Union types cannot be passed in as arguments in the type system + ), + ) + + +class AccountConnectionAttemptsResourceWithRawResponse: + def __init__(self, account_connection_attempts: AccountConnectionAttemptsResource) -> None: + self._account_connection_attempts = account_connection_attempts + + self.retrieve = to_raw_response_wrapper( + account_connection_attempts.retrieve, + ) + + +class AsyncAccountConnectionAttemptsResourceWithRawResponse: + def __init__(self, account_connection_attempts: AsyncAccountConnectionAttemptsResource) -> None: + self._account_connection_attempts = account_connection_attempts + + self.retrieve = async_to_raw_response_wrapper( + account_connection_attempts.retrieve, + ) + + +class AccountConnectionAttemptsResourceWithStreamingResponse: + def __init__(self, account_connection_attempts: AccountConnectionAttemptsResource) -> None: + self._account_connection_attempts = account_connection_attempts + + self.retrieve = to_streamed_response_wrapper( + account_connection_attempts.retrieve, + ) + + +class AsyncAccountConnectionAttemptsResourceWithStreamingResponse: + def __init__(self, account_connection_attempts: AsyncAccountConnectionAttemptsResource) -> None: + self._account_connection_attempts = account_connection_attempts + + self.retrieve = async_to_streamed_response_wrapper( + account_connection_attempts.retrieve, + ) diff --git a/src/x_twitter_scraper/resources/x/accounts.py b/src/x_twitter_scraper/resources/x/accounts.py index 8efaf97..e2c7a34 100644 --- a/src/x_twitter_scraper/resources/x/accounts.py +++ b/src/x_twitter_scraper/resources/x/accounts.py @@ -22,7 +22,6 @@ 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 @@ -57,15 +56,15 @@ def create( *, email: str, password: str, + totp_secret: str, username: str, - totp_secret: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AccountCreateResponse: + ) -> object: """ Connect X account @@ -74,9 +73,9 @@ def create( password: Account password - username: X username + totp_secret: Authenticator App TOTP secret required for durable login - totp_secret: TOTP secret for 2FA + username: X username extra_headers: Send extra headers @@ -92,15 +91,15 @@ def create( { "email": email, "password": password, - "username": username, "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=AccountCreateResponse, + cast_to=object, ) def retrieve( @@ -232,7 +231,7 @@ def reauth( email: Email for the X account (updates stored email) - totp_secret: TOTP secret for 2FA re-authentication + totp_secret: Replacement Authenticator App TOTP secret. Omit it to reuse the saved secret. extra_headers: Send extra headers @@ -288,15 +287,15 @@ async def create( *, email: str, password: str, + totp_secret: str, username: str, - totp_secret: str | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Headers | None = None, extra_query: Query | None = None, extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, - ) -> AccountCreateResponse: + ) -> object: """ Connect X account @@ -305,9 +304,9 @@ async def create( password: Account password - username: X username + totp_secret: Authenticator App TOTP secret required for durable login - totp_secret: TOTP secret for 2FA + username: X username extra_headers: Send extra headers @@ -323,15 +322,15 @@ async def create( { "email": email, "password": password, - "username": username, "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=AccountCreateResponse, + cast_to=object, ) async def retrieve( @@ -463,7 +462,7 @@ async def reauth( email: Email for the X account (updates stored email) - totp_secret: TOTP secret for 2FA re-authentication + totp_secret: Replacement Authenticator App TOTP secret. Omit it to reuse the saved secret. extra_headers: Send extra headers diff --git a/src/x_twitter_scraper/resources/x/x.py b/src/x_twitter_scraper/resources/x/x.py index 8171ec7..041f7ba 100644 --- a/src/x_twitter_scraper/resources/x/x.py +++ b/src/x_twitter_scraper/resources/x/x.py @@ -110,6 +110,14 @@ CommunitiesResourceWithStreamingResponse, AsyncCommunitiesResourceWithStreamingResponse, ) +from .account_connection_attempts import ( + AccountConnectionAttemptsResource, + AsyncAccountConnectionAttemptsResource, + AccountConnectionAttemptsResourceWithRawResponse, + AsyncAccountConnectionAttemptsResourceWithRawResponse, + AccountConnectionAttemptsResourceWithStreamingResponse, + AsyncAccountConnectionAttemptsResourceWithStreamingResponse, +) from ...types.x_get_trends_response import XGetTrendsResponse from .account_connection_challenges import ( AccountConnectionChallengesResource, @@ -172,6 +180,11 @@ def account_connection_challenges(self) -> AccountConnectionChallengesResource: """Connected X account management""" return AccountConnectionChallengesResource(self._client) + @cached_property + def account_connection_attempts(self) -> AccountConnectionAttemptsResource: + """Connected X account management""" + return AccountConnectionAttemptsResource(self._client) + @cached_property def bookmarks(self) -> BookmarksResource: """Look up, search, and analyze individual tweets""" @@ -420,6 +433,11 @@ def account_connection_challenges(self) -> AsyncAccountConnectionChallengesResou """Connected X account management""" return AsyncAccountConnectionChallengesResource(self._client) + @cached_property + def account_connection_attempts(self) -> AsyncAccountConnectionAttemptsResource: + """Connected X account management""" + return AsyncAccountConnectionAttemptsResource(self._client) + @cached_property def bookmarks(self) -> AsyncBookmarksResource: """Look up, search, and analyze individual tweets""" @@ -684,6 +702,11 @@ def account_connection_challenges(self) -> AccountConnectionChallengesResourceWi """Connected X account management""" return AccountConnectionChallengesResourceWithRawResponse(self._x.account_connection_challenges) + @cached_property + def account_connection_attempts(self) -> AccountConnectionAttemptsResourceWithRawResponse: + """Connected X account management""" + return AccountConnectionAttemptsResourceWithRawResponse(self._x.account_connection_attempts) + @cached_property def bookmarks(self) -> BookmarksResourceWithRawResponse: """Look up, search, and analyze individual tweets""" @@ -757,6 +780,11 @@ def account_connection_challenges(self) -> AsyncAccountConnectionChallengesResou """Connected X account management""" return AsyncAccountConnectionChallengesResourceWithRawResponse(self._x.account_connection_challenges) + @cached_property + def account_connection_attempts(self) -> AsyncAccountConnectionAttemptsResourceWithRawResponse: + """Connected X account management""" + return AsyncAccountConnectionAttemptsResourceWithRawResponse(self._x.account_connection_attempts) + @cached_property def bookmarks(self) -> AsyncBookmarksResourceWithRawResponse: """Look up, search, and analyze individual tweets""" @@ -830,6 +858,11 @@ def account_connection_challenges(self) -> AccountConnectionChallengesResourceWi """Connected X account management""" return AccountConnectionChallengesResourceWithStreamingResponse(self._x.account_connection_challenges) + @cached_property + def account_connection_attempts(self) -> AccountConnectionAttemptsResourceWithStreamingResponse: + """Connected X account management""" + return AccountConnectionAttemptsResourceWithStreamingResponse(self._x.account_connection_attempts) + @cached_property def bookmarks(self) -> BookmarksResourceWithStreamingResponse: """Look up, search, and analyze individual tweets""" @@ -903,6 +936,11 @@ def account_connection_challenges(self) -> AsyncAccountConnectionChallengesResou """Connected X account management""" return AsyncAccountConnectionChallengesResourceWithStreamingResponse(self._x.account_connection_challenges) + @cached_property + def account_connection_attempts(self) -> AsyncAccountConnectionAttemptsResourceWithStreamingResponse: + """Connected X account management""" + return AsyncAccountConnectionAttemptsResourceWithStreamingResponse(self._x.account_connection_attempts) + @cached_property def bookmarks(self) -> AsyncBookmarksResourceWithStreamingResponse: """Look up, search, and analyze individual tweets""" diff --git a/src/x_twitter_scraper/types/compose_create_response.py b/src/x_twitter_scraper/types/compose_create_response.py index e0c8162..f6c8b2e 100644 --- a/src/x_twitter_scraper/types/compose_create_response.py +++ b/src/x_twitter_scraper/types/compose_create_response.py @@ -16,6 +16,7 @@ "ComposePrepareResult", "ComposePrepareResultContentRule", "ComposePrepareResultEngagementMultiplier", + "ComposePrepareResultRadarRecommendation", "ComposePrepareResultScorerWeight", "ComposePrepareResultSavedStyle", "ComposeRefineResult", @@ -36,6 +37,19 @@ class ComposePrepareResultEngagementMultiplier(BaseModel): multiplier: Literal["Production weight not published by X"] +class ComposePrepareResultRadarRecommendation(BaseModel): + endpoint: str + """Radar endpoint for this source.""" + + guidance: str + """Source-specific drafting guidance.""" + + source: Literal["reddit", "github", "trustmrr", "hacker_news", "google_trends", "wikipedia", "polymarket"] + + use_for: str = FieldInfo(alias="useFor") + """Current-topic research this source supports.""" + + class ComposePrepareResultScorerWeight(BaseModel): context: str """Signal direction and publication limit.""" @@ -70,6 +84,9 @@ class ComposePrepareResult(BaseModel): next_step: str = FieldInfo(alias="nextStep") + radar_recommendations: List[ComposePrepareResultRadarRecommendation] = FieldInfo(alias="radarRecommendations") + """Sources and guidance for researching a fresh post angle.""" + scorer_weights: List[ComposePrepareResultScorerWeight] = FieldInfo(alias="scorerWeights") """Published signal names with unpublished weights as null.""" diff --git a/src/x_twitter_scraper/types/radar_item.py b/src/x_twitter_scraper/types/radar_item.py index 99f43e1..b6c358c 100644 --- a/src/x_twitter_scraper/types/radar_item.py +++ b/src/x_twitter_scraper/types/radar_item.py @@ -4,7 +4,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Dict, Optional +from typing import TYPE_CHECKING, Dict, Optional from datetime import datetime from typing_extensions import Literal @@ -12,7 +12,70 @@ from .._models import BaseModel -__all__ = ["RadarItem"] +__all__ = ["RadarItem", "Metadata"] + + +class Metadata(BaseModel): + """Source-specific fields. + + Shape varies per source: + - reddit: { author, authorId?, subreddit, subredditId?, + subredditSubscribers?, sourceFormat, score?, upvoteRatio?, + estimatedUpvotes?, estimatedDownvotes?, numberComments?, + numberCrossposts?, selftext?, contentUrl?, domain?, postHint?, + linkFlairText?, distinguished?, totalAwardsReceived?, viewCount?, + editedAt?, galleryImageUrls?, redditVideo?, archived?, contestMode?, + isCrosspostable?, isMeta?, isNsfw?, isOriginalContent?, + isRobotIndexable?, isSelf?, isSpoiler?, isVideo?, locked?, + stickied? }. `score` is Reddit's public net score. Exact public + upvote and downvote counts are not available. Estimated counts + derive from the public score and upvote ratio, which Reddit may + fuzz. Comment bodies are not included. Current items combine + public listing discovery with server-rendered post data and use + `sourceFormat: html`; `json` and `rss` remain for legacy rows. + - github: { starsToday: number } + - hacker_news: { points: number, numberComments: number } + - google_trends: { approxTraffic: number } + - polymarket: { volume24hr: number } + - wikipedia: { views: number } + - trustmrr: { mrr, growthPercent, last30Days, total, customers, activeSubscriptions, onSale, xHandle?, category?, askingPrice?, country?, foundedDate?, googleSearchImpressionsLast30Days?, growthMrrPercent?, multiple?, paymentProvider?, profitMarginLast30Days?, rank?, revenuePerVisitor?, targetAudience?, visitorsLast30Days? } + For the startup growth source, xHandle is the founder's X username + without @. The rank field is the source's revenue rank. Result order + represents reported 30-day revenue-growth rank. + """ + + author: Optional[str] = None + + content_url: Optional[str] = FieldInfo(alias="contentUrl", default=None) + + estimated_downvotes: Optional[int] = FieldInfo(alias="estimatedDownvotes", default=None) + + estimated_upvotes: Optional[int] = FieldInfo(alias="estimatedUpvotes", default=None) + + number_comments: Optional[int] = FieldInfo(alias="numberComments", default=None) + + score: Optional[int] = None + + selftext: Optional[str] = None + + source_format: Optional[Literal["html", "json", "rss"]] = FieldInfo(alias="sourceFormat", default=None) + """Current items use html. json and rss are retained for legacy rows.""" + + subreddit: Optional[str] = None + + upvote_ratio: Optional[float] = FieldInfo(alias="upvoteRatio", default=None) + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, object] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> object: ... + else: + __pydantic_extra__: Dict[str, object] class RadarItem(BaseModel): @@ -28,11 +91,23 @@ class RadarItem(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") language: str + """BCP-47 language code. und means the source did not identify a language.""" - metadata: Dict[str, object] + metadata: Metadata """Source-specific fields. Shape varies per source: - - reddit: { subreddit: string, author: string } + - reddit: { author, authorId?, subreddit, subredditId?, subredditSubscribers?, + sourceFormat, score?, upvoteRatio?, estimatedUpvotes?, estimatedDownvotes?, + numberComments?, numberCrossposts?, selftext?, contentUrl?, domain?, + postHint?, linkFlairText?, distinguished?, totalAwardsReceived?, viewCount?, + editedAt?, galleryImageUrls?, redditVideo?, archived?, contestMode?, + isCrosspostable?, isMeta?, isNsfw?, isOriginalContent?, isRobotIndexable?, + isSelf?, isSpoiler?, isVideo?, locked?, stickied? }. `score` is Reddit's + public net score. Exact public upvote and downvote counts are not available. + Estimated counts derive from the public score and upvote ratio, which Reddit + may fuzz. Comment bodies are not included. Current items combine public + listing discovery with server-rendered post data and use `sourceFormat: html`; + `json` and `rss` remain for legacy rows. - github: { starsToday: number } - hacker_news: { points: number, numberComments: number } - google_trends: { approxTraffic: number } @@ -40,7 +115,12 @@ class RadarItem(BaseModel): - wikipedia: { views: number } - trustmrr: { mrr, growthPercent, last30Days, total, customers, activeSubscriptions, onSale, xHandle?, category?, askingPrice?, country?, - growthMrrPercent?, multiple?, paymentProvider?, rank? } + foundedDate?, googleSearchImpressionsLast30Days?, growthMrrPercent?, + multiple?, paymentProvider?, profitMarginLast30Days?, rank?, + revenuePerVisitor?, targetAudience?, visitorsLast30Days? } For the startup + growth source, xHandle is the founder's X username without @. The rank field + is the source's revenue rank. Result order represents reported 30-day + revenue-growth rank. """ published_at: datetime = FieldInfo(alias="publishedAt") @@ -59,5 +139,6 @@ class RadarItem(BaseModel): description: Optional[str] = None image_url: Optional[str] = FieldInfo(alias="imageUrl", default=None) + """Source image. Startup growth items return the logo here.""" url: Optional[str] = None diff --git a/src/x_twitter_scraper/types/x/__init__.py b/src/x_twitter_scraper/types/x/__init__.py index d96125f..a2de5e3 100644 --- a/src/x_twitter_scraper/types/x/__init__.py +++ b/src/x_twitter_scraper/types/x/__init__.py @@ -27,7 +27,6 @@ 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 @@ -80,6 +79,9 @@ from .account_connection_challenge_submit_params import ( AccountConnectionChallengeSubmitParams as AccountConnectionChallengeSubmitParams, ) +from .account_connection_attempt_retrieve_response import ( + AccountConnectionAttemptRetrieveResponse as AccountConnectionAttemptRetrieveResponse, +) from .account_connection_challenge_submit_response import ( AccountConnectionChallengeSubmitResponse as AccountConnectionChallengeSubmitResponse, ) 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 new file mode 100644 index 0000000..7f8592f --- /dev/null +++ b/src/x_twitter_scraper/types/x/account_connection_attempt_retrieve_response.py @@ -0,0 +1,82 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Union, Optional +from datetime import datetime +from typing_extensions import Literal, TypeAlias + +from pydantic import Field as FieldInfo + +from ..._models import BaseModel + +__all__ = [ + "AccountConnectionAttemptRetrieveResponse", + "XAccountConnectionAttemptPending", + "XAccountConnectionAttemptSuccess", + "XAccountConnectionAttemptFailed", + "XAccountConnectionChallenge", +] + + +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 XAccountConnectionAttemptSuccess(BaseModel): + """The account connected successfully.""" + + id: str + + object: Literal["x_account_connection_attempt"] + + status: Literal["success"] + + +class XAccountConnectionAttemptFailed(BaseModel): + """The connection reached a final failure.""" + + id: str + + error: str + + object: Literal["x_account_connection_attempt"] + + retryable: bool + + status: Literal["failed"] + + reason: Optional[str] = None + + +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 + + +AccountConnectionAttemptRetrieveResponse: TypeAlias = Union[ + XAccountConnectionAttemptPending, + XAccountConnectionAttemptSuccess, + XAccountConnectionAttemptFailed, + XAccountConnectionChallenge, +] diff --git a/src/x_twitter_scraper/types/x/account_create_params.py b/src/x_twitter_scraper/types/x/account_create_params.py index 885e079..834f6f4 100644 --- a/src/x_twitter_scraper/types/x/account_create_params.py +++ b/src/x_twitter_scraper/types/x/account_create_params.py @@ -18,8 +18,8 @@ class AccountCreateParams(TypedDict, total=False): password: Required[str] """Account password""" + totp_secret: Required[str] + """Authenticator App TOTP secret required for durable login""" + username: Required[str] """X username""" - - totp_secret: str - """TOTP secret for 2FA""" diff --git a/src/x_twitter_scraper/types/x/account_create_response.py b/src/x_twitter_scraper/types/x/account_create_response.py deleted file mode 100644 index 26efad3..0000000 --- a/src/x_twitter_scraper/types/x/account_create_response.py +++ /dev/null @@ -1,30 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Xquik contributors -# -# SPDX-License-Identifier: Apache-2.0 - -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from datetime import datetime -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from ..._models import BaseModel - -__all__ = ["AccountCreateResponse"] - - -class AccountCreateResponse(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: str - - x_user_id: str = FieldInfo(alias="xUserId") - - x_username: str = FieldInfo(alias="xUsername") diff --git a/src/x_twitter_scraper/types/x/account_reauth_params.py b/src/x_twitter_scraper/types/x/account_reauth_params.py index 5cb6225..e5158f6 100644 --- a/src/x_twitter_scraper/types/x/account_reauth_params.py +++ b/src/x_twitter_scraper/types/x/account_reauth_params.py @@ -19,4 +19,4 @@ class AccountReauthParams(TypedDict, total=False): """Email for the X account (updates stored email)""" totp_secret: str - """TOTP secret for 2FA re-authentication""" + """Replacement Authenticator App TOTP secret. Omit it to reuse the saved secret.""" diff --git a/src/x_twitter_scraper/types/x/x_account.py b/src/x_twitter_scraper/types/x/x_account.py index 43bcbed..01ffda7 100644 --- a/src/x_twitter_scraper/types/x/x_account.py +++ b/src/x_twitter_scraper/types/x/x_account.py @@ -27,10 +27,11 @@ class XAccount(BaseModel): health: Literal["healthy", "locked", "needsReauth", "recovering", "suspended", "temporaryIssue"] """Derived connection health. - `healthy` = session active. `needsReauth` = user must submit fresh credentials. + `healthy` = ready to use. `needsReauth` = user must submit fresh credentials. `locked` = X locked the account; unlock on x.com first. `suspended` = X banned - the account. `recovering` = past cooldown, will auto-retry on next use. - `temporaryIssue` = temporary connection problem; retry shortly. + the account. `recovering` = cooldown ended; the account can reconnect on its + next use. `temporaryIssue` = temporary connection problem; wait before the next + use. """ status: str diff --git a/tests/api_resources/x/test_account_connection_attempts.py b/tests/api_resources/x/test_account_connection_attempts.py new file mode 100644 index 0000000..e72ae2f --- /dev/null +++ b/tests/api_resources/x/test_account_connection_attempts.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from x_twitter_scraper import XTwitterScraper, AsyncXTwitterScraper +from x_twitter_scraper.types.x import AccountConnectionAttemptRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAccountConnectionAttempts: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: XTwitterScraper) -> None: + account_connection_attempt = client.x.account_connection_attempts.retrieve( + "xatt_0123456789abcdef0123456789abcdef", + ) + assert_matches_type(AccountConnectionAttemptRetrieveResponse, account_connection_attempt, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: XTwitterScraper) -> None: + response = client.x.account_connection_attempts.with_raw_response.retrieve( + "xatt_0123456789abcdef0123456789abcdef", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account_connection_attempt = response.parse() + assert_matches_type(AccountConnectionAttemptRetrieveResponse, account_connection_attempt, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: XTwitterScraper) -> None: + with client.x.account_connection_attempts.with_streaming_response.retrieve( + "xatt_0123456789abcdef0123456789abcdef", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account_connection_attempt = response.parse() + assert_matches_type(AccountConnectionAttemptRetrieveResponse, account_connection_attempt, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: XTwitterScraper) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + client.x.account_connection_attempts.with_raw_response.retrieve( + "", + ) + + +class TestAsyncAccountConnectionAttempts: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncXTwitterScraper) -> None: + account_connection_attempt = await async_client.x.account_connection_attempts.retrieve( + "xatt_0123456789abcdef0123456789abcdef", + ) + assert_matches_type(AccountConnectionAttemptRetrieveResponse, account_connection_attempt, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncXTwitterScraper) -> None: + response = await async_client.x.account_connection_attempts.with_raw_response.retrieve( + "xatt_0123456789abcdef0123456789abcdef", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + account_connection_attempt = await response.parse() + assert_matches_type(AccountConnectionAttemptRetrieveResponse, account_connection_attempt, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncXTwitterScraper) -> None: + async with async_client.x.account_connection_attempts.with_streaming_response.retrieve( + "xatt_0123456789abcdef0123456789abcdef", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + account_connection_attempt = await response.parse() + assert_matches_type(AccountConnectionAttemptRetrieveResponse, account_connection_attempt, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncXTwitterScraper) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): + await async_client.x.account_connection_attempts.with_raw_response.retrieve( + "", + ) diff --git a/tests/api_resources/x/test_accounts.py b/tests/api_resources/x/test_accounts.py index 493525e..c3d34b5 100644 --- a/tests/api_resources/x/test_accounts.py +++ b/tests/api_resources/x/test_accounts.py @@ -16,7 +16,6 @@ from x_twitter_scraper.types.x import ( XAccountDetail, AccountListResponse, - AccountCreateResponse, AccountDeleteResponse, AccountReauthResponse, AccountBulkRetryResponse, @@ -34,20 +33,10 @@ def test_method_create(self, client: XTwitterScraper) -> None: account = client.x.accounts.create( email="account@example.invalid", password="", - username="your_x_username", - ) - assert_matches_type(AccountCreateResponse, account, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_create_with_all_params(self, client: XTwitterScraper) -> None: - account = client.x.accounts.create( - email="account@example.invalid", - password="", - username="your_x_username", totp_secret="", + username="your_x_username", ) - assert_matches_type(AccountCreateResponse, account, path=["response"]) + assert_matches_type(object, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -55,13 +44,14 @@ def test_raw_response_create(self, client: XTwitterScraper) -> None: response = client.x.accounts.with_raw_response.create( email="account@example.invalid", password="", + totp_secret="", username="your_x_username", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = response.parse() - assert_matches_type(AccountCreateResponse, account, path=["response"]) + assert_matches_type(object, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -69,13 +59,14 @@ def test_streaming_response_create(self, client: XTwitterScraper) -> None: with client.x.accounts.with_streaming_response.create( email="account@example.invalid", password="", + totp_secret="", username="your_x_username", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = response.parse() - assert_matches_type(AccountCreateResponse, account, path=["response"]) + assert_matches_type(object, account, path=["response"]) assert cast(Any, response.is_closed) is True @@ -288,20 +279,10 @@ async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: account = await async_client.x.accounts.create( email="account@example.invalid", password="", - username="your_x_username", - ) - assert_matches_type(AccountCreateResponse, account, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_create_with_all_params(self, async_client: AsyncXTwitterScraper) -> None: - account = await async_client.x.accounts.create( - email="account@example.invalid", - password="", - username="your_x_username", totp_secret="", + username="your_x_username", ) - assert_matches_type(AccountCreateResponse, account, path=["response"]) + assert_matches_type(object, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -309,13 +290,14 @@ async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> response = await async_client.x.accounts.with_raw_response.create( email="account@example.invalid", password="", + totp_secret="", username="your_x_username", ) assert response.is_closed is True assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = await response.parse() - assert_matches_type(AccountCreateResponse, account, path=["response"]) + assert_matches_type(object, account, path=["response"]) @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize @@ -323,13 +305,14 @@ async def test_streaming_response_create(self, async_client: AsyncXTwitterScrape async with async_client.x.accounts.with_streaming_response.create( email="account@example.invalid", password="", + totp_secret="", username="your_x_username", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" account = await response.parse() - assert_matches_type(AccountCreateResponse, account, path=["response"]) + assert_matches_type(object, account, path=["response"]) assert cast(Any, response.is_closed) is True diff --git a/tests/mock_api_routes.py b/tests/mock_api_routes.py index ebc5829..fba94ce 100644 --- a/tests/mock_api_routes.py +++ b/tests/mock_api_routes.py @@ -19,6 +19,7 @@ class ResponseKind(Enum): EMPTY = "empty" BINARY = "binary" + JSON_OBJECT = "json_object" ResponseType = type[BaseModel] | ResponseKind @@ -74,6 +75,8 @@ def _response_type(module_name: str, name: str) -> ResponseType: return ResponseKind.EMPTY if name in {"BinaryAPIResponse", "AsyncBinaryAPIResponse"}: return ResponseKind.BINARY + if name == "object": + return ResponseKind.JSON_OBJECT module = importlib.import_module(module_name) response_type = getattr(module, name, None) diff --git a/tests/mock_api_server.py b/tests/mock_api_server.py index ade40af..01d4a97 100644 --- a/tests/mock_api_server.py +++ b/tests/mock_api_server.py @@ -69,6 +69,9 @@ def _handle(self) -> None: if route.response_type is ResponseKind.BINARY: self._write_response(HTTPStatus.OK, b"fixture", "application/octet-stream") return + if route.response_type is ResponseKind.JSON_OBJECT: + self._write_response(HTTPStatus.OK, b"{}", "application/json") + return self._write_response( HTTPStatus.OK, self.model_payloads[route.response_type], diff --git a/uv.lock b/uv.lock index 568c5c9..ad6c6f1 100644 --- a/uv.lock +++ b/uv.lock @@ -1571,7 +1571,7 @@ wheels = [ [[package]] name = "x-twitter-scraper" -version = "0.6.1" +version = "0.7.0" source = { editable = "." } dependencies = [ { name = "anyio" },