py-tw-client 1.0.1__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Trevor Hobenshield
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,489 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-tw-client
3
+ Version: 1.0.1
4
+ Summary: Implementation of X/Twitter v1, v2, and GraphQL APIs.
5
+ Home-page: https://github.com/repen/twitter-api-client
6
+ Author: Plugin.py
7
+ Author-email: 9keepa@gmail.com
8
+ License: MIT
9
+ Keywords: twitter api client async search automation bot scrape
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: Unix
14
+ Classifier: Operating System :: MacOS :: MacOS X
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Requires-Python: >=3.10.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: aiofiles
28
+ Requires-Dist: nest_asyncio
29
+ Requires-Dist: httpx
30
+ Requires-Dist: tqdm
31
+ Requires-Dist: orjson
32
+ Requires-Dist: m3u8
33
+ Requires-Dist: websockets
34
+ Requires-Dist: uvloop; platform_system != "Windows"
35
+ Requires-Dist: XClientTransaction
36
+ Requires-Dist: requests
37
+ Requires-Dist: beautifulsoup4
38
+ Dynamic: author
39
+ Dynamic: author-email
40
+ Dynamic: classifier
41
+ Dynamic: description
42
+ Dynamic: description-content-type
43
+ Dynamic: home-page
44
+ Dynamic: keywords
45
+ Dynamic: license
46
+ Dynamic: license-file
47
+ Dynamic: requires-dist
48
+ Dynamic: requires-python
49
+ Dynamic: summary
50
+
51
+
52
+
53
+ ## Implementation of X/Twitter v1, v2, and GraphQL APIs.
54
+
55
+
56
+ ## Table of Contents
57
+
58
+ * [Installation](#installation)
59
+ * [Automation](#automation)
60
+ * [Scraping](#scraping)
61
+ * [Get all user/tweet data](#get-all-usertweet-data)
62
+ * [Resume Pagination](#resume-pagination)
63
+ * [Search](#search)
64
+ * [Spaces](#spaces)
65
+ * [Live Audio Capture](#live-audio-capture)
66
+ * [Live Transcript Capture](#live-transcript-capture)
67
+ * [Search and Metadata](#search-and-metadata)
68
+ * [Automated Solvers](#automated-solvers)
69
+ * [Example API Responses](#example-api-responses)
70
+
71
+ ### Installation
72
+
73
+ ```bash
74
+ pip install twitter-api-client
75
+ ```
76
+
77
+ ### Automation
78
+
79
+ ```python
80
+ from twitter.account import Account
81
+
82
+ ## sign-in with credentials
83
+ email, username, password = ..., ..., ...
84
+ account = Account(email, username, password)
85
+
86
+ ## or, resume session using cookies
87
+ # account = Account(cookies={"ct0": ..., "auth_token": ...})
88
+
89
+ ## or, resume session using cookies (JSON file)
90
+ # account = Account(cookies='twitter.cookies')
91
+
92
+
93
+ account.tweet('test 123')
94
+ account.untweet(123456)
95
+ account.retweet(123456)
96
+ account.unretweet(123456)
97
+ account.reply('foo', tweet_id=123456)
98
+ account.quote('bar', tweet_id=123456)
99
+ account.schedule_tweet('schedule foo', 1681851240)
100
+ account.unschedule_tweet(123456)
101
+
102
+ account.tweet('hello world', media=[
103
+ {'media': 'test.jpg', 'alt': 'some alt text', 'tagged_users': [123]},
104
+ {'media': 'test.jpeg', 'alt': 'some alt text', 'tagged_users': [123]},
105
+ {'media': 'test.png', 'alt': 'some alt text', 'tagged_users': [123]},
106
+ {'media': 'test.jfif', 'alt': 'some alt text', 'tagged_users': [123]},
107
+ ])
108
+
109
+ account.schedule_tweet('foo bar', '2023-04-18 15:42', media=[
110
+ {'media': 'test.gif', 'alt': 'some alt text'},
111
+ ])
112
+
113
+ account.schedule_reply('hello world', '2023-04-19 15:42', tweet_id=123456, media=[
114
+ {'media': 'test.gif', 'alt': 'some alt text'},
115
+ ])
116
+
117
+ account.dm('my message', [1234], media='test.jpg')
118
+
119
+ account.create_poll('test poll 123', ['hello', 'world', 'foo', 'bar'], 10080)
120
+
121
+ # tweets
122
+ account.like(123456)
123
+ account.unlike(123456)
124
+ account.bookmark(123456)
125
+ account.unbookmark(123456)
126
+ account.pin(123456)
127
+ account.unpin(123456)
128
+
129
+ # users
130
+ account.follow(1234)
131
+ account.unfollow(1234)
132
+ account.mute(1234)
133
+ account.unmute(1234)
134
+ account.enable_notifications(1234)
135
+ account.disable_notifications(1234)
136
+ account.block(1234)
137
+ account.unblock(1234)
138
+
139
+ # user profile
140
+ account.update_profile_image('test.jpg')
141
+ account.update_profile_banner('test.png')
142
+ account.update_profile_info(name='Foo Bar', description='test 123', location='Victoria, BC')
143
+
144
+ # topics
145
+ account.follow_topic(111)
146
+ account.unfollow_topic(111)
147
+
148
+ # lists
149
+ account.create_list('My List', 'description of my list', private=False)
150
+ account.update_list(222, 'My Updated List', 'some updated description', private=False)
151
+ account.update_list_banner(222, 'test.png')
152
+ account.delete_list_banner(222)
153
+ account.add_list_member(222, 1234)
154
+ account.remove_list_member(222, 1234)
155
+ account.delete_list(222)
156
+ account.pin_list(222)
157
+ account.unpin_list(222)
158
+
159
+ # refresh all pinned lists in this order
160
+ account.update_pinned_lists([222, 111, 333])
161
+
162
+ # unpin all lists
163
+ account.update_pinned_lists([])
164
+
165
+ # get timelines
166
+ timeline = account.home_timeline()
167
+ latest_timeline = account.home_latest_timeline(limit=500)
168
+
169
+ # get bookmarks
170
+ bookmarks = account.bookmarks()
171
+
172
+ # get DM inbox metadata
173
+ inbox = account.dm_inbox()
174
+
175
+ # get DMs from all conversations
176
+ dms = account.dm_history()
177
+
178
+ # get DMs from specific conversations
179
+ dms = account.dm_history(['123456-789012', '345678-901234'])
180
+
181
+ # search DMs by keyword
182
+ dms = account.dm_search('test123')
183
+
184
+ # delete entire conversation
185
+ account.dm_delete(conversation_id='123456-789012')
186
+
187
+ # delete (hide) specific DM
188
+ account.dm_delete(message_id='123456')
189
+
190
+ # get all scheduled tweets
191
+ scheduled_tweets = account.scheduled_tweets()
192
+
193
+ # delete a scheduled tweet
194
+ account.delete_scheduled_tweet(12345678)
195
+
196
+ # get all draft tweets
197
+ draft_tweets = account.draft_tweets()
198
+
199
+ # delete a draft tweet
200
+ account.delete_draft_tweet(12345678)
201
+
202
+ # delete all scheduled tweets
203
+ account.clear_scheduled_tweets()
204
+
205
+ # delete all draft tweets
206
+ account.clear_draft_tweets()
207
+
208
+ # example configuration
209
+ account.update_settings({
210
+ "address_book_live_sync_enabled": False,
211
+ "allow_ads_personalization": False,
212
+ "allow_authenticated_periscope_requests": True,
213
+ "allow_dm_groups_from": "following",
214
+ "allow_dms_from": "following",
215
+ "allow_location_history_personalization": False,
216
+ "allow_logged_out_device_personalization": False,
217
+ "allow_media_tagging": "none",
218
+ "allow_sharing_data_for_third_party_personalization": False,
219
+ "alt_text_compose_enabled": None,
220
+ "always_use_https": True,
221
+ "autoplay_disabled": False,
222
+ "country_code": "us",
223
+ "discoverable_by_email": False,
224
+ "discoverable_by_mobile_phone": False,
225
+ "display_sensitive_media": False,
226
+ "dm_quality_filter": "enabled",
227
+ "dm_receipt_setting": "all_disabled",
228
+ "geo_enabled": False,
229
+ "include_alt_text_compose": True,
230
+ "include_mention_filter": True,
231
+ "include_nsfw_admin_flag": True,
232
+ "include_nsfw_user_flag": True,
233
+ "include_ranked_timeline": True,
234
+ "language": "en",
235
+ "mention_filter": "unfiltered",
236
+ "nsfw_admin": False,
237
+ "nsfw_user": False,
238
+ "personalized_trends": True,
239
+ "protected": False,
240
+ "ranked_timeline_eligible": None,
241
+ "ranked_timeline_setting": None,
242
+ "require_password_login": False,
243
+ "requires_login_verification": False,
244
+ "sleep_time": {
245
+ "enabled": False,
246
+ "end_time": None,
247
+ "start_time": None
248
+ },
249
+ "translator_type": "none",
250
+ "universal_quality_filtering_enabled": "enabled",
251
+ "use_cookie_personalization": False,
252
+ })
253
+
254
+ # example configuration
255
+ account.update_search_settings({
256
+ "optInFiltering": True, # filter nsfw content
257
+ "optInBlocking": True, # filter blocked accounts
258
+ })
259
+
260
+ notifications = account.notifications()
261
+
262
+ account.change_password('old pwd','new pwd')
263
+
264
+ ```
265
+
266
+ ### Scraping
267
+
268
+ #### Get all user/tweet data
269
+
270
+ Two special batch queries `scraper.tweets_by_ids` and `scraper.users_by_ids` should be preferred when applicable. These endpoints are more much more efficient and have higher rate limits than their unbatched counterparts. See the table below for a comparison.
271
+
272
+ | Endpoint | Batch Size | Rate Limit |
273
+ |---------------|----------------|---------------|
274
+ | tweets_by_ids | ~220 | 500 / 15 mins |
275
+ | tweets_by_id | 1 | 50 / 15 mins |
276
+ | users_by_ids | ~220 | 100 / 15 mins |
277
+ | users_by_id | 1 | 500 / 15 mins |
278
+
279
+ *As of Fall 2023 login by username/password is unstable. Using cookies is now recommended.*
280
+
281
+ ```python
282
+ from twitter.scraper import Scraper
283
+
284
+ ## sign-in with credentials
285
+ email, username, password = ..., ..., ...
286
+ scraper = Scraper(email, username, password)
287
+
288
+ ## or, resume session using cookies
289
+ # scraper = Scraper(cookies={"ct0": ..., "auth_token": ...})
290
+
291
+ ## or, resume session using cookies (JSON file)
292
+ # scraper = Scraper(cookies='twitter.cookies')
293
+
294
+ ## or, initialize guest session (limited endpoints)
295
+ # from twitter.util import init_session
296
+ # scraper = Scraper(session=init_session())
297
+
298
+ # user data
299
+ users = scraper.users(['foo', 'bar', 'hello', 'world'])
300
+ users = scraper.users_by_ids([123, 234, 345]) # preferred
301
+ users = scraper.users_by_id([123, 234, 345])
302
+ tweets = scraper.tweets([123, 234, 345])
303
+ likes = scraper.likes([123, 234, 345])
304
+ tweets_and_replies = scraper.tweets_and_replies([123, 234, 345])
305
+ media = scraper.media([123, 234, 345])
306
+ following = scraper.following([123, 234, 345])
307
+ followers = scraper.followers([123, 234, 345])
308
+ scraper.tweet_stats([111111, 222222, 333333])
309
+
310
+ # get recommended users based on user
311
+ scraper.recommended_users()
312
+ scraper.recommended_users([123])
313
+
314
+ # tweet data
315
+ tweets = scraper.tweets_by_ids([987, 876, 754]) # preferred
316
+ tweets = scraper.tweets_by_id([987, 876, 754])
317
+ tweet_details = scraper.tweets_details([987, 876, 754])
318
+ retweeters = scraper.retweeters([987, 876, 754])
319
+ favoriters = scraper.favoriters([987, 876, 754])
320
+
321
+ scraper.download_media([
322
+ 111111,
323
+ 222222,
324
+ 333333,
325
+ 444444,
326
+ ])
327
+
328
+ # trends
329
+ scraper.trends()
330
+ ```
331
+
332
+ #### Resume Pagination
333
+ **Pagination is already done by default**, however there are circumstances where you may need to resume pagination from a specific cursor. For example, the `Followers` endpoint only allows for 50 requests every 15 minutes. In this case, we can resume from where we left off by providing a specific cursor value.
334
+ ```python
335
+ from twitter.scraper import Scraper
336
+
337
+ email, username, password = ...,...,...
338
+ scraper = Scraper(email, username, password)
339
+
340
+ user_id = 44196397
341
+ cursor = '1767341853908517597|1663601806447476672' # example cursor
342
+ limit = 100 # arbitrary limit for demonstration
343
+ follower_subset, last_cursor = scraper.followers([user_id], limit=limit, cursor=cursor)
344
+
345
+ # use last_cursor to resume pagination
346
+ ```
347
+
348
+ #### Search
349
+
350
+ ```python
351
+ from twitter.search import Search
352
+
353
+ email, username, password = ..., ..., ...
354
+ # default output directory is `data/search_results` if save=True
355
+ search = Search(email, username, password, save=True, debug=1)
356
+
357
+ res = search.run(
358
+ limit=37,
359
+ retries=5,
360
+ queries=[
361
+ {
362
+ 'category': 'Top',
363
+ 'query': 'paperswithcode -tensorflow -tf'
364
+ },
365
+ {
366
+ 'category': 'Latest',
367
+ 'query': 'test'
368
+ },
369
+ {
370
+ 'category': 'People',
371
+ 'query': 'brasil portugal -argentina'
372
+ },
373
+ {
374
+ 'category': 'Photos',
375
+ 'query': 'greece'
376
+ },
377
+ {
378
+ 'category': 'Videos',
379
+ 'query': 'italy'
380
+ },
381
+ ],
382
+ )
383
+ ```
384
+
385
+ **Search Operators Reference**
386
+
387
+ https://developer.twitter.com/en/docs/twitter-api/v1/rules-and-filtering/search-operators
388
+
389
+ https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/build-a-query
390
+
391
+ ### Spaces
392
+
393
+ #### Live Audio Capture
394
+
395
+ Capture live audio for up to 500 streams per IP
396
+
397
+ ```python
398
+ from twitter.scraper import Scraper
399
+ from twitter.util import init_session
400
+
401
+ session = init_session() # initialize guest session, no login required
402
+ scraper = Scraper(session=session)
403
+
404
+ rooms = [...]
405
+ scraper.spaces_live(rooms=rooms) # capture live audio from list of rooms
406
+ ```
407
+
408
+ #### Live Transcript Capture
409
+
410
+ **Raw transcript chunks**
411
+
412
+ ```python
413
+ from twitter.scraper import Scraper
414
+ from twitter.util import init_session
415
+
416
+ session = init_session() # initialize guest session, no login required
417
+ scraper = Scraper(session=session)
418
+
419
+ # room must be live, i.e. in "Running" state
420
+ scraper.space_live_transcript('1zqKVPlQNApJB', frequency=2) # word-level live transcript. (dirty, on-the-fly transcription before post-processing)
421
+ ```
422
+
423
+ **Processed (final) transcript chunks**
424
+
425
+ ```python
426
+ from twitter.scraper import Scraper
427
+ from twitter.util import init_session
428
+
429
+ session = init_session() # initialize guest session, no login required
430
+ scraper = Scraper(session=session)
431
+
432
+ # room must be live, i.e. in "Running" state
433
+ scraper.space_live_transcript('1zqKVPlQNApJB', frequency=1) # finalized live transcript. (clean)
434
+ ```
435
+
436
+ #### Search and Metadata
437
+ ```python
438
+ from twitter.scraper import Scraper
439
+ from twitter.util import init_session
440
+ from twitter.constants import SpaceCategory
441
+
442
+ session = init_session() # initialize guest session, no login required
443
+ scraper = Scraper(session=session)
444
+
445
+ # download audio and chat-log from space
446
+ spaces = scraper.spaces(rooms=['1eaJbrAPnBVJX', '1eaJbrAlZjjJX'], audio=True, chat=True)
447
+
448
+ # pull metadata only
449
+ spaces = scraper.spaces(rooms=['1eaJbrAPnBVJX', '1eaJbrAlZjjJX'])
450
+
451
+ # search for spaces in "Upcoming", "Top" and "Live" categories
452
+ spaces = scraper.spaces(search=[
453
+ {
454
+ 'filter': SpaceCategory.Upcoming,
455
+ 'query': 'hello'
456
+ },
457
+ {
458
+ 'filter': SpaceCategory.Top,
459
+ 'query': 'world'
460
+ },
461
+ {
462
+ 'filter': SpaceCategory.Live,
463
+ 'query': 'foo bar'
464
+ }
465
+ ])
466
+ ```
467
+
468
+ ### Automated Solvers
469
+
470
+ > This requires installation of the [proton-api-client](https://pypi.org/project/proton-api-client) package
471
+
472
+ To set up automated email confirmation/verification solvers, add your Proton Mail credentials below as shown.
473
+ This removes the need to manually solve email challenges via the web app. These credentials can be used
474
+ in `Scraper`, `Account`, and `Search` constructors.
475
+
476
+ E.g.
477
+
478
+ ```python
479
+ from twitter.account import Account
480
+ from twitter.util import get_code
481
+ from proton.client import ProtonMail
482
+
483
+ proton_username, proton_password = ..., ...
484
+ proton = lambda: get_code(ProtonMail(proton_username, proton_password))
485
+
486
+ email, username, password = ..., ..., ...
487
+ account = Account(email, username, password, proton=proton)
488
+ ```
489
+