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.
- twittrapi-0.2.0/.gitignore +13 -0
- twittrapi-0.2.0/CHANGELOG.md +59 -0
- twittrapi-0.2.0/LICENSE +21 -0
- twittrapi-0.2.0/PKG-INFO +384 -0
- twittrapi-0.2.0/README.md +350 -0
- twittrapi-0.2.0/examples/export_followers_csv.py +70 -0
- twittrapi-0.2.0/examples/keyword_monitor.py +72 -0
- twittrapi-0.2.0/examples/trends_by_country.py +71 -0
- twittrapi-0.2.0/examples/user_timeline_json.py +73 -0
- twittrapi-0.2.0/pyproject.toml +58 -0
- twittrapi-0.2.0/src/twittrapi/__init__.py +55 -0
- twittrapi-0.2.0/src/twittrapi/_base.py +272 -0
- twittrapi-0.2.0/src/twittrapi/_client.py +368 -0
- twittrapi-0.2.0/src/twittrapi/_endpoints.py +264 -0
- twittrapi-0.2.0/src/twittrapi/_exceptions.py +115 -0
- twittrapi-0.2.0/src/twittrapi/_version.py +1 -0
- twittrapi-0.2.0/src/twittrapi/models/__init__.py +355 -0
- twittrapi-0.2.0/src/twittrapi/pagination.py +165 -0
- twittrapi-0.2.0/src/twittrapi/py.typed +0 -0
- twittrapi-0.2.0/src/twittrapi/resources/__init__.py +21 -0
- twittrapi-0.2.0/src/twittrapi/resources/_base.py +31 -0
- twittrapi-0.2.0/src/twittrapi/resources/communities.py +80 -0
- twittrapi-0.2.0/src/twittrapi/resources/lists.py +80 -0
- twittrapi-0.2.0/src/twittrapi/resources/search.py +51 -0
- twittrapi-0.2.0/src/twittrapi/resources/trends.py +54 -0
- twittrapi-0.2.0/src/twittrapi/resources/tweets.py +114 -0
- twittrapi-0.2.0/src/twittrapi/resources/users.py +250 -0
- twittrapi-0.2.0/tests/__init__.py +0 -0
- twittrapi-0.2.0/tests/conftest.py +66 -0
- twittrapi-0.2.0/tests/fixtures/live/communities-communityid-members.json +76 -0
- twittrapi-0.2.0/tests/fixtures/live/communities-communityid-moderators.json +74 -0
- twittrapi-0.2.0/tests/fixtures/live/communities-search-searchterm.json +339 -0
- twittrapi-0.2.0/tests/fixtures/live/jobs-search-searchterm.json +105 -0
- twittrapi-0.2.0/tests/fixtures/live/search.json +213 -0
- twittrapi-0.2.0/tests/fixtures/live/tweet-tweetid-favorites.json +18 -0
- twittrapi-0.2.0/tests/fixtures/live/tweet-tweetid.json +454 -0
- twittrapi-0.2.0/tests/fixtures/live/user-userid-articles.json +299 -0
- twittrapi-0.2.0/tests/fixtures/live/user-userid-followers.json +214 -0
- twittrapi-0.2.0/tests/fixtures/live/user-userid-likes.json +14 -0
- twittrapi-0.2.0/tests/fixtures/live/user-userid-tweets.json +475 -0
- twittrapi-0.2.0/tests/fixtures/simple/capture-user_tweets.json +943 -0
- twittrapi-0.2.0/tests/fixtures/simple/simpletweet-tweetid.json +56 -0
- twittrapi-0.2.0/tests/fixtures/simple/tweet-tweetid-favorites.json +6 -0
- twittrapi-0.2.0/tests/fixtures/simple/user-by-username-username.json +28 -0
- twittrapi-0.2.0/tests/fixtures/simple/user-userid-articles.json +68 -0
- twittrapi-0.2.0/tests/fixtures/simple/user-userid-followers.json +55 -0
- twittrapi-0.2.0/tests/fixtures/simple/user-userid-tweets.json +61 -0
- twittrapi-0.2.0/tests/test_config.py +131 -0
- twittrapi-0.2.0/tests/test_errors.py +125 -0
- twittrapi-0.2.0/tests/test_live_fixtures.py +193 -0
- twittrapi-0.2.0/tests/test_pagination.py +227 -0
- twittrapi-0.2.0/tests/test_resources.py +162 -0
- twittrapi-0.2.0/tests/test_retries.py +107 -0
- twittrapi-0.2.0/tests/test_simple_format.py +337 -0
|
@@ -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
|
twittrapi-0.2.0/LICENSE
ADDED
|
@@ -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.
|
twittrapi-0.2.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/twittrapi/) [](https://pypi.org/project/twittrapi/) [](https://github.com/twittrapi/twittrapi-python/actions)
|
|
42
|
+
[](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.
|