From 5c688922cf0f8e89d401ea5108b06aaa8c12d71b Mon Sep 17 00:00:00 2001 From: Harmon Date: Tue, 5 Oct 2021 13:31:26 -0500 Subject: [PATCH] Add support for Spaces Add Space model Add Client.search_spaces, Client.get_spaces, and Client.get_space --- cassettes/test_get_space.yaml | 63 ++++++++++++++ cassettes/test_get_spaces.yaml | 121 +++++++++++++++++++++++++++ cassettes/test_search_spaces.yaml | 65 +++++++++++++++ docs/client.rst | 72 +++++++++++----- docs/models.rst | 4 + tests/test_client.py | 23 ++++++ tweepy/__init__.py | 1 + tweepy/client.py | 133 ++++++++++++++++++++++++++++++ tweepy/space.py | 61 ++++++++++++++ 9 files changed, 521 insertions(+), 22 deletions(-) create mode 100644 cassettes/test_get_space.yaml create mode 100644 cassettes/test_get_spaces.yaml create mode 100644 cassettes/test_search_spaces.yaml create mode 100644 tweepy/space.py diff --git a/cassettes/test_get_space.yaml b/cassettes/test_get_space.yaml new file mode 100644 index 0000000..c373326 --- /dev/null +++ b/cassettes/test_get_space.yaml @@ -0,0 +1,63 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Python/3.9.7 Requests/2.25.1 Tweepy/4.0.1 + method: GET + uri: https://api.twitter.com/2/spaces/1YpKkzBgBlVxj + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlbKTFGyUjKMLPDOrnJKd8oJq8hS0lEqLkksSQWKp+alpKYo1dYC + AAAA//8DANuEAIIvAAAA + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '72' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 04 Oct 2021 16:20:09 UTC + server: + - tsa_b + set-cookie: + - personalization_id="v1_8sltSYl5ot2fwTzCtHyhmg=="; Max-Age=63072000; Expires=Wed, + 04 Oct 2023 16:20:09 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163336440956178191; Max-Age=63072000; Expires=Wed, 04 Oct 2023 + 16:20:09 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read + x-connection-hash: + - fa3c76458eec0126e6a6b25810e44e72902ac0c3351b444f01b0531fb127b2ac + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '298' + x-rate-limit-reset: + - '1633364979' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/cassettes/test_get_spaces.yaml b/cassettes/test_get_spaces.yaml new file mode 100644 index 0000000..f6e1c2e --- /dev/null +++ b/cassettes/test_get_spaces.yaml @@ -0,0 +1,121 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Python/3.9.7 Requests/2.25.1 Tweepy/4.0.1 + method: GET + uri: https://api.twitter.com/2/spaces?ids=1YpKkzBgBlVxj%2C1OwGWzarWnNKQ + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyiq5WykxRslIyjCzwzq5ySnfKCavIUtJRKi5JLEkFiqfmpaSmKNXq + wJT5l7uHVyUWhef5eQdiKoutBQAAAP//AwAJdS/qWAAAAA== + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '91' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 04 Oct 2021 16:20:10 UTC + server: + - tsa_b + set-cookie: + - personalization_id="v1_yfFprXL2G/FrJYvpbXwcUg=="; Max-Age=63072000; Expires=Wed, + 04 Oct 2023 16:20:10 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163336440996144486; Max-Age=63072000; Expires=Wed, 04 Oct 2023 + 16:20:10 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read + x-connection-hash: + - 611a98e7bbb653d89c38826150224a0fbc2f7ea1a18ac1b80da236f8a6e271b0 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '297' + x-rate-limit-reset: + - '1633364979' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - guest_id=v1%3A163336440996144486; personalization_id="v1_yfFprXL2G/FrJYvpbXwcUg==" + User-Agent: + - Python/3.9.7 Requests/2.25.1 Tweepy/4.0.1 + method: GET + uri: https://api.twitter.com/2/spaces/by/creator_ids?user_ids=1065249714214457345%2C2328002822 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyiq5WykxRslIyLPOvKK/MCSpM9XF3UtJRKi5JLEkFihcnZ6SmlOak + pijVxuoo5aaC9FQrFaUWl+aUxCfnl+aVKFkZ1tYCAAAA//8DAErOjQNPAAAA + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '102' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 04 Oct 2021 16:20:10 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read + x-connection-hash: + - 611a98e7bbb653d89c38826150224a0fbc2f7ea1a18ac1b80da236f8a6e271b0 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '296' + x-rate-limit-reset: + - '1633364979' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/cassettes/test_search_spaces.yaml b/cassettes/test_search_spaces.yaml new file mode 100644 index 0000000..e529a27 --- /dev/null +++ b/cassettes/test_search_spaces.yaml @@ -0,0 +1,65 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - Python/3.9.7 Requests/2.25.1 Tweepy/4.0.1 + method: GET + uri: https://api.twitter.com/2/spaces/search?query=Twitter&state=live + response: + body: + string: !!binary | + H4sIAAAAAAAAAHzQTwuCMBzG8ffyO3vpmDdHYzD/5Q5RRsRsY6jTdE7RxPdeHTqFXh8+8MB3BsEt + B/c6Qy7AhZ1qx0GXE1INQeBAZ7mVn13ng4TF+SnJaRZlBiUnel5XsfANwugQtv6GKgfaeKZqmmTE + 66oIqZIq0JXnB+uqqokUE6sjvPU41TROMT+yF2EbKiL8wiMtBSnWlWB+qouwbQ3973VzoJLfvDMY + 2fXa3h/Pvrbg7pflDQAA//8DAO2i+Rp6AQAA + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '198' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 04 Oct 2021 16:23:43 UTC + server: + - tsa_b + set-cookie: + - personalization_id="v1_Q5Y420A90rTxiUPNvjznYg=="; Max-Age=63072000; Expires=Wed, + 04 Oct 2023 16:23:43 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163336462208298649; Max-Age=63072000; Expires=Wed, 04 Oct 2023 + 16:23:43 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read + x-connection-hash: + - ba8571db95a98d39f2a2ddeac9236e3648f62c3a289efe13ccf35ea85b5de519 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '289' + x-rate-limit-reset: + - '1633364956' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/docs/client.rst b/docs/client.rst index 0df749f..5e7f23b 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -103,6 +103,21 @@ User lookup .. automethod:: Client.get_users +Spaces +====== + +Search Spaces +------------- + +.. automethod:: Client.search_spaces + +Spaces lookup +------------- + +.. automethod:: Client.get_spaces + +.. automethod:: Client.get_space + Expansions and Fields Parameters ================================ @@ -110,15 +125,24 @@ Expansions and Fields Parameters ``expansions`` -------------- -For methods that return Tweets, `Expansions`_ enable you to request additional -data objects that relate to the originally returned Tweets. Submit a list of -desired expansions in a comma-separated list without spaces. The ID that -represents the expanded data object will be included directly in the Tweet data -object, but the expanded object metadata will be returned within the +`Expansions`_ enable you to request additional data objects that relate to the +originally returned Space, Tweets, or users. Submit a list of desired +expansions in a comma-separated list without spaces. The ID that represents the +expanded data object will be included directly in the Space, Tweet, or user +data object, but the expanded object metadata will be returned within the ``includes`` response object, and will also include the ID so that you can -match this data object to the original Tweet object. +match this data object to the original Space or Tweet object. -The following data objects can be expanded using this parameter: +For methods that return Spaces, the following data objects can be expanded +using this parameter: + +* The Spaces creator's user object +* The user objects of any Space co-host +* Any mentioned users’ object +* Any speaker's user object + +For methods that return Tweets, the following data objects can be expanded +using this parameter: * The Tweet author's user object * The user object of the Tweet’s author that the @@ -130,15 +154,9 @@ The following data objects can be expanded using this parameter: * Attached place’s object * Any referenced Tweets’ object -For methods that return users, `Expansions`_ enable you to request additional -data objects that relate to the originally returned users. The ID that -represents the expanded data object will be included directly in the user data -object, but the expanded object metadata will be returned within the -``includes`` response object, and will also include the ID so that you can -match this data object to the original Tweet object. At this time, the only -expansion available to endpoints that primarily return user objects is -``expansions=pinned_tweet_id``. You will find the expanded Tweet data object -living in the ``includes`` response object. +At this time, the only expansion available to endpoints that primarily return +user objects is ``expansions=pinned_tweet_id``. You will find the expanded +Tweet data object living in the ``includes`` response object. .. _media_fields_parameter: @@ -177,6 +195,15 @@ included the ``expansions=attachments.poll_ids`` query parameter in your request. While the poll ID will be located in the Tweet object, you will find this ID and all additional poll fields in the ``includes`` data object. +.. _space_fields_parameter: + +``space_fields`` +---------------- + +This `fields`_ parameter enables you to select which specific `Space fields`_ +will deliver in each returned Space. Specify the desired fields in a +comma-separated list. + .. _tweet_fields_parameter: ``tweet_fields`` @@ -205,12 +232,12 @@ the ``includes`` data object. ``user_fields`` --------------- -For methods that return Tweets, this `fields`_ parameter enables you to select -which specific `user fields`_ will deliver in each returned Tweet. Specify the -desired fields in a comma-separated list without spaces between commas and -fields. While the user ID will be located in the original Tweet object, you -will find this ID and all additional user fields in the ``includes`` data -object. +For methods that return Spaces or Tweets, this `fields`_ parameter enables you +to select which specific `user fields`_ will deliver in each returned Space or +Tweet. Specify the desired fields in a comma-separated list without spaces +between commas and fields. While the user ID will be located in the original +Tweet object, you will find this ID and all additional user fields in the +``includes`` data object. You must also pass one of the user expansions to return the desired user fields: @@ -231,6 +258,7 @@ user data objects. .. _media fields: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/media .. _place fields: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/place .. _poll fields: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/poll +.. _Space fields: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/space .. _Tweet fields: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/tweet .. _user fields: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/user diff --git a/docs/models.rst b/docs/models.rst index 650ea63..f9270f2 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -70,6 +70,10 @@ Models Reference :reference: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/tweet + .. class:: tweepy.Space + + :reference: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/space + .. class:: tweepy.Tweet :reference: https://developer.twitter.com/en/docs/twitter-api/data-dictionary/object-model/tweet diff --git a/tests/test_client.py b/tests/test_client.py index 63168c9..9f7dc56 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -121,3 +121,26 @@ class TweepyTestCase(unittest.TestCase): @tape.use_cassette("test_get_users.yaml", serializer="yaml") def test_get_users(self): self.client.get_users(usernames=["Twitter", "TwitterDev"]) + + @tape.use_cassette("test_search_spaces.yaml", serializer="yaml") + def test_search_spaces(self): + self.client.search_spaces("Twitter", "live") + + @tape.use_cassette("test_get_spaces.yaml", serializer="yaml") + def test_get_spaces(self): + space_ids = ["1YpKkzBgBlVxj", "1OwGWzarWnNKQ"] + # Space ID for @TwitterSpaces Twitter Spaces community gathering + Q&A + # https://twitter.com/TwitterSpaces/status/1436382283347283969 + # Space ID for @NASA #NASAWebb Space Telescope 101 and Q&A + # https://twitter.com/NASA/status/1442961745098653701 + user_ids = [1065249714214457345, 2328002822] + # User IDs for @TwitterSpaces and @TwitterWomen + self.client.get_spaces(ids=space_ids) + self.client.get_spaces(user_ids=user_ids) + + @tape.use_cassette("test_get_space.yaml", serializer="yaml") + def test_get_space(self): + space_id = "1YpKkzBgBlVxj" + # Space ID for @TwitterSpaces Twitter Spaces community gathering + Q&A + # https://twitter.com/TwitterSpaces/status/1436382283347283969 + self.client.get_space(space_id) diff --git a/tweepy/__init__.py b/tweepy/__init__.py index 466418f..156d028 100644 --- a/tweepy/__init__.py +++ b/tweepy/__init__.py @@ -22,6 +22,7 @@ from tweepy.media import Media from tweepy.pagination import Paginator from tweepy.place import Place from tweepy.poll import Poll +from tweepy.space import Space from tweepy.streaming import Stream from tweepy.tweet import ReferencedTweet, Tweet from tweepy.user import User diff --git a/tweepy/client.py b/tweepy/client.py index 59123b4..8e27ee1 100644 --- a/tweepy/client.py +++ b/tweepy/client.py @@ -19,6 +19,7 @@ from tweepy.errors import ( from tweepy.media import Media from tweepy.place import Place from tweepy.poll import Poll +from tweepy.space import Space from tweepy.tweet import Tweet from tweepy.user import User @@ -1503,3 +1504,135 @@ class Client: "ids", "usernames", "expansions", "tweet.fields", "user.fields" ), data_type=User, user_auth=user_auth ) + + # Search Spaces + + def search_spaces(self, query, state, **params): + """search_spaces(query, state, *, expansions, max_results, \ + space_fields, user_fields) + + Return live or scheduled Spaces matching your specified search terms + + Parameters + ---------- + query : str + Your search term. This can be any text (including mentions and + Hashtags) present in the title of the Space. + state : str + Determines the type of results to return. Use ``live`` to return + live Spaces or ``scheduled`` to return upcoming Spaces. + expansions : Union[List[str], str] + :ref:`expansions_parameter` + max_results : int + The maximum number of results to return in this request. Specify a + value between 1 and 100. + space_fields : Union[List[str], str] + :ref:`space_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/spaces/search/api-reference/get-spaces-search + """ + params["query"] = query + params["state"] = state + return self._make_request( + "GET", "/2/spaces/search", params=params, + endpoint_parameters=( + "query", "state", "expansions", "max_results", "space.fields", + "user.fields" + ), data_type=Space + ) + + # Spaces lookup + + def get_spaces(self, *, ids=None, user_ids=None, **params): + """get_spaces(*, ids, user_ids, expansions, space_fields, user_fields) + + Returns details about multiple live or scheduled Spaces (created by the + specified user IDs if specified). Up to 100 comma-separated Space or + user IDs can be looked up using this endpoint. + + Parameters + ---------- + ids : Union[List[str], str] + A comma separated list of Spaces (up to 100). + user_ids : Union[List[int, str], str] + A comma separated list of user IDs (up to 100). + expansions : Union[List[str], str] + :ref:`expansions_parameter` + space_fields : Union[List[str], str] + :ref:`space_fields_parameter` + user_fields : Union[List[str], str] + :ref:`user_fields_parameter` + + Raises + ------ + TypeError + If IDs and user IDs are not passed or both are passed + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/spaces/lookup/api-reference/get-spaces + https://developer.twitter.com/en/docs/twitter-api/spaces/lookup/api-reference/get-spaces-by-creator-ids + """ + if ids is not None and user_ids is not None: + raise TypeError("Expected IDs or user IDs, not both") + + route = "/2/spaces" + + if ids is not None: + params["ids"] = ids + elif user_ids is not None: + route += "/by/creator_ids" + params["user_ids"] = user_ids + else: + raise TypeError("IDs or user IDs are required") + + return self._make_request( + "GET", route, params=params, + endpoint_parameters=( + "ids", "user_ids", "expansions", "space.fields", "user.fields" + ), data_type=Space + ) + + def get_space(self, id, **params): + """get_space(id, *, expansions, space_fields, user_fields) + + Returns a variety of information about a single Space specified by the + requested ID. + + Parameters + ---------- + id : Union[List[str], str] + Unique identifier of the Space to request. + expansions : Union[List[str], str] + :ref:`expansions_parameter` + space_fields : Union[List[str], str] + :ref:`space_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/spaces/lookup/api-reference/get-spaces-id + """ + return self._make_request( + "GET", f"/2/spaces/{id}", params=params, + endpoint_parameters=( + "expansions", "space.fields", "user.fields" + ), data_type=Space + ) diff --git a/tweepy/space.py b/tweepy/space.py new file mode 100644 index 0000000..aaa72d1 --- /dev/null +++ b/tweepy/space.py @@ -0,0 +1,61 @@ +# Tweepy +# Copyright 2009-2021 Joshua Roesslein +# See LICENSE for details. + +import datetime + +from tweepy.mixins import DataMapping, HashableID + + +class Space(HashableID, DataMapping): + + __slots__ = ( + "data", "id", "state", "created_at", "host_ids", "lang", "is_ticketed", + "invited_user_ids", "participant_count", "scheduled_start", + "speaker_ids", "started_at", "title", "updated_at" + ) + + def __init__(self, data): + self.data = data + self.id = data["id"] + self.state = data["state"] + + self.created_at = data.get("created_at") + if self.created_at is not None: + self.created_at = datetime.datetime.strptime( + self.created_at, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + + self.host_ids = data.get("host_ids", []) + self.lang = data.get("lang") + self.is_ticketed = data.get("is_ticketed") + self.invited_user_ids = data.get("invited_user_ids", []) + self.participant_count = data.get("participant_count") + + self.scheduled_start = data.get("scheduled_start") + if self.scheduled_start is not None: + self.scheduled_start = datetime.datetime.strptime( + self.scheduled_start, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + + self.speaker_ids = data.get("speaker_ids", []) + + self.started_at = data.get("started_at") + if self.started_at is not None: + self.started_at = datetime.datetime.strptime( + self.started_at, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + + self.title = data.get("title") + + self.updated_at = data.get("updated_at") + if self.updated_at is not None: + self.updated_at = datetime.datetime.strptime( + self.updated_at, "%Y-%m-%dT%H:%M:%S.%f%z" + ) + + def __repr__(self): + return f"" + + def __str__(self): + return self.full_name -- 2.25.1