From b4985e04acb87b791e4d0d3f45bd57e5410e7d48 Mon Sep 17 00:00:00 2001 From: Harmon Date: Mon, 20 Sep 2021 17:45:53 -0500 Subject: [PATCH] Add Client.unretweet, Client.get_retweeters, and Client.retweet --- cassettes/test_get_retweeters.yaml | 85 +++++++++++++++ cassettes/test_retweet_and_unretweet.yaml | 125 ++++++++++++++++++++++ docs/client.rst | 9 ++ tests/test_client.py | 11 ++ tweepy/client.py | 89 +++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 cassettes/test_get_retweeters.yaml create mode 100644 cassettes/test_retweet_and_unretweet.yaml diff --git a/cassettes/test_get_retweeters.yaml b/cassettes/test_get_retweeters.yaml new file mode 100644 index 0000000..5098367 --- /dev/null +++ b/cassettes/test_get_retweeters.yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Python/3.9.6 Requests/2.25.1 Tweepy/4.0.0-alpha + method: GET + uri: https://api.twitter.com/2/tweets/1415348607813832708/retweeted_by + response: + body: + string: !!binary | + H4sIAAAAAAAAAIxWTW8cRRD9K5PlmkP39Hdu6107IbZDsJ1ICNCqvdPemXhmejMfJpvIEo4iRYAi + LkQCLghxAgmJ5LS5cOGPrPABOPEXqFnHmZoJSBxs9VZNv+6ufu9VPxpEtrKDax8+GiTR4NqAcqaZ + ZMIoHmqhQirV4Oogt5mDZGKzsi4WFiJ16YpuVFESEqHZ4PTqJRbVQhtKEMLYHdoyLpwLbtsq6eJE + k3kTezNdh4yxUITt7Fvex8Etb+PuxCp2OQRzyKLFeWi4CYWQioSKMEnRNt6vfZXkkSu6QPffhFsY + xhQRlLKQUSa1JFS2MMMoqRY22LOf2KqLNCovchep9khUUGm4lNQY2JPAh9vxJy7YT7J5mkxhahev + XMePFjxzNMQlhkNqRTTX1ChljGFoe/txcs9WcTBMe5V+nRh2qiWkZISEWioOhaOixbld2KOjOg32 + Y1tkvcuP4TpdPvXdovGQG0WZElqHkilG0Tn9cR3ZSeROukBtGMOEXHMpQqaJ4IIw0sLM/fTYVSeu + KF0XaIIzqFCcCsOIUMxwxjVhvMXaBt71UNYhqkk5LZI53MYbICi4klRKxdDV1VNbduenTcjP51Ms + B2FkqJg07czzs8fnZ1+fnz0/P3vSBbgxm8S2qjplBUFxqYE/ijHClaZoD6vlN6vly9Xy6Wr53erV + Z8341ePV8ovV8isYf1SPNduE/2O9gcfdNR8sJqV7sCh96bECFPBfGxMaIrkAsqH9v7Na/rBavlgt + v12v/HK9i59Xy+9Xy+er5S/rn0/gm+46N21ZPnTHFi1CQgnCgAq12De9C/aSae9i7vnSzeO0aBLt + pQgZGlCW0Ij9NrVxsD7sqDnslkBj3Y43xyhuUHz4P+Kqu7ls0QySsmzWxuTTjFEoH6GUU26YQird + rWObZTYKtmxyvwu324T2kUxDqbTSzGikpz3v54WrXDoFnB4Jk2M3OXaLQ2+LqEUBW5Sk0ZbC1cpj + H4ziOp8Ffz379Y+nn//54qeeBzWfTJsvWigmgJLKaIQ0r/Oqb6yuTIDLvsgdUhIjWkHTkLSdu2Xj + JAtuAD1s/pbPQOyoyaO6Ki6bDsWRWWVJ8bDnUVkdR+tw9hDrGIpJwDlRc7LgwCNbFD5N3+owESSn + r3NIG41BQaMSinJQNtgx6g6pzYMd1+MvMCM/dHnqEH059FqwJs6Rvw3zqPjtx+Cug33b3naanGOX + KbQbE3JGFJGaKPBdsE1EExvV/aJexMCggVOE4ZsBdsB0TI8ytsHYp7MFktQmlpFM8v/IjHvdZ/ze + zvUP0OnB0ohiHGv/xsKVzgMXt5Mep7eLkc9cha3DEDi5NKh4OXQ3myY9HlxG52mJi0Yo9CmODxvs + Brs+ToI7ZbBf5zANn1mhsekpZP0xWFHHs4GfDNo9iCSEVo25eujsEWglg311gXCihYKnTCigWi3A + 9SQF47gOrS72ea/Jz5rc7DKFNCOVgBcRJuphfa9u/oINm1e+r7x6FmeFRSXjUG4CzzoknTtxHcV1 + 7z1n82mSz5ISGB+VHTfU4PcUGimjhAAWco+DJOtdeBOZHDo3RxsgQgitJDawW7ZyQdvamMB3NsL3 + 9y+kyMssqWJwI/y00mDZoCZUqN0LzKZhjIxC4w00Nu3CW1vs7+WXv3/6rI0QHWzsDEfbwc67dzeB + aMODg829K1eu9Iw7s94f2wW8q8EmPr46AMbDI/3RoHBlnVaTqQeTHVxj+vT0HwAAAP//AwAYEu+6 + wQsAAA== + headers: + api-version: + - '2.24' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '1315' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 20 Sep 2021 22:41:51 UTC + server: + - tsa_b + set-cookie: + - personalization_id="v1_Qr440OWOLwfIbswvEJOdCg=="; Max-Age=63072000; Expires=Wed, + 20 Sep 2023 22:41:51 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163217771075899222; Max-Age=63072000; Expires=Wed, 20 Sep 2023 + 22:41:51 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read + x-connection-hash: + - 78f0efa18b98c6b7ef15b5fd993ba99363018161b418a3344e99dc068f5c8571 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '75' + x-rate-limit-remaining: + - '73' + x-rate-limit-reset: + - '1632178559' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/cassettes/test_retweet_and_unretweet.yaml b/cassettes/test_retweet_and_unretweet.yaml new file mode 100644 index 0000000..510c79f --- /dev/null +++ b/cassettes/test_retweet_and_unretweet.yaml @@ -0,0 +1,125 @@ +interactions: +- request: + body: '{"tweet_id": "1415348607813832708"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '35' + Content-Type: + - application/json + User-Agent: + - Python/3.9.6 Requests/2.25.1 Tweepy/4.0.0-alpha + method: POST + uri: https://api.twitter.com/2/users/1072250532645998596/retweets + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlYqSi0pT00tSU1RsiopKk2trQUAAAD//wMAkF+QNhsAAAA= + headers: + api-version: + - '2.24' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '53' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 20 Sep 2021 22:41:52 UTC + server: + - tsa_b + set-cookie: + - personalization_id="v1_Rj7jnMbi1jj2et9ubmiFRg=="; Max-Age=63072000; Expires=Wed, + 20 Sep 2023 22:41:52 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163217771171423840; Max-Age=63072000; Expires=Wed, 20 Sep 2023 + 22:41:52 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - 568f3d770dd0d22fc050812841b57078977d685822883820ef842162c3a5c5ba + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '48' + x-rate-limit-reset: + - '1632178531' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Cookie: + - guest_id=v1%3A163217771171423840; personalization_id="v1_Rj7jnMbi1jj2et9ubmiFRg==" + User-Agent: + - Python/3.9.6 Requests/2.25.1 Tweepy/4.0.0-alpha + method: DELETE + uri: https://api.twitter.com/2/users/1072250532645998596/retweets/1415348607813832708 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlYqSi0pT00tSU1RskpLzClOra0FAAAA//8DAL0TO3McAAAA + headers: + api-version: + - '2.24' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '54' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 20 Sep 2021 22:41:52 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - 568f3d770dd0d22fc050812841b57078977d685822883820ef842162c3a5c5ba + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '48' + x-rate-limit-reset: + - '1632178543' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/docs/client.rst b/docs/client.rst index acfb2e0..4fd54be 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -29,6 +29,15 @@ Likes .. automethod:: Client.like +Retweets +-------- + +.. automethod:: Client.unretweet + +.. automethod:: Client.get_retweeters + +.. automethod:: Client.retweet + Search Tweets ------------- diff --git a/tests/test_client.py b/tests/test_client.py index b052dc7..8f6bf54 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -36,6 +36,17 @@ class TweepyTestCase(unittest.TestCase): user_id = 783214 # User ID for @Twitter self.client.get_liked_tweets(user_id) + @tape.use_cassette("test_retweet_and_unretweet.yaml", serializer="yaml") + def test_retweet_and_unretweet(self): + tweet_id = 1415348607813832708 # @TwitterDev Tweet announcing API v2 Retweet endpoints + self.client.retweet(tweet_id) + self.client.unretweet(tweet_id) + + @tape.use_cassette("test_get_retweeters.yaml", serializer="yaml") + def test_get_retweeters(self): + tweet_id = 1415348607813832708 # @TwitterDev Tweet announcing API v2 Retweet endpoints + self.client.get_retweeters(tweet_id) + # TODO: test_search_all_tweets with access to Academic Research product track @tape.use_cassette("test_search_recent_tweets.yaml", serializer="yaml") diff --git a/tweepy/client.py b/tweepy/client.py index 21086e2..53ad74b 100644 --- a/tweepy/client.py +++ b/tweepy/client.py @@ -380,6 +380,95 @@ class Client: "POST", route, json={"tweet_id": str(tweet_id)}, user_auth=True ) + # Retweets + + def unretweet(self, source_tweet_id): + """Allows an authenticated user ID to remove the Retweet of a Tweet. + + The request succeeds with no action when the user sends a request to a + user they're not Retweeting the Tweet or have already removed the + Retweet of. + + Parameters + ---------- + source_tweet_id : Union[int, str] + The ID of the Tweet that you would like to remove the Retweet of. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/tweets/retweets/api-reference/delete-users-id-retweets-tweet_id + """ + id = self.access_token.partition('-')[0] + route = f"/2/users/{id}/retweets/{source_tweet_id}" + + return self._make_request("DELETE", route, user_auth=True) + + def get_retweeters(self, id, *, user_auth=False, **params): + """get_retweeters(id, *, expansions, media_fields, place_fields, \ + poll_fields, tweet_fields, user_fields) + + Allows you to get information about who has Retweeted a Tweet. + + Parameters + ---------- + id : Union[int, str] + Tweet ID of the Tweet to request Retweeting users of. + expansions : Union[List[str], str] + :ref:`expansions_parameter` + media_fields : Union[List[str], str] + :ref:`media_fields_parameter` + place_fields : Union[List[str], str] + :ref:`place_fields_parameter` + poll_fields : Union[List[str], str] + :ref:`poll_fields_parameter` + tweet_fields : Union[List[str], str] + :ref:`tweet_fields_parameter` + user_fields : Union[List[str], str] + :ref:`user_fields_parameter` + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/tweets/retweets/api-reference/get-tweets-id-retweeted_by + """ + return self._make_request( + "GET", f"/2/tweets/{id}/retweeted_by", params=params, + endpoint_parameters=( + "expansions", "media.fields", "place.fields", "poll.fields", + "tweet.fields", "user.fields" + ), data_type=User, user_auth=user_auth + ) + + def retweet(self, tweet_id): + """Causes the user ID to Retweet the target Tweet. + + Parameters + ---------- + tweet_id : Union[int, str] + The ID of the Tweet that you would like to Retweet. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/tweets/retweets/api-reference/post-users-id-retweets + """ + id = self.access_token.partition('-')[0] + route = f"/2/users/{id}/retweets" + + return self._make_request( + "POST", route, json={"tweet_id": str(tweet_id)}, user_auth=True + ) + # Search Tweets def search_all_tweets(self, query, **params): -- 2.25.1