diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 6360f06..442f2c3 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -12,9 +12,6 @@ jobs: publish: name: publish runs-on: ubuntu-latest - permissions: - contents: read - id-token: write steps: - uses: actions/checkout@v6 @@ -27,3 +24,5 @@ jobs: - name: Publish to PyPI run: | bash ./bin/publish-pypi + env: + PYPI_TOKEN: ${{ secrets.X_TWITTER_SCRAPER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 8c7b433..a19b2e1 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -17,3 +17,5 @@ jobs: - name: Check release environment run: | bash ./bin/check-release-environment + env: + PYPI_TOKEN: ${{ secrets.X_TWITTER_SCRAPER_PYPI_TOKEN || secrets.PYPI_TOKEN }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cce9240..da59f99 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.3.1" + ".": "0.4.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index db87955..d470135 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 117 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik%2Fx-twitter-scraper-d40c57a05527faf060d21c0e013729f371d88017b10680cea7c8fd6780ffaef5.yml -openapi_spec_hash: 597ebc460cf86740b9f6f7c95478dece -config_hash: 30ce23c9cfbf8fb8be9e5dd28a2124fa +configured_endpoints: 110 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/xquik%2Fx-twitter-scraper-2adc33156b4b42a4be18cc20c0205b38f0432d7958da99c65ee9b3f6a555ea0e.yml +openapi_spec_hash: be760f5620a268521d6793f65576a61f +config_hash: 320a9cb2f1293d1a7b73c63ab5865af5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 388dde5..0e6ef40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 0.4.0 (2026-04-22) + +Full Changelog: [v0.3.1...v0.4.0](https://github.com/Xquik-dev/x-twitter-scraper-python/compare/v0.3.1...v0.4.0) + +### Features + +* **api:** api update ([ea97c97](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/ea97c97b83f9388dfe339655bd7e1e2f39bf8537)) +* **api:** api update ([9ab5907](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/9ab59079a7538deaedc38cb221ba6421ff4dd39a)) +* **api:** api update ([e6edd37](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/e6edd371ab5a80748ae26a0173206e89634c5da5)) +* **api:** api update ([8e576ed](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/8e576ed647806a054a3f6b7f3be7dbcaffb36aef)) + + +### Bug Fixes + +* ensure file data are only sent as 1 parameter ([c86dcf1](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/c86dcf186aed37ef154028b708bc9cdf3a56bf39)) +* escape ampersand in OpenAPI summaries for C# XML docs ([d2d9141](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/d2d9141e286e7f032f9aec934e93f33414c7221e)) + + +### Performance Improvements + +* **client:** optimize file structure copying in multipart requests ([eb0688d](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/eb0688d0ae9da72c17a1b3550e0221d2f81a9ef4)) + + +### Chores + +* sync OpenAPI spec ([e7bfdb7](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/e7bfdb76972a4d2c6b5ddb5f0f1f815579fd175d)) +* wire production_repo for all targets ([18d4cc3](https://github.com/Xquik-dev/x-twitter-scraper-python/commit/18d4cc3d3c5796aaa637f3841224dfaa9e076a5b)) + ## 0.3.1 (2026-04-08) Full Changelog: [v0.3.0...v0.3.1](https://github.com/Xquik-dev/x-twitter-scraper-python/compare/v0.3.0...v0.3.1) diff --git a/README.md b/README.md index 52b5048..b2230c0 100644 --- a/README.md +++ b/README.md @@ -117,24 +117,6 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. -## Nested params - -Nested parameters are dictionaries, typed using `TypedDict`, for example: - -```python -from x_twitter_scraper import XTwitterScraper - -client = XTwitterScraper() - -integration = client.integrations.create( - config={"chat_id": "-1001234567890"}, - event_types=["tweet.new", "follower.gained"], - name="My Telegram Bot", - type="telegram", -) -print(integration.config) -``` - ## File uploads Request parameters that correspond to file uploads can be passed as `bytes`, or a [`PathLike`](https://docs.python.org/3/library/os.html#os.PathLike) instance or a tuple of `(filename, contents, media type)`. diff --git a/api.md b/api.md index 925359a..82e047f 100644 --- a/api.md +++ b/api.md @@ -1,7 +1,14 @@ # Shared Types ```python -from x_twitter_scraper.types import Error, EventType, PaginatedTweets, PaginatedUsers +from x_twitter_scraper.types import ( + Error, + EventType, + PaginatedTweets, + PaginatedUsers, + SearchTweet, + UserProfile, +) ``` # Account @@ -219,31 +226,6 @@ Methods: - client.webhooks.list_deliveries(id) -> WebhookListDeliveriesResponse - client.webhooks.test(id) -> WebhookTestResponse -# Integrations - -Types: - -```python -from x_twitter_scraper.types import ( - Integration, - IntegrationDelivery, - IntegrationListResponse, - IntegrationDeleteResponse, - IntegrationListDeliveriesResponse, - IntegrationSendTestResponse, -) -``` - -Methods: - -- client.integrations.create(\*\*params) -> Integration -- client.integrations.retrieve(id) -> Integration -- client.integrations.update(id, \*\*params) -> Integration -- client.integrations.list() -> IntegrationListResponse -- client.integrations.delete(id) -> IntegrationDeleteResponse -- client.integrations.list_deliveries(id, \*\*params) -> IntegrationListDeliveriesResponse -- client.integrations.send_test(id) -> IntegrationSendTestResponse - # X Types: @@ -261,7 +243,7 @@ Methods: - client.x.get_article(tweet_id) -> XGetArticleResponse - client.x.get_home_timeline(\*\*params) -> PaginatedTweets - client.x.get_notifications(\*\*params) -> XGetNotificationsResponse -- client.x.get_trends() -> XGetTrendsResponse +- client.x.get_trends(\*\*params) -> XGetTrendsResponse ## Tweets @@ -269,7 +251,6 @@ Types: ```python from x_twitter_scraper.types.x import ( - SearchTweet, TweetAuthor, TweetDetail, TweetCreateResponse, @@ -319,15 +300,9 @@ Methods: ## Users -Types: - -```python -from x_twitter_scraper.types.x import UserProfile -``` - Methods: -- client.x.users.retrieve(id) -> UserProfile +- client.x.users.retrieve(id) -> UserProfile - client.x.users.retrieve_batch(\*\*params) -> PaginatedUsers - client.x.users.retrieve_followers(id, \*\*params) -> PaginatedUsers - client.x.users.retrieve_followers_you_know(id, \*\*params) -> PaginatedUsers diff --git a/bin/check-release-environment b/bin/check-release-environment index 1e951e9..b845b0f 100644 --- a/bin/check-release-environment +++ b/bin/check-release-environment @@ -2,6 +2,10 @@ errors=() +if [ -z "${PYPI_TOKEN}" ]; then + errors+=("The PYPI_TOKEN secret has not been set. Please set it in either this repository's secrets or your organization secrets.") +fi + lenErrors=${#errors[@]} if [[ lenErrors -gt 0 ]]; then diff --git a/bin/publish-pypi b/bin/publish-pypi index 5895700..e72ca2f 100644 --- a/bin/publish-pypi +++ b/bin/publish-pypi @@ -4,8 +4,4 @@ set -eux rm -rf dist mkdir -p dist uv build -if [ -n "${PYPI_TOKEN:-}" ]; then - uv publish --token=$PYPI_TOKEN -else - uv publish -fi +uv publish --token=$PYPI_TOKEN diff --git a/pyproject.toml b/pyproject.toml index ad2ba10..785db55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "x_twitter_scraper" -version = "0.3.1" +version = "0.4.0" description = "The official Python library for the x-twitter-scraper API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/x_twitter_scraper/_client.py b/src/x_twitter_scraper/_client.py index 0d70bf3..b0e698e 100644 --- a/src/x_twitter_scraper/_client.py +++ b/src/x_twitter_scraper/_client.py @@ -50,7 +50,6 @@ webhooks, subscribe, extractions, - integrations, ) from .resources.x.x import XResource, AsyncXResource from .resources.draws import DrawsResource, AsyncDrawsResource @@ -67,7 +66,6 @@ from .resources.webhooks import WebhooksResource, AsyncWebhooksResource from .resources.subscribe import SubscribeResource, AsyncSubscribeResource from .resources.extractions import ExtractionsResource, AsyncExtractionsResource - from .resources.integrations import IntegrationsResource, AsyncIntegrationsResource from .resources.support.support import SupportResource, AsyncSupportResource __all__ = [ @@ -143,7 +141,7 @@ def __init__( @cached_property def account(self) -> AccountResource: - """Account info & settings""" + """Account info and settings""" from .resources.account import AccountResource return AccountResource(self) @@ -157,35 +155,35 @@ def api_keys(self) -> APIKeysResource: @cached_property def subscribe(self) -> SubscribeResource: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.subscribe import SubscribeResource return SubscribeResource(self) @cached_property def compose(self) -> ComposeResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.compose import ComposeResource return ComposeResource(self) @cached_property def drafts(self) -> DraftsResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.drafts import DraftsResource return DraftsResource(self) @cached_property def styles(self) -> StylesResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.styles import StylesResource return StylesResource(self) @cached_property def radar(self) -> RadarResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.radar import RadarResource return RadarResource(self) @@ -220,28 +218,20 @@ def draws(self) -> DrawsResource: @cached_property def webhooks(self) -> WebhooksResource: - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" from .resources.webhooks import WebhooksResource return WebhooksResource(self) - @cached_property - def integrations(self) -> IntegrationsResource: - """Push notification integrations (Telegram)""" - from .resources.integrations import IntegrationsResource - - return IntegrationsResource(self) - @cached_property def x(self) -> XResource: - """X data lookups (subscription required)""" from .resources.x import XResource return XResource(self) @cached_property def trends(self) -> TrendsResource: - """Trending topics by region""" + """Trending topics and hashtags by region""" from .resources.trends import TrendsResource return TrendsResource(self) @@ -254,7 +244,7 @@ def support(self) -> SupportResource: @cached_property def credits(self) -> CreditsResource: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.credits import CreditsResource return CreditsResource(self) @@ -462,7 +452,7 @@ def __init__( @cached_property def account(self) -> AsyncAccountResource: - """Account info & settings""" + """Account info and settings""" from .resources.account import AsyncAccountResource return AsyncAccountResource(self) @@ -476,35 +466,35 @@ def api_keys(self) -> AsyncAPIKeysResource: @cached_property def subscribe(self) -> AsyncSubscribeResource: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.subscribe import AsyncSubscribeResource return AsyncSubscribeResource(self) @cached_property def compose(self) -> AsyncComposeResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.compose import AsyncComposeResource return AsyncComposeResource(self) @cached_property def drafts(self) -> AsyncDraftsResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.drafts import AsyncDraftsResource return AsyncDraftsResource(self) @cached_property def styles(self) -> AsyncStylesResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.styles import AsyncStylesResource return AsyncStylesResource(self) @cached_property def radar(self) -> AsyncRadarResource: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.radar import AsyncRadarResource return AsyncRadarResource(self) @@ -539,28 +529,20 @@ def draws(self) -> AsyncDrawsResource: @cached_property def webhooks(self) -> AsyncWebhooksResource: - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" from .resources.webhooks import AsyncWebhooksResource return AsyncWebhooksResource(self) - @cached_property - def integrations(self) -> AsyncIntegrationsResource: - """Push notification integrations (Telegram)""" - from .resources.integrations import AsyncIntegrationsResource - - return AsyncIntegrationsResource(self) - @cached_property def x(self) -> AsyncXResource: - """X data lookups (subscription required)""" from .resources.x import AsyncXResource return AsyncXResource(self) @cached_property def trends(self) -> AsyncTrendsResource: - """Trending topics by region""" + """Trending topics and hashtags by region""" from .resources.trends import AsyncTrendsResource return AsyncTrendsResource(self) @@ -573,7 +555,7 @@ def support(self) -> AsyncSupportResource: @cached_property def credits(self) -> AsyncCreditsResource: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.credits import AsyncCreditsResource return AsyncCreditsResource(self) @@ -728,7 +710,7 @@ def __init__(self, client: XTwitterScraper) -> None: @cached_property def account(self) -> account.AccountResourceWithRawResponse: - """Account info & settings""" + """Account info and settings""" from .resources.account import AccountResourceWithRawResponse return AccountResourceWithRawResponse(self._client.account) @@ -742,35 +724,35 @@ def api_keys(self) -> api_keys.APIKeysResourceWithRawResponse: @cached_property def subscribe(self) -> subscribe.SubscribeResourceWithRawResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.subscribe import SubscribeResourceWithRawResponse return SubscribeResourceWithRawResponse(self._client.subscribe) @cached_property def compose(self) -> compose.ComposeResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.compose import ComposeResourceWithRawResponse return ComposeResourceWithRawResponse(self._client.compose) @cached_property def drafts(self) -> drafts.DraftsResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.drafts import DraftsResourceWithRawResponse return DraftsResourceWithRawResponse(self._client.drafts) @cached_property def styles(self) -> styles.StylesResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.styles import StylesResourceWithRawResponse return StylesResourceWithRawResponse(self._client.styles) @cached_property def radar(self) -> radar.RadarResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.radar import RadarResourceWithRawResponse return RadarResourceWithRawResponse(self._client.radar) @@ -805,28 +787,20 @@ def draws(self) -> draws.DrawsResourceWithRawResponse: @cached_property def webhooks(self) -> webhooks.WebhooksResourceWithRawResponse: - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" from .resources.webhooks import WebhooksResourceWithRawResponse return WebhooksResourceWithRawResponse(self._client.webhooks) - @cached_property - def integrations(self) -> integrations.IntegrationsResourceWithRawResponse: - """Push notification integrations (Telegram)""" - from .resources.integrations import IntegrationsResourceWithRawResponse - - return IntegrationsResourceWithRawResponse(self._client.integrations) - @cached_property def x(self) -> x.XResourceWithRawResponse: - """X data lookups (subscription required)""" from .resources.x import XResourceWithRawResponse return XResourceWithRawResponse(self._client.x) @cached_property def trends(self) -> trends.TrendsResourceWithRawResponse: - """Trending topics by region""" + """Trending topics and hashtags by region""" from .resources.trends import TrendsResourceWithRawResponse return TrendsResourceWithRawResponse(self._client.trends) @@ -839,7 +813,7 @@ def support(self) -> support.SupportResourceWithRawResponse: @cached_property def credits(self) -> credits.CreditsResourceWithRawResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.credits import CreditsResourceWithRawResponse return CreditsResourceWithRawResponse(self._client.credits) @@ -853,7 +827,7 @@ def __init__(self, client: AsyncXTwitterScraper) -> None: @cached_property def account(self) -> account.AsyncAccountResourceWithRawResponse: - """Account info & settings""" + """Account info and settings""" from .resources.account import AsyncAccountResourceWithRawResponse return AsyncAccountResourceWithRawResponse(self._client.account) @@ -867,35 +841,35 @@ def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithRawResponse: @cached_property def subscribe(self) -> subscribe.AsyncSubscribeResourceWithRawResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.subscribe import AsyncSubscribeResourceWithRawResponse return AsyncSubscribeResourceWithRawResponse(self._client.subscribe) @cached_property def compose(self) -> compose.AsyncComposeResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.compose import AsyncComposeResourceWithRawResponse return AsyncComposeResourceWithRawResponse(self._client.compose) @cached_property def drafts(self) -> drafts.AsyncDraftsResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.drafts import AsyncDraftsResourceWithRawResponse return AsyncDraftsResourceWithRawResponse(self._client.drafts) @cached_property def styles(self) -> styles.AsyncStylesResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.styles import AsyncStylesResourceWithRawResponse return AsyncStylesResourceWithRawResponse(self._client.styles) @cached_property def radar(self) -> radar.AsyncRadarResourceWithRawResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.radar import AsyncRadarResourceWithRawResponse return AsyncRadarResourceWithRawResponse(self._client.radar) @@ -930,28 +904,20 @@ def draws(self) -> draws.AsyncDrawsResourceWithRawResponse: @cached_property def webhooks(self) -> webhooks.AsyncWebhooksResourceWithRawResponse: - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" from .resources.webhooks import AsyncWebhooksResourceWithRawResponse return AsyncWebhooksResourceWithRawResponse(self._client.webhooks) - @cached_property - def integrations(self) -> integrations.AsyncIntegrationsResourceWithRawResponse: - """Push notification integrations (Telegram)""" - from .resources.integrations import AsyncIntegrationsResourceWithRawResponse - - return AsyncIntegrationsResourceWithRawResponse(self._client.integrations) - @cached_property def x(self) -> x.AsyncXResourceWithRawResponse: - """X data lookups (subscription required)""" from .resources.x import AsyncXResourceWithRawResponse return AsyncXResourceWithRawResponse(self._client.x) @cached_property def trends(self) -> trends.AsyncTrendsResourceWithRawResponse: - """Trending topics by region""" + """Trending topics and hashtags by region""" from .resources.trends import AsyncTrendsResourceWithRawResponse return AsyncTrendsResourceWithRawResponse(self._client.trends) @@ -964,7 +930,7 @@ def support(self) -> support.AsyncSupportResourceWithRawResponse: @cached_property def credits(self) -> credits.AsyncCreditsResourceWithRawResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.credits import AsyncCreditsResourceWithRawResponse return AsyncCreditsResourceWithRawResponse(self._client.credits) @@ -978,7 +944,7 @@ def __init__(self, client: XTwitterScraper) -> None: @cached_property def account(self) -> account.AccountResourceWithStreamingResponse: - """Account info & settings""" + """Account info and settings""" from .resources.account import AccountResourceWithStreamingResponse return AccountResourceWithStreamingResponse(self._client.account) @@ -992,35 +958,35 @@ def api_keys(self) -> api_keys.APIKeysResourceWithStreamingResponse: @cached_property def subscribe(self) -> subscribe.SubscribeResourceWithStreamingResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.subscribe import SubscribeResourceWithStreamingResponse return SubscribeResourceWithStreamingResponse(self._client.subscribe) @cached_property def compose(self) -> compose.ComposeResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.compose import ComposeResourceWithStreamingResponse return ComposeResourceWithStreamingResponse(self._client.compose) @cached_property def drafts(self) -> drafts.DraftsResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.drafts import DraftsResourceWithStreamingResponse return DraftsResourceWithStreamingResponse(self._client.drafts) @cached_property def styles(self) -> styles.StylesResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.styles import StylesResourceWithStreamingResponse return StylesResourceWithStreamingResponse(self._client.styles) @cached_property def radar(self) -> radar.RadarResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.radar import RadarResourceWithStreamingResponse return RadarResourceWithStreamingResponse(self._client.radar) @@ -1055,28 +1021,20 @@ def draws(self) -> draws.DrawsResourceWithStreamingResponse: @cached_property def webhooks(self) -> webhooks.WebhooksResourceWithStreamingResponse: - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" from .resources.webhooks import WebhooksResourceWithStreamingResponse return WebhooksResourceWithStreamingResponse(self._client.webhooks) - @cached_property - def integrations(self) -> integrations.IntegrationsResourceWithStreamingResponse: - """Push notification integrations (Telegram)""" - from .resources.integrations import IntegrationsResourceWithStreamingResponse - - return IntegrationsResourceWithStreamingResponse(self._client.integrations) - @cached_property def x(self) -> x.XResourceWithStreamingResponse: - """X data lookups (subscription required)""" from .resources.x import XResourceWithStreamingResponse return XResourceWithStreamingResponse(self._client.x) @cached_property def trends(self) -> trends.TrendsResourceWithStreamingResponse: - """Trending topics by region""" + """Trending topics and hashtags by region""" from .resources.trends import TrendsResourceWithStreamingResponse return TrendsResourceWithStreamingResponse(self._client.trends) @@ -1089,7 +1047,7 @@ def support(self) -> support.SupportResourceWithStreamingResponse: @cached_property def credits(self) -> credits.CreditsResourceWithStreamingResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.credits import CreditsResourceWithStreamingResponse return CreditsResourceWithStreamingResponse(self._client.credits) @@ -1103,7 +1061,7 @@ def __init__(self, client: AsyncXTwitterScraper) -> None: @cached_property def account(self) -> account.AsyncAccountResourceWithStreamingResponse: - """Account info & settings""" + """Account info and settings""" from .resources.account import AsyncAccountResourceWithStreamingResponse return AsyncAccountResourceWithStreamingResponse(self._client.account) @@ -1117,35 +1075,35 @@ def api_keys(self) -> api_keys.AsyncAPIKeysResourceWithStreamingResponse: @cached_property def subscribe(self) -> subscribe.AsyncSubscribeResourceWithStreamingResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.subscribe import AsyncSubscribeResourceWithStreamingResponse return AsyncSubscribeResourceWithStreamingResponse(self._client.subscribe) @cached_property def compose(self) -> compose.AsyncComposeResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.compose import AsyncComposeResourceWithStreamingResponse return AsyncComposeResourceWithStreamingResponse(self._client.compose) @cached_property def drafts(self) -> drafts.AsyncDraftsResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.drafts import AsyncDraftsResourceWithStreamingResponse return AsyncDraftsResourceWithStreamingResponse(self._client.drafts) @cached_property def styles(self) -> styles.AsyncStylesResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.styles import AsyncStylesResourceWithStreamingResponse return AsyncStylesResourceWithStreamingResponse(self._client.styles) @cached_property def radar(self) -> radar.AsyncRadarResourceWithStreamingResponse: - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" from .resources.radar import AsyncRadarResourceWithStreamingResponse return AsyncRadarResourceWithStreamingResponse(self._client.radar) @@ -1180,28 +1138,20 @@ def draws(self) -> draws.AsyncDrawsResourceWithStreamingResponse: @cached_property def webhooks(self) -> webhooks.AsyncWebhooksResourceWithStreamingResponse: - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" from .resources.webhooks import AsyncWebhooksResourceWithStreamingResponse return AsyncWebhooksResourceWithStreamingResponse(self._client.webhooks) - @cached_property - def integrations(self) -> integrations.AsyncIntegrationsResourceWithStreamingResponse: - """Push notification integrations (Telegram)""" - from .resources.integrations import AsyncIntegrationsResourceWithStreamingResponse - - return AsyncIntegrationsResourceWithStreamingResponse(self._client.integrations) - @cached_property def x(self) -> x.AsyncXResourceWithStreamingResponse: - """X data lookups (subscription required)""" from .resources.x import AsyncXResourceWithStreamingResponse return AsyncXResourceWithStreamingResponse(self._client.x) @cached_property def trends(self) -> trends.AsyncTrendsResourceWithStreamingResponse: - """Trending topics by region""" + """Trending topics and hashtags by region""" from .resources.trends import AsyncTrendsResourceWithStreamingResponse return AsyncTrendsResourceWithStreamingResponse(self._client.trends) @@ -1214,7 +1164,7 @@ def support(self) -> support.AsyncSupportResourceWithStreamingResponse: @cached_property def credits(self) -> credits.AsyncCreditsResourceWithStreamingResponse: - """Subscription & billing""" + """Subscription, billing, and credits""" from .resources.credits import AsyncCreditsResourceWithStreamingResponse return AsyncCreditsResourceWithStreamingResponse(self._client.credits) diff --git a/src/x_twitter_scraper/_files.py b/src/x_twitter_scraper/_files.py index c10891e..75258aa 100644 --- a/src/x_twitter_scraper/_files.py +++ b/src/x_twitter_scraper/_files.py @@ -3,8 +3,8 @@ import io import os import pathlib -from typing import overload -from typing_extensions import TypeGuard +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard import anyio @@ -17,7 +17,9 @@ HttpxFileContent, HttpxRequestFiles, ) -from ._utils import is_tuple_t, is_mapping_t, is_sequence_t +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: @@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent: return await anyio.Path(file).read_bytes() return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/x_twitter_scraper/_utils/__init__.py b/src/x_twitter_scraper/_utils/__init__.py index 10cb66d..1c090e5 100644 --- a/src/x_twitter_scraper/_utils/__init__.py +++ b/src/x_twitter_scraper/_utils/__init__.py @@ -24,7 +24,6 @@ coerce_integer as coerce_integer, file_from_path as file_from_path, strip_not_given as strip_not_given, - deepcopy_minimal as deepcopy_minimal, get_async_library as get_async_library, maybe_coerce_float as maybe_coerce_float, get_required_header as get_required_header, diff --git a/src/x_twitter_scraper/_utils/_utils.py b/src/x_twitter_scraper/_utils/_utils.py index eec7f4a..771859f 100644 --- a/src/x_twitter_scraper/_utils/_utils.py +++ b/src/x_twitter_scraper/_utils/_utils.py @@ -86,8 +86,9 @@ def _extract_items( index += 1 if is_dict(obj): try: - # We are at the last entry in the path so we must remove the field - if (len(path)) == index: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): item = obj.pop(key) else: item = obj[key] @@ -176,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: return isinstance(obj, Iterable) -def deepcopy_minimal(item: _T) -> _T: - """Minimal reimplementation of copy.deepcopy() that will only copy certain object types: - - - mappings, e.g. `dict` - - list - - This is done for performance reasons. - """ - if is_mapping(item): - return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()}) - if is_list(item): - return cast(_T, [deepcopy_minimal(entry) for entry in item]) - return item - - # copied from https://github.com/Rapptz/RoboDanny def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: size = len(seq) diff --git a/src/x_twitter_scraper/_version.py b/src/x_twitter_scraper/_version.py index 8f460b4..d424d9e 100644 --- a/src/x_twitter_scraper/_version.py +++ b/src/x_twitter_scraper/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "x_twitter_scraper" -__version__ = "0.3.1" # x-release-please-version +__version__ = "0.4.0" # x-release-please-version diff --git a/src/x_twitter_scraper/resources/__init__.py b/src/x_twitter_scraper/resources/__init__.py index 0170526..8df9f13 100644 --- a/src/x_twitter_scraper/resources/__init__.py +++ b/src/x_twitter_scraper/resources/__init__.py @@ -128,14 +128,6 @@ ExtractionsResourceWithStreamingResponse, AsyncExtractionsResourceWithStreamingResponse, ) -from .integrations import ( - IntegrationsResource, - AsyncIntegrationsResource, - IntegrationsResourceWithRawResponse, - AsyncIntegrationsResourceWithRawResponse, - IntegrationsResourceWithStreamingResponse, - AsyncIntegrationsResourceWithStreamingResponse, -) __all__ = [ "AccountResource", @@ -210,12 +202,6 @@ "AsyncWebhooksResourceWithRawResponse", "WebhooksResourceWithStreamingResponse", "AsyncWebhooksResourceWithStreamingResponse", - "IntegrationsResource", - "AsyncIntegrationsResource", - "IntegrationsResourceWithRawResponse", - "AsyncIntegrationsResourceWithRawResponse", - "IntegrationsResourceWithStreamingResponse", - "AsyncIntegrationsResourceWithStreamingResponse", "XResource", "AsyncXResource", "XResourceWithRawResponse", diff --git a/src/x_twitter_scraper/resources/account.py b/src/x_twitter_scraper/resources/account.py index 7afa743..0739c88 100644 --- a/src/x_twitter_scraper/resources/account.py +++ b/src/x_twitter_scraper/resources/account.py @@ -26,7 +26,7 @@ class AccountResource(SyncAPIResource): - """Account info & settings""" + """Account info and settings""" @cached_property def with_raw_response(self) -> AccountResourceWithRawResponse: @@ -61,11 +61,7 @@ def retrieve( return self._get( "/account", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountRetrieveResponse, ) @@ -131,18 +127,14 @@ def update_locale( "/account", body=maybe_transform({"locale": locale}, account_update_locale_params.AccountUpdateLocaleParams), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountUpdateLocaleResponse, ) class AsyncAccountResource(AsyncAPIResource): - """Account info & settings""" + """Account info and settings""" @cached_property def with_raw_response(self) -> AsyncAccountResourceWithRawResponse: @@ -177,11 +169,7 @@ async def retrieve( return await self._get( "/account", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountRetrieveResponse, ) @@ -251,11 +239,7 @@ async def update_locale( {"locale": locale}, account_update_locale_params.AccountUpdateLocaleParams ), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountUpdateLocaleResponse, ) diff --git a/src/x_twitter_scraper/resources/api_keys.py b/src/x_twitter_scraper/resources/api_keys.py index ff312de..a139ac6 100644 --- a/src/x_twitter_scraper/resources/api_keys.py +++ b/src/x_twitter_scraper/resources/api_keys.py @@ -72,11 +72,7 @@ def create( "/api-keys", body=maybe_transform({"name": name}, api_key_create_params.APIKeyCreateParams), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=APIKeyCreateResponse, ) @@ -95,11 +91,7 @@ def list( return self._get( "/api-keys", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=APIKeyListResponse, ) @@ -132,11 +124,7 @@ def revoke( return self._delete( path_template("/api-keys/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=APIKeyRevokeResponse, ) @@ -191,11 +179,7 @@ async def create( "/api-keys", body=await async_maybe_transform({"name": name}, api_key_create_params.APIKeyCreateParams), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=APIKeyCreateResponse, ) @@ -214,11 +198,7 @@ async def list( return await self._get( "/api-keys", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=APIKeyListResponse, ) @@ -251,11 +231,7 @@ async def revoke( return await self._delete( path_template("/api-keys/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=APIKeyRevokeResponse, ) diff --git a/src/x_twitter_scraper/resources/compose.py b/src/x_twitter_scraper/resources/compose.py index 99ccaae..89dc8f4 100644 --- a/src/x_twitter_scraper/resources/compose.py +++ b/src/x_twitter_scraper/resources/compose.py @@ -24,7 +24,7 @@ class ComposeResource(SyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> ComposeResourceWithRawResponse: @@ -126,7 +126,7 @@ def create( class AsyncComposeResource(AsyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> AsyncComposeResourceWithRawResponse: diff --git a/src/x_twitter_scraper/resources/credits.py b/src/x_twitter_scraper/resources/credits.py index abf84a4..6ab90c1 100644 --- a/src/x_twitter_scraper/resources/credits.py +++ b/src/x_twitter_scraper/resources/credits.py @@ -23,7 +23,7 @@ class CreditsResource(SyncAPIResource): - """Subscription & billing""" + """Subscription, billing, and credits""" @cached_property def with_raw_response(self) -> CreditsResourceWithRawResponse: @@ -99,7 +99,7 @@ def topup_balance( class AsyncCreditsResource(AsyncAPIResource): - """Subscription & billing""" + """Subscription, billing, and credits""" @cached_property def with_raw_response(self) -> AsyncCreditsResourceWithRawResponse: diff --git a/src/x_twitter_scraper/resources/drafts.py b/src/x_twitter_scraper/resources/drafts.py index 817da19..6919d1c 100644 --- a/src/x_twitter_scraper/resources/drafts.py +++ b/src/x_twitter_scraper/resources/drafts.py @@ -25,7 +25,7 @@ class DraftsResource(SyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> DraftsResourceWithRawResponse: @@ -202,7 +202,7 @@ def delete( class AsyncDraftsResource(AsyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> AsyncDraftsResourceWithRawResponse: diff --git a/src/x_twitter_scraper/resources/events.py b/src/x_twitter_scraper/resources/events.py index dffcdbb..d0e2a55 100644 --- a/src/x_twitter_scraper/resources/events.py +++ b/src/x_twitter_scraper/resources/events.py @@ -128,7 +128,6 @@ def list( }, event_list_params.EventListParams, ), - security={"api_key": True}, ), cast_to=EventListResponse, ) @@ -239,7 +238,6 @@ async def list( }, event_list_params.EventListParams, ), - security={"api_key": True}, ), cast_to=EventListResponse, ) diff --git a/src/x_twitter_scraper/resources/integrations.py b/src/x_twitter_scraper/resources/integrations.py deleted file mode 100644 index 75b94aa..0000000 --- a/src/x_twitter_scraper/resources/integrations.py +++ /dev/null @@ -1,715 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, List -from typing_extensions import Literal - -import httpx - -from ..types import integration_create_params, integration_update_params, integration_list_deliveries_params -from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import path_template, maybe_transform, async_maybe_transform -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.integration import Integration -from ..types.shared.event_type import EventType -from ..types.integration_list_response import IntegrationListResponse -from ..types.integration_delete_response import IntegrationDeleteResponse -from ..types.integration_send_test_response import IntegrationSendTestResponse -from ..types.integration_list_deliveries_response import IntegrationListDeliveriesResponse - -__all__ = ["IntegrationsResource", "AsyncIntegrationsResource"] - - -class IntegrationsResource(SyncAPIResource): - """Push notification integrations (Telegram)""" - - @cached_property - def with_raw_response(self) -> IntegrationsResourceWithRawResponse: - """ - 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 IntegrationsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> IntegrationsResourceWithStreamingResponse: - """ - 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 IntegrationsResourceWithStreamingResponse(self) - - def create( - self, - *, - config: integration_create_params.Config, - event_types: List[EventType], - name: str, - type: Literal["telegram"], - # 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, - ) -> Integration: - """Create integration - - Args: - config: Integration config (e.g. - - Telegram chatId) - - event_types: Array of event types to subscribe to. - - 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 - """ - return self._post( - "/integrations", - body=maybe_transform( - { - "config": config, - "event_types": event_types, - "name": name, - "type": type, - }, - integration_create_params.IntegrationCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Integration, - ) - - 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, - ) -> Integration: - """ - Get integration details - - 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 self._get( - path_template("/integrations/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Integration, - ) - - def update( - self, - id: str, - *, - event_types: List[EventType] | Omit = omit, - filters: Dict[str, object] | Omit = omit, - is_active: bool | Omit = omit, - message_template: Dict[str, object] | Omit = omit, - name: str | Omit = omit, - scope_all_monitors: bool | Omit = omit, - silent_push: bool | 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, - ) -> Integration: - """ - Update integration - - Args: - event_types: Array of event types to subscribe to. - - filters: Event filter rules (JSON) - - message_template: Custom message template (JSON) - - 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 self._patch( - path_template("/integrations/{id}", id=id), - body=maybe_transform( - { - "event_types": event_types, - "filters": filters, - "is_active": is_active, - "message_template": message_template, - "name": name, - "scope_all_monitors": scope_all_monitors, - "silent_push": silent_push, - }, - integration_update_params.IntegrationUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Integration, - ) - - def list( - self, - *, - # 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, - ) -> IntegrationListResponse: - """List integrations""" - return self._get( - "/integrations", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=IntegrationListResponse, - ) - - def delete( - 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, - ) -> IntegrationDeleteResponse: - """ - Delete integration - - 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 self._delete( - path_template("/integrations/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=IntegrationDeleteResponse, - ) - - def list_deliveries( - self, - id: str, - *, - limit: int | 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, - ) -> IntegrationListDeliveriesResponse: - """ - List integration delivery history - - Args: - limit: Maximum number of items to return (1-100, default 50) - - 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 self._get( - path_template("/integrations/{id}/deliveries", id=id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=maybe_transform( - {"limit": limit}, integration_list_deliveries_params.IntegrationListDeliveriesParams - ), - ), - cast_to=IntegrationListDeliveriesResponse, - ) - - def send_test( - 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, - ) -> IntegrationSendTestResponse: - """ - Send test delivery - - 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 self._post( - path_template("/integrations/{id}/test", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=IntegrationSendTestResponse, - ) - - -class AsyncIntegrationsResource(AsyncAPIResource): - """Push notification integrations (Telegram)""" - - @cached_property - def with_raw_response(self) -> AsyncIntegrationsResourceWithRawResponse: - """ - 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 AsyncIntegrationsResourceWithRawResponse(self) - - @cached_property - def with_streaming_response(self) -> AsyncIntegrationsResourceWithStreamingResponse: - """ - 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 AsyncIntegrationsResourceWithStreamingResponse(self) - - async def create( - self, - *, - config: integration_create_params.Config, - event_types: List[EventType], - name: str, - type: Literal["telegram"], - # 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, - ) -> Integration: - """Create integration - - Args: - config: Integration config (e.g. - - Telegram chatId) - - event_types: Array of event types to subscribe to. - - 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 - """ - return await self._post( - "/integrations", - body=await async_maybe_transform( - { - "config": config, - "event_types": event_types, - "name": name, - "type": type, - }, - integration_create_params.IntegrationCreateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Integration, - ) - - 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, - ) -> Integration: - """ - Get integration details - - 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 await self._get( - path_template("/integrations/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Integration, - ) - - async def update( - self, - id: str, - *, - event_types: List[EventType] | Omit = omit, - filters: Dict[str, object] | Omit = omit, - is_active: bool | Omit = omit, - message_template: Dict[str, object] | Omit = omit, - name: str | Omit = omit, - scope_all_monitors: bool | Omit = omit, - silent_push: bool | 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, - ) -> Integration: - """ - Update integration - - Args: - event_types: Array of event types to subscribe to. - - filters: Event filter rules (JSON) - - message_template: Custom message template (JSON) - - 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 await self._patch( - path_template("/integrations/{id}", id=id), - body=await async_maybe_transform( - { - "event_types": event_types, - "filters": filters, - "is_active": is_active, - "message_template": message_template, - "name": name, - "scope_all_monitors": scope_all_monitors, - "silent_push": silent_push, - }, - integration_update_params.IntegrationUpdateParams, - ), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=Integration, - ) - - async def list( - self, - *, - # 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, - ) -> IntegrationListResponse: - """List integrations""" - return await self._get( - "/integrations", - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=IntegrationListResponse, - ) - - async def delete( - 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, - ) -> IntegrationDeleteResponse: - """ - Delete integration - - 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 await self._delete( - path_template("/integrations/{id}", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=IntegrationDeleteResponse, - ) - - async def list_deliveries( - self, - id: str, - *, - limit: int | 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, - ) -> IntegrationListDeliveriesResponse: - """ - List integration delivery history - - Args: - limit: Maximum number of items to return (1-100, default 50) - - 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 await self._get( - path_template("/integrations/{id}/deliveries", id=id), - options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - query=await async_maybe_transform( - {"limit": limit}, integration_list_deliveries_params.IntegrationListDeliveriesParams - ), - ), - cast_to=IntegrationListDeliveriesResponse, - ) - - async def send_test( - 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, - ) -> IntegrationSendTestResponse: - """ - Send test delivery - - 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 await self._post( - path_template("/integrations/{id}/test", id=id), - options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout - ), - cast_to=IntegrationSendTestResponse, - ) - - -class IntegrationsResourceWithRawResponse: - def __init__(self, integrations: IntegrationsResource) -> None: - self._integrations = integrations - - self.create = to_raw_response_wrapper( - integrations.create, - ) - self.retrieve = to_raw_response_wrapper( - integrations.retrieve, - ) - self.update = to_raw_response_wrapper( - integrations.update, - ) - self.list = to_raw_response_wrapper( - integrations.list, - ) - self.delete = to_raw_response_wrapper( - integrations.delete, - ) - self.list_deliveries = to_raw_response_wrapper( - integrations.list_deliveries, - ) - self.send_test = to_raw_response_wrapper( - integrations.send_test, - ) - - -class AsyncIntegrationsResourceWithRawResponse: - def __init__(self, integrations: AsyncIntegrationsResource) -> None: - self._integrations = integrations - - self.create = async_to_raw_response_wrapper( - integrations.create, - ) - self.retrieve = async_to_raw_response_wrapper( - integrations.retrieve, - ) - self.update = async_to_raw_response_wrapper( - integrations.update, - ) - self.list = async_to_raw_response_wrapper( - integrations.list, - ) - self.delete = async_to_raw_response_wrapper( - integrations.delete, - ) - self.list_deliveries = async_to_raw_response_wrapper( - integrations.list_deliveries, - ) - self.send_test = async_to_raw_response_wrapper( - integrations.send_test, - ) - - -class IntegrationsResourceWithStreamingResponse: - def __init__(self, integrations: IntegrationsResource) -> None: - self._integrations = integrations - - self.create = to_streamed_response_wrapper( - integrations.create, - ) - self.retrieve = to_streamed_response_wrapper( - integrations.retrieve, - ) - self.update = to_streamed_response_wrapper( - integrations.update, - ) - self.list = to_streamed_response_wrapper( - integrations.list, - ) - self.delete = to_streamed_response_wrapper( - integrations.delete, - ) - self.list_deliveries = to_streamed_response_wrapper( - integrations.list_deliveries, - ) - self.send_test = to_streamed_response_wrapper( - integrations.send_test, - ) - - -class AsyncIntegrationsResourceWithStreamingResponse: - def __init__(self, integrations: AsyncIntegrationsResource) -> None: - self._integrations = integrations - - self.create = async_to_streamed_response_wrapper( - integrations.create, - ) - self.retrieve = async_to_streamed_response_wrapper( - integrations.retrieve, - ) - self.update = async_to_streamed_response_wrapper( - integrations.update, - ) - self.list = async_to_streamed_response_wrapper( - integrations.list, - ) - self.delete = async_to_streamed_response_wrapper( - integrations.delete, - ) - self.list_deliveries = async_to_streamed_response_wrapper( - integrations.list_deliveries, - ) - self.send_test = async_to_streamed_response_wrapper( - integrations.send_test, - ) diff --git a/src/x_twitter_scraper/resources/radar.py b/src/x_twitter_scraper/resources/radar.py index d0c03ad..904cd41 100644 --- a/src/x_twitter_scraper/resources/radar.py +++ b/src/x_twitter_scraper/resources/radar.py @@ -24,7 +24,7 @@ class RadarResource(SyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> RadarResourceWithRawResponse: @@ -48,9 +48,11 @@ def with_streaming_response(self) -> RadarResourceWithStreamingResponse: def retrieve_trending_topics( self, *, - category: str | Omit = omit, - count: int | Omit = omit, + after: str | Omit = omit, + category: Literal["general", "tech", "dev", "science", "culture", "politics", "business", "entertainment"] + | Omit = omit, hours: int | Omit = omit, + limit: int | Omit = omit, region: str | Omit = omit, source: Literal["github", "google_trends", "hacker_news", "polymarket", "reddit", "trustmrr", "wikipedia"] | Omit = omit, @@ -65,11 +67,13 @@ def retrieve_trending_topics( Get trending topics from curated sources Args: - category: Filter by category (general, tech, dev, etc.) + after: Cursor for pagination (from prior response nextCursor). + + category: Filter by category. - count: Number of items to return + hours: Lookback window in hours (1-168, default 24). - hours: Lookback window in hours + limit: Number of items to return (1-100, default 50). region: Region filter (us, global, etc.) @@ -93,9 +97,10 @@ def retrieve_trending_topics( timeout=timeout, query=maybe_transform( { + "after": after, "category": category, - "count": count, "hours": hours, + "limit": limit, "region": region, "source": source, }, @@ -107,7 +112,7 @@ def retrieve_trending_topics( class AsyncRadarResource(AsyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> AsyncRadarResourceWithRawResponse: @@ -131,9 +136,11 @@ def with_streaming_response(self) -> AsyncRadarResourceWithStreamingResponse: async def retrieve_trending_topics( self, *, - category: str | Omit = omit, - count: int | Omit = omit, + after: str | Omit = omit, + category: Literal["general", "tech", "dev", "science", "culture", "politics", "business", "entertainment"] + | Omit = omit, hours: int | Omit = omit, + limit: int | Omit = omit, region: str | Omit = omit, source: Literal["github", "google_trends", "hacker_news", "polymarket", "reddit", "trustmrr", "wikipedia"] | Omit = omit, @@ -148,11 +155,13 @@ async def retrieve_trending_topics( Get trending topics from curated sources Args: - category: Filter by category (general, tech, dev, etc.) + after: Cursor for pagination (from prior response nextCursor). + + category: Filter by category. - count: Number of items to return + hours: Lookback window in hours (1-168, default 24). - hours: Lookback window in hours + limit: Number of items to return (1-100, default 50). region: Region filter (us, global, etc.) @@ -176,9 +185,10 @@ async def retrieve_trending_topics( timeout=timeout, query=await async_maybe_transform( { + "after": after, "category": category, - "count": count, "hours": hours, + "limit": limit, "region": region, "source": source, }, diff --git a/src/x_twitter_scraper/resources/styles.py b/src/x_twitter_scraper/resources/styles.py index ab4fe05..ebee277 100644 --- a/src/x_twitter_scraper/resources/styles.py +++ b/src/x_twitter_scraper/resources/styles.py @@ -27,7 +27,7 @@ class StylesResource(SyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> StylesResourceWithRawResponse: @@ -295,7 +295,7 @@ def get_performance( class AsyncStylesResource(AsyncAPIResource): - """Tweet composition, drafts, writing styles & radar""" + """AI tweet composition, drafts, writing styles, and radar""" @cached_property def with_raw_response(self) -> AsyncStylesResourceWithRawResponse: diff --git a/src/x_twitter_scraper/resources/subscribe.py b/src/x_twitter_scraper/resources/subscribe.py index fe74d90..f5c716c 100644 --- a/src/x_twitter_scraper/resources/subscribe.py +++ b/src/x_twitter_scraper/resources/subscribe.py @@ -20,7 +20,7 @@ class SubscribeResource(SyncAPIResource): - """Subscription & billing""" + """Subscription, billing, and credits""" @cached_property def with_raw_response(self) -> SubscribeResourceWithRawResponse: @@ -62,7 +62,7 @@ def create( class AsyncSubscribeResource(AsyncAPIResource): - """Subscription & billing""" + """Subscription, billing, and credits""" @cached_property def with_raw_response(self) -> AsyncSubscribeResourceWithRawResponse: diff --git a/src/x_twitter_scraper/resources/trends.py b/src/x_twitter_scraper/resources/trends.py index eee4338..5febbe2 100644 --- a/src/x_twitter_scraper/resources/trends.py +++ b/src/x_twitter_scraper/resources/trends.py @@ -22,7 +22,7 @@ class TrendsResource(SyncAPIResource): - """Trending topics by region""" + """Trending topics and hashtags by region""" @cached_property def with_raw_response(self) -> TrendsResourceWithRawResponse: @@ -56,7 +56,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TrendListResponse: """ - Get regional trending topics + Get trending hashtags and topics by region (alias) Args: count: Number of trending topics to return (1-50, default 30) @@ -91,7 +91,7 @@ def list( class AsyncTrendsResource(AsyncAPIResource): - """Trending topics by region""" + """Trending topics and hashtags by region""" @cached_property def with_raw_response(self) -> AsyncTrendsResourceWithRawResponse: @@ -125,7 +125,7 @@ async def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TrendListResponse: """ - Get regional trending topics + Get trending hashtags and topics by region (alias) Args: count: Number of trending topics to return (1-50, default 30) diff --git a/src/x_twitter_scraper/resources/webhooks.py b/src/x_twitter_scraper/resources/webhooks.py index 8d818b9..956941c 100644 --- a/src/x_twitter_scraper/resources/webhooks.py +++ b/src/x_twitter_scraper/resources/webhooks.py @@ -30,7 +30,7 @@ class WebhooksResource(SyncAPIResource): - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" @cached_property def with_raw_response(self) -> WebhooksResourceWithRawResponse: @@ -260,7 +260,7 @@ def test( class AsyncWebhooksResource(AsyncAPIResource): - """Webhook endpoint management & delivery""" + """Webhook endpoint management and delivery""" @cached_property def with_raw_response(self) -> AsyncWebhooksResourceWithRawResponse: diff --git a/src/x_twitter_scraper/resources/x/accounts.py b/src/x_twitter_scraper/resources/x/accounts.py index 0561667..974b964 100644 --- a/src/x_twitter_scraper/resources/x/accounts.py +++ b/src/x_twitter_scraper/resources/x/accounts.py @@ -98,11 +98,7 @@ def create( account_create_params.AccountCreateParams, ), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountCreateResponse, ) @@ -135,11 +131,7 @@ def retrieve( return self._get( path_template("/x/accounts/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=XAccountDetail, ) @@ -158,11 +150,7 @@ def list( return self._get( "/x/accounts", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountListResponse, ) @@ -195,11 +183,7 @@ def delete( return self._delete( path_template("/x/accounts/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountDeleteResponse, ) @@ -221,11 +205,7 @@ def bulk_retry( return self._post( "/x/accounts/bulk-retry", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountBulkRetryResponse, ) @@ -235,6 +215,8 @@ def reauth( id: str, *, password: str, + email: str | Omit = omit, + proxy_country: str | Omit = omit, 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. @@ -249,6 +231,10 @@ def reauth( Args: password: Updated account password + email: Email for the X account (updates stored email) + + proxy_country: Two-letter country code for login proxy region + totp_secret: TOTP secret for 2FA re-authentication extra_headers: Send extra headers @@ -266,16 +252,14 @@ def reauth( body=maybe_transform( { "password": password, + "email": email, + "proxy_country": proxy_country, "totp_secret": totp_secret, }, account_reauth_params.AccountReauthParams, ), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountReauthResponse, ) @@ -353,11 +337,7 @@ async def create( account_create_params.AccountCreateParams, ), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountCreateResponse, ) @@ -390,11 +370,7 @@ async def retrieve( return await self._get( path_template("/x/accounts/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=XAccountDetail, ) @@ -413,11 +389,7 @@ async def list( return await self._get( "/x/accounts", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountListResponse, ) @@ -450,11 +422,7 @@ async def delete( return await self._delete( path_template("/x/accounts/{id}", id=id), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountDeleteResponse, ) @@ -476,11 +444,7 @@ async def bulk_retry( return await self._post( "/x/accounts/bulk-retry", options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountBulkRetryResponse, ) @@ -490,6 +454,8 @@ async def reauth( id: str, *, password: str, + email: str | Omit = omit, + proxy_country: str | Omit = omit, 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. @@ -504,6 +470,10 @@ async def reauth( Args: password: Updated account password + email: Email for the X account (updates stored email) + + proxy_country: Two-letter country code for login proxy region + totp_secret: TOTP secret for 2FA re-authentication extra_headers: Send extra headers @@ -521,16 +491,14 @@ async def reauth( body=await async_maybe_transform( { "password": password, + "email": email, + "proxy_country": proxy_country, "totp_secret": totp_secret, }, account_reauth_params.AccountReauthParams, ), options=make_request_options( - extra_headers=extra_headers, - extra_query=extra_query, - extra_body=extra_body, - timeout=timeout, - security={"api_key": True}, + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout ), cast_to=AccountReauthResponse, ) diff --git a/src/x_twitter_scraper/resources/x/bookmarks.py b/src/x_twitter_scraper/resources/x/bookmarks.py index 7de2f2a..5dd4c23 100644 --- a/src/x_twitter_scraper/resources/x/bookmarks.py +++ b/src/x_twitter_scraper/resources/x/bookmarks.py @@ -23,7 +23,7 @@ class BookmarksResource(SyncAPIResource): - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" @cached_property def with_raw_response(self) -> BookmarksResourceWithRawResponse: @@ -111,7 +111,7 @@ def retrieve_folders( class AsyncBookmarksResource(AsyncAPIResource): - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" @cached_property def with_raw_response(self) -> AsyncBookmarksResourceWithRawResponse: diff --git a/src/x_twitter_scraper/resources/x/communities/communities.py b/src/x_twitter_scraper/resources/x/communities/communities.py index 6ff5d37..79f91af 100644 --- a/src/x_twitter_scraper/resources/x/communities/communities.py +++ b/src/x_twitter_scraper/resources/x/communities/communities.py @@ -55,7 +55,7 @@ def join(self) -> JoinResource: @cached_property def tweets(self) -> TweetsResource: - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" return TweetsResource(self._client) @cached_property @@ -182,7 +182,7 @@ def retrieve_info( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> CommunityRetrieveInfoResponse: """ - Get community details + Get community name, description and member count Args: extra_headers: Send extra headers @@ -216,7 +216,7 @@ def retrieve_members( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get community members + List members of a community Args: cursor: Pagination cursor @@ -258,7 +258,7 @@ def retrieve_moderators( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get community moderators + List moderators of a community Args: cursor: Pagination cursor for community moderators @@ -301,7 +301,7 @@ def retrieve_search( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Search tweets across communities + Search for communities by keyword Args: q: Search query @@ -346,7 +346,7 @@ def join(self) -> AsyncJoinResource: @cached_property def tweets(self) -> AsyncTweetsResource: - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" return AsyncTweetsResource(self._client) @cached_property @@ -473,7 +473,7 @@ async def retrieve_info( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> CommunityRetrieveInfoResponse: """ - Get community details + Get community name, description and member count Args: extra_headers: Send extra headers @@ -507,7 +507,7 @@ async def retrieve_members( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get community members + List members of a community Args: cursor: Pagination cursor @@ -549,7 +549,7 @@ async def retrieve_moderators( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get community moderators + List moderators of a community Args: cursor: Pagination cursor for community moderators @@ -592,7 +592,7 @@ async def retrieve_search( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Search tweets across communities + Search for communities by keyword Args: q: Search query @@ -659,7 +659,7 @@ def join(self) -> JoinResourceWithRawResponse: @cached_property def tweets(self) -> TweetsResourceWithRawResponse: - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" return TweetsResourceWithRawResponse(self._communities.tweets) @@ -693,7 +693,7 @@ def join(self) -> AsyncJoinResourceWithRawResponse: @cached_property def tweets(self) -> AsyncTweetsResourceWithRawResponse: - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" return AsyncTweetsResourceWithRawResponse(self._communities.tweets) @@ -727,7 +727,7 @@ def join(self) -> JoinResourceWithStreamingResponse: @cached_property def tweets(self) -> TweetsResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" return TweetsResourceWithStreamingResponse(self._communities.tweets) @@ -761,5 +761,5 @@ def join(self) -> AsyncJoinResourceWithStreamingResponse: @cached_property def tweets(self) -> AsyncTweetsResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" return AsyncTweetsResourceWithStreamingResponse(self._communities.tweets) diff --git a/src/x_twitter_scraper/resources/x/communities/tweets.py b/src/x_twitter_scraper/resources/x/communities/tweets.py index 3d0a88f..96fd4cf 100644 --- a/src/x_twitter_scraper/resources/x/communities/tweets.py +++ b/src/x_twitter_scraper/resources/x/communities/tweets.py @@ -22,7 +22,7 @@ class TweetsResource(SyncAPIResource): - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" @cached_property def with_raw_response(self) -> TweetsResourceWithRawResponse: @@ -57,7 +57,7 @@ def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Search tweets across all communities + List tweets across all communities Args: q: Search query for cross-community tweets @@ -106,7 +106,7 @@ def list_by_community( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get community tweets + List tweets posted in a community Args: cursor: Pagination cursor for community tweets @@ -135,7 +135,7 @@ def list_by_community( class AsyncTweetsResource(AsyncAPIResource): - """X data lookups (subscription required)""" + """X Community info, members, and tweets""" @cached_property def with_raw_response(self) -> AsyncTweetsResourceWithRawResponse: @@ -170,7 +170,7 @@ async def list( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Search tweets across all communities + List tweets across all communities Args: q: Search query for cross-community tweets @@ -219,7 +219,7 @@ async def list_by_community( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get community tweets + List tweets posted in a community Args: cursor: Pagination cursor for community tweets diff --git a/src/x_twitter_scraper/resources/x/followers.py b/src/x_twitter_scraper/resources/x/followers.py index f8412e3..76edcb7 100644 --- a/src/x_twitter_scraper/resources/x/followers.py +++ b/src/x_twitter_scraper/resources/x/followers.py @@ -22,7 +22,7 @@ class FollowersResource(SyncAPIResource): - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" @cached_property def with_raw_response(self) -> FollowersResourceWithRawResponse: @@ -56,7 +56,7 @@ def check( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FollowerCheckResponse: """ - Check follow relationship + Check if one user follows another Args: source: Username to check (without @) @@ -91,7 +91,7 @@ def check( class AsyncFollowersResource(AsyncAPIResource): - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" @cached_property def with_raw_response(self) -> AsyncFollowersResourceWithRawResponse: @@ -125,7 +125,7 @@ async def check( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> FollowerCheckResponse: """ - Check follow relationship + Check if one user follows another Args: source: Username to check (without @) diff --git a/src/x_twitter_scraper/resources/x/lists.py b/src/x_twitter_scraper/resources/x/lists.py index a4da5fb..a98e6b3 100644 --- a/src/x_twitter_scraper/resources/x/lists.py +++ b/src/x_twitter_scraper/resources/x/lists.py @@ -23,7 +23,7 @@ class ListsResource(SyncAPIResource): - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" @cached_property def with_raw_response(self) -> ListsResourceWithRawResponse: @@ -57,7 +57,7 @@ def retrieve_followers( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get list followers + List followers of an X List Args: cursor: Pagination cursor for list followers @@ -97,7 +97,7 @@ def retrieve_members( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get list members + List members of an X List Args: cursor: Pagination cursor for list members @@ -140,7 +140,7 @@ def retrieve_tweets( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get list tweets + List tweets from an X List Args: cursor: Pagination cursor for list tweets @@ -183,7 +183,7 @@ def retrieve_tweets( class AsyncListsResource(AsyncAPIResource): - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" @cached_property def with_raw_response(self) -> AsyncListsResourceWithRawResponse: @@ -217,7 +217,7 @@ async def retrieve_followers( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get list followers + List followers of an X List Args: cursor: Pagination cursor for list followers @@ -259,7 +259,7 @@ async def retrieve_members( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get list members + List members of an X List Args: cursor: Pagination cursor for list members @@ -304,7 +304,7 @@ async def retrieve_tweets( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get list tweets + List tweets from an X List Args: cursor: Pagination cursor for list tweets diff --git a/src/x_twitter_scraper/resources/x/media.py b/src/x_twitter_scraper/resources/x/media.py index cb084bd..18c2db5 100644 --- a/src/x_twitter_scraper/resources/x/media.py +++ b/src/x_twitter_scraper/resources/x/media.py @@ -6,6 +6,7 @@ import httpx +from ..._files import deepcopy_with_paths from ..._types import ( Body, Omit, @@ -17,7 +18,7 @@ omit, not_given, ) -from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, maybe_transform, async_maybe_transform from ..._compat import cached_property from ...types.x import media_upload_params, media_download_params from ..._resource import SyncAPIResource, AsyncAPIResource @@ -35,7 +36,7 @@ class MediaResource(SyncAPIResource): - """Media upload & download""" + """Media upload and download""" @cached_property def with_raw_response(self) -> MediaResourceWithRawResponse: @@ -69,7 +70,7 @@ def download( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> MediaDownloadResponse: """ - Download tweet media + Download images and videos from tweets Args: tweet_ids: Array of tweet URLs or IDs (bulk, max 50) @@ -128,12 +129,13 @@ def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "account": account, "file": file, "is_long_video": is_long_video, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -152,7 +154,7 @@ def upload( class AsyncMediaResource(AsyncAPIResource): - """Media upload & download""" + """Media upload and download""" @cached_property def with_raw_response(self) -> AsyncMediaResourceWithRawResponse: @@ -186,7 +188,7 @@ async def download( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> MediaDownloadResponse: """ - Download tweet media + Download images and videos from tweets Args: tweet_ids: Array of tweet URLs or IDs (bulk, max 50) @@ -245,12 +247,13 @@ async def upload( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "account": account, "file": file, "is_long_video": is_long_video, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be diff --git a/src/x_twitter_scraper/resources/x/profile.py b/src/x_twitter_scraper/resources/x/profile.py index bdabc5e..375cc98 100644 --- a/src/x_twitter_scraper/resources/x/profile.py +++ b/src/x_twitter_scraper/resources/x/profile.py @@ -6,8 +6,9 @@ import httpx +from ..._files import deepcopy_with_paths from ..._types import Body, Omit, Query, Headers, NotGiven, FileTypes, omit, not_given -from ..._utils import extract_files, maybe_transform, deepcopy_minimal, async_maybe_transform +from ..._utils import extract_files, maybe_transform, async_maybe_transform from ..._compat import cached_property from ...types.x import profile_update_params, profile_update_avatar_params, profile_update_banner_params from ..._resource import SyncAPIResource, AsyncAPIResource @@ -128,11 +129,12 @@ def update_avatar( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "account": account, "file": file, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -177,11 +179,12 @@ def update_banner( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "account": account, "file": file, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -302,11 +305,12 @@ async def update_avatar( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "account": account, "file": file, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -351,11 +355,12 @@ async def update_banner( timeout: Override the client-level default timeout for this request, in seconds """ - body = deepcopy_minimal( + body = deepcopy_with_paths( { "account": account, "file": file, - } + }, + [["file"]], ) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be diff --git a/src/x_twitter_scraper/resources/x/tweets/tweets.py b/src/x_twitter_scraper/resources/x/tweets/tweets.py index 9c10dbe..33e62f0 100644 --- a/src/x_twitter_scraper/resources/x/tweets/tweets.py +++ b/src/x_twitter_scraper/resources/x/tweets/tweets.py @@ -87,12 +87,13 @@ def create( self, *, account: str, - text: str, attachment_url: str | Omit = omit, community_id: str | Omit = omit, is_note_tweet: bool | Omit = omit, + media: SequenceNotStr[str] | Omit = omit, media_ids: SequenceNotStr[str] | Omit = omit, reply_to_tweet_id: str | Omit = omit, + text: 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, @@ -106,6 +107,12 @@ def create( Args: account: X account (@username or account ID) + media: Array of media URLs to attach (mutually exclusive with media_ids) + + media_ids: Array of media IDs to attach (mutually exclusive with media) + + text: Tweet text (optional when media is provided) + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -119,12 +126,13 @@ def create( body=maybe_transform( { "account": account, - "text": text, "attachment_url": attachment_url, "community_id": community_id, "is_note_tweet": is_note_tweet, + "media": media, "media_ids": media_ids, "reply_to_tweet_id": reply_to_tweet_id, + "text": text, }, tweet_create_params.TweetCreateParams, ), @@ -146,7 +154,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TweetRetrieveResponse: """ - Look up tweet + Get tweet with full text, author, metrics and media Args: extra_headers: Send extra headers @@ -254,7 +262,7 @@ def get_favoriters( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get users who liked a tweet + List users who liked a tweet Args: cursor: Pagination cursor for favoriters @@ -297,7 +305,7 @@ def get_quotes( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get quote tweets of a tweet + List quote tweets of a tweet Args: cursor: Pagination cursor for quote tweets @@ -353,7 +361,7 @@ def get_replies( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get replies to a tweet + List replies to a tweet Args: cursor: Pagination cursor for tweet replies @@ -404,7 +412,7 @@ def get_retweeters( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get users who retweeted a tweet + List users who retweeted a tweet Args: cursor: Pagination cursor for retweeters @@ -444,7 +452,7 @@ def get_thread( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get thread context for a tweet + Get full conversation thread for a tweet Args: cursor: Pagination cursor for thread tweets @@ -488,7 +496,7 @@ def search( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Search tweets + Search tweets with X query operators and pagination Args: q: Search query (keywords, @@ -568,12 +576,13 @@ async def create( self, *, account: str, - text: str, attachment_url: str | Omit = omit, community_id: str | Omit = omit, is_note_tweet: bool | Omit = omit, + media: SequenceNotStr[str] | Omit = omit, media_ids: SequenceNotStr[str] | Omit = omit, reply_to_tweet_id: str | Omit = omit, + text: 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, @@ -587,6 +596,12 @@ async def create( Args: account: X account (@username or account ID) + media: Array of media URLs to attach (mutually exclusive with media_ids) + + media_ids: Array of media IDs to attach (mutually exclusive with media) + + text: Tweet text (optional when media is provided) + extra_headers: Send extra headers extra_query: Add additional query parameters to the request @@ -600,12 +615,13 @@ async def create( body=await async_maybe_transform( { "account": account, - "text": text, "attachment_url": attachment_url, "community_id": community_id, "is_note_tweet": is_note_tweet, + "media": media, "media_ids": media_ids, "reply_to_tweet_id": reply_to_tweet_id, + "text": text, }, tweet_create_params.TweetCreateParams, ), @@ -627,7 +643,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> TweetRetrieveResponse: """ - Look up tweet + Get tweet with full text, author, metrics and media Args: extra_headers: Send extra headers @@ -735,7 +751,7 @@ async def get_favoriters( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get users who liked a tweet + List users who liked a tweet Args: cursor: Pagination cursor for favoriters @@ -780,7 +796,7 @@ async def get_quotes( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get quote tweets of a tweet + List quote tweets of a tweet Args: cursor: Pagination cursor for quote tweets @@ -836,7 +852,7 @@ async def get_replies( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get replies to a tweet + List replies to a tweet Args: cursor: Pagination cursor for tweet replies @@ -887,7 +903,7 @@ async def get_retweeters( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get users who retweeted a tweet + List users who retweeted a tweet Args: cursor: Pagination cursor for retweeters @@ -929,7 +945,7 @@ async def get_thread( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get thread context for a tweet + Get full conversation thread for a tweet Args: cursor: Pagination cursor for thread tweets @@ -973,7 +989,7 @@ async def search( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Search tweets + Search tweets with X query operators and pagination Args: q: Search query (keywords, diff --git a/src/x_twitter_scraper/resources/x/users/users.py b/src/x_twitter_scraper/resources/x/users/users.py index 3fa63e2..5dfd741 100644 --- a/src/x_twitter_scraper/resources/x/users/users.py +++ b/src/x_twitter_scraper/resources/x/users/users.py @@ -35,7 +35,7 @@ async_to_streamed_response_wrapper, ) from ...._base_client import make_request_options -from ....types.x.user_profile import UserProfile +from ....types.shared.user_profile import UserProfile from ....types.shared.paginated_users import PaginatedUsers from ....types.shared.paginated_tweets import PaginatedTweets @@ -43,7 +43,7 @@ class UsersResource(SyncAPIResource): - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" @cached_property def follow(self) -> FollowResource: @@ -81,7 +81,7 @@ def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UserProfile: """ - Look up X user + Get user profile with follower counts and verification Args: extra_headers: Send extra headers @@ -114,7 +114,7 @@ def retrieve_batch( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get multiple users by IDs + Look up multiple users by IDs in one call Args: ids: Comma-separated user IDs (max 100) @@ -153,7 +153,7 @@ def retrieve_followers( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get user followers + List followers of a user Args: cursor: Pagination cursor for followers list @@ -201,7 +201,7 @@ def retrieve_followers_you_know( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get followers you know for a user + List mutual followers between you and a user Args: cursor: Pagination cursor for followers-you-know @@ -244,7 +244,7 @@ def retrieve_following( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get users this user follows + List accounts a user follows Args: cursor: Pagination cursor for following list @@ -292,7 +292,7 @@ def retrieve_likes( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get tweets liked by a user + List tweets liked by a user Args: cursor: Pagination cursor for liked tweets @@ -332,7 +332,7 @@ def retrieve_media( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get media tweets by a user + List media tweets posted by a user Args: cursor: Pagination cursor for media tweets @@ -374,7 +374,7 @@ def retrieve_mentions( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get tweets mentioning a user + List tweets mentioning a user Args: cursor: Pagination cursor for mentions @@ -473,7 +473,7 @@ def retrieve_tweets( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get recent tweets by a user + List recent tweets posted by a user Args: cursor: Pagination cursor for user tweets @@ -524,7 +524,7 @@ def retrieve_verified_followers( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get verified followers + List verified followers of a user Args: cursor: Pagination cursor for verified followers @@ -555,7 +555,7 @@ def retrieve_verified_followers( class AsyncUsersResource(AsyncAPIResource): - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" @cached_property def follow(self) -> AsyncFollowResource: @@ -593,7 +593,7 @@ async def retrieve( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> UserProfile: """ - Look up X user + Get user profile with follower counts and verification Args: extra_headers: Send extra headers @@ -626,7 +626,7 @@ async def retrieve_batch( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get multiple users by IDs + Look up multiple users by IDs in one call Args: ids: Comma-separated user IDs (max 100) @@ -665,7 +665,7 @@ async def retrieve_followers( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get user followers + List followers of a user Args: cursor: Pagination cursor for followers list @@ -713,7 +713,7 @@ async def retrieve_followers_you_know( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get followers you know for a user + List mutual followers between you and a user Args: cursor: Pagination cursor for followers-you-know @@ -756,7 +756,7 @@ async def retrieve_following( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get users this user follows + List accounts a user follows Args: cursor: Pagination cursor for following list @@ -804,7 +804,7 @@ async def retrieve_likes( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get tweets liked by a user + List tweets liked by a user Args: cursor: Pagination cursor for liked tweets @@ -846,7 +846,7 @@ async def retrieve_media( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get media tweets by a user + List media tweets posted by a user Args: cursor: Pagination cursor for media tweets @@ -890,7 +890,7 @@ async def retrieve_mentions( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get tweets mentioning a user + List tweets mentioning a user Args: cursor: Pagination cursor for mentions @@ -989,7 +989,7 @@ async def retrieve_tweets( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedTweets: """ - Get recent tweets by a user + List recent tweets posted by a user Args: cursor: Pagination cursor for user tweets @@ -1040,7 +1040,7 @@ async def retrieve_verified_followers( timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> PaginatedUsers: """ - Get verified followers + List verified followers of a user Args: cursor: Pagination cursor for verified followers diff --git a/src/x_twitter_scraper/resources/x/x.py b/src/x_twitter_scraper/resources/x/x.py index 51c5014..eca4791 100644 --- a/src/x_twitter_scraper/resources/x/x.py +++ b/src/x_twitter_scraper/resources/x/x.py @@ -30,7 +30,7 @@ MediaResourceWithStreamingResponse, AsyncMediaResourceWithStreamingResponse, ) -from ...types import x_get_home_timeline_params, x_get_notifications_params +from ...types import x_get_trends_params, x_get_home_timeline_params, x_get_notifications_params from .profile import ( ProfileResource, AsyncProfileResource, @@ -107,20 +107,18 @@ class XResource(SyncAPIResource): - """X data lookups (subscription required)""" - @cached_property def tweets(self) -> TweetsResource: return TweetsResource(self._client) @cached_property def users(self) -> UsersResource: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return UsersResource(self._client) @cached_property def followers(self) -> FollowersResource: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return FollowersResource(self._client) @cached_property @@ -129,7 +127,7 @@ def dm(self) -> DmResource: @cached_property def media(self) -> MediaResource: - """Media upload & download""" + """Media upload and download""" return MediaResource(self._client) @cached_property @@ -148,12 +146,12 @@ def accounts(self) -> AccountsResource: @cached_property def bookmarks(self) -> BookmarksResource: - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" return BookmarksResource(self._client) @cached_property def lists(self) -> ListsResource: - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" return ListsResource(self._client) @cached_property @@ -303,6 +301,8 @@ def get_notifications( def get_trends( self, *, + count: int | Omit = omit, + woeid: int | 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, @@ -310,31 +310,54 @@ def get_trends( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> XGetTrendsResponse: - """Get trending topics""" + """ + Get trending hashtags and topics from X by region + + Args: + count: Number of trending topics to return (1-50, default 30) + + woeid: Region WOEID (1=Worldwide, 23424977=US, 23424975=UK, 23424969=Turkey) + + 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 + """ return self._get( "/x/trends", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "count": count, + "woeid": woeid, + }, + x_get_trends_params.XGetTrendsParams, + ), ), cast_to=XGetTrendsResponse, ) class AsyncXResource(AsyncAPIResource): - """X data lookups (subscription required)""" - @cached_property def tweets(self) -> AsyncTweetsResource: return AsyncTweetsResource(self._client) @cached_property def users(self) -> AsyncUsersResource: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return AsyncUsersResource(self._client) @cached_property def followers(self) -> AsyncFollowersResource: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return AsyncFollowersResource(self._client) @cached_property @@ -343,7 +366,7 @@ def dm(self) -> AsyncDmResource: @cached_property def media(self) -> AsyncMediaResource: - """Media upload & download""" + """Media upload and download""" return AsyncMediaResource(self._client) @cached_property @@ -362,12 +385,12 @@ def accounts(self) -> AsyncAccountsResource: @cached_property def bookmarks(self) -> AsyncBookmarksResource: - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" return AsyncBookmarksResource(self._client) @cached_property def lists(self) -> AsyncListsResource: - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" return AsyncListsResource(self._client) @cached_property @@ -517,6 +540,8 @@ async def get_notifications( async def get_trends( self, *, + count: int | Omit = omit, + woeid: int | 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, @@ -524,11 +549,36 @@ async def get_trends( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> XGetTrendsResponse: - """Get trending topics""" + """ + Get trending hashtags and topics from X by region + + Args: + count: Number of trending topics to return (1-50, default 30) + + woeid: Region WOEID (1=Worldwide, 23424977=US, 23424975=UK, 23424969=Turkey) + + 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 + """ return await self._get( "/x/trends", options=make_request_options( - extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "count": count, + "woeid": woeid, + }, + x_get_trends_params.XGetTrendsParams, + ), ), cast_to=XGetTrendsResponse, ) @@ -557,12 +607,12 @@ def tweets(self) -> TweetsResourceWithRawResponse: @cached_property def users(self) -> UsersResourceWithRawResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return UsersResourceWithRawResponse(self._x.users) @cached_property def followers(self) -> FollowersResourceWithRawResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return FollowersResourceWithRawResponse(self._x.followers) @cached_property @@ -571,7 +621,7 @@ def dm(self) -> DmResourceWithRawResponse: @cached_property def media(self) -> MediaResourceWithRawResponse: - """Media upload & download""" + """Media upload and download""" return MediaResourceWithRawResponse(self._x.media) @cached_property @@ -590,12 +640,12 @@ def accounts(self) -> AccountsResourceWithRawResponse: @cached_property def bookmarks(self) -> BookmarksResourceWithRawResponse: - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" return BookmarksResourceWithRawResponse(self._x.bookmarks) @cached_property def lists(self) -> ListsResourceWithRawResponse: - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" return ListsResourceWithRawResponse(self._x.lists) @@ -622,12 +672,12 @@ def tweets(self) -> AsyncTweetsResourceWithRawResponse: @cached_property def users(self) -> AsyncUsersResourceWithRawResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return AsyncUsersResourceWithRawResponse(self._x.users) @cached_property def followers(self) -> AsyncFollowersResourceWithRawResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return AsyncFollowersResourceWithRawResponse(self._x.followers) @cached_property @@ -636,7 +686,7 @@ def dm(self) -> AsyncDmResourceWithRawResponse: @cached_property def media(self) -> AsyncMediaResourceWithRawResponse: - """Media upload & download""" + """Media upload and download""" return AsyncMediaResourceWithRawResponse(self._x.media) @cached_property @@ -655,12 +705,12 @@ def accounts(self) -> AsyncAccountsResourceWithRawResponse: @cached_property def bookmarks(self) -> AsyncBookmarksResourceWithRawResponse: - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" return AsyncBookmarksResourceWithRawResponse(self._x.bookmarks) @cached_property def lists(self) -> AsyncListsResourceWithRawResponse: - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" return AsyncListsResourceWithRawResponse(self._x.lists) @@ -687,12 +737,12 @@ def tweets(self) -> TweetsResourceWithStreamingResponse: @cached_property def users(self) -> UsersResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return UsersResourceWithStreamingResponse(self._x.users) @cached_property def followers(self) -> FollowersResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return FollowersResourceWithStreamingResponse(self._x.followers) @cached_property @@ -701,7 +751,7 @@ def dm(self) -> DmResourceWithStreamingResponse: @cached_property def media(self) -> MediaResourceWithStreamingResponse: - """Media upload & download""" + """Media upload and download""" return MediaResourceWithStreamingResponse(self._x.media) @cached_property @@ -720,12 +770,12 @@ def accounts(self) -> AccountsResourceWithStreamingResponse: @cached_property def bookmarks(self) -> BookmarksResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" return BookmarksResourceWithStreamingResponse(self._x.bookmarks) @cached_property def lists(self) -> ListsResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" return ListsResourceWithStreamingResponse(self._x.lists) @@ -752,12 +802,12 @@ def tweets(self) -> AsyncTweetsResourceWithStreamingResponse: @cached_property def users(self) -> AsyncUsersResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return AsyncUsersResourceWithStreamingResponse(self._x.users) @cached_property def followers(self) -> AsyncFollowersResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """Look up, search, and explore user profiles and relationships""" return AsyncFollowersResourceWithStreamingResponse(self._x.followers) @cached_property @@ -766,7 +816,7 @@ def dm(self) -> AsyncDmResourceWithStreamingResponse: @cached_property def media(self) -> AsyncMediaResourceWithStreamingResponse: - """Media upload & download""" + """Media upload and download""" return AsyncMediaResourceWithStreamingResponse(self._x.media) @cached_property @@ -785,10 +835,10 @@ def accounts(self) -> AsyncAccountsResourceWithStreamingResponse: @cached_property def bookmarks(self) -> AsyncBookmarksResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """Look up, search, and analyze individual tweets""" return AsyncBookmarksResourceWithStreamingResponse(self._x.bookmarks) @cached_property def lists(self) -> AsyncListsResourceWithStreamingResponse: - """X data lookups (subscription required)""" + """X List followers, members, and tweets""" return AsyncListsResourceWithStreamingResponse(self._x.lists) diff --git a/src/x_twitter_scraper/types/__init__.py b/src/x_twitter_scraper/types/__init__.py index 8d15985..0713a7c 100644 --- a/src/x_twitter_scraper/types/__init__.py +++ b/src/x_twitter_scraper/types/__init__.py @@ -7,6 +7,8 @@ from .shared import ( Error as Error, EventType as EventType, + SearchTweet as SearchTweet, + UserProfile as UserProfile, PaginatedUsers as PaginatedUsers, PaginatedTweets as PaginatedTweets, ) @@ -17,7 +19,6 @@ from .delivery import Delivery as Delivery from .radar_item import RadarItem as RadarItem from .draw_detail import DrawDetail as DrawDetail -from .integration import Integration as Integration from .draft_detail import DraftDetail as DraftDetail from .event_detail import EventDetail as EventDetail from .style_profile import StyleProfile as StyleProfile @@ -37,7 +38,7 @@ from .style_list_response import StyleListResponse as StyleListResponse from .style_update_params import StyleUpdateParams as StyleUpdateParams from .trend_list_response import TrendListResponse as TrendListResponse -from .integration_delivery import IntegrationDelivery as IntegrationDelivery +from .x_get_trends_params import XGetTrendsParams as XGetTrendsParams from .style_analyze_params import StyleAnalyzeParams as StyleAnalyzeParams from .style_compare_params import StyleCompareParams as StyleCompareParams from .api_key_create_params import APIKeyCreateParams as APIKeyCreateParams @@ -65,15 +66,11 @@ from .webhook_create_response import WebhookCreateResponse as WebhookCreateResponse from .extraction_list_response import ExtractionListResponse as ExtractionListResponse from .account_retrieve_response import AccountRetrieveResponse as AccountRetrieveResponse -from .integration_create_params import IntegrationCreateParams as IntegrationCreateParams -from .integration_list_response import IntegrationListResponse as IntegrationListResponse -from .integration_update_params import IntegrationUpdateParams as IntegrationUpdateParams from .subscribe_create_response import SubscribeCreateResponse as SubscribeCreateResponse from .extraction_retrieve_params import ExtractionRetrieveParams as ExtractionRetrieveParams from .x_get_home_timeline_params import XGetHomeTimelineParams as XGetHomeTimelineParams from .x_get_notifications_params import XGetNotificationsParams as XGetNotificationsParams from .credit_topup_balance_params import CreditTopupBalanceParams as CreditTopupBalanceParams -from .integration_delete_response import IntegrationDeleteResponse as IntegrationDeleteResponse from .monitor_deactivate_response import MonitorDeactivateResponse as MonitorDeactivateResponse from .webhook_deactivate_response import WebhookDeactivateResponse as WebhookDeactivateResponse from .account_update_locale_params import AccountUpdateLocaleParams as AccountUpdateLocaleParams @@ -82,7 +79,6 @@ from .account_set_x_username_params import AccountSetXUsernameParams as AccountSetXUsernameParams from .credit_topup_balance_response import CreditTopupBalanceResponse as CreditTopupBalanceResponse from .account_update_locale_response import AccountUpdateLocaleResponse as AccountUpdateLocaleResponse -from .integration_send_test_response import IntegrationSendTestResponse as IntegrationSendTestResponse from .style_get_performance_response import StyleGetPerformanceResponse as StyleGetPerformanceResponse from .account_set_x_username_response import AccountSetXUsernameResponse as AccountSetXUsernameResponse from .extraction_estimate_cost_params import ExtractionEstimateCostParams as ExtractionEstimateCostParams @@ -90,8 +86,6 @@ from .extraction_export_results_params import ExtractionExportResultsParams as ExtractionExportResultsParams from .webhook_list_deliveries_response import WebhookListDeliveriesResponse as WebhookListDeliveriesResponse from .extraction_estimate_cost_response import ExtractionEstimateCostResponse as ExtractionEstimateCostResponse -from .integration_list_deliveries_params import IntegrationListDeliveriesParams as IntegrationListDeliveriesParams -from .integration_list_deliveries_response import IntegrationListDeliveriesResponse as IntegrationListDeliveriesResponse from .radar_retrieve_trending_topics_params import ( RadarRetrieveTrendingTopicsParams as RadarRetrieveTrendingTopicsParams, ) diff --git a/src/x_twitter_scraper/types/account_retrieve_response.py b/src/x_twitter_scraper/types/account_retrieve_response.py index f43a115..ae6b655 100644 --- a/src/x_twitter_scraper/types/account_retrieve_response.py +++ b/src/x_twitter_scraper/types/account_retrieve_response.py @@ -1,22 +1,23 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. from typing import Optional -from datetime import datetime from typing_extensions import Literal from pydantic import Field as FieldInfo from .._models import BaseModel -__all__ = ["AccountRetrieveResponse", "CurrentPeriod"] +__all__ = ["AccountRetrieveResponse", "CreditInfo"] -class CurrentPeriod(BaseModel): - end: datetime +class CreditInfo(BaseModel): + auto_topup_enabled: bool = FieldInfo(alias="autoTopupEnabled") - start: datetime + balance: int - usage_percent: float = FieldInfo(alias="usagePercent") + lifetime_purchased: int = FieldInfo(alias="lifetimePurchased") + + lifetime_used: int = FieldInfo(alias="lifetimeUsed") class AccountRetrieveResponse(BaseModel): @@ -26,4 +27,4 @@ class AccountRetrieveResponse(BaseModel): plan: Literal["active", "inactive"] - current_period: Optional[CurrentPeriod] = FieldInfo(alias="currentPeriod", default=None) + credit_info: Optional[CreditInfo] = FieldInfo(alias="creditInfo", default=None) diff --git a/src/x_twitter_scraper/types/bot/__init__.py b/src/x_twitter_scraper/types/bot/__init__.py deleted file mode 100644 index f8ee8b1..0000000 --- a/src/x_twitter_scraper/types/bot/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations diff --git a/src/x_twitter_scraper/types/extraction_estimate_cost_response.py b/src/x_twitter_scraper/types/extraction_estimate_cost_response.py index 641b731..6893256 100644 --- a/src/x_twitter_scraper/types/extraction_estimate_cost_response.py +++ b/src/x_twitter_scraper/types/extraction_estimate_cost_response.py @@ -1,5 +1,7 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional + from pydantic import Field as FieldInfo from .._models import BaseModel @@ -10,10 +12,12 @@ class ExtractionEstimateCostResponse(BaseModel): allowed: bool - estimated_results: int = FieldInfo(alias="estimatedResults") + credits_available: str = FieldInfo(alias="creditsAvailable") - projected_percent: float = FieldInfo(alias="projectedPercent") + credits_required: str = FieldInfo(alias="creditsRequired") + + estimated_results: int = FieldInfo(alias="estimatedResults") source: str - usage_percent: float = FieldInfo(alias="usagePercent") + resolved_x_user_id: Optional[str] = FieldInfo(alias="resolvedXUserId", default=None) diff --git a/src/x_twitter_scraper/types/integration.py b/src/x_twitter_scraper/types/integration.py deleted file mode 100644 index 22f6b40..0000000 --- a/src/x_twitter_scraper/types/integration.py +++ /dev/null @@ -1,41 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Dict, List, Optional -from datetime import datetime -from typing_extensions import Literal - -from pydantic import Field as FieldInfo - -from .._models import BaseModel -from .shared.event_type import EventType - -__all__ = ["Integration"] - - -class Integration(BaseModel): - """Third-party integration (e.g. Telegram) subscribed to monitor events.""" - - id: str - - config: Dict[str, object] - """Integration config — shape varies by type (JSON)""" - - created_at: datetime = FieldInfo(alias="createdAt") - - event_types: List[EventType] = FieldInfo(alias="eventTypes") - """Array of event types to subscribe to.""" - - is_active: bool = FieldInfo(alias="isActive") - - name: str - - type: Literal["telegram"] - - filters: Optional[Dict[str, object]] = None - """Event filter rules (JSON)""" - - message_template: Optional[str] = FieldInfo(alias="messageTemplate", default=None) - - scope_all_monitors: Optional[bool] = FieldInfo(alias="scopeAllMonitors", default=None) - - silent_push: Optional[bool] = FieldInfo(alias="silentPush", default=None) diff --git a/src/x_twitter_scraper/types/integration_create_params.py b/src/x_twitter_scraper/types/integration_create_params.py deleted file mode 100644 index e77c840..0000000 --- a/src/x_twitter_scraper/types/integration_create_params.py +++ /dev/null @@ -1,29 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import List -from typing_extensions import Literal, Required, Annotated, TypedDict - -from .._utils import PropertyInfo -from .shared.event_type import EventType - -__all__ = ["IntegrationCreateParams", "Config"] - - -class IntegrationCreateParams(TypedDict, total=False): - config: Required[Config] - """Integration config (e.g. Telegram chatId)""" - - event_types: Required[Annotated[List[EventType], PropertyInfo(alias="eventTypes")]] - """Array of event types to subscribe to.""" - - name: Required[str] - - type: Required[Literal["telegram"]] - - -class Config(TypedDict, total=False): - """Integration config (e.g. Telegram chatId)""" - - chat_id: Required[Annotated[str, PropertyInfo(alias="chatId")]] diff --git a/src/x_twitter_scraper/types/integration_delete_response.py b/src/x_twitter_scraper/types/integration_delete_response.py deleted file mode 100644 index 76da0b5..0000000 --- a/src/x_twitter_scraper/types/integration_delete_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["IntegrationDeleteResponse"] - - -class IntegrationDeleteResponse(BaseModel): - success: Literal[True] diff --git a/src/x_twitter_scraper/types/integration_delivery.py b/src/x_twitter_scraper/types/integration_delivery.py deleted file mode 100644 index bda8435..0000000 --- a/src/x_twitter_scraper/types/integration_delivery.py +++ /dev/null @@ -1,34 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import Optional -from datetime import datetime - -from pydantic import Field as FieldInfo - -from .._models import BaseModel - -__all__ = ["IntegrationDelivery"] - - -class IntegrationDelivery(BaseModel): - """Integration delivery attempt record with status and retry count.""" - - id: str - - attempts: int - - created_at: datetime = FieldInfo(alias="createdAt") - - event_type: str = FieldInfo(alias="eventType") - - status: str - - delivered_at: Optional[datetime] = FieldInfo(alias="deliveredAt", default=None) - - last_error: Optional[str] = FieldInfo(alias="lastError", default=None) - - last_status_code: Optional[int] = FieldInfo(alias="lastStatusCode", default=None) - - source_id: Optional[str] = FieldInfo(alias="sourceId", default=None) - - source_type: Optional[str] = FieldInfo(alias="sourceType", default=None) diff --git a/src/x_twitter_scraper/types/integration_list_deliveries_params.py b/src/x_twitter_scraper/types/integration_list_deliveries_params.py deleted file mode 100644 index 948aefd..0000000 --- a/src/x_twitter_scraper/types/integration_list_deliveries_params.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing_extensions import TypedDict - -__all__ = ["IntegrationListDeliveriesParams"] - - -class IntegrationListDeliveriesParams(TypedDict, total=False): - limit: int - """Maximum number of items to return (1-100, default 50)""" diff --git a/src/x_twitter_scraper/types/integration_list_deliveries_response.py b/src/x_twitter_scraper/types/integration_list_deliveries_response.py deleted file mode 100644 index ec5a8ed..0000000 --- a/src/x_twitter_scraper/types/integration_list_deliveries_response.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .._models import BaseModel -from .integration_delivery import IntegrationDelivery - -__all__ = ["IntegrationListDeliveriesResponse"] - - -class IntegrationListDeliveriesResponse(BaseModel): - deliveries: List[IntegrationDelivery] diff --git a/src/x_twitter_scraper/types/integration_list_response.py b/src/x_twitter_scraper/types/integration_list_response.py deleted file mode 100644 index 95aa13a..0000000 --- a/src/x_twitter_scraper/types/integration_list_response.py +++ /dev/null @@ -1,12 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing import List - -from .._models import BaseModel -from .integration import Integration - -__all__ = ["IntegrationListResponse"] - - -class IntegrationListResponse(BaseModel): - integrations: List[Integration] diff --git a/src/x_twitter_scraper/types/integration_send_test_response.py b/src/x_twitter_scraper/types/integration_send_test_response.py deleted file mode 100644 index b45b687..0000000 --- a/src/x_twitter_scraper/types/integration_send_test_response.py +++ /dev/null @@ -1,11 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from typing_extensions import Literal - -from .._models import BaseModel - -__all__ = ["IntegrationSendTestResponse"] - - -class IntegrationSendTestResponse(BaseModel): - success: Literal[True] diff --git a/src/x_twitter_scraper/types/integration_update_params.py b/src/x_twitter_scraper/types/integration_update_params.py deleted file mode 100644 index a09d038..0000000 --- a/src/x_twitter_scraper/types/integration_update_params.py +++ /dev/null @@ -1,30 +0,0 @@ -# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. - -from __future__ import annotations - -from typing import Dict, List -from typing_extensions import Annotated, TypedDict - -from .._utils import PropertyInfo -from .shared.event_type import EventType - -__all__ = ["IntegrationUpdateParams"] - - -class IntegrationUpdateParams(TypedDict, total=False): - event_types: Annotated[List[EventType], PropertyInfo(alias="eventTypes")] - """Array of event types to subscribe to.""" - - filters: Dict[str, object] - """Event filter rules (JSON)""" - - is_active: Annotated[bool, PropertyInfo(alias="isActive")] - - message_template: Annotated[Dict[str, object], PropertyInfo(alias="messageTemplate")] - """Custom message template (JSON)""" - - name: str - - scope_all_monitors: Annotated[bool, PropertyInfo(alias="scopeAllMonitors")] - - silent_push: Annotated[bool, PropertyInfo(alias="silentPush")] diff --git a/src/x_twitter_scraper/types/radar_item.py b/src/x_twitter_scraper/types/radar_item.py index ccf954a..958b410 100644 --- a/src/x_twitter_scraper/types/radar_item.py +++ b/src/x_twitter_scraper/types/radar_item.py @@ -1,7 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import Dict, Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -11,9 +12,32 @@ class RadarItem(BaseModel): - """Trending topic with score, category, source, and region.""" + """ + Trending topic with score, category, source, region, language, and source-specific metadata. + """ - category: str + id: str + """Internal numeric identifier (stringified bigint).""" + + category: Literal["general", "tech", "dev", "science", "culture", "politics", "business", "entertainment"] + + created_at: datetime = FieldInfo(alias="createdAt") + + language: str + + metadata: Dict[str, object] + """Source-specific fields. Shape varies per source: + + - reddit: { subreddit: string, author: string } + - 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?, + growthMrrPercent?, multiple?, paymentProvider?, rank? } + """ published_at: datetime = FieldInfo(alias="publishedAt") @@ -21,7 +45,10 @@ class RadarItem(BaseModel): score: float - source: str + source: Literal["github", "google_trends", "hacker_news", "polymarket", "reddit", "trustmrr", "wikipedia"] + + source_id: str = FieldInfo(alias="sourceId") + """Source-specific identifier used for deduplication.""" title: str diff --git a/src/x_twitter_scraper/types/radar_retrieve_trending_topics_params.py b/src/x_twitter_scraper/types/radar_retrieve_trending_topics_params.py index 3dd5d4c..cda5dc6 100644 --- a/src/x_twitter_scraper/types/radar_retrieve_trending_topics_params.py +++ b/src/x_twitter_scraper/types/radar_retrieve_trending_topics_params.py @@ -8,14 +8,17 @@ class RadarRetrieveTrendingTopicsParams(TypedDict, total=False): - category: str - """Filter by category (general, tech, dev, etc.)""" + after: str + """Cursor for pagination (from prior response nextCursor).""" - count: int - """Number of items to return""" + category: Literal["general", "tech", "dev", "science", "culture", "politics", "business", "entertainment"] + """Filter by category.""" hours: int - """Lookback window in hours""" + """Lookback window in hours (1-168, default 24).""" + + limit: int + """Number of items to return (1-100, default 50).""" region: str """Region filter (us, global, etc.)""" diff --git a/src/x_twitter_scraper/types/radar_retrieve_trending_topics_response.py b/src/x_twitter_scraper/types/radar_retrieve_trending_topics_response.py index a786b57..4c94a3b 100644 --- a/src/x_twitter_scraper/types/radar_retrieve_trending_topics_response.py +++ b/src/x_twitter_scraper/types/radar_retrieve_trending_topics_response.py @@ -1,6 +1,8 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import List +from typing import List, Optional + +from pydantic import Field as FieldInfo from .._models import BaseModel from .radar_item import RadarItem @@ -9,6 +11,9 @@ class RadarRetrieveTrendingTopicsResponse(BaseModel): + has_more: bool = FieldInfo(alias="hasMore") + items: List[RadarItem] - total: int + next_cursor: Optional[str] = FieldInfo(alias="nextCursor", default=None) + """Opaque cursor for the next page (present only when hasMore is true).""" diff --git a/src/x_twitter_scraper/types/shared/__init__.py b/src/x_twitter_scraper/types/shared/__init__.py index a9ea29f..8796775 100644 --- a/src/x_twitter_scraper/types/shared/__init__.py +++ b/src/x_twitter_scraper/types/shared/__init__.py @@ -2,5 +2,7 @@ from .error import Error as Error from .event_type import EventType as EventType +from .search_tweet import SearchTweet as SearchTweet +from .user_profile import UserProfile as UserProfile from .paginated_users import PaginatedUsers as PaginatedUsers from .paginated_tweets import PaginatedTweets as PaginatedTweets diff --git a/src/x_twitter_scraper/types/shared/error.py b/src/x_twitter_scraper/types/shared/error.py index 251e5b6..a4afc39 100644 --- a/src/x_twitter_scraper/types/shared/error.py +++ b/src/x_twitter_scraper/types/shared/error.py @@ -20,17 +20,17 @@ class Error(BaseModel): "invalid_tweet_id", "invalid_tweet_url", "invalid_username", + "insufficient_credits", "missing_params", "missing_query", "monitor_already_exists", "monitor_limit_reached", + "no_credits", "no_subscription", "not_found", - "stream_registration_failed", "subscription_inactive", "tweet_not_found", "unauthenticated", - "usage_limit_reached", "user_not_found", "webhook_inactive", "x_api_rate_limited", diff --git a/src/x_twitter_scraper/types/shared/event_type.py b/src/x_twitter_scraper/types/shared/event_type.py index ecf46ca..f4bafa6 100644 --- a/src/x_twitter_scraper/types/shared/event_type.py +++ b/src/x_twitter_scraper/types/shared/event_type.py @@ -4,6 +4,4 @@ __all__ = ["EventType"] -EventType: TypeAlias = Literal[ - "tweet.new", "tweet.reply", "tweet.retweet", "tweet.quote", "follower.gained", "follower.lost" -] +EventType: TypeAlias = Literal["tweet.new", "tweet.reply", "tweet.retweet", "tweet.quote"] diff --git a/src/x_twitter_scraper/types/shared/paginated_tweets.py b/src/x_twitter_scraper/types/shared/paginated_tweets.py index 5852ddd..edd6227 100644 --- a/src/x_twitter_scraper/types/shared/paginated_tweets.py +++ b/src/x_twitter_scraper/types/shared/paginated_tweets.py @@ -3,7 +3,7 @@ from typing import List from ..._models import BaseModel -from ..x.search_tweet import SearchTweet +from .search_tweet import SearchTweet __all__ = ["PaginatedTweets"] diff --git a/src/x_twitter_scraper/types/shared/paginated_users.py b/src/x_twitter_scraper/types/shared/paginated_users.py index 4065b4a..0982c4a 100644 --- a/src/x_twitter_scraper/types/shared/paginated_users.py +++ b/src/x_twitter_scraper/types/shared/paginated_users.py @@ -3,7 +3,7 @@ from typing import List from ..._models import BaseModel -from ..x.user_profile import UserProfile +from .user_profile import UserProfile __all__ = ["PaginatedUsers"] diff --git a/src/x_twitter_scraper/types/x/search_tweet.py b/src/x_twitter_scraper/types/shared/search_tweet.py similarity index 100% rename from src/x_twitter_scraper/types/x/search_tweet.py rename to src/x_twitter_scraper/types/shared/search_tweet.py diff --git a/src/x_twitter_scraper/types/x/user_profile.py b/src/x_twitter_scraper/types/shared/user_profile.py similarity index 100% rename from src/x_twitter_scraper/types/x/user_profile.py rename to src/x_twitter_scraper/types/shared/user_profile.py diff --git a/src/x_twitter_scraper/types/shared_params/event_type.py b/src/x_twitter_scraper/types/shared_params/event_type.py index ed5adaa..aea9e2a 100644 --- a/src/x_twitter_scraper/types/shared_params/event_type.py +++ b/src/x_twitter_scraper/types/shared_params/event_type.py @@ -6,6 +6,4 @@ __all__ = ["EventType"] -EventType: TypeAlias = Literal[ - "tweet.new", "tweet.reply", "tweet.retweet", "tweet.quote", "follower.gained", "follower.lost" -] +EventType: TypeAlias = Literal["tweet.new", "tweet.reply", "tweet.retweet", "tweet.quote"] diff --git a/src/x_twitter_scraper/types/x/__init__.py b/src/x_twitter_scraper/types/x/__init__.py index d40e6e6..7f79a0c 100644 --- a/src/x_twitter_scraper/types/x/__init__.py +++ b/src/x_twitter_scraper/types/x/__init__.py @@ -3,10 +3,8 @@ from __future__ import annotations from .x_account import XAccount as XAccount -from .search_tweet import SearchTweet as SearchTweet from .tweet_author import TweetAuthor as TweetAuthor from .tweet_detail import TweetDetail as TweetDetail -from .user_profile import UserProfile as UserProfile from .dm_send_params import DmSendParams as DmSendParams from .dm_send_response import DmSendResponse as DmSendResponse from .x_account_detail import XAccountDetail as XAccountDetail diff --git a/src/x_twitter_scraper/types/x/account_create_response.py b/src/x_twitter_scraper/types/x/account_create_response.py index 3c64957..0a83f21 100644 --- a/src/x_twitter_scraper/types/x/account_create_response.py +++ b/src/x_twitter_scraper/types/x/account_create_response.py @@ -1,5 +1,9 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + from pydantic import Field as FieldInfo from ..._models import BaseModel @@ -8,10 +12,26 @@ class AccountCreateResponse(BaseModel): + """Sanitized X account summary returned by connect and reauth. + + Includes an optional `loginCountry` field surfaced only when the declared proxy region had no Driver capacity and the login fell back to a single US consumer device for this one-time action. Future activity continues to use the selected `proxy_country`; the field is omitted on normal logins. + """ + 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") + + login_country: Optional[str] = FieldInfo(alias="loginCountry", default=None) + """ + ISO-3166-1 alpha-2 country code of the Driver consumer device used for this + login. Present only when the US fallback was triggered because Driver had no + capacity in the declared region. Omitted otherwise. + """ 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 8263ba1..fcde3a8 100644 --- a/src/x_twitter_scraper/types/x/account_reauth_params.py +++ b/src/x_twitter_scraper/types/x/account_reauth_params.py @@ -11,5 +11,11 @@ class AccountReauthParams(TypedDict, total=False): password: Required[str] """Updated account password""" + email: str + """Email for the X account (updates stored email)""" + + proxy_country: str + """Two-letter country code for login proxy region""" + totp_secret: str """TOTP secret for 2FA re-authentication""" 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 dbeb483..062ed1f 100644 --- a/src/x_twitter_scraper/types/x/account_reauth_response.py +++ b/src/x_twitter_scraper/types/x/account_reauth_response.py @@ -1,5 +1,9 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + from pydantic import Field as FieldInfo from ..._models import BaseModel @@ -8,8 +12,26 @@ class AccountReauthResponse(BaseModel): + """Sanitized X account summary returned by connect and reauth. + + Includes an optional `loginCountry` field surfaced only when the declared proxy region had no Driver capacity and the login fell back to a single US consumer device for this one-time action. Future activity continues to use the selected `proxy_country`; the field is omitted on normal logins. + """ + 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") + + login_country: Optional[str] = FieldInfo(alias="loginCountry", default=None) + """ + ISO-3166-1 alpha-2 country code of the Driver consumer device used for this + login. Present only when the US fallback was triggered because Driver had no + capacity in the declared region. Omitted otherwise. + """ diff --git a/src/x_twitter_scraper/types/x/tweet_create_params.py b/src/x_twitter_scraper/types/x/tweet_create_params.py index ff4d4d1..ee055bb 100644 --- a/src/x_twitter_scraper/types/x/tweet_create_params.py +++ b/src/x_twitter_scraper/types/x/tweet_create_params.py @@ -13,14 +13,19 @@ class TweetCreateParams(TypedDict, total=False): account: Required[str] """X account (@username or account ID)""" - text: Required[str] - attachment_url: str community_id: str is_note_tweet: bool + media: SequenceNotStr[str] + """Array of media URLs to attach (mutually exclusive with media_ids)""" + media_ids: SequenceNotStr[str] + """Array of media IDs to attach (mutually exclusive with media)""" reply_to_tweet_id: str + + text: str + """Tweet text (optional when media is provided)""" diff --git a/src/x_twitter_scraper/types/x/x_account.py b/src/x_twitter_scraper/types/x/x_account.py index 6036370..4f80553 100644 --- a/src/x_twitter_scraper/types/x/x_account.py +++ b/src/x_twitter_scraper/types/x/x_account.py @@ -1,6 +1,7 @@ # 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 @@ -16,6 +17,15 @@ class XAccount(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") + health: Literal["healthy", "locked", "needsReauth", "recovering", "suspended", "temporaryIssue"] + """Derived login/cookie health. + + `healthy` = cookies valid. `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` = transient backend problem; retry shortly. + """ + status: str x_user_id: str = FieldInfo(alias="xUserId") diff --git a/src/x_twitter_scraper/types/x/x_account_detail.py b/src/x_twitter_scraper/types/x/x_account_detail.py index 5b339de..f2c2fb5 100644 --- a/src/x_twitter_scraper/types/x/x_account_detail.py +++ b/src/x_twitter_scraper/types/x/x_account_detail.py @@ -2,6 +2,7 @@ from typing import Optional from datetime import datetime +from typing_extensions import Literal from pydantic import Field as FieldInfo @@ -17,6 +18,8 @@ class XAccountDetail(BaseModel): created_at: datetime = FieldInfo(alias="createdAt") + health: Literal["healthy", "locked", "needsReauth", "recovering", "suspended", "temporaryIssue"] + status: str x_user_id: str = FieldInfo(alias="xUserId") diff --git a/src/x_twitter_scraper/types/x_get_trends_params.py b/src/x_twitter_scraper/types/x_get_trends_params.py new file mode 100644 index 0000000..44cabeb --- /dev/null +++ b/src/x_twitter_scraper/types/x_get_trends_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["XGetTrendsParams"] + + +class XGetTrendsParams(TypedDict, total=False): + count: int + """Number of trending topics to return (1-50, default 30)""" + + woeid: int + """Region WOEID (1=Worldwide, 23424977=US, 23424975=UK, 23424969=Turkey)""" diff --git a/tests/api_resources/test_integrations.py b/tests/api_resources/test_integrations.py deleted file mode 100644 index ede13a0..0000000 --- a/tests/api_resources/test_integrations.py +++ /dev/null @@ -1,640 +0,0 @@ -# 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 import ( - Integration, - IntegrationListResponse, - IntegrationDeleteResponse, - IntegrationSendTestResponse, - IntegrationListDeliveriesResponse, -) - -base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") - - -class TestIntegrations: - 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_create(self, client: XTwitterScraper) -> None: - integration = client.integrations.create( - config={"chat_id": "-1001234567890"}, - event_types=["tweet.new", "follower.gained"], - name="My Telegram Bot", - type="telegram", - ) - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_create(self, client: XTwitterScraper) -> None: - response = client.integrations.with_raw_response.create( - config={"chat_id": "-1001234567890"}, - event_types=["tweet.new", "follower.gained"], - name="My Telegram Bot", - type="telegram", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_create(self, client: XTwitterScraper) -> None: - with client.integrations.with_streaming_response.create( - config={"chat_id": "-1001234567890"}, - event_types=["tweet.new", "follower.gained"], - name="My Telegram Bot", - type="telegram", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_retrieve(self, client: XTwitterScraper) -> None: - integration = client.integrations.retrieve( - "id", - ) - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_retrieve(self, client: XTwitterScraper) -> None: - response = client.integrations.with_raw_response.retrieve( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_retrieve(self, client: XTwitterScraper) -> None: - with client.integrations.with_streaming_response.retrieve( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = response.parse() - assert_matches_type(Integration, integration, 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.integrations.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update(self, client: XTwitterScraper) -> None: - integration = client.integrations.update( - id="id", - ) - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_update_with_all_params(self, client: XTwitterScraper) -> None: - integration = client.integrations.update( - id="id", - event_types=["tweet.new"], - filters={}, - is_active=True, - message_template={}, - name="My Telegram Bot", - scope_all_monitors=True, - silent_push=False, - ) - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_update(self, client: XTwitterScraper) -> None: - response = client.integrations.with_raw_response.update( - id="id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_update(self, client: XTwitterScraper) -> None: - with client.integrations.with_streaming_response.update( - id="id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_update(self, client: XTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.integrations.with_raw_response.update( - id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list(self, client: XTwitterScraper) -> None: - integration = client.integrations.list() - assert_matches_type(IntegrationListResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_list(self, client: XTwitterScraper) -> None: - response = client.integrations.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = response.parse() - assert_matches_type(IntegrationListResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_list(self, client: XTwitterScraper) -> None: - with client.integrations.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = response.parse() - assert_matches_type(IntegrationListResponse, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_delete(self, client: XTwitterScraper) -> None: - integration = client.integrations.delete( - "id", - ) - assert_matches_type(IntegrationDeleteResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_delete(self, client: XTwitterScraper) -> None: - response = client.integrations.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = response.parse() - assert_matches_type(IntegrationDeleteResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_delete(self, client: XTwitterScraper) -> None: - with client.integrations.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = response.parse() - assert_matches_type(IntegrationDeleteResponse, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_delete(self, client: XTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.integrations.with_raw_response.delete( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_deliveries(self, client: XTwitterScraper) -> None: - integration = client.integrations.list_deliveries( - id="id", - ) - assert_matches_type(IntegrationListDeliveriesResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_list_deliveries_with_all_params(self, client: XTwitterScraper) -> None: - integration = client.integrations.list_deliveries( - id="id", - limit=1, - ) - assert_matches_type(IntegrationListDeliveriesResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_list_deliveries(self, client: XTwitterScraper) -> None: - response = client.integrations.with_raw_response.list_deliveries( - id="id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = response.parse() - assert_matches_type(IntegrationListDeliveriesResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_list_deliveries(self, client: XTwitterScraper) -> None: - with client.integrations.with_streaming_response.list_deliveries( - id="id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = response.parse() - assert_matches_type(IntegrationListDeliveriesResponse, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_list_deliveries(self, client: XTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.integrations.with_raw_response.list_deliveries( - id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_method_send_test(self, client: XTwitterScraper) -> None: - integration = client.integrations.send_test( - "id", - ) - assert_matches_type(IntegrationSendTestResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_raw_response_send_test(self, client: XTwitterScraper) -> None: - response = client.integrations.with_raw_response.send_test( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = response.parse() - assert_matches_type(IntegrationSendTestResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_streaming_response_send_test(self, client: XTwitterScraper) -> None: - with client.integrations.with_streaming_response.send_test( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = response.parse() - assert_matches_type(IntegrationSendTestResponse, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - def test_path_params_send_test(self, client: XTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - client.integrations.with_raw_response.send_test( - "", - ) - - -class TestAsyncIntegrations: - 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_create(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.create( - config={"chat_id": "-1001234567890"}, - event_types=["tweet.new", "follower.gained"], - name="My Telegram Bot", - type="telegram", - ) - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> None: - response = await async_client.integrations.with_raw_response.create( - config={"chat_id": "-1001234567890"}, - event_types=["tweet.new", "follower.gained"], - name="My Telegram Bot", - type="telegram", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = await response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_create(self, async_client: AsyncXTwitterScraper) -> None: - async with async_client.integrations.with_streaming_response.create( - config={"chat_id": "-1001234567890"}, - event_types=["tweet.new", "follower.gained"], - name="My Telegram Bot", - type="telegram", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = await response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_retrieve(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.retrieve( - "id", - ) - assert_matches_type(Integration, integration, 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.integrations.with_raw_response.retrieve( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = await response.parse() - assert_matches_type(Integration, integration, 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.integrations.with_streaming_response.retrieve( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = await response.parse() - assert_matches_type(Integration, integration, 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.integrations.with_raw_response.retrieve( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.update( - id="id", - ) - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_update_with_all_params(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.update( - id="id", - event_types=["tweet.new"], - filters={}, - is_active=True, - message_template={}, - name="My Telegram Bot", - scope_all_monitors=True, - silent_push=False, - ) - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_update(self, async_client: AsyncXTwitterScraper) -> None: - response = await async_client.integrations.with_raw_response.update( - id="id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = await response.parse() - assert_matches_type(Integration, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_update(self, async_client: AsyncXTwitterScraper) -> None: - async with async_client.integrations.with_streaming_response.update( - id="id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = await response.parse() - assert_matches_type(Integration, integration, 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_update(self, async_client: AsyncXTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.integrations.with_raw_response.update( - id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.list() - assert_matches_type(IntegrationListResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_list(self, async_client: AsyncXTwitterScraper) -> None: - response = await async_client.integrations.with_raw_response.list() - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = await response.parse() - assert_matches_type(IntegrationListResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_list(self, async_client: AsyncXTwitterScraper) -> None: - async with async_client.integrations.with_streaming_response.list() as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = await response.parse() - assert_matches_type(IntegrationListResponse, integration, path=["response"]) - - assert cast(Any, response.is_closed) is True - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_delete(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.delete( - "id", - ) - assert_matches_type(IntegrationDeleteResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_delete(self, async_client: AsyncXTwitterScraper) -> None: - response = await async_client.integrations.with_raw_response.delete( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = await response.parse() - assert_matches_type(IntegrationDeleteResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_delete(self, async_client: AsyncXTwitterScraper) -> None: - async with async_client.integrations.with_streaming_response.delete( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = await response.parse() - assert_matches_type(IntegrationDeleteResponse, integration, 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_delete(self, async_client: AsyncXTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.integrations.with_raw_response.delete( - "", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_deliveries(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.list_deliveries( - id="id", - ) - assert_matches_type(IntegrationListDeliveriesResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_list_deliveries_with_all_params(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.list_deliveries( - id="id", - limit=1, - ) - assert_matches_type(IntegrationListDeliveriesResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_list_deliveries(self, async_client: AsyncXTwitterScraper) -> None: - response = await async_client.integrations.with_raw_response.list_deliveries( - id="id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = await response.parse() - assert_matches_type(IntegrationListDeliveriesResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_list_deliveries(self, async_client: AsyncXTwitterScraper) -> None: - async with async_client.integrations.with_streaming_response.list_deliveries( - id="id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = await response.parse() - assert_matches_type(IntegrationListDeliveriesResponse, integration, 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_list_deliveries(self, async_client: AsyncXTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.integrations.with_raw_response.list_deliveries( - id="", - ) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_method_send_test(self, async_client: AsyncXTwitterScraper) -> None: - integration = await async_client.integrations.send_test( - "id", - ) - assert_matches_type(IntegrationSendTestResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_raw_response_send_test(self, async_client: AsyncXTwitterScraper) -> None: - response = await async_client.integrations.with_raw_response.send_test( - "id", - ) - - assert response.is_closed is True - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - integration = await response.parse() - assert_matches_type(IntegrationSendTestResponse, integration, path=["response"]) - - @pytest.mark.skip(reason="Mock server tests are disabled") - @parametrize - async def test_streaming_response_send_test(self, async_client: AsyncXTwitterScraper) -> None: - async with async_client.integrations.with_streaming_response.send_test( - "id", - ) as response: - assert not response.is_closed - assert response.http_request.headers.get("X-Stainless-Lang") == "python" - - integration = await response.parse() - assert_matches_type(IntegrationSendTestResponse, integration, 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_send_test(self, async_client: AsyncXTwitterScraper) -> None: - with pytest.raises(ValueError, match=r"Expected a non-empty value for `id` but received ''"): - await async_client.integrations.with_raw_response.send_test( - "", - ) diff --git a/tests/api_resources/test_monitors.py b/tests/api_resources/test_monitors.py index 7574362..e705257 100644 --- a/tests/api_resources/test_monitors.py +++ b/tests/api_resources/test_monitors.py @@ -26,7 +26,7 @@ class TestMonitors: @parametrize def test_method_create(self, client: XTwitterScraper) -> None: monitor = client.monitors.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], username="elonmusk", ) assert_matches_type(MonitorCreateResponse, monitor, path=["response"]) @@ -35,7 +35,7 @@ def test_method_create(self, client: XTwitterScraper) -> None: @parametrize def test_raw_response_create(self, client: XTwitterScraper) -> None: response = client.monitors.with_raw_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], username="elonmusk", ) @@ -48,7 +48,7 @@ def test_raw_response_create(self, client: XTwitterScraper) -> None: @parametrize def test_streaming_response_create(self, client: XTwitterScraper) -> None: with client.monitors.with_streaming_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], username="elonmusk", ) as response: assert not response.is_closed @@ -233,7 +233,7 @@ class TestAsyncMonitors: @parametrize async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: monitor = await async_client.monitors.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], username="elonmusk", ) assert_matches_type(MonitorCreateResponse, monitor, path=["response"]) @@ -242,7 +242,7 @@ async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: @parametrize async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> None: response = await async_client.monitors.with_raw_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], username="elonmusk", ) @@ -255,7 +255,7 @@ async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> @parametrize async def test_streaming_response_create(self, async_client: AsyncXTwitterScraper) -> None: async with async_client.monitors.with_streaming_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], username="elonmusk", ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_radar.py b/tests/api_resources/test_radar.py index 341a210..979323c 100644 --- a/tests/api_resources/test_radar.py +++ b/tests/api_resources/test_radar.py @@ -27,9 +27,10 @@ def test_method_retrieve_trending_topics(self, client: XTwitterScraper) -> None: @parametrize def test_method_retrieve_trending_topics_with_all_params(self, client: XTwitterScraper) -> None: radar = client.radar.retrieve_trending_topics( - category="category", - count=0, - hours=0, + after="after", + category="general", + hours=1, + limit=1, region="region", source="github", ) @@ -73,9 +74,10 @@ async def test_method_retrieve_trending_topics(self, async_client: AsyncXTwitter @parametrize async def test_method_retrieve_trending_topics_with_all_params(self, async_client: AsyncXTwitterScraper) -> None: radar = await async_client.radar.retrieve_trending_topics( - category="category", - count=0, - hours=0, + after="after", + category="general", + hours=1, + limit=1, region="region", source="github", ) diff --git a/tests/api_resources/test_webhooks.py b/tests/api_resources/test_webhooks.py index f7f4c0f..07c149b 100644 --- a/tests/api_resources/test_webhooks.py +++ b/tests/api_resources/test_webhooks.py @@ -28,7 +28,7 @@ class TestWebhooks: @parametrize def test_method_create(self, client: XTwitterScraper) -> None: webhook = client.webhooks.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], url="https://example.com/webhook", ) assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @@ -37,7 +37,7 @@ def test_method_create(self, client: XTwitterScraper) -> None: @parametrize def test_raw_response_create(self, client: XTwitterScraper) -> None: response = client.webhooks.with_raw_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], url="https://example.com/webhook", ) @@ -50,7 +50,7 @@ def test_raw_response_create(self, client: XTwitterScraper) -> None: @parametrize def test_streaming_response_create(self, client: XTwitterScraper) -> None: with client.webhooks.with_streaming_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], url="https://example.com/webhook", ) as response: assert not response.is_closed @@ -278,7 +278,7 @@ class TestAsyncWebhooks: @parametrize async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: webhook = await async_client.webhooks.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], url="https://example.com/webhook", ) assert_matches_type(WebhookCreateResponse, webhook, path=["response"]) @@ -287,7 +287,7 @@ async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: @parametrize async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> None: response = await async_client.webhooks.with_raw_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], url="https://example.com/webhook", ) @@ -300,7 +300,7 @@ async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> @parametrize async def test_streaming_response_create(self, async_client: AsyncXTwitterScraper) -> None: async with async_client.webhooks.with_streaming_response.create( - event_types=["tweet.new", "follower.gained"], + event_types=["tweet.new", "tweet.reply"], url="https://example.com/webhook", ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_x.py b/tests/api_resources/test_x.py index 3ac84a7..781cc84 100644 --- a/tests/api_resources/test_x.py +++ b/tests/api_resources/test_x.py @@ -144,6 +144,15 @@ def test_method_get_trends(self, client: XTwitterScraper) -> None: x = client.x.get_trends() assert_matches_type(XGetTrendsResponse, x, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_get_trends_with_all_params(self, client: XTwitterScraper) -> None: + x = client.x.get_trends( + count=1, + woeid=0, + ) + assert_matches_type(XGetTrendsResponse, x, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize def test_raw_response_get_trends(self, client: XTwitterScraper) -> None: @@ -294,6 +303,15 @@ async def test_method_get_trends(self, async_client: AsyncXTwitterScraper) -> No x = await async_client.x.get_trends() assert_matches_type(XGetTrendsResponse, x, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_get_trends_with_all_params(self, async_client: AsyncXTwitterScraper) -> None: + x = await async_client.x.get_trends( + count=1, + woeid=0, + ) + assert_matches_type(XGetTrendsResponse, x, path=["response"]) + @pytest.mark.skip(reason="Mock server tests are disabled") @parametrize async def test_raw_response_get_trends(self, async_client: AsyncXTwitterScraper) -> None: diff --git a/tests/api_resources/x/test_accounts.py b/tests/api_resources/x/test_accounts.py index 9446fdf..b22fa6e 100644 --- a/tests/api_resources/x/test_accounts.py +++ b/tests/api_resources/x/test_accounts.py @@ -231,6 +231,8 @@ def test_method_reauth_with_all_params(self, client: XTwitterScraper) -> None: account = client.x.accounts.reauth( id="id", password="password_value", + email="user@example.com", + proxy_country="US", totp_secret="totp_secret_value", ) assert_matches_type(AccountReauthResponse, account, path=["response"]) @@ -485,6 +487,8 @@ async def test_method_reauth_with_all_params(self, async_client: AsyncXTwitterSc account = await async_client.x.accounts.reauth( id="id", password="password_value", + email="user@example.com", + proxy_country="US", totp_secret="totp_secret_value", ) assert_matches_type(AccountReauthResponse, account, path=["response"]) diff --git a/tests/api_resources/x/test_tweets.py b/tests/api_resources/x/test_tweets.py index bfb5030..42f4a54 100644 --- a/tests/api_resources/x/test_tweets.py +++ b/tests/api_resources/x/test_tweets.py @@ -27,7 +27,6 @@ class TestTweets: def test_method_create(self, client: XTwitterScraper) -> None: tweet = client.x.tweets.create( account="@elonmusk", - text="Just launched our new feature!", ) assert_matches_type(TweetCreateResponse, tweet, path=["response"]) @@ -36,12 +35,13 @@ def test_method_create(self, client: XTwitterScraper) -> None: def test_method_create_with_all_params(self, client: XTwitterScraper) -> None: tweet = client.x.tweets.create( account="@elonmusk", - text="Just launched our new feature!", attachment_url="https://x.com/elonmusk/status/1234567890", community_id="1500000000000000000", is_note_tweet=False, + media=["https://example.com/image.jpg"], media_ids=["1234567890123456789"], reply_to_tweet_id="1234567890", + text="Just launched our new feature!", ) assert_matches_type(TweetCreateResponse, tweet, path=["response"]) @@ -50,7 +50,6 @@ def test_method_create_with_all_params(self, client: XTwitterScraper) -> None: def test_raw_response_create(self, client: XTwitterScraper) -> None: response = client.x.tweets.with_raw_response.create( account="@elonmusk", - text="Just launched our new feature!", ) assert response.is_closed is True @@ -63,7 +62,6 @@ def test_raw_response_create(self, client: XTwitterScraper) -> None: def test_streaming_response_create(self, client: XTwitterScraper) -> None: with client.x.tweets.with_streaming_response.create( account="@elonmusk", - text="Just launched our new feature!", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" @@ -513,7 +511,6 @@ class TestAsyncTweets: async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: tweet = await async_client.x.tweets.create( account="@elonmusk", - text="Just launched our new feature!", ) assert_matches_type(TweetCreateResponse, tweet, path=["response"]) @@ -522,12 +519,13 @@ async def test_method_create(self, async_client: AsyncXTwitterScraper) -> None: async def test_method_create_with_all_params(self, async_client: AsyncXTwitterScraper) -> None: tweet = await async_client.x.tweets.create( account="@elonmusk", - text="Just launched our new feature!", attachment_url="https://x.com/elonmusk/status/1234567890", community_id="1500000000000000000", is_note_tweet=False, + media=["https://example.com/image.jpg"], media_ids=["1234567890123456789"], reply_to_tweet_id="1234567890", + text="Just launched our new feature!", ) assert_matches_type(TweetCreateResponse, tweet, path=["response"]) @@ -536,7 +534,6 @@ async def test_method_create_with_all_params(self, async_client: AsyncXTwitterSc async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> None: response = await async_client.x.tweets.with_raw_response.create( account="@elonmusk", - text="Just launched our new feature!", ) assert response.is_closed is True @@ -549,7 +546,6 @@ async def test_raw_response_create(self, async_client: AsyncXTwitterScraper) -> async def test_streaming_response_create(self, async_client: AsyncXTwitterScraper) -> None: async with async_client.x.tweets.with_streaming_response.create( account="@elonmusk", - text="Just launched our new feature!", ) as response: assert not response.is_closed assert response.http_request.headers.get("X-Stainless-Lang") == "python" diff --git a/tests/api_resources/x/test_users.py b/tests/api_resources/x/test_users.py index 3ed2323..2d17796 100644 --- a/tests/api_resources/x/test_users.py +++ b/tests/api_resources/x/test_users.py @@ -9,10 +9,7 @@ from tests.utils import assert_matches_type from x_twitter_scraper import XTwitterScraper, AsyncXTwitterScraper -from x_twitter_scraper.types.x import ( - UserProfile, -) -from x_twitter_scraper.types.shared import PaginatedUsers, PaginatedTweets +from x_twitter_scraper.types.shared import UserProfile, PaginatedUsers, PaginatedTweets base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/conftest.py b/tests/conftest.py index be384ff..4083279 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -46,7 +46,6 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" -bearer_token = "My Bearer Token" @pytest.fixture(scope="session") @@ -55,9 +54,7 @@ def client(request: FixtureRequest) -> Iterator[XTwitterScraper]: if not isinstance(strict, bool): raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") - with XTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=strict - ) as client: + with XTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: yield client @@ -82,10 +79,6 @@ async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncXTwitterSc raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") async with AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=strict, - http_client=http_client, + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client ) as client: yield client diff --git a/tests/test_client.py b/tests/test_client.py index 0d36937..af6b57a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -40,7 +40,6 @@ T = TypeVar("T") base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") api_key = "My API Key" -bearer_token = "My Bearer Token" def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: @@ -141,10 +140,6 @@ def test_copy(self, client: XTwitterScraper) -> None: assert copied.api_key == "another My API Key" assert client.api_key == "My API Key" - copied = client.copy(bearer_token="another My Bearer Token") - assert copied.bearer_token == "another My Bearer Token" - assert client.bearer_token == "My Bearer Token" - def test_copy_default_options(self, client: XTwitterScraper) -> None: # options that have a default are overridden correctly copied = client.copy(max_retries=7) @@ -163,11 +158,7 @@ def test_copy_default_options(self, client: XTwitterScraper) -> None: def test_copy_default_headers(self) -> None: client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -202,11 +193,7 @@ def test_copy_default_headers(self) -> None: def test_copy_default_query(self) -> None: client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_query={"foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -332,11 +319,7 @@ def test_request_timeout(self, client: XTwitterScraper) -> None: def test_client_timeout_option(self) -> None: client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - timeout=httpx.Timeout(0), + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -349,11 +332,7 @@ def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used with httpx.Client(timeout=None) as http_client: client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -365,11 +344,7 @@ def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default with httpx.Client() as http_client: client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -381,11 +356,7 @@ def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -400,18 +371,13 @@ async def test_invalid_http_client(self) -> None: XTwitterScraper( base_url=base_url, api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=cast(Any, http_client), ) def test_default_headers_option(self) -> None: test_client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" @@ -420,7 +386,6 @@ def test_default_headers_option(self) -> None: test_client2 = XTwitterScraper( base_url=base_url, api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -435,21 +400,12 @@ def test_default_headers_option(self) -> None: test_client2.close() def test_validate_headers(self) -> None: - client = XTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + client = XTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("X-Api-Key") == api_key - with update_env( - **{ - "X_TWITTER_SCRAPER_API_KEY": Omit(), - "X_TWITTER_SCRAPER_BEARER_TOKEN": Omit(), - } - ): - client2 = XTwitterScraper( - base_url=base_url, api_key=None, bearer_token=None, _strict_response_validation=True - ) + with update_env(**{"X_TWITTER_SCRAPER_API_KEY": Omit()}): + client2 = XTwitterScraper(base_url=base_url, api_key=None, _strict_response_validation=True) with pytest.raises( TypeError, @@ -462,11 +418,7 @@ def test_validate_headers(self) -> None: def test_default_query_option(self) -> None: client = XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_query={"query_param": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -663,7 +615,6 @@ def mock_handler(request: httpx.Request) -> httpx.Response: with XTwitterScraper( base_url=base_url, api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), ) as client: @@ -762,10 +713,7 @@ class Model(BaseModel): def test_base_url_setter(self) -> None: client = XTwitterScraper( - base_url="https://example.com/from_init", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -777,22 +725,18 @@ def test_base_url_setter(self) -> None: def test_base_url_env(self) -> None: with update_env(X_TWITTER_SCRAPER_BASE_URL="http://localhost:5000/from/env"): - client = XTwitterScraper(api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True) + client = XTwitterScraper(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ XTwitterScraper( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), XTwitterScraper( base_url="http://localhost:5000/custom/path/", api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -814,15 +758,11 @@ def test_base_url_trailing_slash(self, client: XTwitterScraper) -> None: "client", [ XTwitterScraper( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), XTwitterScraper( base_url="http://localhost:5000/custom/path/", api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -844,15 +784,11 @@ def test_base_url_no_trailing_slash(self, client: XTwitterScraper) -> None: "client", [ XTwitterScraper( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), XTwitterScraper( base_url="http://localhost:5000/custom/path/", api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.Client(), ), @@ -871,9 +807,7 @@ def test_absolute_request_url(self, client: XTwitterScraper) -> None: client.close() def test_copied_client_does_not_close_http(self) -> None: - test_client = XTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + test_client = XTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -884,9 +818,7 @@ def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() def test_client_context_manager(self) -> None: - test_client = XTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + test_client = XTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -908,11 +840,7 @@ class Model(BaseModel): def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): XTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - max_retries=cast(Any, None), + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) ) @pytest.mark.respx(base_url=base_url) @@ -922,16 +850,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = XTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + strict_client = XTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): strict_client.get("/foo", cast_to=Model) - non_strict_client = XTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=False - ) + non_strict_client = XTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] @@ -1150,10 +1074,6 @@ def test_copy(self, async_client: AsyncXTwitterScraper) -> None: assert copied.api_key == "another My API Key" assert async_client.api_key == "My API Key" - copied = async_client.copy(bearer_token="another My Bearer Token") - assert copied.bearer_token == "another My Bearer Token" - assert async_client.bearer_token == "My Bearer Token" - def test_copy_default_options(self, async_client: AsyncXTwitterScraper) -> None: # options that have a default are overridden correctly copied = async_client.copy(max_retries=7) @@ -1172,11 +1092,7 @@ def test_copy_default_options(self, async_client: AsyncXTwitterScraper) -> None: async def test_copy_default_headers(self) -> None: client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) assert client.default_headers["X-Foo"] == "bar" @@ -1211,11 +1127,7 @@ async def test_copy_default_headers(self) -> None: async def test_copy_default_query(self) -> None: client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_query={"foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} ) assert _get_params(client)["foo"] == "bar" @@ -1343,11 +1255,7 @@ async def test_request_timeout(self, async_client: AsyncXTwitterScraper) -> None async def test_client_timeout_option(self) -> None: client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - timeout=httpx.Timeout(0), + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1360,11 +1268,7 @@ async def test_http_client_timeout_option(self) -> None: # custom timeout given to the httpx client should be used async with httpx.AsyncClient(timeout=None) as http_client: client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1376,11 +1280,7 @@ async def test_http_client_timeout_option(self) -> None: # no timeout given to the httpx client should not use the httpx default async with httpx.AsyncClient() as http_client: client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1392,11 +1292,7 @@ async def test_http_client_timeout_option(self) -> None: # explicitly passing the default timeout currently results in it being ignored async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - http_client=http_client, + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) @@ -1411,18 +1307,13 @@ def test_invalid_http_client(self) -> None: AsyncXTwitterScraper( base_url=base_url, api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=cast(Any, http_client), ) async def test_default_headers_option(self) -> None: test_client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_headers={"X-Foo": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} ) request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("x-foo") == "bar" @@ -1431,7 +1322,6 @@ async def test_default_headers_option(self) -> None: test_client2 = AsyncXTwitterScraper( base_url=base_url, api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, default_headers={ "X-Foo": "stainless", @@ -1446,21 +1336,12 @@ async def test_default_headers_option(self) -> None: await test_client2.close() def test_validate_headers(self) -> None: - client = AsyncXTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + client = AsyncXTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) assert request.headers.get("X-Api-Key") == api_key - with update_env( - **{ - "X_TWITTER_SCRAPER_API_KEY": Omit(), - "X_TWITTER_SCRAPER_BEARER_TOKEN": Omit(), - } - ): - client2 = AsyncXTwitterScraper( - base_url=base_url, api_key=None, bearer_token=None, _strict_response_validation=True - ) + with update_env(**{"X_TWITTER_SCRAPER_API_KEY": Omit()}): + client2 = AsyncXTwitterScraper(base_url=base_url, api_key=None, _strict_response_validation=True) with pytest.raises( TypeError, @@ -1473,11 +1354,7 @@ def test_validate_headers(self) -> None: async def test_default_query_option(self) -> None: client = AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - default_query={"query_param": "bar"}, + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} ) request = client._build_request(FinalRequestOptions(method="get", url="/foo")) url = httpx.URL(request.url) @@ -1674,7 +1551,6 @@ async def mock_handler(request: httpx.Request) -> httpx.Response: async with AsyncXTwitterScraper( base_url=base_url, api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), ) as client: @@ -1775,10 +1651,7 @@ class Model(BaseModel): async def test_base_url_setter(self) -> None: client = AsyncXTwitterScraper( - base_url="https://example.com/from_init", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True ) assert client.base_url == "https://example.com/from_init/" @@ -1790,22 +1663,18 @@ async def test_base_url_setter(self) -> None: async def test_base_url_env(self) -> None: with update_env(X_TWITTER_SCRAPER_BASE_URL="http://localhost:5000/from/env"): - client = AsyncXTwitterScraper(api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True) + client = AsyncXTwitterScraper(api_key=api_key, _strict_response_validation=True) assert client.base_url == "http://localhost:5000/from/env/" @pytest.mark.parametrize( "client", [ AsyncXTwitterScraper( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncXTwitterScraper( base_url="http://localhost:5000/custom/path/", api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1827,15 +1696,11 @@ async def test_base_url_trailing_slash(self, client: AsyncXTwitterScraper) -> No "client", [ AsyncXTwitterScraper( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncXTwitterScraper( base_url="http://localhost:5000/custom/path/", api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1857,15 +1722,11 @@ async def test_base_url_no_trailing_slash(self, client: AsyncXTwitterScraper) -> "client", [ AsyncXTwitterScraper( - base_url="http://localhost:5000/custom/path/", - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True ), AsyncXTwitterScraper( base_url="http://localhost:5000/custom/path/", api_key=api_key, - bearer_token=bearer_token, _strict_response_validation=True, http_client=httpx.AsyncClient(), ), @@ -1884,9 +1745,7 @@ async def test_absolute_request_url(self, client: AsyncXTwitterScraper) -> None: await client.close() async def test_copied_client_does_not_close_http(self) -> None: - test_client = AsyncXTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + test_client = AsyncXTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) assert not test_client.is_closed() copied = test_client.copy() @@ -1898,9 +1757,7 @@ async def test_copied_client_does_not_close_http(self) -> None: assert not test_client.is_closed() async def test_client_context_manager(self) -> None: - test_client = AsyncXTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + test_client = AsyncXTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) async with test_client as c2: assert c2 is test_client assert not c2.is_closed() @@ -1924,11 +1781,7 @@ class Model(BaseModel): async def test_client_max_retries_validation(self) -> None: with pytest.raises(TypeError, match=r"max_retries cannot be None"): AsyncXTwitterScraper( - base_url=base_url, - api_key=api_key, - bearer_token=bearer_token, - _strict_response_validation=True, - max_retries=cast(Any, None), + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) ) @pytest.mark.respx(base_url=base_url) @@ -1938,16 +1791,12 @@ class Model(BaseModel): respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) - strict_client = AsyncXTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=True - ) + strict_client = AsyncXTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=True) with pytest.raises(APIResponseValidationError): await strict_client.get("/foo", cast_to=Model) - non_strict_client = AsyncXTwitterScraper( - base_url=base_url, api_key=api_key, bearer_token=bearer_token, _strict_response_validation=False - ) + non_strict_client = AsyncXTwitterScraper(base_url=base_url, api_key=api_key, _strict_response_validation=False) response = await non_strict_client.get("/foo", cast_to=Model) assert isinstance(response, str) # type: ignore[unreachable] diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py deleted file mode 100644 index 66bd733..0000000 --- a/tests/test_deepcopy.py +++ /dev/null @@ -1,58 +0,0 @@ -from x_twitter_scraper._utils import deepcopy_minimal - - -def assert_different_identities(obj1: object, obj2: object) -> None: - assert obj1 == obj2 - assert id(obj1) != id(obj2) - - -def test_simple_dict() -> None: - obj1 = {"foo": "bar"} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_dict() -> None: - obj1 = {"foo": {"bar": True}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - - -def test_complex_nested_dict() -> None: - obj1 = {"foo": {"bar": [{"hello": "world"}]}} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1["foo"], obj2["foo"]) - assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"]) - assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0]) - - -def test_simple_list() -> None: - obj1 = ["a", "b", "c"] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - - -def test_nested_list() -> None: - obj1 = ["a", [1, 2, 3]] - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert_different_identities(obj1[1], obj2[1]) - - -class MyObject: ... - - -def test_ignores_other_types() -> None: - # custom classes - my_obj = MyObject() - obj1 = {"foo": my_obj} - obj2 = deepcopy_minimal(obj1) - assert_different_identities(obj1, obj2) - assert obj1["foo"] is my_obj - - # tuples - obj3 = ("a", "b") - obj4 = deepcopy_minimal(obj3) - assert obj3 is obj4 diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index dffec1f..7a9bf3b 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -35,6 +35,15 @@ def test_multiple_files() -> None: assert query == {"documents": [{}, {}]} +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [ + ("files[]", b"file one"), + ("files[]", b"file two"), + ] + assert query == {"title": "hello"} + + @pytest.mark.parametrize( "query,paths,expected", [ diff --git a/tests/test_files.py b/tests/test_files.py index b72b88e..34013ae 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -4,7 +4,8 @@ import pytest from dirty_equals import IsDict, IsList, IsBytes, IsTuple -from x_twitter_scraper._files import to_httpx_files, async_to_httpx_files +from x_twitter_scraper._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from x_twitter_scraper._utils import extract_files readme_path = Path(__file__).parent.parent.joinpath("README.md") @@ -49,3 +50,99 @@ def test_string_not_allowed() -> None: "file": "foo", # type: ignore } ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + }