From 9238398b3cc1276844b833150019ddb98b9a52ee Mon Sep 17 00:00:00 2001 From: Vlad Pronsky Date: Fri, 7 Jul 2023 12:25:50 +0300 Subject: [PATCH] feat: update tweets parser (viewsCount, retweetedTweet, quotedTweet) #28 --- tests/mocked-data/_issue_28.2.json | 906 +++++++++++++++++++++++++++++ tests/mocked-data/_issue_28.json | 839 ++++++++++++++++++++++++++ tests/test_parser.py | 58 +- twscrape/api.py | 2 +- twscrape/models.py | 34 +- twscrape/queue_client.py | 2 +- twscrape/utils.py | 8 +- 7 files changed, 1831 insertions(+), 18 deletions(-) create mode 100644 tests/mocked-data/_issue_28.2.json create mode 100644 tests/mocked-data/_issue_28.json diff --git a/tests/mocked-data/_issue_28.2.json b/tests/mocked-data/_issue_28.2.json new file mode 100644 index 0000000..89ac9b0 --- /dev/null +++ b/tests/mocked-data/_issue_28.2.json @@ -0,0 +1,906 @@ +{ + "data": { + "threaded_conversation_with_injections_v2": { + "instructions": [ + { + "type": "TimelineAddEntries", + "entries": [ + { + "entryId": "tweet-1658421690001502208", + "sortIndex": "7564950346853273599", + "content": { + "entryType": "TimelineTimelineItem", + "__typename": "TimelineTimelineItem", + "itemContent": { + "itemType": "TimelineTweet", + "__typename": "TimelineTweet", + "tweet_results": { + "result": { + "__typename": "Tweet", + "rest_id": "1658421690001502208", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjo0MzYxNDk1OTg=", + "rest_id": "436149598", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": false, + "can_media_tag": true, + "created_at": "Tue Dec 13 21:10:47 +0000 2011", + "default_profile": false, + "default_profile_image": false, + "description": "De Salamanca,", + "entities": { + "description": { + "urls": [] + } + }, + "fast_followers_count": 0, + "favourites_count": 18457, + "followers_count": 3177, + "friends_count": 3040, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 5, + "location": "españa", + "media_count": 1613, + "name": "Moreno", + "normal_followers_count": 3177, + "pinned_tweet_ids_str": [], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/436149598/1368225000", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1664391740881510400/TMMIzBow_normal.jpg", + "profile_interstitial_type": "", + "screen_name": "morenoagosto", + "statuses_count": 24367, + "translator_type": "none", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1658421690001502208" + ], + "editable_until_msecs": "1684235317000", + "is_edit_eligible": true, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": false, + "views": { + "count": "67", + "state": "EnabledWithCount" + }, + "source": "Twitter for Android", + "quoted_status_result": { + "result": { + "__typename": "Tweet", + "rest_id": "1658407192859492355", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjoyOTQ5MDY1NzIz", + "rest_id": "2949065723", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": true, + "can_media_tag": false, + "created_at": "Mon Dec 29 11:54:26 +0000 2014", + "default_profile": true, + "default_profile_image": false, + "description": "MI LUCHA ESTÁ EN TODAS PARTES. POR LOS DERECHOS DE LOS ANIMALES. POR UN MUNDO MEJOR.ACTIVISTA", + "entities": { + "description": { + "urls": [] + } + }, + "fast_followers_count": 0, + "favourites_count": 108032, + "followers_count": 15455, + "friends_count": 13034, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 34, + "location": "En donde me den amor ❤️😊🥰", + "media_count": 12987, + "name": "SisuMisu", + "normal_followers_count": 15455, + "pinned_tweet_ids_str": [ + "1087440689368043520" + ], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/2949065723/1513372062", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1528038627317538817/wvoi8QLf_normal.jpg", + "profile_interstitial_type": "", + "screen_name": "SisuMisu3", + "statuses_count": 164349, + "translator_type": "none", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "professional": { + "rest_id": "1490288411835523072", + "professional_type": "Creator", + "category": [ + { + "id": 934, + "name": "Social Media Influencer", + "icon_name": "IconBriefcaseStroke" + } + ] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1658407192859492355" + ], + "editable_until_msecs": "1684231861000", + "is_edit_eligible": true, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": true, + "views": { + "count": "1110", + "state": "EnabledWithCount" + }, + "source": "Twitter for Android", + "legacy": { + "created_at": "Tue May 16 09:41:01 +0000 2023", + "conversation_id_str": "1658407192859492355", + "display_text_range": [ + 0, + 121 + ], + "entities": { + "media": [ + { + "display_url": "pic.twitter.com/apswi6Wwiw", + "expanded_url": "https://twitter.com/SisuMisu3/status/1658407192859492355/photo/1", + "id_str": "1658407190594453505", + "indices": [ + 122, + 145 + ], + "media_url_https": "https://pbs.twimg.com/media/FwPYjaBWAAETpRI.jpg", + "type": "photo", + "url": "https://t.co/apswi6Wwiw", + "features": { + "large": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + }, + "medium": { + "faces": [ + { + "x": 255, + "y": 237, + "h": 329, + "w": 329 + } + ] + }, + "small": { + "faces": [ + { + "x": 145, + "y": 134, + "h": 186, + "w": 186 + } + ] + }, + "orig": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + } + }, + "sizes": { + "large": { + "h": 1280, + "w": 1024, + "resize": "fit" + }, + "medium": { + "h": 1200, + "w": 960, + "resize": "fit" + }, + "small": { + "h": 680, + "w": 544, + "resize": "fit" + }, + "thumb": { + "h": 150, + "w": 150, + "resize": "crop" + } + }, + "original_info": { + "height": 1280, + "width": 1024, + "focus_rects": [ + { + "x": 0, + "y": 642, + "w": 1024, + "h": 573 + }, + { + "x": 0, + "y": 256, + "w": 1024, + "h": 1024 + }, + { + "x": 0, + "y": 113, + "w": 1024, + "h": 1167 + }, + { + "x": 32, + "y": 0, + "w": 640, + "h": 1280 + }, + { + "x": 0, + "y": 0, + "w": 1024, + "h": 1280 + } + ] + } + } + ], + "user_mentions": [ + { + "id_str": "1860300192", + "name": "Alejandra Jacinto", + "screen_name": "AleJacintoUrang", + "indices": [ + 81, + 97 + ] + } + ], + "urls": [], + "hashtags": [ + { + "indices": [ + 116, + 121 + ], + "text": "Sisu" + } + ], + "symbols": [] + }, + "extended_entities": { + "media": [ + { + "display_url": "pic.twitter.com/apswi6Wwiw", + "expanded_url": "https://twitter.com/SisuMisu3/status/1658407192859492355/photo/1", + "id_str": "1658407190594453505", + "indices": [ + 122, + 145 + ], + "media_key": "3_1658407190594453505", + "media_url_https": "https://pbs.twimg.com/media/FwPYjaBWAAETpRI.jpg", + "type": "photo", + "url": "https://t.co/apswi6Wwiw", + "ext_media_availability": { + "status": "Available" + }, + "features": { + "large": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + }, + "medium": { + "faces": [ + { + "x": 255, + "y": 237, + "h": 329, + "w": 329 + } + ] + }, + "small": { + "faces": [ + { + "x": 145, + "y": 134, + "h": 186, + "w": 186 + } + ] + }, + "orig": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + } + }, + "sizes": { + "large": { + "h": 1280, + "w": 1024, + "resize": "fit" + }, + "medium": { + "h": 1200, + "w": 960, + "resize": "fit" + }, + "small": { + "h": 680, + "w": 544, + "resize": "fit" + }, + "thumb": { + "h": 150, + "w": 150, + "resize": "crop" + } + }, + "original_info": { + "height": 1280, + "width": 1024, + "focus_rects": [ + { + "x": 0, + "y": 642, + "w": 1024, + "h": 573 + }, + { + "x": 0, + "y": 256, + "w": 1024, + "h": 1024 + }, + { + "x": 0, + "y": 113, + "w": 1024, + "h": 1167 + }, + { + "x": 32, + "y": 0, + "w": 640, + "h": 1280 + }, + { + "x": 0, + "y": 0, + "w": 1024, + "h": 1280 + } + ] + } + } + ] + }, + "favorite_count": 60, + "favorited": false, + "full_text": "Esta noche el circo está asegurado 😂😂😂\nAyuso se presenta al único debate.\nVamos \n@AleJacintoUrang \nEstamos contigo.\n#Sisu https://t.co/apswi6Wwiw", + "is_quote_status": false, + "lang": "es", + "possibly_sensitive": false, + "possibly_sensitive_editable": true, + "quote_count": 2, + "reply_count": 5, + "retweet_count": 42, + "retweeted": false, + "user_id_str": "2949065723", + "id_str": "1658407192859492355" + } + } + }, + "legacy": { + "created_at": "Tue May 16 10:38:37 +0000 2023", + "conversation_id_str": "1658421690001502208", + "display_text_range": [ + 0, + 35 + ], + "entities": { + "user_mentions": [], + "urls": [], + "hashtags": [], + "symbols": [] + }, + "favorite_count": 2, + "favorited": false, + "full_text": "Se sabe si le permiten pinganillo??", + "is_quote_status": true, + "lang": "es", + "quote_count": 1, + "quoted_status_id_str": "1658407192859492355", + "quoted_status_permalink": { + "url": "https://t.co/03JmJW6BYc", + "expanded": "https://twitter.com/SisuMisu3/status/1658407192859492355", + "display": "twitter.com/SisuMisu3/stat…" + }, + "reply_count": 0, + "retweet_count": 0, + "retweeted": false, + "user_id_str": "436149598", + "id_str": "1658421690001502208" + }, + "quick_promote_eligibility": { + "eligibility": "IneligibleNotProfessional" + } + } + }, + "tweetDisplayType": "Tweet", + "hasModeratedReplies": false + } + } + }, + { + "entryId": "tweetdetailrelatedtweets-1676889656225411072", + "sortIndex": "1", + "content": { + "entryType": "TimelineTimelineModule", + "__typename": "TimelineTimelineModule", + "items": [ + { + "entryId": "tweetdetailrelatedtweets-1676889656225411072-tweet-1676889656225411072", + "item": { + "itemContent": { + "itemType": "TimelineTweet", + "__typename": "TimelineTweet", + "tweet_results": { + "result": { + "__typename": "Tweet", + "rest_id": "1676889656225411072", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjo0MzYxNDk1OTg=", + "rest_id": "436149598", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": false, + "can_media_tag": true, + "created_at": "Tue Dec 13 21:10:47 +0000 2011", + "default_profile": false, + "default_profile_image": false, + "description": "De Salamanca,", + "entities": { + "description": { + "urls": [] + } + }, + "fast_followers_count": 0, + "favourites_count": 18457, + "followers_count": 3177, + "friends_count": 3040, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 5, + "location": "españa", + "media_count": 1613, + "name": "Moreno", + "normal_followers_count": 3177, + "pinned_tweet_ids_str": [], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/436149598/1368225000", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1664391740881510400/TMMIzBow_normal.jpg", + "profile_interstitial_type": "", + "screen_name": "morenoagosto", + "statuses_count": 24367, + "translator_type": "none", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1676889656225411072" + ], + "editable_until_msecs": "1688640223000", + "is_edit_eligible": true, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": true, + "views": { + "count": "40", + "state": "EnabledWithCount" + }, + "source": "Twitter for Android", + "quoted_status_result": { + "result": { + "__typename": "Tweet", + "rest_id": "1676697529461743622", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjozMjg0OTg3MTA=", + "rest_id": "328498710", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": false, + "can_media_tag": true, + "created_at": "Sun Jul 03 13:57:45 +0000 2011", + "default_profile": true, + "default_profile_image": false, + "description": "** Erik Urano ** // *** Flat Erik *** / Astronauta a tiempo parcial // Extraterrestre a tiempo completo// Neovalladolor // MNGMNT & BKNG luis@sonidomuchacho.com", + "entities": { + "description": { + "urls": [] + }, + "url": { + "urls": [ + { + "display_url": "linktr.ee/erikurano", + "expanded_url": "https://linktr.ee/erikurano", + "url": "https://t.co/qrNiieuzZw", + "indices": [ + 0, + 23 + ] + } + ] + } + }, + "fast_followers_count": 0, + "favourites_count": 35402, + "followers_count": 15175, + "friends_count": 1602, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 59, + "location": "", + "media_count": 1344, + "name": "DON DATA®️", + "normal_followers_count": 15175, + "pinned_tweet_ids_str": [ + "1674336677597159424" + ], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/328498710/1687971178", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1674098569953902592/5tisrAy__normal.jpg", + "profile_interstitial_type": "", + "screen_name": "Erik_Urano", + "statuses_count": 12901, + "translator_type": "none", + "url": "https://t.co/qrNiieuzZw", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "professional": { + "rest_id": "1554576748209229824", + "professional_type": "Creator", + "category": [ + { + "id": 1006, + "name": "Musical Artist", + "icon_name": "IconBriefcaseStroke" + } + ] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1676697529461743622" + ], + "editable_until_msecs": "1688594417000", + "is_edit_eligible": true, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": false, + "views": { + "count": "973266", + "state": "EnabledWithCount" + }, + "source": "Twitter for iPhone", + "legacy": { + "created_at": "Wed Jul 05 21:00:17 +0000 2023", + "conversation_id_str": "1676697529461743622", + "display_text_range": [ + 0, + 71 + ], + "entities": { + "media": [ + { + "display_url": "pic.twitter.com/chX4MREd5H", + "expanded_url": "https://twitter.com/Erik_Urano/status/1676697529461743622/video/1", + "id_str": "1676697491226468353", + "indices": [ + 72, + 95 + ], + "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/1676697491226468353/pu/img/7VGLjWMYpFsXWvq1.jpg", + "type": "photo", + "url": "https://t.co/chX4MREd5H", + "features": {}, + "sizes": { + "large": { + "h": 716, + "w": 1280, + "resize": "fit" + }, + "medium": { + "h": 671, + "w": 1200, + "resize": "fit" + }, + "small": { + "h": 380, + "w": 680, + "resize": "fit" + }, + "thumb": { + "h": 150, + "w": 150, + "resize": "crop" + } + }, + "original_info": { + "height": 716, + "width": 1280 + } + } + ], + "user_mentions": [], + "urls": [], + "hashtags": [], + "symbols": [] + }, + "extended_entities": { + "media": [ + { + "display_url": "pic.twitter.com/chX4MREd5H", + "expanded_url": "https://twitter.com/Erik_Urano/status/1676697529461743622/video/1", + "id_str": "1676697491226468353", + "indices": [ + 72, + 95 + ], + "media_key": "7_1676697491226468353", + "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/1676697491226468353/pu/img/7VGLjWMYpFsXWvq1.jpg", + "type": "video", + "url": "https://t.co/chX4MREd5H", + "additional_media_info": { + "monetizable": false + }, + "mediaStats": { + "viewCount": 622775 + }, + "ext_media_availability": { + "status": "Available" + }, + "features": {}, + "sizes": { + "large": { + "h": 716, + "w": 1280, + "resize": "fit" + }, + "medium": { + "h": 671, + "w": 1200, + "resize": "fit" + }, + "small": { + "h": 380, + "w": 680, + "resize": "fit" + }, + "thumb": { + "h": 150, + "w": 150, + "resize": "crop" + } + }, + "original_info": { + "height": 716, + "width": 1280 + }, + "video_info": { + "aspect_ratio": [ + 320, + 179 + ], + "duration_millis": 8536, + "variants": [ + { + "content_type": "application/x-mpegURL", + "url": "https://video.twimg.com/ext_tw_video/1676697491226468353/pu/pl/hmKfq_gU8h5Pj_bi.m3u8?tag=12&container=fmp4" + }, + { + "bitrate": 256000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/1676697491226468353/pu/vid/482x270/CxOsBliV2KQSDySg.mp4?tag=12" + }, + { + "bitrate": 2176000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/1676697491226468353/pu/vid/1280x716/ir6MOu-zeqHCOMDZ.mp4?tag=12" + }, + { + "bitrate": 832000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/1676697491226468353/pu/vid/642x360/vyOPi9cH5pjAcgSS.mp4?tag=12" + } + ] + } + } + ] + }, + "favorite_count": 9694, + "favorited": false, + "full_text": "Frase a empezar a utilizar cada vez que me pregunten “¿Que tal estás?”: https://t.co/chX4MREd5H", + "is_quote_status": false, + "lang": "es", + "possibly_sensitive": false, + "possibly_sensitive_editable": true, + "quote_count": 401, + "reply_count": 123, + "retweet_count": 2289, + "retweeted": false, + "user_id_str": "328498710", + "id_str": "1676697529461743622" + } + } + }, + "legacy": { + "created_at": "Thu Jul 06 09:43:43 +0000 2023", + "conversation_id_str": "1676889656225411072", + "display_text_range": [ + 0, + 66 + ], + "entities": { + "user_mentions": [], + "urls": [], + "hashtags": [], + "symbols": [] + }, + "favorite_count": 0, + "favorited": false, + "full_text": "No, solo quiere decir que aún no ha empezado a darle al \" morapio\"", + "is_quote_status": true, + "lang": "es", + "quote_count": 0, + "quoted_status_id_str": "1676697529461743622", + "quoted_status_permalink": { + "url": "https://t.co/SIZUcNQRrS", + "expanded": "https://twitter.com/Erik_Urano/status/1676697529461743622", + "display": "twitter.com/Erik_Urano/sta…" + }, + "reply_count": 0, + "retweet_count": 0, + "retweeted": false, + "user_id_str": "436149598", + "id_str": "1676889656225411072" + }, + "quick_promote_eligibility": { + "eligibility": "IneligibleNotProfessional" + } + } + }, + "tweetDisplayType": "Tweet" + }, + "clientEventInfo": { + "component": "related_tweet", + "details": { + "conversationDetails": { + "conversationSection": "RelatedTweet" + } + } + } + } + } + ], + "displayType": "Vertical", + "header": { + "displayType": "Classic", + "text": "Discover more", + "socialContext": { + "type": "TimelineGeneralContext", + "contextType": "TextOnly", + "text": "Sourced from across Twitter" + }, + "sticky": true + }, + "clientEventInfo": { + "component": "related_tweet", + "details": { + "conversationDetails": { + "conversationSection": "RelatedTweet" + } + } + } + } + } + ] + }, + { + "type": "TimelineTerminateTimeline", + "direction": "Top" + } + ] + } + } +} diff --git a/tests/mocked-data/_issue_28.json b/tests/mocked-data/_issue_28.json new file mode 100644 index 0000000..a14b540 --- /dev/null +++ b/tests/mocked-data/_issue_28.json @@ -0,0 +1,839 @@ +{ + "data": { + "threaded_conversation_with_injections_v2": { + "instructions": [ + { + "type": "TimelineAddEntries", + "entries": [ + { + "entryId": "tweet-1658409412799737856", + "sortIndex": "7564962624055037951", + "content": { + "entryType": "TimelineTimelineItem", + "__typename": "TimelineTimelineItem", + "itemContent": { + "itemType": "TimelineTweet", + "__typename": "TimelineTweet", + "tweet_results": { + "result": { + "__typename": "Tweet", + "rest_id": "1658409412799737856", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjoxMjk2ODAxOTcwNzc1MzY3Njgw", + "rest_id": "1296801970775367680", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": true, + "can_media_tag": true, + "created_at": "Fri Aug 21 13:31:26 +0000 2020", + "default_profile": true, + "default_profile_image": false, + "description": "Por una unidad de izquierdas real .Bienvenidos a la #RedRoja. Sigue a quien te siga . Red de seguridad: @RedRoj4 @REDROJA@mastodon.world", + "entities": { + "description": { + "urls": [] + }, + "url": { + "urls": [ + { + "display_url": "instagram.com/redroja4?igshi…", + "expanded_url": "https://instagram.com/redroja4?igshid=1d8cfo0z5wf2t", + "url": "https://t.co/yLqbCaFwck", + "indices": [ + 0, + 23 + ] + } + ] + } + }, + "fast_followers_count": 0, + "favourites_count": 60839, + "followers_count": 43398, + "friends_count": 31537, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 100, + "location": "Izquierda ", + "media_count": 1469, + "name": "RED ROJA 🔻", + "normal_followers_count": 43398, + "pinned_tweet_ids_str": [ + "1592141363050860546" + ], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/1296801970775367680/1618465956", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1541282511816376322/4UQWhvwj_normal.jpg", + "profile_interstitial_type": "", + "screen_name": "REDROJA4", + "statuses_count": 30422, + "translator_type": "none", + "url": "https://t.co/yLqbCaFwck", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1658409412799737856" + ], + "editable_until_msecs": "1684232390425", + "is_edit_eligible": false, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": true, + "views": { + "state": "Enabled" + }, + "source": "Twitter for iPhone", + "legacy": { + "created_at": "Tue May 16 09:49:50 +0000 2023", + "conversation_id_str": "1658409412799737856", + "display_text_range": [ + 0, + 140 + ], + "entities": { + "user_mentions": [ + { + "id_str": "2949065723", + "name": "SisuMisu", + "screen_name": "SisuMisu3", + "indices": [ + 3, + 13 + ] + }, + { + "id_str": "1860300192", + "name": "Alejandra Jacinto", + "screen_name": "AleJacintoUrang", + "indices": [ + 96, + 112 + ] + } + ], + "urls": [], + "hashtags": [ + { + "indices": [ + 131, + 136 + ], + "text": "Sisu" + } + ], + "symbols": [] + }, + "favorite_count": 0, + "favorited": false, + "full_text": "RT @SisuMisu3: Esta noche el circo está asegurado 😂😂😂\nAyuso se presenta al único debate.\nVamos \n@AleJacintoUrang \nEstamos contigo.\n#Sisu ht…", + "is_quote_status": false, + "lang": "es", + "quote_count": 0, + "reply_count": 0, + "retweet_count": 42, + "retweeted": false, + "user_id_str": "1296801970775367680", + "id_str": "1658409412799737856", + "retweeted_status_result": { + "result": { + "__typename": "Tweet", + "rest_id": "1658407192859492355", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjoyOTQ5MDY1NzIz", + "rest_id": "2949065723", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": true, + "can_media_tag": false, + "created_at": "Mon Dec 29 11:54:26 +0000 2014", + "default_profile": true, + "default_profile_image": false, + "description": "MI LUCHA ESTÁ EN TODAS PARTES. POR LOS DERECHOS DE LOS ANIMALES. POR UN MUNDO MEJOR.ACTIVISTA", + "entities": { + "description": { + "urls": [] + } + }, + "fast_followers_count": 0, + "favourites_count": 108021, + "followers_count": 15454, + "friends_count": 13032, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 34, + "location": "En donde me den amor ❤️😊🥰", + "media_count": 12985, + "name": "SisuMisu", + "normal_followers_count": 15454, + "pinned_tweet_ids_str": [ + "1087440689368043520" + ], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/2949065723/1513372062", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1528038627317538817/wvoi8QLf_normal.jpg", + "profile_interstitial_type": "", + "screen_name": "SisuMisu3", + "statuses_count": 164333, + "translator_type": "none", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "professional": { + "rest_id": "1490288411835523072", + "professional_type": "Creator", + "category": [ + { + "id": 934, + "name": "Social Media Influencer", + "icon_name": "IconBriefcaseStroke" + } + ] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1658407192859492355" + ], + "editable_until_msecs": "1684231861000", + "is_edit_eligible": true, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": true, + "views": { + "count": "1110", + "state": "EnabledWithCount" + }, + "source": "Twitter for Android", + "legacy": { + "created_at": "Tue May 16 09:41:01 +0000 2023", + "conversation_id_str": "1658407192859492355", + "display_text_range": [ + 0, + 121 + ], + "entities": { + "media": [ + { + "display_url": "pic.twitter.com/apswi6Wwiw", + "expanded_url": "https://twitter.com/SisuMisu3/status/1658407192859492355/photo/1", + "id_str": "1658407190594453505", + "indices": [ + 122, + 145 + ], + "media_url_https": "https://pbs.twimg.com/media/FwPYjaBWAAETpRI.jpg", + "type": "photo", + "url": "https://t.co/apswi6Wwiw", + "features": { + "large": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + }, + "medium": { + "faces": [ + { + "x": 255, + "y": 237, + "h": 329, + "w": 329 + } + ] + }, + "small": { + "faces": [ + { + "x": 145, + "y": 134, + "h": 186, + "w": 186 + } + ] + }, + "orig": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + } + }, + "sizes": { + "large": { + "h": 1280, + "w": 1024, + "resize": "fit" + }, + "medium": { + "h": 1200, + "w": 960, + "resize": "fit" + }, + "small": { + "h": 680, + "w": 544, + "resize": "fit" + }, + "thumb": { + "h": 150, + "w": 150, + "resize": "crop" + } + }, + "original_info": { + "height": 1280, + "width": 1024, + "focus_rects": [ + { + "x": 0, + "y": 642, + "w": 1024, + "h": 573 + }, + { + "x": 0, + "y": 256, + "w": 1024, + "h": 1024 + }, + { + "x": 0, + "y": 113, + "w": 1024, + "h": 1167 + }, + { + "x": 32, + "y": 0, + "w": 640, + "h": 1280 + }, + { + "x": 0, + "y": 0, + "w": 1024, + "h": 1280 + } + ] + } + } + ], + "user_mentions": [ + { + "id_str": "1860300192", + "name": "Alejandra Jacinto", + "screen_name": "AleJacintoUrang", + "indices": [ + 81, + 97 + ] + } + ], + "urls": [], + "hashtags": [ + { + "indices": [ + 116, + 121 + ], + "text": "Sisu" + } + ], + "symbols": [] + }, + "extended_entities": { + "media": [ + { + "display_url": "pic.twitter.com/apswi6Wwiw", + "expanded_url": "https://twitter.com/SisuMisu3/status/1658407192859492355/photo/1", + "id_str": "1658407190594453505", + "indices": [ + 122, + 145 + ], + "media_key": "3_1658407190594453505", + "media_url_https": "https://pbs.twimg.com/media/FwPYjaBWAAETpRI.jpg", + "type": "photo", + "url": "https://t.co/apswi6Wwiw", + "ext_media_availability": { + "status": "Available" + }, + "features": { + "large": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + }, + "medium": { + "faces": [ + { + "x": 255, + "y": 237, + "h": 329, + "w": 329 + } + ] + }, + "small": { + "faces": [ + { + "x": 145, + "y": 134, + "h": 186, + "w": 186 + } + ] + }, + "orig": { + "faces": [ + { + "x": 273, + "y": 253, + "h": 351, + "w": 351 + } + ] + } + }, + "sizes": { + "large": { + "h": 1280, + "w": 1024, + "resize": "fit" + }, + "medium": { + "h": 1200, + "w": 960, + "resize": "fit" + }, + "small": { + "h": 680, + "w": 544, + "resize": "fit" + }, + "thumb": { + "h": 150, + "w": 150, + "resize": "crop" + } + }, + "original_info": { + "height": 1280, + "width": 1024, + "focus_rects": [ + { + "x": 0, + "y": 642, + "w": 1024, + "h": 573 + }, + { + "x": 0, + "y": 256, + "w": 1024, + "h": 1024 + }, + { + "x": 0, + "y": 113, + "w": 1024, + "h": 1167 + }, + { + "x": 32, + "y": 0, + "w": 640, + "h": 1280 + }, + { + "x": 0, + "y": 0, + "w": 1024, + "h": 1280 + } + ] + } + } + ] + }, + "favorite_count": 60, + "favorited": false, + "full_text": "Esta noche el circo está asegurado 😂😂😂\nAyuso se presenta al único debate.\nVamos \n@AleJacintoUrang \nEstamos contigo.\n#Sisu https://t.co/apswi6Wwiw", + "is_quote_status": false, + "lang": "es", + "possibly_sensitive": false, + "possibly_sensitive_editable": true, + "quote_count": 2, + "reply_count": 5, + "retweet_count": 42, + "retweeted": false, + "user_id_str": "2949065723", + "id_str": "1658407192859492355" + } + } + } + }, + "quick_promote_eligibility": { + "eligibility": "IneligibleNotProfessional" + } + } + }, + "tweetDisplayType": "Tweet", + "hasModeratedReplies": false + } + } + }, + { + "entryId": "tweetdetailrelatedtweets-1677037919880507392", + "sortIndex": "1", + "content": { + "entryType": "TimelineTimelineModule", + "__typename": "TimelineTimelineModule", + "items": [ + { + "entryId": "tweetdetailrelatedtweets-1677037919880507392-tweet-1677037919880507392", + "item": { + "itemContent": { + "itemType": "TimelineTweet", + "__typename": "TimelineTweet", + "tweet_results": { + "result": { + "__typename": "Tweet", + "rest_id": "1677037919880507392", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjoyOTQ5MDY1NzIz", + "rest_id": "2949065723", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": true, + "can_media_tag": false, + "created_at": "Mon Dec 29 11:54:26 +0000 2014", + "default_profile": true, + "default_profile_image": false, + "description": "MI LUCHA ESTÁ EN TODAS PARTES. POR LOS DERECHOS DE LOS ANIMALES. POR UN MUNDO MEJOR.ACTIVISTA", + "entities": { + "description": { + "urls": [] + } + }, + "fast_followers_count": 0, + "favourites_count": 108021, + "followers_count": 15454, + "friends_count": 13032, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 34, + "location": "En donde me den amor ❤️😊🥰", + "media_count": 12985, + "name": "SisuMisu", + "normal_followers_count": 15454, + "pinned_tweet_ids_str": [ + "1087440689368043520" + ], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/2949065723/1513372062", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/1528038627317538817/wvoi8QLf_normal.jpg", + "profile_interstitial_type": "", + "screen_name": "SisuMisu3", + "statuses_count": 164333, + "translator_type": "none", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "professional": { + "rest_id": "1490288411835523072", + "professional_type": "Creator", + "category": [ + { + "id": 934, + "name": "Social Media Influencer", + "icon_name": "IconBriefcaseStroke" + } + ] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1677037919880507392" + ], + "editable_until_msecs": "1688675572000", + "is_edit_eligible": true, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": true, + "views": { + "count": "75", + "state": "EnabledWithCount" + }, + "source": "Twitter for Android", + "quoted_status_result": { + "result": { + "__typename": "Tweet", + "rest_id": "1677035434214412289", + "has_birdwatch_notes": false, + "core": { + "user_results": { + "result": { + "__typename": "User", + "id": "VXNlcjoxMzA1NDYwNDg5", + "rest_id": "1305460489", + "affiliates_highlighted_label": {}, + "has_graduated_access": true, + "is_blue_verified": false, + "legacy": { + "can_dm": false, + "can_media_tag": false, + "created_at": "Tue Mar 26 19:56:20 +0000 2013", + "default_profile": false, + "default_profile_image": false, + "description": "Apartidario\n\"No puedo decir que no estoy en desacuerdo contigo\"\nMi humilde opinión . . .", + "entities": { + "description": { + "urls": [] + } + }, + "fast_followers_count": 0, + "favourites_count": 24976, + "followers_count": 3394, + "friends_count": 3386, + "has_custom_timelines": true, + "is_translator": false, + "listed_count": 35, + "location": "Madrid, Comunidad de Madrid", + "media_count": 5191, + "name": "P.S.V.", + "normal_followers_count": 3394, + "pinned_tweet_ids_str": [ + "1075291506767003648" + ], + "possibly_sensitive": false, + "profile_banner_url": "https://pbs.twimg.com/profile_banners/1305460489/1437468007", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/869271246885793794/6YZVDC_r_normal.jpg", + "profile_interstitial_type": "", + "screen_name": "PShermanV", + "statuses_count": 175081, + "translator_type": "none", + "verified": false, + "want_retweets": false, + "withheld_in_countries": [] + }, + "professional": { + "rest_id": "1580505200997138433", + "professional_type": "Business", + "category": [ + { + "id": 15, + "name": "Entertainment & Recreation", + "icon_name": "IconBriefcaseStroke" + } + ] + }, + "business_account": {} + } + } + }, + "edit_control": { + "edit_tweet_ids": [ + "1677035434214412289" + ], + "editable_until_msecs": "1688674979000", + "is_edit_eligible": false, + "edits_remaining": "5" + }, + "edit_perspective": { + "favorited": false, + "retweeted": false + }, + "is_translatable": false, + "views": { + "count": "83", + "state": "EnabledWithCount" + }, + "source": "Twitter Web App", + "legacy": { + "created_at": "Thu Jul 06 19:22:59 +0000 2023", + "conversation_id_str": "1676995076461821958", + "display_text_range": [ + 28, + 99 + ], + "entities": { + "user_mentions": [ + { + "id_str": "2949065723", + "name": "SisuMisu", + "screen_name": "SisuMisu3", + "indices": [ + 0, + 10 + ] + }, + { + "id_str": "888853157912227840", + "name": "Rosa Isabel", + "screen_name": "RosaIsa38309247", + "indices": [ + 11, + 27 + ] + } + ], + "urls": [], + "hashtags": [], + "symbols": [] + }, + "favorite_count": 1, + "favorited": false, + "full_text": "@SisuMisu3 @RosaIsa38309247 O ESPABILAMOS O A LIMPIAR LAS ESCOPETAS\nANTE AQUELLO DE LOS 26 MILLONES", + "in_reply_to_screen_name": "SisuMisu3", + "in_reply_to_status_id_str": "1676995076461821958", + "in_reply_to_user_id_str": "2949065723", + "is_quote_status": false, + "lang": "es", + "quote_count": 1, + "reply_count": 0, + "retweet_count": 0, + "retweeted": false, + "user_id_str": "1305460489", + "id_str": "1677035434214412289" + } + } + }, + "legacy": { + "created_at": "Thu Jul 06 19:32:52 +0000 2023", + "conversation_id_str": "1677037919880507392", + "display_text_range": [ + 0, + 90 + ], + "entities": { + "user_mentions": [], + "urls": [], + "hashtags": [], + "symbols": [] + }, + "favorite_count": 2, + "favorited": false, + "full_text": "Tengo fé en que todo saldrá bien.\nNi como esperamos pero nos librará de este sin sentido .", + "is_quote_status": true, + "lang": "es", + "quote_count": 0, + "quoted_status_id_str": "1677035434214412289", + "quoted_status_permalink": { + "url": "https://t.co/X6fr18ZfNN", + "expanded": "https://twitter.com/PShermanV/status/1677035434214412289", + "display": "twitter.com/PShermanV/stat…" + }, + "reply_count": 0, + "retweet_count": 0, + "retweeted": false, + "user_id_str": "2949065723", + "id_str": "1677037919880507392" + }, + "quick_promote_eligibility": { + "eligibility": "IneligibleNotProfessional" + } + } + }, + "tweetDisplayType": "Tweet" + }, + "clientEventInfo": { + "component": "related_tweet", + "details": { + "conversationDetails": { + "conversationSection": "RelatedTweet" + } + } + } + } + } + ], + "displayType": "Vertical", + "header": { + "displayType": "Classic", + "text": "Discover more", + "socialContext": { + "type": "TimelineGeneralContext", + "contextType": "TextOnly", + "text": "Sourced from across Twitter" + }, + "sticky": true + }, + "clientEventInfo": { + "component": "related_tweet", + "details": { + "conversationDetails": { + "conversationSection": "RelatedTweet" + } + } + } + } + } + ] + }, + { + "type": "TimelineTerminateTimeline", + "direction": "Top" + } + ] + } + } +} \ No newline at end of file diff --git a/tests/test_parser.py b/tests/test_parser.py index 8a8f5af..05e4119 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -53,7 +53,8 @@ def mock_gen(obj, fn: str): setattr(obj, fn, cb_gen) -def check_tweet(doc: Tweet): +def check_tweet(doc: Tweet | None): + assert doc is not None assert doc.id is not None assert doc.id_str is not None assert isinstance(doc.id, int) @@ -122,7 +123,7 @@ def check_user(doc: User): async def test_search(): - api = API(AccountsPool()) + api = API() mock_gen(api, "search_raw") items = await gather(api.search("elon musk lang:en", limit=20)) @@ -133,7 +134,7 @@ async def test_search(): async def test_user_by_id(): - api = API(AccountsPool()) + api = API() mock_rep(api, "user_by_id_raw") doc = await api.user_by_id(2244994945) @@ -150,7 +151,7 @@ async def test_user_by_id(): async def test_user_by_login(): - api = API(AccountsPool()) + api = API() mock_rep(api, "user_by_login_raw") doc = await api.user_by_login("twitterdev") @@ -167,17 +168,19 @@ async def test_user_by_login(): async def test_tweet_details(): - api = API(AccountsPool()) + api = API() mock_rep(api, "tweet_details_raw") doc = await api.tweet_details(1649191520250245121) + assert doc is not None check_tweet(doc) + assert doc.id == 1649191520250245121 assert doc.user is not None async def test_followers(): - api = API(AccountsPool()) + api = API() mock_gen(api, "followers_raw") users = await gather(api.followers(2244994945)) @@ -188,7 +191,7 @@ async def test_followers(): async def test_following(): - api = API(AccountsPool()) + api = API() mock_gen(api, "following_raw") users = await gather(api.following(2244994945)) @@ -199,7 +202,7 @@ async def test_following(): async def test_retweters(): - api = API(AccountsPool()) + api = API() mock_gen(api, "retweeters_raw") users = await gather(api.retweeters(1649191520250245121)) @@ -210,7 +213,7 @@ async def test_retweters(): async def test_favoriters(): - api = API(AccountsPool()) + api = API() mock_gen(api, "favoriters_raw") users = await gather(api.favoriters(1649191520250245121)) @@ -221,7 +224,7 @@ async def test_favoriters(): async def test_user_tweets(): - api = API(AccountsPool()) + api = API() mock_gen(api, "user_tweets_raw") tweets = await gather(api.user_tweets(2244994945)) @@ -232,7 +235,7 @@ async def test_user_tweets(): async def test_user_tweets_and_replies(): - api = API(AccountsPool()) + api = API() mock_gen(api, "user_tweets_and_replies_raw") tweets = await gather(api.user_tweets_and_replies(2244994945)) @@ -243,7 +246,7 @@ async def test_user_tweets_and_replies(): async def test_tweet_with_video(): - api = API(AccountsPool()) + api = API() files = [ ("manual_tweet_with_video_1.json", 1671508600538161153), @@ -253,9 +256,40 @@ async def test_tweet_with_video(): for file, twid in files: mock_rep(api, "tweet_details_raw", os.path.join(DATA_DIR, file)) doc = await api.tweet_details(twid) + assert doc is not None check_tweet(doc) +async def test_issue_28(): + api = API() + mock_rep(api, "issue_28") + + doc = await api.tweet_details(1658409412799737856) + assert doc is not None + check_tweet(doc) + + assert doc.id == 1658409412799737856 + assert doc.user is not None + + assert doc.retweetedTweet is not None + assert doc.retweetedTweet.viewCount is not None + assert doc.viewCount is not None # views should come from retweetedTweet + assert doc.viewCount == doc.retweetedTweet.viewCount + check_tweet(doc.retweetedTweet) + + mock_rep(api, "issue_28.2") + doc = await api.tweet_details(1658421690001502208) + assert doc is not None + check_tweet(doc) + assert doc.id == 1658421690001502208 + assert doc.viewCount is not None + + assert doc.quotedTweet is not None + assert doc.quotedTweet.id != doc.id + check_tweet(doc.quotedTweet) + assert doc.quotedTweet.viewCount is not None + + async def main(): # prepare mock files from real twitter replies # you need to have some account to perform this diff --git a/twscrape/api.py b/twscrape/api.py index d237021..7989495 100644 --- a/twscrape/api.py +++ b/twscrape/api.py @@ -18,7 +18,7 @@ SEARCH_FEATURES = { class API: - def __init__(self, pool: AccountsPool | None, debug=False): + def __init__(self, pool: AccountsPool | None = None, debug=False): self.pool = pool if pool is not None else AccountsPool() self.debug = debug diff --git a/twscrape/models.py b/twscrape/models.py index 3122b75..4b6183a 100644 --- a/twscrape/models.py +++ b/twscrape/models.py @@ -178,8 +178,19 @@ class Tweet(JSONTrait): @staticmethod def parse(obj: dict, res: dict): tw_usr = User.parse(res["users"][obj["user_id_str"]]) - rt_obj = get_or(res, f"tweets.{obj.get('retweeted_status_id_str')}") - qt_obj = get_or(res, f"tweets.{obj.get('quoted_status_id_str')}") + + rt_id = _first(obj, ["retweeted_status_id_str", "retweeted_status_result.result.rest_id"]) + rt_obj = get_or(res, f"tweets.{rt_id}") + + qt_id = _first(obj, ["quoted_status_id_str", "quoted_status_result.result.rest_id"]) + qt_obj = get_or(res, f"tweets.{qt_id}") + + # for development + # print() + # print("-" * 80) + # print(res["tweets"].keys()) + # print(rt_id, rt_obj is not None) + # print(qt_id, qt_obj is not None) return Tweet( id=int(obj["id_str"]), @@ -198,7 +209,7 @@ class Tweet(JSONTrait): cashtags=[x["text"] for x in get_or(obj, "entities.symbols", [])], mentionedUsers=[UserRef.parse(x) for x in get_or(obj, "entities.user_mentions", [])], links=_parse_links(obj, ["entities.urls"]), - viewCount=int_or_none(obj, "ext_views.count"), + viewCount=_get_views(obj, rt_obj or {}), retweetedTweet=Tweet.parse(rt_obj, res) if rt_obj else None, quotedTweet=Tweet.parse(qt_obj, res) if qt_obj else None, place=Place.parse(obj["place"]) if obj.get("place") else None, @@ -345,3 +356,20 @@ def _parse_links(obj: dict, paths: list[str]): links = [TextLink.parse(x) for x in links] links = [x for x in links if x is not None] return links + + +def _first(obj: dict, paths: list[str]): + for x in paths: + cid = get_or(obj, x, None) + if cid is not None: + return cid + return None + + +def _get_views(obj: dict, rt_obj: dict): + for x in [obj, rt_obj]: + for y in ["ext_views.count", "views.count"]: + k = int_or_none(x, y) + if k is not None: + return k + return None diff --git a/twscrape/queue_client.py b/twscrape/queue_client.py index 6aaf07b..70aec63 100644 --- a/twscrape/queue_client.py +++ b/twscrape/queue_client.py @@ -114,7 +114,7 @@ class QueueClient: logger.debug(f"Rate limit for {log_id}") elif rep.status_code == 400: - # twitter can return different types of cursors that not transfers between accounts + # api can return different types of cursors that not transfers between accounts # just take the next account, the current cursor can work in it logger.debug(f"Cursor not valid for {log_id}") diff --git a/twscrape/utils.py b/twscrape/utils.py index 4ea72ff..405ca21 100644 --- a/twscrape/utils.py +++ b/twscrape/utils.py @@ -121,7 +121,13 @@ def get_typed_object(obj: dict, res: defaultdict[str, list]): def to_old_obj(obj: dict): - return {**obj, **obj["legacy"], "id_str": str(obj["rest_id"]), "id": int(obj["rest_id"])} + return { + **obj, + **obj["legacy"], + "id_str": str(obj["rest_id"]), + "id": int(obj["rest_id"]), + "legacy": None, + } def to_old_rep(obj: dict):