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.
Files changed (36) hide show
  1. tweetapi-1.2.0/AGENTS.md +92 -0
  2. {tweetapi-1.0.0 → tweetapi-1.2.0}/PKG-INFO +167 -15
  3. {tweetapi-1.0.0 → tweetapi-1.2.0}/README.md +164 -12
  4. {tweetapi-1.0.0 → tweetapi-1.2.0}/pyproject.toml +3 -3
  5. tweetapi-1.2.0/tests/test_client.py +492 -0
  6. tweetapi-1.2.0/tests/test_pagination.py +101 -0
  7. tweetapi-1.2.0/tests/test_retry.py +148 -0
  8. tweetapi-1.2.0/tests/test_timeout.py +56 -0
  9. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/__init__.py +4 -1
  10. tweetapi-1.2.0/tweetapi/client.py +234 -0
  11. tweetapi-1.2.0/tweetapi/pagination.py +64 -0
  12. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/auth.py +2 -1
  13. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/community.py +35 -11
  14. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/explore.py +2 -1
  15. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/interaction.py +17 -12
  16. tweetapi-1.2.0/tweetapi/resources/list_.py +54 -0
  17. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/post.py +15 -8
  18. tweetapi-1.2.0/tweetapi/resources/profile.py +31 -0
  19. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/space.py +3 -2
  20. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/tweet.py +10 -4
  21. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/unencrypted_dm.py +9 -8
  22. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/user.py +26 -17
  23. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/xchat.py +6 -5
  24. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/types.py +138 -1
  25. tweetapi-1.0.0/tests/test_client.py +0 -245
  26. tweetapi-1.0.0/tweetapi/client.py +0 -146
  27. tweetapi-1.0.0/tweetapi/resources/list_.py +0 -27
  28. {tweetapi-1.0.0 → tweetapi-1.2.0}/.claude/settings.local.json +0 -0
  29. {tweetapi-1.0.0 → tweetapi-1.2.0}/.gitignore +0 -0
  30. {tweetapi-1.0.0 → tweetapi-1.2.0}/examples/create_post.py +0 -0
  31. {tweetapi-1.0.0 → tweetapi-1.2.0}/examples/get_user.py +0 -0
  32. {tweetapi-1.0.0 → tweetapi-1.2.0}/examples/search_tweets.py +0 -0
  33. {tweetapi-1.0.0 → tweetapi-1.2.0}/tests/__init__.py +0 -0
  34. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/errors.py +0 -0
  35. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/py.typed +0 -0
  36. {tweetapi-1.0.0 → tweetapi-1.2.0}/tweetapi/resources/__init__.py +0 -0
@@ -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.0.0
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/tweetapi-python
8
- Project-URL: Issues, https://github.com/tweetapi/tweetapi-python/issues
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 definitions for IDE autocomplete
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 throws typed exceptions you can catch and handle:
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 (default: 30 seconds)
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/tweetapi-node)
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 definitions for IDE autocomplete
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 throws typed exceptions you can catch and handle:
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 (default: 30 seconds)
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/tweetapi-node)
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.0.0"
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/tweetapi-python"
51
- Issues = "https://github.com/tweetapi/tweetapi-python/issues"
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"]