tweetapi 1.1.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.1.0 → tweetapi-1.2.0}/PKG-INFO +88 -4
- {tweetapi-1.1.0 → tweetapi-1.2.0}/README.md +87 -3
- {tweetapi-1.1.0 → tweetapi-1.2.0}/pyproject.toml +1 -1
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tests/test_client.py +246 -1
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/__init__.py +1 -1
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/client.py +2 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/community.py +19 -2
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/list_.py +23 -1
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/post.py +12 -6
- tweetapi-1.2.0/tweetapi/resources/profile.py +31 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/types.py +59 -1
- {tweetapi-1.1.0 → tweetapi-1.2.0}/.claude/settings.local.json +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/.gitignore +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/examples/create_post.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/examples/get_user.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/examples/search_tweets.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tests/__init__.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tests/test_pagination.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tests/test_retry.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tests/test_timeout.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/errors.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/pagination.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/py.typed +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/__init__.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/auth.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/explore.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/interaction.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/space.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/tweet.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/unencrypted_dm.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/user.py +0 -0
- {tweetapi-1.1.0 → tweetapi-1.2.0}/tweetapi/resources/xchat.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,6 +1,6 @@
|
|
|
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
|
|
@@ -113,13 +113,21 @@ next_page = client.user.get_followers(
|
|
|
113
113
|
|
|
114
114
|
| Method | Description |
|
|
115
115
|
|--------|-------------|
|
|
116
|
-
| `client.post.create_post(auth_token=..., text=..., proxy=...)` | Create a tweet |
|
|
117
|
-
| `client.post.create_post_quote(auth_token=..., text=..., attachment_url=..., proxy=...)` | Quote tweet |
|
|
118
|
-
| `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 |
|
|
119
119
|
| `client.post.reply_post(auth_token=..., text=..., tweet_id=..., proxy=...)` | Reply to a tweet |
|
|
120
120
|
| `client.post.reply_post_with_media(...)` | Reply with media |
|
|
121
121
|
| `client.post.delete_post(auth_token=..., tweet_id=...)` | Delete a tweet |
|
|
122
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
|
+
|
|
123
131
|
### Interaction
|
|
124
132
|
|
|
125
133
|
| Method | Description |
|
|
@@ -145,6 +153,9 @@ next_page = client.user.get_followers(
|
|
|
145
153
|
| `client.list.get_tweets(list_id=...)` | Get tweets in a list |
|
|
146
154
|
| `client.list.get_members(list_id=...)` | Get list members |
|
|
147
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 |
|
|
148
159
|
|
|
149
160
|
### Community
|
|
150
161
|
|
|
@@ -156,6 +167,8 @@ next_page = client.user.get_followers(
|
|
|
156
167
|
| `client.community.search(query=...)` | Search communities |
|
|
157
168
|
| `client.community.create_post(auth_token=..., text=..., community_id=..., proxy=...)` | Post in community |
|
|
158
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 |
|
|
159
172
|
| `client.community.reply_post(...)` | Reply to community post |
|
|
160
173
|
| `client.community.reply_post_with_media(...)` | Reply with media |
|
|
161
174
|
| `client.community.join(auth_token=..., community_id=...)` | Join |
|
|
@@ -203,6 +216,77 @@ next_page = client.user.get_followers(
|
|
|
203
216
|
| `client.dm.get_dm_user_updates(auth_token=..., cursor=...)` | DM user updates |
|
|
204
217
|
| `client.dm.accept_conversation(auth_token=..., conversation_id=...)` | Accept request |
|
|
205
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
|
+
|
|
206
290
|
## Auto-Pagination
|
|
207
291
|
|
|
208
292
|
Use the `paginate()` and `paginate_pages()` helpers to iterate through all pages automatically:
|
|
@@ -83,13 +83,21 @@ next_page = client.user.get_followers(
|
|
|
83
83
|
|
|
84
84
|
| Method | Description |
|
|
85
85
|
|--------|-------------|
|
|
86
|
-
| `client.post.create_post(auth_token=..., text=..., proxy=...)` | Create a tweet |
|
|
87
|
-
| `client.post.create_post_quote(auth_token=..., text=..., attachment_url=..., proxy=...)` | Quote tweet |
|
|
88
|
-
| `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 |
|
|
89
89
|
| `client.post.reply_post(auth_token=..., text=..., tweet_id=..., proxy=...)` | Reply to a tweet |
|
|
90
90
|
| `client.post.reply_post_with_media(...)` | Reply with media |
|
|
91
91
|
| `client.post.delete_post(auth_token=..., tweet_id=...)` | Delete a tweet |
|
|
92
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
|
+
|
|
93
101
|
### Interaction
|
|
94
102
|
|
|
95
103
|
| Method | Description |
|
|
@@ -115,6 +123,9 @@ next_page = client.user.get_followers(
|
|
|
115
123
|
| `client.list.get_tweets(list_id=...)` | Get tweets in a list |
|
|
116
124
|
| `client.list.get_members(list_id=...)` | Get list members |
|
|
117
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 |
|
|
118
129
|
|
|
119
130
|
### Community
|
|
120
131
|
|
|
@@ -126,6 +137,8 @@ next_page = client.user.get_followers(
|
|
|
126
137
|
| `client.community.search(query=...)` | Search communities |
|
|
127
138
|
| `client.community.create_post(auth_token=..., text=..., community_id=..., proxy=...)` | Post in community |
|
|
128
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 |
|
|
129
142
|
| `client.community.reply_post(...)` | Reply to community post |
|
|
130
143
|
| `client.community.reply_post_with_media(...)` | Reply with media |
|
|
131
144
|
| `client.community.join(auth_token=..., community_id=...)` | Join |
|
|
@@ -173,6 +186,77 @@ next_page = client.user.get_followers(
|
|
|
173
186
|
| `client.dm.get_dm_user_updates(auth_token=..., cursor=...)` | DM user updates |
|
|
174
187
|
| `client.dm.accept_conversation(auth_token=..., conversation_id=...)` | Accept request |
|
|
175
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
|
+
|
|
176
260
|
## Auto-Pagination
|
|
177
261
|
|
|
178
262
|
Use the `paginate()` and `paginate_pages()` helpers to iterate through all pages automatically:
|
|
@@ -35,7 +35,7 @@ class TestClientInit:
|
|
|
35
35
|
def test_all_resources_exist(self):
|
|
36
36
|
client = make_client()
|
|
37
37
|
for attr in ["user", "tweet", "post", "interaction", "list", "community",
|
|
38
|
-
"space", "explore", "auth", "xchat", "dm"]:
|
|
38
|
+
"profile", "space", "explore", "auth", "xchat", "dm"]:
|
|
39
39
|
assert hasattr(client, attr), f"Missing resource: {attr}"
|
|
40
40
|
|
|
41
41
|
|
|
@@ -132,6 +132,251 @@ class TestPostRequests:
|
|
|
132
132
|
body = json.loads(responses.calls[0].request.body)
|
|
133
133
|
assert "disableLinkPreview" not in body
|
|
134
134
|
|
|
135
|
+
@responses.activate
|
|
136
|
+
def test_create_post_serializes_reply_option(self):
|
|
137
|
+
responses.add(
|
|
138
|
+
responses.POST,
|
|
139
|
+
f"{BASE_URL}/tw-v2/interaction/create-post",
|
|
140
|
+
json={"data": {"id": "1", "action": "create_tweet", "success": True}},
|
|
141
|
+
status=200,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
client = make_client()
|
|
145
|
+
client.post.create_post(
|
|
146
|
+
auth_token="auth",
|
|
147
|
+
text="Hello",
|
|
148
|
+
proxy="h:p@u:p",
|
|
149
|
+
reply_option={"mode": "followers", "regions": ["US"]},
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
body = json.loads(responses.calls[0].request.body)
|
|
153
|
+
assert body["replyOption"] == {"mode": "followers", "regions": ["US"]}
|
|
154
|
+
|
|
155
|
+
@responses.activate
|
|
156
|
+
def test_create_post_with_media_preserves_media_id(self):
|
|
157
|
+
responses.add(
|
|
158
|
+
responses.POST,
|
|
159
|
+
f"{BASE_URL}/tw-v2/interaction/create-post-with-media",
|
|
160
|
+
json={"data": {"id": "1", "action": "create_tweet", "success": True}},
|
|
161
|
+
status=200,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
client = make_client()
|
|
165
|
+
client.post.create_post_with_media(
|
|
166
|
+
auth_token="auth",
|
|
167
|
+
text="Hello",
|
|
168
|
+
media=[{"media_id": "1971008286821380096"}],
|
|
169
|
+
proxy="h:p@u:p",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
body = json.loads(responses.calls[0].request.body)
|
|
173
|
+
assert body["media"] == [{"media_id": "1971008286821380096"}]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TestListParity:
|
|
177
|
+
@responses.activate
|
|
178
|
+
def test_create_sends_camel_case_body(self):
|
|
179
|
+
responses.add(
|
|
180
|
+
responses.POST,
|
|
181
|
+
f"{BASE_URL}/tw-v2/list/create",
|
|
182
|
+
json={"data": {"id": "list-1", "name": "Research"}},
|
|
183
|
+
status=200,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
client = make_client()
|
|
187
|
+
client.list.create(
|
|
188
|
+
auth_token="auth",
|
|
189
|
+
name="Research",
|
|
190
|
+
description="Signals",
|
|
191
|
+
is_private=False,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
body = json.loads(responses.calls[0].request.body)
|
|
195
|
+
assert body == {
|
|
196
|
+
"authToken": "auth",
|
|
197
|
+
"name": "Research",
|
|
198
|
+
"description": "Signals",
|
|
199
|
+
"isPrivate": False,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@responses.activate
|
|
203
|
+
def test_add_member_sends_camel_case_body_and_strips_none(self):
|
|
204
|
+
responses.add(
|
|
205
|
+
responses.POST,
|
|
206
|
+
f"{BASE_URL}/tw-v2/list/add-member",
|
|
207
|
+
json={"data": {"success": True}},
|
|
208
|
+
status=200,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
client = make_client()
|
|
212
|
+
client.list.add_member(auth_token="auth", list_id="list-1", user_id="user-1")
|
|
213
|
+
|
|
214
|
+
body = json.loads(responses.calls[0].request.body)
|
|
215
|
+
assert body == {
|
|
216
|
+
"authToken": "auth",
|
|
217
|
+
"listId": "list-1",
|
|
218
|
+
"userId": "user-1",
|
|
219
|
+
}
|
|
220
|
+
assert "proxy" not in body
|
|
221
|
+
|
|
222
|
+
@responses.activate
|
|
223
|
+
def test_remove_member_sends_camel_case_body(self):
|
|
224
|
+
responses.add(
|
|
225
|
+
responses.POST,
|
|
226
|
+
f"{BASE_URL}/tw-v2/list/remove-member",
|
|
227
|
+
json={"data": {"success": True}},
|
|
228
|
+
status=200,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
client = make_client()
|
|
232
|
+
client.list.remove_member(
|
|
233
|
+
auth_token="auth",
|
|
234
|
+
list_id="list-1",
|
|
235
|
+
user_id="user-1",
|
|
236
|
+
proxy="h:p@u:p",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
body = json.loads(responses.calls[0].request.body)
|
|
240
|
+
assert body == {
|
|
241
|
+
"authToken": "auth",
|
|
242
|
+
"listId": "list-1",
|
|
243
|
+
"userId": "user-1",
|
|
244
|
+
"proxy": "h:p@u:p",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestProfileParity:
|
|
249
|
+
@responses.activate
|
|
250
|
+
def test_update_sends_body_and_strips_none(self):
|
|
251
|
+
responses.add(
|
|
252
|
+
responses.POST,
|
|
253
|
+
f"{BASE_URL}/tw-v2/profile/update",
|
|
254
|
+
json={"data": {"id": "user-1", "name": "New Name"}},
|
|
255
|
+
status=200,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
client = make_client()
|
|
259
|
+
client.profile.update(
|
|
260
|
+
auth_token="auth",
|
|
261
|
+
name="New Name",
|
|
262
|
+
bio=None,
|
|
263
|
+
location="Bangkok",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
body = json.loads(responses.calls[0].request.body)
|
|
267
|
+
assert body == {
|
|
268
|
+
"authToken": "auth",
|
|
269
|
+
"name": "New Name",
|
|
270
|
+
"location": "Bangkok",
|
|
271
|
+
}
|
|
272
|
+
assert "bio" not in body
|
|
273
|
+
assert "website" not in body
|
|
274
|
+
assert "proxy" not in body
|
|
275
|
+
|
|
276
|
+
@responses.activate
|
|
277
|
+
def test_avatar_sends_media_body(self):
|
|
278
|
+
responses.add(
|
|
279
|
+
responses.POST,
|
|
280
|
+
f"{BASE_URL}/tw-v2/profile/avatar",
|
|
281
|
+
json={"data": {"id": "user-1", "avatar": "https://example.com/a.jpg"}},
|
|
282
|
+
status=200,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
client = make_client()
|
|
286
|
+
client.profile.avatar(
|
|
287
|
+
auth_token="auth",
|
|
288
|
+
media={"url": "https://example.com/a.jpg"},
|
|
289
|
+
proxy="h:p@u:p",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
body = json.loads(responses.calls[0].request.body)
|
|
293
|
+
assert body == {
|
|
294
|
+
"authToken": "auth",
|
|
295
|
+
"media": {"url": "https://example.com/a.jpg"},
|
|
296
|
+
"proxy": "h:p@u:p",
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@responses.activate
|
|
300
|
+
def test_banner_sends_media_body(self):
|
|
301
|
+
responses.add(
|
|
302
|
+
responses.POST,
|
|
303
|
+
f"{BASE_URL}/tw-v2/profile/banner",
|
|
304
|
+
json={"data": {"id": "user-1", "banner": "https://example.com/b.jpg"}},
|
|
305
|
+
status=200,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
client = make_client()
|
|
309
|
+
client.profile.banner(
|
|
310
|
+
auth_token="auth",
|
|
311
|
+
media={"data": "base64", "type": "image/png"},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
body = json.loads(responses.calls[0].request.body)
|
|
315
|
+
assert body == {
|
|
316
|
+
"authToken": "auth",
|
|
317
|
+
"media": {"data": "base64", "type": "image/png"},
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class TestCommunityParity:
|
|
322
|
+
@responses.activate
|
|
323
|
+
def test_create_quote_sends_path_and_body(self):
|
|
324
|
+
responses.add(
|
|
325
|
+
responses.POST,
|
|
326
|
+
f"{BASE_URL}/tw-v2/interaction/create-community-quote",
|
|
327
|
+
json={"data": {"id": "tweet-1", "success": True}},
|
|
328
|
+
status=200,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
client = make_client()
|
|
332
|
+
client.community.create_quote(
|
|
333
|
+
auth_token="auth",
|
|
334
|
+
text="Quote",
|
|
335
|
+
attachment_url="https://x.com/user/status/1",
|
|
336
|
+
community_id="community-1",
|
|
337
|
+
proxy="h:p@u:p",
|
|
338
|
+
disable_link_preview=True,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
body = json.loads(responses.calls[0].request.body)
|
|
342
|
+
assert body == {
|
|
343
|
+
"authToken": "auth",
|
|
344
|
+
"text": "Quote",
|
|
345
|
+
"attachmentUrl": "https://x.com/user/status/1",
|
|
346
|
+
"communityId": "community-1",
|
|
347
|
+
"proxy": "h:p@u:p",
|
|
348
|
+
"disableLinkPreview": True,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
@responses.activate
|
|
352
|
+
def test_create_quote_with_media_sends_path_and_body(self):
|
|
353
|
+
responses.add(
|
|
354
|
+
responses.POST,
|
|
355
|
+
f"{BASE_URL}/tw-v2/interaction/create-community-quote-with-media",
|
|
356
|
+
json={"data": {"id": "tweet-1", "success": True}},
|
|
357
|
+
status=200,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
client = make_client()
|
|
361
|
+
client.community.create_quote_with_media(
|
|
362
|
+
auth_token="auth",
|
|
363
|
+
text="Quote",
|
|
364
|
+
attachment_url="https://x.com/user/status/1",
|
|
365
|
+
community_id="community-1",
|
|
366
|
+
media=[{"media_id": "1971008286821380096"}],
|
|
367
|
+
proxy="h:p@u:p",
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
body = json.loads(responses.calls[0].request.body)
|
|
371
|
+
assert body == {
|
|
372
|
+
"authToken": "auth",
|
|
373
|
+
"text": "Quote",
|
|
374
|
+
"attachmentUrl": "https://x.com/user/status/1",
|
|
375
|
+
"communityId": "community-1",
|
|
376
|
+
"media": [{"media_id": "1971008286821380096"}],
|
|
377
|
+
"proxy": "h:p@u:p",
|
|
378
|
+
}
|
|
379
|
+
|
|
135
380
|
|
|
136
381
|
class TestErrorHandling:
|
|
137
382
|
@responses.activate
|
|
@@ -78,6 +78,7 @@ class TweetAPI:
|
|
|
78
78
|
from .resources.post import PostResource
|
|
79
79
|
from .resources.interaction import InteractionResource
|
|
80
80
|
from .resources.list_ import ListResource
|
|
81
|
+
from .resources.profile import ProfileResource
|
|
81
82
|
from .resources.community import CommunityResource
|
|
82
83
|
from .resources.space import SpaceResource
|
|
83
84
|
from .resources.explore import ExploreResource
|
|
@@ -90,6 +91,7 @@ class TweetAPI:
|
|
|
90
91
|
self.post = PostResource(self)
|
|
91
92
|
self.interaction = InteractionResource(self)
|
|
92
93
|
self.list = ListResource(self)
|
|
94
|
+
self.profile = ProfileResource(self)
|
|
93
95
|
self.community = CommunityResource(self)
|
|
94
96
|
self.space = SpaceResource(self)
|
|
95
97
|
self.explore = ExploreResource(self)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from ..client import TweetAPI
|
|
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
|
|
|
9
9
|
CommunityResponse,
|
|
10
10
|
CommunityMemberPaginatedResponse,
|
|
11
11
|
CommunitySearchPaginatedResponse,
|
|
12
|
+
TweetMediaInput,
|
|
12
13
|
TweetsPaginatedResponse,
|
|
13
14
|
)
|
|
14
15
|
|
|
@@ -40,13 +41,29 @@ class CommunityResource:
|
|
|
40
41
|
"proxy": proxy, "disableLinkPreview": disable_link_preview,
|
|
41
42
|
})
|
|
42
43
|
|
|
43
|
-
def create_post_with_media(self, *, auth_token: str, text: str, community_id: str, media: list[
|
|
44
|
+
def create_post_with_media(self, *, auth_token: str, text: str, community_id: str, media: list[TweetMediaInput], proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
|
|
44
45
|
"""Create a post with media in a community."""
|
|
45
46
|
return self._client._post("/tw-v2/interaction/create-community-post-with-media", {
|
|
46
47
|
"authToken": auth_token, "text": text, "communityId": community_id,
|
|
47
48
|
"media": media, "proxy": proxy, "disableLinkPreview": disable_link_preview,
|
|
48
49
|
})
|
|
49
50
|
|
|
51
|
+
def create_quote(self, *, auth_token: str, text: str, attachment_url: str, community_id: str, proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
|
|
52
|
+
"""Create a quote post in a community."""
|
|
53
|
+
return self._client._post("/tw-v2/interaction/create-community-quote", {
|
|
54
|
+
"authToken": auth_token, "text": text, "attachmentUrl": attachment_url,
|
|
55
|
+
"communityId": community_id, "proxy": proxy,
|
|
56
|
+
"disableLinkPreview": disable_link_preview,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
def create_quote_with_media(self, *, auth_token: str, text: str, attachment_url: str, community_id: str, media: list[TweetMediaInput], proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
|
|
60
|
+
"""Create a quote post with media in a community."""
|
|
61
|
+
return self._client._post("/tw-v2/interaction/create-community-quote-with-media", {
|
|
62
|
+
"authToken": auth_token, "text": text, "attachmentUrl": attachment_url,
|
|
63
|
+
"communityId": community_id, "media": media, "proxy": proxy,
|
|
64
|
+
"disableLinkPreview": disable_link_preview,
|
|
65
|
+
})
|
|
66
|
+
|
|
50
67
|
def reply_post(self, *, auth_token: str, text: str, tweet_id: str, community_id: str, proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
|
|
51
68
|
"""Reply to a community post."""
|
|
52
69
|
return self._client._post("/tw-v2/interaction/reply-community-post", {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional, TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from ..client import TweetAPI
|
|
7
7
|
from ..types import (
|
|
8
|
+
ActionResponse,
|
|
8
9
|
ListResponse,
|
|
9
10
|
TweetsPaginatedResponse,
|
|
10
11
|
UserPaginatedResponse,
|
|
@@ -30,3 +31,24 @@ class ListResource:
|
|
|
30
31
|
def get_followers(self, *, list_id: str, cursor: Optional[str] = None) -> UserPaginatedResponse:
|
|
31
32
|
"""Get followers of a list."""
|
|
32
33
|
return self._client._get("/tw-v2/list/followers", {"listId": list_id, "cursor": cursor})
|
|
34
|
+
|
|
35
|
+
def create(self, *, auth_token: str, name: str, description: str = "", is_private: bool = True) -> ListResponse:
|
|
36
|
+
"""Create a list."""
|
|
37
|
+
return self._client._post("/tw-v2/list/create", {
|
|
38
|
+
"authToken": auth_token, "name": name, "description": description,
|
|
39
|
+
"isPrivate": is_private,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
def add_member(self, *, auth_token: str, list_id: str, user_id: str, proxy: Optional[str] = None) -> ActionResponse:
|
|
43
|
+
"""Add a user to a list."""
|
|
44
|
+
return self._client._post("/tw-v2/list/add-member", {
|
|
45
|
+
"authToken": auth_token, "listId": list_id, "userId": user_id,
|
|
46
|
+
"proxy": proxy,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
def remove_member(self, *, auth_token: str, list_id: str, user_id: str, proxy: Optional[str] = None) -> ActionResponse:
|
|
50
|
+
"""Remove a user from a list."""
|
|
51
|
+
return self._client._post("/tw-v2/list/remove-member", {
|
|
52
|
+
"authToken": auth_token, "listId": list_id, "userId": user_id,
|
|
53
|
+
"proxy": proxy,
|
|
54
|
+
})
|
|
@@ -4,32 +4,38 @@ from typing import Any, Optional, TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
if TYPE_CHECKING:
|
|
6
6
|
from ..client import TweetAPI
|
|
7
|
-
from ..types import ActionResponse
|
|
7
|
+
from ..types import ActionResponse, ReplyOption, TweetMediaInput
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class PostResource:
|
|
11
11
|
def __init__(self, client: TweetAPI) -> None:
|
|
12
12
|
self._client = client
|
|
13
13
|
|
|
14
|
-
def create_post(self, *, auth_token: str, text: str, proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
|
|
14
|
+
def create_post(self, *, auth_token: str, text: str, proxy: str, disable_link_preview: Optional[bool] = None, reply_option: Optional[ReplyOption] = None) -> ActionResponse:
|
|
15
15
|
"""Create a new tweet."""
|
|
16
16
|
return self._client._post("/tw-v2/interaction/create-post", {
|
|
17
17
|
"authToken": auth_token, "text": text, "proxy": proxy,
|
|
18
|
-
"disableLinkPreview": disable_link_preview,
|
|
18
|
+
"disableLinkPreview": disable_link_preview, "replyOption": reply_option,
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
def create_post_quote(self, *, auth_token: str, text: str, attachment_url: str, proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
|
|
21
|
+
def create_post_quote(self, *, auth_token: str, text: str, attachment_url: str, proxy: str, disable_link_preview: Optional[bool] = None, reply_option: Optional[ReplyOption] = None) -> ActionResponse:
|
|
22
22
|
"""Create a quote tweet."""
|
|
23
23
|
return self._client._post("/tw-v2/interaction/create-post-quote", {
|
|
24
24
|
"authToken": auth_token, "text": text, "attachmentUrl": attachment_url,
|
|
25
25
|
"proxy": proxy, "disableLinkPreview": disable_link_preview,
|
|
26
|
+
"replyOption": reply_option,
|
|
26
27
|
})
|
|
27
28
|
|
|
28
|
-
def create_post_with_media(self, *, auth_token: str, text: str, media: list[
|
|
29
|
-
"""Create a tweet with media attachments.
|
|
29
|
+
def create_post_with_media(self, *, auth_token: str, text: str, media: list[TweetMediaInput], proxy: str, disable_link_preview: Optional[bool] = None, reply_option: Optional[ReplyOption] = None) -> ActionResponse:
|
|
30
|
+
"""Create a tweet with media attachments.
|
|
31
|
+
|
|
32
|
+
``media`` accepts items shaped as ``{"media_id": str}``,
|
|
33
|
+
``{"url": str, "type": str | None}``, or ``{"data": str, "type": str}``.
|
|
34
|
+
"""
|
|
30
35
|
return self._client._post("/tw-v2/interaction/create-post-with-media", {
|
|
31
36
|
"authToken": auth_token, "text": text, "media": media,
|
|
32
37
|
"proxy": proxy, "disableLinkPreview": disable_link_preview,
|
|
38
|
+
"replyOption": reply_option,
|
|
33
39
|
})
|
|
34
40
|
|
|
35
41
|
def reply_post(self, *, auth_token: str, text: str, tweet_id: str, proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from ..client import TweetAPI
|
|
7
|
+
from ..types import ProfileMediaInput, UserResponse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProfileResource:
|
|
11
|
+
def __init__(self, client: TweetAPI) -> None:
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def update(self, *, auth_token: str, name: Optional[str] = None, bio: Optional[str] = None, location: Optional[str] = None, website: Optional[str] = None, proxy: Optional[str] = None) -> UserResponse:
|
|
15
|
+
"""Update the authenticated user's profile."""
|
|
16
|
+
return self._client._post("/tw-v2/profile/update", {
|
|
17
|
+
"authToken": auth_token, "name": name, "bio": bio,
|
|
18
|
+
"location": location, "website": website, "proxy": proxy,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
def avatar(self, *, auth_token: str, media: ProfileMediaInput, proxy: Optional[str] = None) -> UserResponse:
|
|
22
|
+
"""Update the authenticated user's avatar."""
|
|
23
|
+
return self._client._post("/tw-v2/profile/avatar", {
|
|
24
|
+
"authToken": auth_token, "media": media, "proxy": proxy,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
def banner(self, *, auth_token: str, media: ProfileMediaInput, proxy: Optional[str] = None) -> UserResponse:
|
|
28
|
+
"""Update the authenticated user's banner."""
|
|
29
|
+
return self._client._post("/tw-v2/profile/banner", {
|
|
30
|
+
"authToken": auth_token, "media": media, "proxy": proxy,
|
|
31
|
+
})
|
|
@@ -6,7 +6,7 @@ All types mirror the API's JSON response shapes exactly.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from typing import Any, List as TypingList, Optional, TypedDict
|
|
9
|
+
from typing import Any, List as TypingList, Optional, TypedDict, Union
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
# ─── Response Wrappers ───────────────────────────────────────────────────────
|
|
@@ -146,6 +146,42 @@ class UserAnalytics(TypedDict, total=False):
|
|
|
146
146
|
# ─── Media ───────────────────────────────────────────────────────────────────
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
class TweetMediaIdInput(TypedDict):
|
|
150
|
+
media_id: str
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TweetMediaUrlInputRequired(TypedDict):
|
|
154
|
+
url: str
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TweetMediaUrlInput(TweetMediaUrlInputRequired, total=False):
|
|
158
|
+
type: str
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TweetMediaDataInput(TypedDict):
|
|
162
|
+
data: str
|
|
163
|
+
type: str
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
TweetMediaInput = Union[TweetMediaIdInput, TweetMediaUrlInput, TweetMediaDataInput]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class ProfileMediaUrlInputRequired(TypedDict):
|
|
170
|
+
url: str
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ProfileMediaUrlInput(ProfileMediaUrlInputRequired, total=False):
|
|
174
|
+
type: str
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ProfileMediaDataInput(TypedDict):
|
|
178
|
+
data: str
|
|
179
|
+
type: str
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
ProfileMediaInput = Union[ProfileMediaUrlInput, ProfileMediaDataInput]
|
|
183
|
+
|
|
184
|
+
|
|
149
185
|
class MediaSize(TypedDict, total=False):
|
|
150
186
|
width: int
|
|
151
187
|
height: int
|
|
@@ -280,6 +316,27 @@ class ConversationControl(TypedDict, total=False):
|
|
|
280
316
|
allowedUserIds: list[str]
|
|
281
317
|
|
|
282
318
|
|
|
319
|
+
class ReplyOptionRequired(TypedDict):
|
|
320
|
+
mode: str
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class ReplyOption(ReplyOptionRequired, total=False):
|
|
324
|
+
regions: list[str]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class ReactionTargetAuthor(TypedDict, total=False):
|
|
328
|
+
id: str
|
|
329
|
+
username: str
|
|
330
|
+
name: str
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class ReactionContext(TypedDict, total=False):
|
|
334
|
+
type: str
|
|
335
|
+
targetTweetId: str
|
|
336
|
+
targetTweetUrl: str
|
|
337
|
+
targetAuthor: ReactionTargetAuthor
|
|
338
|
+
|
|
339
|
+
|
|
283
340
|
class Tweet(TypedDict, total=False):
|
|
284
341
|
id: str
|
|
285
342
|
conversationId: Optional[str]
|
|
@@ -316,6 +373,7 @@ class Tweet(TypedDict, total=False):
|
|
|
316
373
|
hasBirdwatchNotes: bool
|
|
317
374
|
birdwatchPivot: Optional[BirdwatchPivot]
|
|
318
375
|
conversationControl: Optional[ConversationControl]
|
|
376
|
+
reactionContext: Optional[ReactionContext]
|
|
319
377
|
isPromoted: bool
|
|
320
378
|
communityId: Optional[str]
|
|
321
379
|
createdAt: Optional[str]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|