Add support for Spaces
authorHarmon <Harmon758@gmail.com>
Tue, 5 Oct 2021 18:31:26 +0000 (13:31 -0500)
committerHarmon <Harmon758@gmail.com>
Tue, 5 Oct 2021 18:38:01 +0000 (13:38 -0500)
Add Space model
Add Client.search_spaces, Client.get_spaces, and Client.get_space

cassettes/test_get_space.yaml [new file with mode: 0644]
cassettes/test_get_spaces.yaml [new file with mode: 0644]
cassettes/test_search_spaces.yaml [new file with mode: 0644]
docs/client.rst
docs/models.rst
tests/test_client.py
tweepy/__init__.py
tweepy/client.py
tweepy/space.py [new file with mode: 0644]

diff --git a/cassettes/test_get_space.yaml b/cassettes/test_get_space.yaml
new file mode 100644 (file)
index 0000000..c373326
--- /dev/null
@@ -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 (file)
index 0000000..f6e1c2e
--- /dev/null
@@ -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 (file)
index 0000000..e529a27
--- /dev/null
@@ -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
index 0df749fff90b5d223a664a3a403d1473295e3b6c..5e7f23bcb272fe26eaf88278efbcef2049736f3d 100644 (file)
@@ -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
 
index 650ea631397946df4c01ad544aea386906624e31..f9270f2f6b497813e7a2bc31d7ec6a9700eefd70 100644 (file)
@@ -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
index 63168c9ae24f46c7e50637f9cb693c20ced45726..9f7dc564e197dc61469e9404fd6477c35e062849 100644 (file)
@@ -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)
index 466418fd69d28272b6fde4833d3e4e7f6fcc9076..156d028952ef6fd2637a93e2224c75f439f6db8d 100644 (file)
@@ -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
index 59123b44f5540a4586fc96907a950180f70d3eeb..8e27ee1fef600f7c6b5f21d79d0bdd0bb0a79cc2 100644 (file)
@@ -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 (file)
index 0000000..aaa72d1
--- /dev/null
@@ -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"<Space id={self.id} state={self.state}>"
+
+    def __str__(self):
+        return self.full_name