tweetapi 1.0.0__tar.gz → 1.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.
- tweetapi-1.2.0/AGENTS.md +92 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/PKG-INFO +167 -15
- {tweetapi-1.0.0 → tweetapi-1.2.0}/README.md +164 -12
- {tweetapi-1.0.0 → tweetapi-1.2.0}/pyproject.toml +3 -3
- tweetapi-1.2.0/tests/test_client.py +492 -0
- tweetapi-1.2.0/tests/test_pagination.py +101 -0
- tweetapi-1.2.0/tests/test_retry.py +148 -0
- tweetapi-1.2.0/tests/test_timeout.py +56 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/__init__.py +4 -1
- tweetapi-1.2.0/tweetapi/client.py +234 -0
- tweetapi-1.2.0/tweetapi/pagination.py +64 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/auth.py +2 -1
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/community.py +35 -11
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/explore.py +2 -1
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/interaction.py +17 -12
- tweetapi-1.2.0/tweetapi/resources/list_.py +54 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/post.py +15 -8
- tweetapi-1.2.0/tweetapi/resources/profile.py +31 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/space.py +3 -2
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/tweet.py +10 -4
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/unencrypted_dm.py +9 -8
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/user.py +26 -17
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/xchat.py +6 -5
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/types.py +138 -1
- tweetapi-1.0.0/tests/test_client.py +0 -245
- tweetapi-1.0.0/tweetapi/client.py +0 -146
- tweetapi-1.0.0/tweetapi/resources/list_.py +0 -27
- {tweetapi-1.0.0 → tweetapi-1.2.0}/.claude/settings.local.json +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/.gitignore +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/examples/create_post.py +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/examples/get_user.py +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/examples/search_tweets.py +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tests/__init__.py +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/errors.py +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/py.typed +0 -0
- {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/__init__.py +0 -0
tweetapi-1.2.0/AGENTS.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# TweetAPI Python SDK Agent Notes
|
|
2
|
+
|
|
3
|
+
## Repository Context
|
|
4
|
+
|
|
5
|
+
This repository is the official Python SDK for TweetAPI.
|
|
6
|
+
|
|
7
|
+
- GitHub: https://github.com/tweetapi/python
|
|
8
|
+
- PyPI package: https://pypi.org/project/tweetapi/
|
|
9
|
+
- Import package: `tweetapi`
|
|
10
|
+
- Distribution package: `tweetapi`
|
|
11
|
+
|
|
12
|
+
This repo is not a deployed web service. Customer impact happens when changes are
|
|
13
|
+
merged to the public GitHub repository and, separately, when a new package
|
|
14
|
+
version is built and uploaded to PyPI.
|
|
15
|
+
|
|
16
|
+
## Package And Release Model
|
|
17
|
+
|
|
18
|
+
Package metadata lives in `pyproject.toml`. The package uses Hatchling:
|
|
19
|
+
|
|
20
|
+
```toml
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["hatchling"]
|
|
23
|
+
build-backend = "hatchling.build"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
When preparing a release:
|
|
27
|
+
|
|
28
|
+
1. Update the version in `pyproject.toml`.
|
|
29
|
+
2. Update `tweetapi/__init__.py` so `__version__` matches.
|
|
30
|
+
3. Run tests and compile checks.
|
|
31
|
+
4. Build distributions with `python -m build`.
|
|
32
|
+
5. Inspect the generated `dist/` contents.
|
|
33
|
+
6. Upload to PyPI only when explicitly requested by the maintainer.
|
|
34
|
+
|
|
35
|
+
Do not publish to PyPI from an agent run unless the user explicitly asks for it.
|
|
36
|
+
Do not reuse a version number that already exists on PyPI.
|
|
37
|
+
|
|
38
|
+
## API Surface Policy
|
|
39
|
+
|
|
40
|
+
Expose documented customer-facing routes only. The SDK should map Pythonic
|
|
41
|
+
snake_case method parameters to the backend's camelCase JSON body and query
|
|
42
|
+
parameters.
|
|
43
|
+
|
|
44
|
+
Do not expose hidden, internal, or undocumented routes, including:
|
|
45
|
+
|
|
46
|
+
- `/tw-v2/internal/*`
|
|
47
|
+
- `/tw-v2/interaction/home-timeline`
|
|
48
|
+
- `/tw-v2/interaction/following-timeline`
|
|
49
|
+
- `/tw-v2/interaction/search`
|
|
50
|
+
- `/tw-v2/interaction/tweet-details`
|
|
51
|
+
- device follow notification routes
|
|
52
|
+
|
|
53
|
+
Authenticated Twitter/X actions still require a customer-provided `auth_token`
|
|
54
|
+
and, when required by the endpoint, `proxy`. The TweetAPI API key is passed by
|
|
55
|
+
the client through the `X-API-Key` header.
|
|
56
|
+
|
|
57
|
+
## Code Patterns
|
|
58
|
+
|
|
59
|
+
- Resource classes live in `tweetapi/resources/`.
|
|
60
|
+
- `TweetAPI` registers resources in `tweetapi/client.py`.
|
|
61
|
+
- Lightweight JSON-compatible response and request shapes live in
|
|
62
|
+
`tweetapi/types.py` as `TypedDict` definitions and aliases.
|
|
63
|
+
- Keep method names and arguments Pythonic (`snake_case`), but serialize request
|
|
64
|
+
payload keys exactly as the backend expects (`camelCase`).
|
|
65
|
+
- `TweetAPI._get` and `TweetAPI._post` strip `None` values. Prefer passing
|
|
66
|
+
optional fields through those helpers instead of hand-filtering in resources.
|
|
67
|
+
- Add tests with `responses` for new request paths and serialized bodies.
|
|
68
|
+
|
|
69
|
+
## Verification
|
|
70
|
+
|
|
71
|
+
Run these before considering changes ready:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pytest
|
|
75
|
+
python -m compileall tweetapi
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For release candidates that add mutating authenticated endpoints, add mocked
|
|
79
|
+
request-body tests locally and consider a controlled live smoke test against a
|
|
80
|
+
test Twitter/X account before publishing to PyPI.
|
|
81
|
+
|
|
82
|
+
## Customer Safety
|
|
83
|
+
|
|
84
|
+
Merging code to GitHub does not automatically update installed customer
|
|
85
|
+
packages. Customers receive new SDK code only after a PyPI release and a local
|
|
86
|
+
upgrade such as:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
python -m pip install --upgrade tweetapi
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Treat PyPI publishing as the production release step for this SDK.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tweetapi
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Official Python SDK for TweetAPI — Twitter/X Data API for developers and researchers
|
|
5
5
|
Project-URL: Homepage, https://tweetapi.com?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk
|
|
6
6
|
Project-URL: Documentation, https://tweetapi.com/docs?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk
|
|
7
|
-
Project-URL: Repository, https://github.com/tweetapi/
|
|
8
|
-
Project-URL: Issues, https://github.com/tweetapi/
|
|
7
|
+
Project-URL: Repository, https://github.com/tweetapi/python
|
|
8
|
+
Project-URL: Issues, https://github.com/tweetapi/python/issues
|
|
9
9
|
Author-email: TweetAPI <support@tweetapi.com>
|
|
10
10
|
License-Expression: MIT
|
|
11
11
|
Keywords: social media api,tweet api,tweetapi,tweets,twitter,twitter api,twitter client,twitter data,twitter library,twitter sdk,twitter wrapper,x api
|
|
@@ -67,10 +67,13 @@ next_page = client.user.get_followers(
|
|
|
67
67
|
## Features
|
|
68
68
|
|
|
69
69
|
- **70+ endpoints** covering users, tweets, posts, interactions, DMs, communities, spaces, and search
|
|
70
|
-
- **Full type hints** with TypedDict
|
|
70
|
+
- **Full type hints** with TypedDict response types for IDE autocomplete
|
|
71
|
+
- **Automatic retry with backoff** on rate limits (429) and server errors (5xx)
|
|
72
|
+
- **Auto-pagination helpers** — iterate all pages with a simple `for` loop
|
|
71
73
|
- **Solid error handling** with typed exceptions (`RateLimitError`, `NotFoundError`, etc.)
|
|
74
|
+
- **Rate limit awareness** — `retry_after` respected automatically, state exposed via `client.rate_limit_info`
|
|
75
|
+
- **Split timeouts** — separate connect and read timeouts
|
|
72
76
|
- **Single dependency** — `requests` only
|
|
73
|
-
- **Pagination support** with cursor-based navigation
|
|
74
77
|
- **Python 3.9+** compatible
|
|
75
78
|
|
|
76
79
|
## API Reference
|
|
@@ -110,13 +113,21 @@ next_page = client.user.get_followers(
|
|
|
110
113
|
|
|
111
114
|
| Method | Description |
|
|
112
115
|
|--------|-------------|
|
|
113
|
-
| `client.post.create_post(auth_token=..., text=..., proxy=...)` | Create a tweet |
|
|
114
|
-
| `client.post.create_post_quote(auth_token=..., text=..., attachment_url=..., proxy=...)` | Quote tweet |
|
|
115
|
-
| `client.post.create_post_with_media(auth_token=..., text=..., media=..., proxy=...)` | Tweet with media |
|
|
116
|
+
| `client.post.create_post(auth_token=..., text=..., proxy=..., reply_option=...)` | Create a tweet |
|
|
117
|
+
| `client.post.create_post_quote(auth_token=..., text=..., attachment_url=..., proxy=..., reply_option=...)` | Quote tweet |
|
|
118
|
+
| `client.post.create_post_with_media(auth_token=..., text=..., media=..., proxy=..., reply_option=...)` | Tweet with media |
|
|
116
119
|
| `client.post.reply_post(auth_token=..., text=..., tweet_id=..., proxy=...)` | Reply to a tweet |
|
|
117
120
|
| `client.post.reply_post_with_media(...)` | Reply with media |
|
|
118
121
|
| `client.post.delete_post(auth_token=..., tweet_id=...)` | Delete a tweet |
|
|
119
122
|
|
|
123
|
+
### Profile
|
|
124
|
+
|
|
125
|
+
| Method | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `client.profile.update(auth_token=..., name=..., bio=..., location=..., website=..., proxy=...)` | Update profile fields |
|
|
128
|
+
| `client.profile.avatar(auth_token=..., media=..., proxy=...)` | Update profile avatar |
|
|
129
|
+
| `client.profile.banner(auth_token=..., media=..., proxy=...)` | Update profile banner |
|
|
130
|
+
|
|
120
131
|
### Interaction
|
|
121
132
|
|
|
122
133
|
| Method | Description |
|
|
@@ -142,6 +153,9 @@ next_page = client.user.get_followers(
|
|
|
142
153
|
| `client.list.get_tweets(list_id=...)` | Get tweets in a list |
|
|
143
154
|
| `client.list.get_members(list_id=...)` | Get list members |
|
|
144
155
|
| `client.list.get_followers(list_id=...)` | Get list followers |
|
|
156
|
+
| `client.list.create(auth_token=..., name=..., description=..., is_private=...)` | Create a list |
|
|
157
|
+
| `client.list.add_member(auth_token=..., list_id=..., user_id=..., proxy=...)` | Add a member |
|
|
158
|
+
| `client.list.remove_member(auth_token=..., list_id=..., user_id=..., proxy=...)` | Remove a member |
|
|
145
159
|
|
|
146
160
|
### Community
|
|
147
161
|
|
|
@@ -153,6 +167,8 @@ next_page = client.user.get_followers(
|
|
|
153
167
|
| `client.community.search(query=...)` | Search communities |
|
|
154
168
|
| `client.community.create_post(auth_token=..., text=..., community_id=..., proxy=...)` | Post in community |
|
|
155
169
|
| `client.community.create_post_with_media(...)` | Post with media |
|
|
170
|
+
| `client.community.create_quote(auth_token=..., text=..., attachment_url=..., community_id=..., proxy=...)` | Quote post in community |
|
|
171
|
+
| `client.community.create_quote_with_media(...)` | Quote post with media |
|
|
156
172
|
| `client.community.reply_post(...)` | Reply to community post |
|
|
157
173
|
| `client.community.reply_post_with_media(...)` | Reply with media |
|
|
158
174
|
| `client.community.join(auth_token=..., community_id=...)` | Join |
|
|
@@ -200,12 +216,142 @@ next_page = client.user.get_followers(
|
|
|
200
216
|
| `client.dm.get_dm_user_updates(auth_token=..., cursor=...)` | DM user updates |
|
|
201
217
|
| `client.dm.accept_conversation(auth_token=..., conversation_id=...)` | Accept request |
|
|
202
218
|
|
|
219
|
+
## Posting and Profile Media
|
|
220
|
+
|
|
221
|
+
Tweet media accepts an existing TweetAPI media ID, a URL, or inline base64 data:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
client.post.create_post_with_media(
|
|
225
|
+
auth_token="AUTH_TOKEN",
|
|
226
|
+
text="Launch update",
|
|
227
|
+
media=[{"media_id": "1971008286821380096"}],
|
|
228
|
+
proxy="host:port:user:pass",
|
|
229
|
+
)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Use `reply_option` to set reply controls when creating standard, quote, or media posts:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
client.post.create_post(
|
|
236
|
+
auth_token="AUTH_TOKEN",
|
|
237
|
+
text="Followers can reply",
|
|
238
|
+
proxy="host:port:user:pass",
|
|
239
|
+
reply_option={"mode": "followers"},
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Profile avatar and banner uploads accept a URL or inline `data` plus `type`:
|
|
244
|
+
|
|
245
|
+
```python
|
|
246
|
+
client.profile.update(auth_token="AUTH_TOKEN", name="New Name", bio="Builder")
|
|
247
|
+
client.profile.avatar(
|
|
248
|
+
auth_token="AUTH_TOKEN",
|
|
249
|
+
media={"url": "https://example.com/avatar.jpg"},
|
|
250
|
+
)
|
|
251
|
+
client.profile.banner(
|
|
252
|
+
auth_token="AUTH_TOKEN",
|
|
253
|
+
media={"data": "BASE64_IMAGE_DATA", "type": "image/png"},
|
|
254
|
+
)
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Canonical list mutations live under `client.list`; the legacy interaction list helpers remain available:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
created = client.list.create(
|
|
261
|
+
auth_token="AUTH_TOKEN",
|
|
262
|
+
name="Research",
|
|
263
|
+
description="Accounts to watch",
|
|
264
|
+
is_private=True,
|
|
265
|
+
)
|
|
266
|
+
client.list.add_member(auth_token="AUTH_TOKEN", list_id=created["data"]["id"], user_id="123")
|
|
267
|
+
client.list.remove_member(auth_token="AUTH_TOKEN", list_id=created["data"]["id"], user_id="123")
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Community quote posts support the same URL and media attachment shapes:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
client.community.create_quote(
|
|
274
|
+
auth_token="AUTH_TOKEN",
|
|
275
|
+
text="Useful context",
|
|
276
|
+
attachment_url="https://x.com/user/status/123",
|
|
277
|
+
community_id="987",
|
|
278
|
+
proxy="host:port:user:pass",
|
|
279
|
+
)
|
|
280
|
+
client.community.create_quote_with_media(
|
|
281
|
+
auth_token="AUTH_TOKEN",
|
|
282
|
+
text="Useful context",
|
|
283
|
+
attachment_url="https://x.com/user/status/123",
|
|
284
|
+
community_id="987",
|
|
285
|
+
media=[{"media_id": "1971008286821380096"}],
|
|
286
|
+
proxy="host:port:user:pass",
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## Auto-Pagination
|
|
291
|
+
|
|
292
|
+
Use the `paginate()` and `paginate_pages()` helpers to iterate through all pages automatically:
|
|
293
|
+
|
|
294
|
+
```python
|
|
295
|
+
from tweetapi import TweetAPI, paginate, paginate_pages
|
|
296
|
+
|
|
297
|
+
client = TweetAPI(api_key="YOUR_API_KEY")
|
|
298
|
+
|
|
299
|
+
# Iterate individual items across all pages
|
|
300
|
+
for user in paginate(
|
|
301
|
+
lambda cursor: client.user.get_followers(user_id="123456", cursor=cursor),
|
|
302
|
+
):
|
|
303
|
+
print(user["username"])
|
|
304
|
+
|
|
305
|
+
# Iterate full pages (access page-level data)
|
|
306
|
+
for page in paginate_pages(
|
|
307
|
+
lambda cursor: client.explore.search(query="bitcoin", type="Latest", cursor=cursor),
|
|
308
|
+
max_pages=5, # optional: limit number of pages
|
|
309
|
+
):
|
|
310
|
+
print(f"Got {len(page['data'])} results")
|
|
311
|
+
print(f"Next cursor: {page['pagination']['nextCursor']}")
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Works with any paginated endpoint — followers, tweets, search results, list members, community posts, etc.
|
|
315
|
+
|
|
316
|
+
## Automatic Retry with Backoff
|
|
317
|
+
|
|
318
|
+
The SDK automatically retries on transient errors with exponential backoff:
|
|
319
|
+
|
|
320
|
+
- **429 (Rate Limit)** — waits the `retry_after` duration from the API, then retries
|
|
321
|
+
- **5xx (Server Error)** — retries with exponential backoff + jitter
|
|
322
|
+
- **Network errors** — retries on timeouts and connection failures
|
|
323
|
+
- **4xx (Client Error)** — never retried (400, 401, 403, 404 fail immediately)
|
|
324
|
+
|
|
325
|
+
Default: 3 retries, 2x backoff, 1s initial delay, 30s max delay.
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
# Customize retry behavior
|
|
329
|
+
client = TweetAPI(
|
|
330
|
+
api_key="YOUR_API_KEY",
|
|
331
|
+
max_retries=5, # default: 3
|
|
332
|
+
initial_retry_delay=2.0, # default: 1.0 (seconds)
|
|
333
|
+
backoff_multiplier=3.0, # default: 2.0
|
|
334
|
+
max_retry_delay=60.0, # default: 30.0 (seconds)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Disable retries entirely
|
|
338
|
+
client = TweetAPI(api_key="YOUR_API_KEY", max_retries=0)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Rate Limit Awareness
|
|
342
|
+
|
|
343
|
+
After a 429 response, the SDK exposes the last known rate limit state:
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
print(client.rate_limit_info)
|
|
347
|
+
# {"retry_after": 30, "timestamp": 1712345678.0} — or None if no 429 encountered
|
|
348
|
+
```
|
|
349
|
+
|
|
203
350
|
## Error Handling
|
|
204
351
|
|
|
205
|
-
The SDK
|
|
352
|
+
The SDK raises typed exceptions you can catch and handle. With automatic retries enabled (default), you'll only see these after all retry attempts are exhausted:
|
|
206
353
|
|
|
207
354
|
```python
|
|
208
|
-
import time
|
|
209
355
|
from tweetapi import (
|
|
210
356
|
TweetAPI,
|
|
211
357
|
TweetAPIError,
|
|
@@ -222,9 +368,7 @@ client = TweetAPI(api_key="YOUR_API_KEY")
|
|
|
222
368
|
try:
|
|
223
369
|
user = client.user.get_by_username(username="elonmusk")
|
|
224
370
|
except RateLimitError as e:
|
|
225
|
-
# Wait and retry
|
|
226
371
|
print(f"Rate limited. Retry in {e.retry_after}s")
|
|
227
|
-
time.sleep(e.retry_after)
|
|
228
372
|
except NotFoundError:
|
|
229
373
|
print("User not found")
|
|
230
374
|
except AuthenticationError:
|
|
@@ -236,7 +380,6 @@ except ServerError:
|
|
|
236
380
|
except NetworkError:
|
|
237
381
|
print("Network error — check your connection")
|
|
238
382
|
except TweetAPIError as e:
|
|
239
|
-
# Catch-all for any other API error
|
|
240
383
|
print(f"Error [{e.code}]: {e.message}")
|
|
241
384
|
```
|
|
242
385
|
|
|
@@ -252,8 +395,17 @@ Every error includes:
|
|
|
252
395
|
client = TweetAPI(
|
|
253
396
|
api_key="YOUR_API_KEY", # Required
|
|
254
397
|
base_url="https://...", # Optional (default: https://api.tweetapi.com)
|
|
255
|
-
timeout=30, # Optional
|
|
398
|
+
timeout=30, # Optional — single value for both connect + read
|
|
399
|
+
connect_timeout=10.0, # Optional (default: 10s)
|
|
400
|
+
read_timeout=30.0, # Optional (default: 30s)
|
|
401
|
+
max_retries=3, # Optional (default: 3, set 0 to disable)
|
|
402
|
+
backoff_multiplier=2.0, # Optional (default: 2.0)
|
|
403
|
+
initial_retry_delay=1.0, # Optional (default: 1.0s)
|
|
404
|
+
max_retry_delay=30.0, # Optional (default: 30.0s)
|
|
256
405
|
)
|
|
406
|
+
|
|
407
|
+
# You can also pass a tuple for timeout
|
|
408
|
+
client = TweetAPI(api_key="YOUR_API_KEY", timeout=(5, 30)) # (connect, read)
|
|
257
409
|
```
|
|
258
410
|
|
|
259
411
|
## Requirements
|
|
@@ -266,7 +418,7 @@ client = TweetAPI(
|
|
|
266
418
|
- [Full Documentation](https://tweetapi.com/docs?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
|
|
267
419
|
- [Get API Key (Free)](https://tweetapi.com?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
|
|
268
420
|
- [Dashboard](https://tweetapi.com/dashboard?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
|
|
269
|
-
- [Node.js SDK](https://github.com/tweetapi/
|
|
421
|
+
- [Node.js SDK](https://github.com/tweetapi/node)
|
|
270
422
|
|
|
271
423
|
## License
|
|
272
424
|
|
|
@@ -37,10 +37,13 @@ next_page = client.user.get_followers(
|
|
|
37
37
|
## Features
|
|
38
38
|
|
|
39
39
|
- **70+ endpoints** covering users, tweets, posts, interactions, DMs, communities, spaces, and search
|
|
40
|
-
- **Full type hints** with TypedDict
|
|
40
|
+
- **Full type hints** with TypedDict response types for IDE autocomplete
|
|
41
|
+
- **Automatic retry with backoff** on rate limits (429) and server errors (5xx)
|
|
42
|
+
- **Auto-pagination helpers** — iterate all pages with a simple `for` loop
|
|
41
43
|
- **Solid error handling** with typed exceptions (`RateLimitError`, `NotFoundError`, etc.)
|
|
44
|
+
- **Rate limit awareness** — `retry_after` respected automatically, state exposed via `client.rate_limit_info`
|
|
45
|
+
- **Split timeouts** — separate connect and read timeouts
|
|
42
46
|
- **Single dependency** — `requests` only
|
|
43
|
-
- **Pagination support** with cursor-based navigation
|
|
44
47
|
- **Python 3.9+** compatible
|
|
45
48
|
|
|
46
49
|
## API Reference
|
|
@@ -80,13 +83,21 @@ next_page = client.user.get_followers(
|
|
|
80
83
|
|
|
81
84
|
| Method | Description |
|
|
82
85
|
|--------|-------------|
|
|
83
|
-
| `client.post.create_post(auth_token=..., text=..., proxy=...)` | Create a tweet |
|
|
84
|
-
| `client.post.create_post_quote(auth_token=..., text=..., attachment_url=..., proxy=...)` | Quote tweet |
|
|
85
|
-
| `client.post.create_post_with_media(auth_token=..., text=..., media=..., proxy=...)` | Tweet with media |
|
|
86
|
+
| `client.post.create_post(auth_token=..., text=..., proxy=..., reply_option=...)` | Create a tweet |
|
|
87
|
+
| `client.post.create_post_quote(auth_token=..., text=..., attachment_url=..., proxy=..., reply_option=...)` | Quote tweet |
|
|
88
|
+
| `client.post.create_post_with_media(auth_token=..., text=..., media=..., proxy=..., reply_option=...)` | Tweet with media |
|
|
86
89
|
| `client.post.reply_post(auth_token=..., text=..., tweet_id=..., proxy=...)` | Reply to a tweet |
|
|
87
90
|
| `client.post.reply_post_with_media(...)` | Reply with media |
|
|
88
91
|
| `client.post.delete_post(auth_token=..., tweet_id=...)` | Delete a tweet |
|
|
89
92
|
|
|
93
|
+
### Profile
|
|
94
|
+
|
|
95
|
+
| Method | Description |
|
|
96
|
+
|--------|-------------|
|
|
97
|
+
| `client.profile.update(auth_token=..., name=..., bio=..., location=..., website=..., proxy=...)` | Update profile fields |
|
|
98
|
+
| `client.profile.avatar(auth_token=..., media=..., proxy=...)` | Update profile avatar |
|
|
99
|
+
| `client.profile.banner(auth_token=..., media=..., proxy=...)` | Update profile banner |
|
|
100
|
+
|
|
90
101
|
### Interaction
|
|
91
102
|
|
|
92
103
|
| Method | Description |
|
|
@@ -112,6 +123,9 @@ next_page = client.user.get_followers(
|
|
|
112
123
|
| `client.list.get_tweets(list_id=...)` | Get tweets in a list |
|
|
113
124
|
| `client.list.get_members(list_id=...)` | Get list members |
|
|
114
125
|
| `client.list.get_followers(list_id=...)` | Get list followers |
|
|
126
|
+
| `client.list.create(auth_token=..., name=..., description=..., is_private=...)` | Create a list |
|
|
127
|
+
| `client.list.add_member(auth_token=..., list_id=..., user_id=..., proxy=...)` | Add a member |
|
|
128
|
+
| `client.list.remove_member(auth_token=..., list_id=..., user_id=..., proxy=...)` | Remove a member |
|
|
115
129
|
|
|
116
130
|
### Community
|
|
117
131
|
|
|
@@ -123,6 +137,8 @@ next_page = client.user.get_followers(
|
|
|
123
137
|
| `client.community.search(query=...)` | Search communities |
|
|
124
138
|
| `client.community.create_post(auth_token=..., text=..., community_id=..., proxy=...)` | Post in community |
|
|
125
139
|
| `client.community.create_post_with_media(...)` | Post with media |
|
|
140
|
+
| `client.community.create_quote(auth_token=..., text=..., attachment_url=..., community_id=..., proxy=...)` | Quote post in community |
|
|
141
|
+
| `client.community.create_quote_with_media(...)` | Quote post with media |
|
|
126
142
|
| `client.community.reply_post(...)` | Reply to community post |
|
|
127
143
|
| `client.community.reply_post_with_media(...)` | Reply with media |
|
|
128
144
|
| `client.community.join(auth_token=..., community_id=...)` | Join |
|
|
@@ -170,12 +186,142 @@ next_page = client.user.get_followers(
|
|
|
170
186
|
| `client.dm.get_dm_user_updates(auth_token=..., cursor=...)` | DM user updates |
|
|
171
187
|
| `client.dm.accept_conversation(auth_token=..., conversation_id=...)` | Accept request |
|
|
172
188
|
|
|
189
|
+
## Posting and Profile Media
|
|
190
|
+
|
|
191
|
+
Tweet media accepts an existing TweetAPI media ID, a URL, or inline base64 data:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
client.post.create_post_with_media(
|
|
195
|
+
auth_token="AUTH_TOKEN",
|
|
196
|
+
text="Launch update",
|
|
197
|
+
media=[{"media_id": "1971008286821380096"}],
|
|
198
|
+
proxy="host:port:user:pass",
|
|
199
|
+
)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Use `reply_option` to set reply controls when creating standard, quote, or media posts:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
client.post.create_post(
|
|
206
|
+
auth_token="AUTH_TOKEN",
|
|
207
|
+
text="Followers can reply",
|
|
208
|
+
proxy="host:port:user:pass",
|
|
209
|
+
reply_option={"mode": "followers"},
|
|
210
|
+
)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Profile avatar and banner uploads accept a URL or inline `data` plus `type`:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
client.profile.update(auth_token="AUTH_TOKEN", name="New Name", bio="Builder")
|
|
217
|
+
client.profile.avatar(
|
|
218
|
+
auth_token="AUTH_TOKEN",
|
|
219
|
+
media={"url": "https://example.com/avatar.jpg"},
|
|
220
|
+
)
|
|
221
|
+
client.profile.banner(
|
|
222
|
+
auth_token="AUTH_TOKEN",
|
|
223
|
+
media={"data": "BASE64_IMAGE_DATA", "type": "image/png"},
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Canonical list mutations live under `client.list`; the legacy interaction list helpers remain available:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
created = client.list.create(
|
|
231
|
+
auth_token="AUTH_TOKEN",
|
|
232
|
+
name="Research",
|
|
233
|
+
description="Accounts to watch",
|
|
234
|
+
is_private=True,
|
|
235
|
+
)
|
|
236
|
+
client.list.add_member(auth_token="AUTH_TOKEN", list_id=created["data"]["id"], user_id="123")
|
|
237
|
+
client.list.remove_member(auth_token="AUTH_TOKEN", list_id=created["data"]["id"], user_id="123")
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Community quote posts support the same URL and media attachment shapes:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
client.community.create_quote(
|
|
244
|
+
auth_token="AUTH_TOKEN",
|
|
245
|
+
text="Useful context",
|
|
246
|
+
attachment_url="https://x.com/user/status/123",
|
|
247
|
+
community_id="987",
|
|
248
|
+
proxy="host:port:user:pass",
|
|
249
|
+
)
|
|
250
|
+
client.community.create_quote_with_media(
|
|
251
|
+
auth_token="AUTH_TOKEN",
|
|
252
|
+
text="Useful context",
|
|
253
|
+
attachment_url="https://x.com/user/status/123",
|
|
254
|
+
community_id="987",
|
|
255
|
+
media=[{"media_id": "1971008286821380096"}],
|
|
256
|
+
proxy="host:port:user:pass",
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Auto-Pagination
|
|
261
|
+
|
|
262
|
+
Use the `paginate()` and `paginate_pages()` helpers to iterate through all pages automatically:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from tweetapi import TweetAPI, paginate, paginate_pages
|
|
266
|
+
|
|
267
|
+
client = TweetAPI(api_key="YOUR_API_KEY")
|
|
268
|
+
|
|
269
|
+
# Iterate individual items across all pages
|
|
270
|
+
for user in paginate(
|
|
271
|
+
lambda cursor: client.user.get_followers(user_id="123456", cursor=cursor),
|
|
272
|
+
):
|
|
273
|
+
print(user["username"])
|
|
274
|
+
|
|
275
|
+
# Iterate full pages (access page-level data)
|
|
276
|
+
for page in paginate_pages(
|
|
277
|
+
lambda cursor: client.explore.search(query="bitcoin", type="Latest", cursor=cursor),
|
|
278
|
+
max_pages=5, # optional: limit number of pages
|
|
279
|
+
):
|
|
280
|
+
print(f"Got {len(page['data'])} results")
|
|
281
|
+
print(f"Next cursor: {page['pagination']['nextCursor']}")
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Works with any paginated endpoint — followers, tweets, search results, list members, community posts, etc.
|
|
285
|
+
|
|
286
|
+
## Automatic Retry with Backoff
|
|
287
|
+
|
|
288
|
+
The SDK automatically retries on transient errors with exponential backoff:
|
|
289
|
+
|
|
290
|
+
- **429 (Rate Limit)** — waits the `retry_after` duration from the API, then retries
|
|
291
|
+
- **5xx (Server Error)** — retries with exponential backoff + jitter
|
|
292
|
+
- **Network errors** — retries on timeouts and connection failures
|
|
293
|
+
- **4xx (Client Error)** — never retried (400, 401, 403, 404 fail immediately)
|
|
294
|
+
|
|
295
|
+
Default: 3 retries, 2x backoff, 1s initial delay, 30s max delay.
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
# Customize retry behavior
|
|
299
|
+
client = TweetAPI(
|
|
300
|
+
api_key="YOUR_API_KEY",
|
|
301
|
+
max_retries=5, # default: 3
|
|
302
|
+
initial_retry_delay=2.0, # default: 1.0 (seconds)
|
|
303
|
+
backoff_multiplier=3.0, # default: 2.0
|
|
304
|
+
max_retry_delay=60.0, # default: 30.0 (seconds)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Disable retries entirely
|
|
308
|
+
client = TweetAPI(api_key="YOUR_API_KEY", max_retries=0)
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Rate Limit Awareness
|
|
312
|
+
|
|
313
|
+
After a 429 response, the SDK exposes the last known rate limit state:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
print(client.rate_limit_info)
|
|
317
|
+
# {"retry_after": 30, "timestamp": 1712345678.0} — or None if no 429 encountered
|
|
318
|
+
```
|
|
319
|
+
|
|
173
320
|
## Error Handling
|
|
174
321
|
|
|
175
|
-
The SDK
|
|
322
|
+
The SDK raises typed exceptions you can catch and handle. With automatic retries enabled (default), you'll only see these after all retry attempts are exhausted:
|
|
176
323
|
|
|
177
324
|
```python
|
|
178
|
-
import time
|
|
179
325
|
from tweetapi import (
|
|
180
326
|
TweetAPI,
|
|
181
327
|
TweetAPIError,
|
|
@@ -192,9 +338,7 @@ client = TweetAPI(api_key="YOUR_API_KEY")
|
|
|
192
338
|
try:
|
|
193
339
|
user = client.user.get_by_username(username="elonmusk")
|
|
194
340
|
except RateLimitError as e:
|
|
195
|
-
# Wait and retry
|
|
196
341
|
print(f"Rate limited. Retry in {e.retry_after}s")
|
|
197
|
-
time.sleep(e.retry_after)
|
|
198
342
|
except NotFoundError:
|
|
199
343
|
print("User not found")
|
|
200
344
|
except AuthenticationError:
|
|
@@ -206,7 +350,6 @@ except ServerError:
|
|
|
206
350
|
except NetworkError:
|
|
207
351
|
print("Network error — check your connection")
|
|
208
352
|
except TweetAPIError as e:
|
|
209
|
-
# Catch-all for any other API error
|
|
210
353
|
print(f"Error [{e.code}]: {e.message}")
|
|
211
354
|
```
|
|
212
355
|
|
|
@@ -222,8 +365,17 @@ Every error includes:
|
|
|
222
365
|
client = TweetAPI(
|
|
223
366
|
api_key="YOUR_API_KEY", # Required
|
|
224
367
|
base_url="https://...", # Optional (default: https://api.tweetapi.com)
|
|
225
|
-
timeout=30, # Optional
|
|
368
|
+
timeout=30, # Optional — single value for both connect + read
|
|
369
|
+
connect_timeout=10.0, # Optional (default: 10s)
|
|
370
|
+
read_timeout=30.0, # Optional (default: 30s)
|
|
371
|
+
max_retries=3, # Optional (default: 3, set 0 to disable)
|
|
372
|
+
backoff_multiplier=2.0, # Optional (default: 2.0)
|
|
373
|
+
initial_retry_delay=1.0, # Optional (default: 1.0s)
|
|
374
|
+
max_retry_delay=30.0, # Optional (default: 30.0s)
|
|
226
375
|
)
|
|
376
|
+
|
|
377
|
+
# You can also pass a tuple for timeout
|
|
378
|
+
client = TweetAPI(api_key="YOUR_API_KEY", timeout=(5, 30)) # (connect, read)
|
|
227
379
|
```
|
|
228
380
|
|
|
229
381
|
## Requirements
|
|
@@ -236,7 +388,7 @@ client = TweetAPI(
|
|
|
236
388
|
- [Full Documentation](https://tweetapi.com/docs?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
|
|
237
389
|
- [Get API Key (Free)](https://tweetapi.com?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
|
|
238
390
|
- [Dashboard](https://tweetapi.com/dashboard?utm_source=github&utm_medium=readme&utm_campaign=python-sdk)
|
|
239
|
-
- [Node.js SDK](https://github.com/tweetapi/
|
|
391
|
+
- [Node.js SDK](https://github.com/tweetapi/node)
|
|
240
392
|
|
|
241
393
|
## License
|
|
242
394
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tweetapi"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.2.0"
|
|
8
8
|
description = "Official Python SDK for TweetAPI — Twitter/X Data API for developers and researchers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -47,8 +47,8 @@ dependencies = [
|
|
|
47
47
|
[project.urls]
|
|
48
48
|
Homepage = "https://tweetapi.com?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk"
|
|
49
49
|
Documentation = "https://tweetapi.com/docs?utm_source=pypi&utm_medium=readme&utm_campaign=python-sdk"
|
|
50
|
-
Repository = "https://github.com/tweetapi/
|
|
51
|
-
Issues = "https://github.com/tweetapi/
|
|
50
|
+
Repository = "https://github.com/tweetapi/python"
|
|
51
|
+
Issues = "https://github.com/tweetapi/python/issues"
|
|
52
52
|
|
|
53
53
|
[tool.hatch.build.targets.wheel]
|
|
54
54
|
packages = ["tweetapi"]
|