twittrapi 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. twittrapi-0.2.0/.gitignore +13 -0
  2. twittrapi-0.2.0/CHANGELOG.md +59 -0
  3. twittrapi-0.2.0/LICENSE +21 -0
  4. twittrapi-0.2.0/PKG-INFO +384 -0
  5. twittrapi-0.2.0/README.md +350 -0
  6. twittrapi-0.2.0/examples/export_followers_csv.py +70 -0
  7. twittrapi-0.2.0/examples/keyword_monitor.py +72 -0
  8. twittrapi-0.2.0/examples/trends_by_country.py +71 -0
  9. twittrapi-0.2.0/examples/user_timeline_json.py +73 -0
  10. twittrapi-0.2.0/pyproject.toml +58 -0
  11. twittrapi-0.2.0/src/twittrapi/__init__.py +55 -0
  12. twittrapi-0.2.0/src/twittrapi/_base.py +272 -0
  13. twittrapi-0.2.0/src/twittrapi/_client.py +368 -0
  14. twittrapi-0.2.0/src/twittrapi/_endpoints.py +264 -0
  15. twittrapi-0.2.0/src/twittrapi/_exceptions.py +115 -0
  16. twittrapi-0.2.0/src/twittrapi/_version.py +1 -0
  17. twittrapi-0.2.0/src/twittrapi/models/__init__.py +355 -0
  18. twittrapi-0.2.0/src/twittrapi/pagination.py +165 -0
  19. twittrapi-0.2.0/src/twittrapi/py.typed +0 -0
  20. twittrapi-0.2.0/src/twittrapi/resources/__init__.py +21 -0
  21. twittrapi-0.2.0/src/twittrapi/resources/_base.py +31 -0
  22. twittrapi-0.2.0/src/twittrapi/resources/communities.py +80 -0
  23. twittrapi-0.2.0/src/twittrapi/resources/lists.py +80 -0
  24. twittrapi-0.2.0/src/twittrapi/resources/search.py +51 -0
  25. twittrapi-0.2.0/src/twittrapi/resources/trends.py +54 -0
  26. twittrapi-0.2.0/src/twittrapi/resources/tweets.py +114 -0
  27. twittrapi-0.2.0/src/twittrapi/resources/users.py +250 -0
  28. twittrapi-0.2.0/tests/__init__.py +0 -0
  29. twittrapi-0.2.0/tests/conftest.py +66 -0
  30. twittrapi-0.2.0/tests/fixtures/live/communities-communityid-members.json +76 -0
  31. twittrapi-0.2.0/tests/fixtures/live/communities-communityid-moderators.json +74 -0
  32. twittrapi-0.2.0/tests/fixtures/live/communities-search-searchterm.json +339 -0
  33. twittrapi-0.2.0/tests/fixtures/live/jobs-search-searchterm.json +105 -0
  34. twittrapi-0.2.0/tests/fixtures/live/search.json +213 -0
  35. twittrapi-0.2.0/tests/fixtures/live/tweet-tweetid-favorites.json +18 -0
  36. twittrapi-0.2.0/tests/fixtures/live/tweet-tweetid.json +454 -0
  37. twittrapi-0.2.0/tests/fixtures/live/user-userid-articles.json +299 -0
  38. twittrapi-0.2.0/tests/fixtures/live/user-userid-followers.json +214 -0
  39. twittrapi-0.2.0/tests/fixtures/live/user-userid-likes.json +14 -0
  40. twittrapi-0.2.0/tests/fixtures/live/user-userid-tweets.json +475 -0
  41. twittrapi-0.2.0/tests/fixtures/simple/capture-user_tweets.json +943 -0
  42. twittrapi-0.2.0/tests/fixtures/simple/simpletweet-tweetid.json +56 -0
  43. twittrapi-0.2.0/tests/fixtures/simple/tweet-tweetid-favorites.json +6 -0
  44. twittrapi-0.2.0/tests/fixtures/simple/user-by-username-username.json +28 -0
  45. twittrapi-0.2.0/tests/fixtures/simple/user-userid-articles.json +68 -0
  46. twittrapi-0.2.0/tests/fixtures/simple/user-userid-followers.json +55 -0
  47. twittrapi-0.2.0/tests/fixtures/simple/user-userid-tweets.json +61 -0
  48. twittrapi-0.2.0/tests/test_config.py +131 -0
  49. twittrapi-0.2.0/tests/test_errors.py +125 -0
  50. twittrapi-0.2.0/tests/test_live_fixtures.py +193 -0
  51. twittrapi-0.2.0/tests/test_pagination.py +227 -0
  52. twittrapi-0.2.0/tests/test_resources.py +162 -0
  53. twittrapi-0.2.0/tests/test_retries.py +107 -0
  54. twittrapi-0.2.0/tests/test_simple_format.py +337 -0
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .env
13
+ .DS_Store
@@ -0,0 +1,59 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.0] - 2026-10-11
10
+
11
+ ### Added
12
+
13
+ - Opt-in simple response format (`format=simple`, see https://twittrapi.com/docs/simple-format):
14
+ `TwittrAPI(response_format="simple")` / `AsyncTwittrAPI(response_format="simple")` for the
15
+ whole client, and a `response_format="raw" | "simple"` keyword on every resource method to
16
+ override it per call. The default stays `"raw"`; raw requests are sent exactly as before.
17
+ - Typed models for the simple format in `twittrapi.models`: `Tweet`, `User`, `Page`
18
+ (`data`, `next_cursor`, `previous_cursor`, `has_next_page`) and their parts (`TweetCounts`,
19
+ `UserCounts`, `Media`, `ReplyTo`, `UserRef`, `Article`, `UnavailableTweet`), plus the
20
+ `tweets_page()`, `users_page()` and `parse_tweet()` helpers.
21
+ - `paginate()` follows `nextCursor` until `hasNextPage` is false when the simple format is
22
+ active (client default or `response_format="simple"` passed to `paginate()`). Raw pagination
23
+ is unchanged, and a raw body on a simple request falls back to it.
24
+ - `simple_next_cursor()` and the `ResponseFormat` type are exported from `twittrapi`.
25
+ - Tests against the API's simple-format output (`tests/fixtures/simple/`).
26
+ - Tests against trimmed real API responses (`tests/fixtures/live/`): followers (Bottom cursor
27
+ before Top), tweet detail, search wrapper, slice `next_cursor`, empty likes and favorites
28
+ timelines, articles with an empty `tweet_results`.
29
+
30
+ ### Changed
31
+
32
+ - `count` must be between 1 and 100 (`MAX_COUNT`), matching the API and the other TwittrAPI
33
+ SDKs. Larger values now raise `ValueError` before any request is sent.
34
+ - Usernames: only one leading `@` is stripped.
35
+
36
+ ### Fixed
37
+
38
+ - `paginate()` also stops on slice-based pages (communities search, jobs search) that return a
39
+ `next_cursor` but no items.
40
+
41
+ ## [0.1.0] - 2026-10-10
42
+
43
+ ### Added
44
+
45
+ - `TwittrAPI` (sync) and `AsyncTwittrAPI` (async) clients.
46
+ - Direct API auth (`api_key`, `X-API-KEY`) and RapidAPI auth (`rapidapi_key`), with
47
+ `TWITTRAPI_API_KEY` / `RAPIDAPI_KEY` / `TWITTRAPI_BASE_URL` environment variables.
48
+ - Resources for all 23 read endpoints: `users`, `tweets`, `search`, `lists`, `communities`,
49
+ `trends`.
50
+ - Typed exceptions: `ValidationError`, `AuthenticationError`, `InsufficientBalanceError`,
51
+ `NotFoundError`, `RateLimitError` (with `retry_after`), `ServerError`, `APIConnectionError`,
52
+ `APITimeoutError`.
53
+ - Automatic retries with exponential backoff for 429, 5xx and network errors.
54
+ - `paginate()` and `extract_bottom_cursor()` for following X's timeline cursors (best-effort).
55
+ - `py.typed` marker.
56
+
57
+ [Unreleased]: https://github.com/twittrapi/twittrapi-python/compare/v0.2.0...HEAD
58
+ [0.2.0]: https://github.com/twittrapi/twittrapi-python/compare/v0.1.0...v0.2.0
59
+ [0.1.0]: https://github.com/twittrapi/twittrapi-python/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TwittrAPI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,384 @@
1
+ Metadata-Version: 2.5
2
+ Name: twittrapi
3
+ Version: 0.2.0
4
+ Summary: Python client for TwittrAPI: read public Twitter/X tweets, profiles, followers and search results. Pay as you go from $0.065 per 1,000 calls.
5
+ Project-URL: Homepage, https://twittrapi.com
6
+ Project-URL: Documentation, https://twittrapi.com
7
+ Project-URL: Source, https://github.com/twittrapi/twittrapi-python
8
+ Project-URL: Issues, https://github.com/twittrapi/twittrapi-python/issues
9
+ Project-URL: Changelog, https://github.com/twittrapi/twittrapi-python/blob/main/CHANGELOG.md
10
+ Author: TwittrAPI
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: python,sdk,social media data,tweets,twitter api,twitter followers,twitter scraper,twitter search,twittrapi,x api
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Requires-Dist: httpx<1.0,>=0.23.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: mypy>=1.5; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
32
+ Requires-Dist: pytest>=7; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ <p align="center">
36
+ <a href="https://twittrapi.com"><img src=".github/banner.png" alt="twittrapi-python: Python client for Twitter/X data, sync and async, no X developer account" width="100%"></a>
37
+ </p>
38
+
39
+ # twittrapi – Twitter/X API client for Python
40
+
41
+ [![PyPI](https://img.shields.io/pypi/v/twittrapi.svg)](https://pypi.org/project/twittrapi/) [![Python](https://img.shields.io/pypi/pyversions/twittrapi.svg)](https://pypi.org/project/twittrapi/) [![Tests](https://github.com/twittrapi/twittrapi-python/actions/workflows/test.yml/badge.svg)](https://github.com/twittrapi/twittrapi-python/actions)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
43
+
44
+ Official Python client for [TwittrAPI](https://twittrapi.com), a REST API for public Twitter/X data:
45
+ tweets, profiles, followers, search, lists and communities. From $0.065 per 1,000 calls, no X developer account.
46
+
47
+ > **Get an API key:** [twittrapi.com/start](https://twittrapi.com/start?utm_source=github&utm_medium=readme&utm_campaign=twittrapi-python) · [Pricing](https://twittrapi.com/pricing?utm_source=github&utm_medium=readme&utm_campaign=twittrapi-python) · [Docs](https://twittrapi.com/docs?utm_source=github&utm_medium=readme&utm_campaign=twittrapi-python) · [Status](https://twittrapi.com/status) · also available on [RapidAPI](https://rapidapi.com/viperscores-viperscores-default/api/twitter-aio)
48
+
49
+ **SDKs and integrations:** [Node.js](https://github.com/twittrapi/twittrapi-node) · [Go](https://github.com/twittrapi/twittrapi-go) · [MCP server](https://github.com/twittrapi/twittrapi-mcp) · [n8n](https://github.com/twittrapi/n8n-nodes-twittrapi) · [Make](https://github.com/twittrapi/twittrapi-make) · [Apify](https://github.com/twittrapi/twittrapi-apify) · [LangChain](https://github.com/twittrapi/langchain-twittrapi) · [LlamaIndex](https://github.com/twittrapi/llama-index-tools-twittrapi) · [Agent skill](https://github.com/twittrapi/twittrapi-skills) · [Examples](https://github.com/twittrapi/twittrapi-examples)
50
+
51
+ - Sync (`TwittrAPI`) and async (`AsyncTwittrAPI`) clients with the same methods
52
+ - Works with a direct TwittrAPI key or a RapidAPI key
53
+ - Retries 429, 5xx and network errors with backoff
54
+ - Typed exceptions for every error class
55
+ - A cursor-following paginator
56
+ - Python 3.9+, one dependency (`httpx`), fully type-annotated
57
+
58
+ Responses are X's raw GraphQL JSON, returned as plain `dict`/`list` values. The client does not
59
+ reshape them.
60
+
61
+ Not affiliated with X Corp.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install twittrapi
67
+ ```
68
+
69
+ ## Quickstart
70
+
71
+ ### Direct API key
72
+
73
+ Get a key at [twittrapi.com](https://twittrapi.com).
74
+
75
+ ```python
76
+ from twittrapi import TwittrAPI
77
+
78
+ client = TwittrAPI(api_key="YOUR_KEY")
79
+ user = client.users.get_by_username("jack")
80
+ print(user)
81
+ ```
82
+
83
+ Requests go to `https://api.twittrapi.com` with an `X-API-KEY` header.
84
+
85
+ ### RapidAPI key
86
+
87
+ ```python
88
+ from twittrapi import TwittrAPI
89
+
90
+ client = TwittrAPI(rapidapi_key="YOUR_RAPIDAPI_KEY")
91
+ user = client.users.get_by_username("jack")
92
+ ```
93
+
94
+ Requests go to `https://twitter-aio.p.rapidapi.com` with `x-rapidapi-key` and `x-rapidapi-host`
95
+ headers.
96
+
97
+ ### Environment variables
98
+
99
+ With no key argument, the client reads `TWITTRAPI_API_KEY`, then `RAPIDAPI_KEY`:
100
+
101
+ ```bash
102
+ export TWITTRAPI_API_KEY=YOUR_KEY
103
+ ```
104
+
105
+ ```python
106
+ client = TwittrAPI()
107
+ ```
108
+
109
+ `TWITTRAPI_BASE_URL` overrides the direct API base URL (useful for a proxy or staging).
110
+
111
+ ### Client options
112
+
113
+ ```python
114
+ client = TwittrAPI(
115
+ api_key="YOUR_KEY",
116
+ base_url="https://api.twittrapi.com", # default depends on the auth mode
117
+ timeout=60.0, # seconds, or an httpx.Timeout
118
+ max_retries=2, # retries for 429, 5xx and network errors
119
+ response_format="raw", # or "simple", see "Simple format" below
120
+ )
121
+ ```
122
+
123
+ You can also pass your own `httpx.Client` (or `httpx.AsyncClient`) as `http_client=` to set
124
+ proxies, transports or connection limits. The client does not close an `http_client` you pass in.
125
+
126
+ Close the client when done, or use it as a context manager:
127
+
128
+ ```python
129
+ with TwittrAPI() as client:
130
+ ...
131
+ ```
132
+
133
+ ## Examples by resource
134
+
135
+ IDs can be passed as `str` or `int`. Usernames can include a leading `@`.
136
+
137
+ ### Users
138
+
139
+ ```python
140
+ client.users.get_by_username("jack")
141
+ client.users.get_by_id("12")
142
+ client.users.username_to_id("jack") # smaller response when you only need the ID
143
+ client.users.get_many(["12", "44196397"]) # up to 100 IDs per call
144
+
145
+ # Timelines take user_id or username=
146
+ client.users.tweets("12", count=20)
147
+ client.users.tweets(username="jack")
148
+ client.users.tweets_and_replies("12")
149
+ client.users.media("12")
150
+ client.users.followers("12", count=100)
151
+ client.users.following(username="jack")
152
+ client.users.verified_followers("12")
153
+ ```
154
+
155
+ Passing `user_id` is a little faster than `username`, because the API does not have to look the
156
+ handle up first.
157
+
158
+ ### Tweets
159
+
160
+ ```python
161
+ client.tweets.get("1585841080431321088") # tweet plus reply thread
162
+ client.tweets.get("1585841080431321088", ranking_mode="Recency") # Relevance, Recency or Likes
163
+ client.tweets.retweeters("1585841080431321088")
164
+ client.tweets.likers("1585841080431321088") # often empty: X hides likers for many tweets
165
+ client.tweets.quotes("1585841080431321088")
166
+ ```
167
+
168
+ ### Search
169
+
170
+ ```python
171
+ client.search.tweets("python")
172
+ client.search.tweets('from:jack "hello" lang:en', category="Latest", count=20)
173
+ ```
174
+
175
+ `query` supports X search operators (`from:`, `to:`, `lang:`, `since:`, `until:`, `min_faves:`,
176
+ `filter:media`, `-filter:replies`, ...). `category` is one of `Top` (default), `Latest`,
177
+ `People`, `Photos`, `Videos`.
178
+
179
+ ### Lists
180
+
181
+ ```python
182
+ client.lists.details("1234567890")
183
+ client.lists.tweets("1234567890")
184
+ client.lists.members("1234567890")
185
+ ```
186
+
187
+ ### Communities
188
+
189
+ ```python
190
+ client.communities.search("python")
191
+ client.communities.get("1500000000000000000")
192
+ client.communities.tweets("1500000000000000000")
193
+ ```
194
+
195
+ ### Trends
196
+
197
+ ```python
198
+ client.trends.get(1) # worldwide
199
+ client.trends.get(23424977) # United States
200
+ client.trends.locations() # every location with trends, with its WOEID
201
+ ```
202
+
203
+ ## Simple format
204
+
205
+ By default every method returns X's raw JSON. The API can also answer in a compact, flat,
206
+ camelCase format with a fixed schema ([docs](https://twittrapi.com/docs/simple-format)). It is
207
+ opt-in: set it for the whole client, or per call with `response_format=` on any method.
208
+
209
+ ```python
210
+ client = TwittrAPI(response_format="simple")
211
+ page = client.users.tweets(username="XDevelopers", count=20)
212
+ # {"data": [{"id": ..., "text": ..., "author": {...}, "counts": {...}, ...}],
213
+ # "nextCursor": "...", "previousCursor": "...", "hasNextPage": true}
214
+
215
+ raw = client.users.tweets(username="XDevelopers", response_format="raw") # this call only
216
+ ```
217
+
218
+ `twittrapi.models` has typed dataclasses for it (`Tweet`, `User`, `Page`, ...), with
219
+ snake_case attributes:
220
+
221
+ ```python
222
+ from twittrapi.models import User, tweets_page, users_page
223
+
224
+ page = tweets_page(client.users.tweets(username="XDevelopers"))
225
+ for tweet in page.data:
226
+ print(tweet.id, tweet.counts.likes, tweet.author.username if tweet.author else None)
227
+ print(page.next_cursor, page.has_next_page)
228
+
229
+ user = User.from_dict(client.users.get_by_username("XDevelopers")["data"])
230
+ ```
231
+
232
+ Same price, parameters and errors as the raw format; only successful responses change.
233
+ Account and status routes ignore the option and answer as usual.
234
+
235
+ ## Pagination
236
+
237
+ Paginated methods accept `count` (1 to 100; leave it out for the server default of 20) and
238
+ `cursor`. `count` is only a hint to X: a page can hold more or fewer results. X puts the next-page cursor inside the response
239
+ (a timeline entry with `cursorType: "Bottom"`). `client.paginate()` finds it and keeps calling
240
+ the method for you, yielding each raw page:
241
+
242
+ ```python
243
+ for page in client.paginate(client.users.followers, username="jack", count=100, max_pages=5):
244
+ ... # page is the raw JSON for one request
245
+ ```
246
+
247
+ Paging stops when any of these happens:
248
+
249
+ - `max_pages` pages were fetched (default 10, `None` for no limit)
250
+ - no bottom cursor is found in the page
251
+ - the cursor repeats
252
+ - the page has no results, only cursor entries
253
+
254
+ Each page is a separate billed request, so set `max_pages` to what you need.
255
+
256
+ To start from a saved cursor, pass `cursor=...`. If you want to page by hand, use
257
+ `extract_bottom_cursor`:
258
+
259
+ ```python
260
+ from twittrapi import extract_bottom_cursor
261
+
262
+ page = client.search.tweets("python", category="Latest")
263
+ next_cursor = extract_bottom_cursor(page)
264
+ if next_cursor:
265
+ page = client.search.tweets("python", category="Latest", cursor=next_cursor)
266
+ ```
267
+
268
+ Cursor detection is best-effort. X does not document its response format, and the helper
269
+ searches the whole response for the shapes X uses today (`cursorType: "Bottom"` objects and
270
+ `cursor-bottom-*` entry IDs). If X changes the format, the paginator may stop early.
271
+
272
+ In the simple format none of this guessing is needed: every list response carries
273
+ `nextCursor` and `hasNextPage`, and `paginate()` follows `nextCursor` until `hasNextPage` is
274
+ false. That applies when the client was created with `response_format="simple"` or when you
275
+ pass `response_format="simple"` to `paginate()`:
276
+
277
+ ```python
278
+ for page in client.paginate(client.users.followers, username="jack", response_format="simple"):
279
+ for user in page["data"]:
280
+ print(user["username"])
281
+ ```
282
+
283
+ ## Error handling
284
+
285
+ All errors raised by the client inherit from `TwittrAPIError`.
286
+
287
+ | Exception | When |
288
+ | --- | --- |
289
+ | `ValidationError` | 400, 422: bad parameters |
290
+ | `AuthenticationError` | 401, 403: missing or invalid key, or no access |
291
+ | `InsufficientBalanceError` | 402: no balance left on the account |
292
+ | `NotFoundError` | 404: user, tweet, list or community not found |
293
+ | `RateLimitError` | 429: too many requests; has `retry_after` (seconds or `None`) |
294
+ | `ServerError` | 5xx |
295
+ | `APIStatusError` | any other 4xx; base class of all the above |
296
+ | `APIConnectionError` | network failure, no response |
297
+ | `APITimeoutError` | request timed out (subclass of `APIConnectionError`) |
298
+
299
+ HTTP errors carry `status_code`, `message` (the API's `error` text), `body` (parsed JSON) and
300
+ `response` (the `httpx.Response`).
301
+
302
+ ```python
303
+ from twittrapi import (
304
+ TwittrAPI,
305
+ InsufficientBalanceError,
306
+ NotFoundError,
307
+ RateLimitError,
308
+ TwittrAPIError,
309
+ )
310
+
311
+ client = TwittrAPI()
312
+ try:
313
+ user = client.users.get_by_username("someone")
314
+ except NotFoundError:
315
+ print("No such user")
316
+ except InsufficientBalanceError:
317
+ print("Top up at https://twittrapi.com")
318
+ except RateLimitError as exc:
319
+ print(f"Rate limited, retry in {exc.retry_after}s")
320
+ except TwittrAPIError as exc:
321
+ print(f"API error: {exc}")
322
+ ```
323
+
324
+ Invalid arguments (a non-numeric ID, more than 100 IDs, an unknown `category`) raise
325
+ `ValueError` or `TypeError` before any request is sent.
326
+
327
+ ### Retries
328
+
329
+ 429 responses, 5xx responses and network errors are retried up to `max_retries` times (default 2,
330
+ so at most 3 attempts). The wait is 0.5 s, 1 s, 2 s, ... with some jitter, capped at 8 s. For 429
331
+ the client waits for the `Retry-After` header if the API sends one (capped at 60 s). Other 4xx
332
+ errors, including 402, are never retried. Set `max_retries=0` to turn retries off.
333
+
334
+ ## Async
335
+
336
+ `AsyncTwittrAPI` has the same constructor and methods. Every API method is a coroutine, and
337
+ `paginate` returns an async iterator.
338
+
339
+ ```python
340
+ import asyncio
341
+ from twittrapi import AsyncTwittrAPI
342
+
343
+
344
+ async def main() -> None:
345
+ async with AsyncTwittrAPI() as client:
346
+ user, trends = await asyncio.gather(
347
+ client.users.get_by_username("jack"),
348
+ client.trends.get(1),
349
+ )
350
+ async for page in client.paginate(client.search.tweets, "python", max_pages=3):
351
+ ...
352
+
353
+
354
+ asyncio.run(main())
355
+ ```
356
+
357
+ ## More examples
358
+
359
+ The [`examples/`](examples) folder has short scripts:
360
+
361
+ - `export_followers_csv.py`: export a user's followers to CSV
362
+ - `keyword_monitor.py`: poll search and print new matching tweets
363
+ - `user_timeline_json.py`: save a user's tweets to JSON
364
+ - `trends_by_country.py`: trends for a country or city by name
365
+
366
+ ## Development
367
+
368
+ ```bash
369
+ python -m venv .venv
370
+ .venv/bin/pip install -e ".[dev]"
371
+ .venv/bin/pytest
372
+ .venv/bin/mypy
373
+ ```
374
+
375
+ ## Support
376
+
377
+ Questions, bugs or a higher rate limit: email support@twittrapi.com or open an issue in this repository. Service status: [twittrapi.com/status](https://twittrapi.com/status). News and changes: the TwittrAPI Telegram channel (link on the website).
378
+
379
+ ## License
380
+
381
+ MIT. See [LICENSE](LICENSE).
382
+
383
+ TwittrAPI is an independent service and is not affiliated with, endorsed by or sponsored by
384
+ X Corp. "X" and "Twitter" are trademarks of X Corp.