tweetapi 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tweetapi/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """TweetAPI Python SDK — Official client for the Twitter/X Data API.
2
+
3
+ Usage::
4
+
5
+ from tweetapi import TweetAPI
6
+
7
+ client = TweetAPI(api_key="YOUR_API_KEY")
8
+
9
+ user = client.user.get_by_username(username="elonmusk")
10
+ print(user["data"]["followerCount"])
11
+ """
12
+
13
+ from .client import TweetAPI
14
+ from .errors import (
15
+ TweetAPIError,
16
+ AuthenticationError,
17
+ ForbiddenError,
18
+ NotFoundError,
19
+ ValidationError,
20
+ RateLimitError,
21
+ ServerError,
22
+ ConnectionError_ as NetworkError,
23
+ )
24
+ from .pagination import paginate, paginate_pages
25
+
26
+ __all__ = [
27
+ "TweetAPI",
28
+ "TweetAPIError",
29
+ "AuthenticationError",
30
+ "ForbiddenError",
31
+ "NotFoundError",
32
+ "ValidationError",
33
+ "RateLimitError",
34
+ "ServerError",
35
+ "NetworkError",
36
+ "paginate",
37
+ "paginate_pages",
38
+ ]
39
+
40
+ __version__ = "1.1.0"
tweetapi/client.py ADDED
@@ -0,0 +1,232 @@
1
+ from __future__ import annotations
2
+
3
+ import random
4
+ import time
5
+ from typing import Any, Optional, Union
6
+
7
+ import requests
8
+
9
+ from .errors import (
10
+ TweetAPIError,
11
+ AuthenticationError,
12
+ ForbiddenError,
13
+ NotFoundError,
14
+ ValidationError,
15
+ RateLimitError,
16
+ ServerError,
17
+ ConnectionError_,
18
+ )
19
+
20
+
21
+ class TweetAPI:
22
+ """TweetAPI Python SDK client.
23
+
24
+ Args:
25
+ api_key: Your TweetAPI API key.
26
+ base_url: Base URL for the API (default: https://api.tweetapi.com).
27
+ timeout: Request timeout in seconds. Pass a tuple ``(connect, read)`` for
28
+ split timeouts. Default: ``(10, 30)``.
29
+ connect_timeout: Connection timeout in seconds (used when *timeout* is not set).
30
+ read_timeout: Read timeout in seconds (used when *timeout* is not set).
31
+ max_retries: Maximum retry attempts on transient errors (default: 3, set 0 to disable).
32
+ backoff_multiplier: Multiplier for exponential backoff (default: 2.0).
33
+ initial_retry_delay: Initial retry delay in seconds (default: 1.0).
34
+ max_retry_delay: Maximum retry delay cap in seconds (default: 30.0).
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ api_key: str,
40
+ base_url: str = "https://api.tweetapi.com",
41
+ timeout: Optional[Union[float, tuple[float, float]]] = None,
42
+ connect_timeout: Optional[float] = None,
43
+ read_timeout: Optional[float] = None,
44
+ max_retries: int = 3,
45
+ backoff_multiplier: float = 2.0,
46
+ initial_retry_delay: float = 1.0,
47
+ max_retry_delay: float = 30.0,
48
+ ) -> None:
49
+ if not api_key:
50
+ raise ValueError("TweetAPI: api_key is required")
51
+
52
+ self._base_url = base_url.rstrip("/")
53
+
54
+ # Timeout resolution
55
+ if timeout is not None:
56
+ self._timeout: Union[float, tuple[float, float]] = timeout
57
+ elif connect_timeout is not None or read_timeout is not None:
58
+ self._timeout = (
59
+ connect_timeout if connect_timeout is not None else 10.0,
60
+ read_timeout if read_timeout is not None else 30.0,
61
+ )
62
+ else:
63
+ self._timeout = (10.0, 30.0)
64
+
65
+ # Retry configuration
66
+ self._max_retries = max_retries
67
+ self._backoff_multiplier = backoff_multiplier
68
+ self._initial_retry_delay = initial_retry_delay
69
+ self._max_retry_delay = max_retry_delay
70
+ self._rate_limit_info: Optional[dict[str, Any]] = None
71
+
72
+ self._session = requests.Session()
73
+ self._session.headers.update({"X-API-Key": api_key})
74
+
75
+ # Lazy imports to avoid circular dependencies
76
+ from .resources.user import UserResource
77
+ from .resources.tweet import TweetResource
78
+ from .resources.post import PostResource
79
+ from .resources.interaction import InteractionResource
80
+ from .resources.list_ import ListResource
81
+ from .resources.community import CommunityResource
82
+ from .resources.space import SpaceResource
83
+ from .resources.explore import ExploreResource
84
+ from .resources.auth import AuthResource
85
+ from .resources.xchat import XChatResource
86
+ from .resources.unencrypted_dm import UnencryptedDMResource
87
+
88
+ self.user = UserResource(self)
89
+ self.tweet = TweetResource(self)
90
+ self.post = PostResource(self)
91
+ self.interaction = InteractionResource(self)
92
+ self.list = ListResource(self)
93
+ self.community = CommunityResource(self)
94
+ self.space = SpaceResource(self)
95
+ self.explore = ExploreResource(self)
96
+ self.auth = AuthResource(self)
97
+ self.xchat = XChatResource(self)
98
+ self.dm = UnencryptedDMResource(self)
99
+
100
+ def _get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
101
+ """Send a GET request to the API."""
102
+ clean_params = None
103
+ if params:
104
+ clean_params = {
105
+ k: v for k, v in params.items() if v is not None
106
+ }
107
+ return self._request("GET", path, params=clean_params)
108
+
109
+ def _post(self, path: str, body: Optional[dict[str, Any]] = None) -> Any:
110
+ """Send a POST request to the API."""
111
+ clean_body = None
112
+ if body:
113
+ clean_body = {k: v for k, v in body.items() if v is not None}
114
+ return self._request("POST", path, json=clean_body)
115
+
116
+ @property
117
+ def rate_limit_info(self) -> Optional[dict[str, Any]]:
118
+ """Last known rate limit info from a 429 response, or ``None``."""
119
+ return self._rate_limit_info
120
+
121
+ # ── internal helpers ─────────────────────────────────────────────────
122
+
123
+ @property
124
+ def _timeout_desc(self) -> str:
125
+ if isinstance(self._timeout, tuple):
126
+ return f"connect={self._timeout[0]}s, read={self._timeout[1]}s"
127
+ return f"{self._timeout}s"
128
+
129
+ def _is_retryable(self, error: TweetAPIError) -> bool:
130
+ if isinstance(error, ConnectionError_):
131
+ return True
132
+ return error.status_code == 429 or error.status_code >= 500
133
+
134
+ def _calculate_retry_delay(self, error: Optional[TweetAPIError], attempt: int) -> float:
135
+ if isinstance(error, RateLimitError) and error.retry_after > 0:
136
+ return min(float(error.retry_after), self._max_retry_delay)
137
+ base = self._initial_retry_delay * (self._backoff_multiplier ** attempt)
138
+ capped = min(base, self._max_retry_delay)
139
+ return capped + random.random() * capped * 0.25
140
+
141
+ def _request(
142
+ self,
143
+ method: str,
144
+ path: str,
145
+ params: Optional[dict[str, Any]] = None,
146
+ json: Optional[dict[str, Any]] = None,
147
+ ) -> Any:
148
+ url = f"{self._base_url}{path}"
149
+ last_error: Optional[TweetAPIError] = None
150
+
151
+ for attempt in range(self._max_retries + 1):
152
+ try:
153
+ response = self._session.request(
154
+ method,
155
+ url,
156
+ params=params,
157
+ json=json,
158
+ timeout=self._timeout,
159
+ )
160
+ except requests.exceptions.Timeout as e:
161
+ last_error = ConnectionError_(
162
+ f"Request timed out ({self._timeout_desc})", e
163
+ )
164
+ if attempt < self._max_retries:
165
+ time.sleep(self._calculate_retry_delay(last_error, attempt))
166
+ continue
167
+ raise last_error from e
168
+ except requests.exceptions.ConnectionError as e:
169
+ last_error = ConnectionError_(f"Network error: {e}", e)
170
+ if attempt < self._max_retries:
171
+ time.sleep(self._calculate_retry_delay(last_error, attempt))
172
+ continue
173
+ raise last_error from e
174
+ except requests.exceptions.RequestException as e:
175
+ last_error = ConnectionError_(f"Request failed: {e}", e)
176
+ if attempt < self._max_retries:
177
+ time.sleep(self._calculate_retry_delay(last_error, attempt))
178
+ continue
179
+ raise last_error from e
180
+
181
+ if not response.ok:
182
+ try:
183
+ self._handle_error_response(response)
184
+ except TweetAPIError as err:
185
+ last_error = err
186
+ if isinstance(err, RateLimitError):
187
+ self._rate_limit_info = {
188
+ "retry_after": err.retry_after,
189
+ "timestamp": time.time(),
190
+ }
191
+ if attempt < self._max_retries and self._is_retryable(err):
192
+ time.sleep(self._calculate_retry_delay(err, attempt))
193
+ continue
194
+ raise
195
+
196
+ return response.json()
197
+
198
+ raise last_error # type: ignore[misc]
199
+
200
+ def _handle_error_response(self, response: requests.Response) -> None:
201
+ """Parse error response and raise the appropriate exception."""
202
+ try:
203
+ body = response.json()
204
+ except (ValueError, requests.exceptions.JSONDecodeError):
205
+ body = None
206
+
207
+ code = "UNKNOWN_ERROR"
208
+ message = f"HTTP {response.status_code}"
209
+ details = None
210
+
211
+ if body and isinstance(body.get("error"), dict):
212
+ error = body["error"]
213
+ code = error.get("code", code)
214
+ message = error.get("message", message)
215
+ details = error.get("details")
216
+
217
+ status = response.status_code
218
+
219
+ if status == 400:
220
+ raise ValidationError(message, code, details)
221
+ elif status == 401:
222
+ raise AuthenticationError(message, code, details)
223
+ elif status == 403:
224
+ raise ForbiddenError(message, code, details)
225
+ elif status == 404:
226
+ raise NotFoundError(message, code, details)
227
+ elif status == 429:
228
+ raise RateLimitError(message, code, details)
229
+ elif status >= 500:
230
+ raise ServerError(message, code, status, details)
231
+ else:
232
+ raise TweetAPIError(message, code, status, details)
tweetapi/errors.py ADDED
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+
6
+ class TweetAPIError(Exception):
7
+ """Base error for all TweetAPI errors.
8
+
9
+ Contains the API error code, HTTP status, and optional details.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ code: str,
16
+ status_code: int,
17
+ details: Optional[dict[str, Any]] = None,
18
+ ) -> None:
19
+ super().__init__(message)
20
+ self.message = message
21
+ self.code = code
22
+ self.status_code = status_code
23
+ self.details = details
24
+
25
+ def __str__(self) -> str:
26
+ return f"[{self.code}] {self.message}"
27
+
28
+ def __repr__(self) -> str:
29
+ return f"{self.__class__.__name__}(code={self.code!r}, status_code={self.status_code}, message={self.message!r})"
30
+
31
+
32
+ class AuthenticationError(TweetAPIError):
33
+ """Thrown when the API key or auth token is invalid or missing (HTTP 401)."""
34
+
35
+ def __init__(
36
+ self,
37
+ message: str,
38
+ code: str,
39
+ details: Optional[dict[str, Any]] = None,
40
+ ) -> None:
41
+ super().__init__(message, code, 401, details)
42
+
43
+
44
+ class ForbiddenError(TweetAPIError):
45
+ """Thrown when the request is forbidden (HTTP 403)."""
46
+
47
+ def __init__(
48
+ self,
49
+ message: str,
50
+ code: str,
51
+ details: Optional[dict[str, Any]] = None,
52
+ ) -> None:
53
+ super().__init__(message, code, 403, details)
54
+
55
+
56
+ class NotFoundError(TweetAPIError):
57
+ """Thrown when the requested resource is not found (HTTP 404)."""
58
+
59
+ def __init__(
60
+ self,
61
+ message: str,
62
+ code: str,
63
+ details: Optional[dict[str, Any]] = None,
64
+ ) -> None:
65
+ super().__init__(message, code, 404, details)
66
+
67
+
68
+ class ValidationError(TweetAPIError):
69
+ """Thrown when the request parameters are invalid (HTTP 400)."""
70
+
71
+ def __init__(
72
+ self,
73
+ message: str,
74
+ code: str,
75
+ details: Optional[dict[str, Any]] = None,
76
+ ) -> None:
77
+ super().__init__(message, code, 400, details)
78
+
79
+
80
+ class RateLimitError(TweetAPIError):
81
+ """Thrown when you've exceeded rate limits (HTTP 429).
82
+
83
+ Check ``retry_after`` for seconds until you can retry.
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ message: str,
89
+ code: str,
90
+ details: Optional[dict[str, Any]] = None,
91
+ ) -> None:
92
+ super().__init__(message, code, 429, details)
93
+ self.retry_after: int = (details or {}).get("retryAfter", 60)
94
+
95
+
96
+ class ServerError(TweetAPIError):
97
+ """Thrown when the API encounters a server error (HTTP 5xx)."""
98
+
99
+ def __init__(
100
+ self,
101
+ message: str,
102
+ code: str,
103
+ status_code: int,
104
+ details: Optional[dict[str, Any]] = None,
105
+ ) -> None:
106
+ super().__init__(message, code, status_code, details)
107
+
108
+
109
+ class ConnectionError_(TweetAPIError):
110
+ """Thrown when a network error occurs (DNS failure, timeout, connection refused).
111
+
112
+ Named ``ConnectionError_`` to avoid shadowing the built-in ``ConnectionError``.
113
+ Also importable as ``NetworkError``.
114
+ """
115
+
116
+ def __init__(self, message: str, cause: Optional[Exception] = None) -> None:
117
+ super().__init__(message, "CONNECTION_ERROR", 0, None)
118
+ self.__cause__ = cause
119
+
120
+
121
+ # Alias for cleaner imports
122
+ NetworkError = ConnectionError_
tweetapi/pagination.py ADDED
@@ -0,0 +1,64 @@
1
+ """Auto-pagination helpers for cursor-based paginated endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable, Generator, Optional
6
+
7
+
8
+ def paginate_pages(
9
+ fetcher: Callable[[Optional[str]], dict[str, Any]],
10
+ *,
11
+ max_pages: Optional[int] = None,
12
+ ) -> Generator[dict[str, Any], None, None]:
13
+ """Yield each full page (response dict) from a paginated endpoint.
14
+
15
+ Args:
16
+ fetcher: A callable that takes an optional cursor string and returns
17
+ a paginated response dict with ``data`` and ``pagination`` keys.
18
+ max_pages: Stop after this many pages. ``None`` means fetch all.
19
+
20
+ Example::
21
+
22
+ for page in paginate_pages(
23
+ lambda cursor: client.user.get_followers(user_id="123", cursor=cursor),
24
+ ):
25
+ print(f"Got {len(page['data'])} users")
26
+ """
27
+ cursor: Optional[str] = None
28
+ count = 0
29
+
30
+ while True:
31
+ page = fetcher(cursor)
32
+ yield page
33
+ count += 1
34
+
35
+ if max_pages is not None and count >= max_pages:
36
+ break
37
+
38
+ cursor = page.get("pagination", {}).get("nextCursor")
39
+ if not cursor:
40
+ break
41
+
42
+
43
+ def paginate(
44
+ fetcher: Callable[[Optional[str]], dict[str, Any]],
45
+ *,
46
+ max_pages: Optional[int] = None,
47
+ ) -> Generator[Any, None, None]:
48
+ """Yield individual items from all pages of a paginated endpoint.
49
+
50
+ Args:
51
+ fetcher: A callable that takes an optional cursor string and returns
52
+ a paginated response dict.
53
+ max_pages: Stop after this many pages. ``None`` means fetch all.
54
+
55
+ Example::
56
+
57
+ for user in paginate(
58
+ lambda cursor: client.user.get_followers(user_id="123", cursor=cursor),
59
+ max_pages=5,
60
+ ):
61
+ print(user["username"])
62
+ """
63
+ for page in paginate_pages(fetcher, max_pages=max_pages):
64
+ yield from page.get("data", [])
tweetapi/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import TweetAPI
7
+ from ..types import LoginApiResponse
8
+
9
+
10
+ class AuthResource:
11
+ def __init__(self, client: TweetAPI) -> None:
12
+ self._client = client
13
+
14
+ def login(self, *, username: str, password: str, proxy: str, two_factor_secret: Optional[str] = None) -> LoginApiResponse:
15
+ """Log in to a Twitter account and get auth tokens."""
16
+ return self._client._post("/tw-v2/auth/login", {
17
+ "username": username, "password": password,
18
+ "proxy": proxy, "twoFactorSecret": two_factor_secret,
19
+ })
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import TweetAPI
7
+ from ..types import (
8
+ ActionResponse,
9
+ CommunityResponse,
10
+ CommunityMemberPaginatedResponse,
11
+ CommunitySearchPaginatedResponse,
12
+ TweetsPaginatedResponse,
13
+ )
14
+
15
+
16
+ class CommunityResource:
17
+ def __init__(self, client: TweetAPI) -> None:
18
+ self._client = client
19
+
20
+ def get_details(self, *, community_id: str) -> CommunityResponse:
21
+ """Get community details."""
22
+ return self._client._get("/tw-v2/community/details", {"communityId": community_id})
23
+
24
+ def get_tweets(self, *, community_id: str, sort_by: str, cursor: Optional[str] = None) -> TweetsPaginatedResponse:
25
+ """Get tweets in a community."""
26
+ return self._client._get("/tw-v2/community/tweets", {"communityId": community_id, "sortBy": sort_by, "cursor": cursor})
27
+
28
+ def get_members(self, *, community_id: str, cursor: Optional[str] = None) -> CommunityMemberPaginatedResponse:
29
+ """Get community members."""
30
+ return self._client._get("/tw-v2/community/members", {"communityId": community_id, "cursor": cursor})
31
+
32
+ def search(self, *, query: str, cursor: Optional[str] = None) -> CommunitySearchPaginatedResponse:
33
+ """Search communities."""
34
+ return self._client._get("/tw-v2/community/search", {"query": query, "cursor": cursor})
35
+
36
+ def create_post(self, *, auth_token: str, text: str, community_id: str, proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
37
+ """Create a post in a community."""
38
+ return self._client._post("/tw-v2/interaction/create-community-post", {
39
+ "authToken": auth_token, "text": text, "communityId": community_id,
40
+ "proxy": proxy, "disableLinkPreview": disable_link_preview,
41
+ })
42
+
43
+ def create_post_with_media(self, *, auth_token: str, text: str, community_id: str, media: list[Any], proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
44
+ """Create a post with media in a community."""
45
+ return self._client._post("/tw-v2/interaction/create-community-post-with-media", {
46
+ "authToken": auth_token, "text": text, "communityId": community_id,
47
+ "media": media, "proxy": proxy, "disableLinkPreview": disable_link_preview,
48
+ })
49
+
50
+ def reply_post(self, *, auth_token: str, text: str, tweet_id: str, community_id: str, proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
51
+ """Reply to a community post."""
52
+ return self._client._post("/tw-v2/interaction/reply-community-post", {
53
+ "authToken": auth_token, "text": text, "tweetId": tweet_id,
54
+ "communityId": community_id, "proxy": proxy, "disableLinkPreview": disable_link_preview,
55
+ })
56
+
57
+ def reply_post_with_media(self, *, auth_token: str, text: str, tweet_id: str, community_id: str, media: list[Any], proxy: str, disable_link_preview: Optional[bool] = None) -> ActionResponse:
58
+ """Reply to a community post with media."""
59
+ return self._client._post("/tw-v2/interaction/reply-community-post-with-media", {
60
+ "authToken": auth_token, "text": text, "tweetId": tweet_id,
61
+ "communityId": community_id, "media": media, "proxy": proxy,
62
+ "disableLinkPreview": disable_link_preview,
63
+ })
64
+
65
+ def join(self, *, auth_token: str, community_id: str, proxy: Optional[str] = None) -> ActionResponse:
66
+ """Join a community."""
67
+ return self._client._post("/tw-v2/interaction/join-community", {
68
+ "authToken": auth_token, "communityId": community_id, "proxy": proxy,
69
+ })
70
+
71
+ def leave(self, *, auth_token: str, community_id: str, proxy: Optional[str] = None) -> ActionResponse:
72
+ """Leave a community."""
73
+ return self._client._post("/tw-v2/interaction/leave-community", {
74
+ "authToken": auth_token, "communityId": community_id, "proxy": proxy,
75
+ })
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import TweetAPI
7
+ from ..types import SearchResponse
8
+
9
+
10
+ class ExploreResource:
11
+ def __init__(self, client: TweetAPI) -> None:
12
+ self._client = client
13
+
14
+ def search(self, *, query: str, type: str, cursor: Optional[str] = None) -> SearchResponse:
15
+ """Search for tweets, users, photos, or videos.
16
+
17
+ Args:
18
+ query: Search query string. Supports Twitter search operators.
19
+ type: Type of search results — "Latest", "Top", "People", "Photos", or "Videos".
20
+ cursor: Pagination cursor for fetching next page.
21
+ """
22
+ return self._client._get("/tw-v2/search", {"query": query, "type": type, "cursor": cursor})
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from ..client import TweetAPI
7
+ from ..types import (
8
+ ActionResponse,
9
+ NotificationPaginatedResponse,
10
+ UserAnalyticsResponse,
11
+ )
12
+
13
+
14
+ class InteractionResource:
15
+ def __init__(self, client: TweetAPI) -> None:
16
+ self._client = client
17
+
18
+ def favorite_post(self, *, auth_token: str, tweet_id: str, proxy: Optional[str] = None) -> ActionResponse:
19
+ """Like a tweet."""
20
+ return self._client._post("/tw-v2/interaction/like-post", {
21
+ "authToken": auth_token, "tweetId": tweet_id, "proxy": proxy,
22
+ })
23
+
24
+ def unfavorite_post(self, *, auth_token: str, tweet_id: str, proxy: Optional[str] = None) -> ActionResponse:
25
+ """Unlike a tweet."""
26
+ return self._client._post("/tw-v2/interaction/unlike-post", {
27
+ "authToken": auth_token, "tweetId": tweet_id, "proxy": proxy,
28
+ })
29
+
30
+ def retweet(self, *, auth_token: str, tweet_id: str, proxy: Optional[str] = None) -> ActionResponse:
31
+ """Retweet a tweet."""
32
+ return self._client._post("/tw-v2/interaction/retweet", {
33
+ "authToken": auth_token, "tweetId": tweet_id, "proxy": proxy,
34
+ })
35
+
36
+ def delete_retweet(self, *, auth_token: str, tweet_id: str, proxy: Optional[str] = None) -> ActionResponse:
37
+ """Remove a retweet."""
38
+ return self._client._post("/tw-v2/interaction/delete-retweet", {
39
+ "authToken": auth_token, "tweetId": tweet_id, "proxy": proxy,
40
+ })
41
+
42
+ def bookmark(self, *, auth_token: str, tweet_id: str, proxy: Optional[str] = None) -> ActionResponse:
43
+ """Bookmark a tweet."""
44
+ return self._client._post("/tw-v2/interaction/bookmark", {
45
+ "authToken": auth_token, "tweetId": tweet_id, "proxy": proxy,
46
+ })
47
+
48
+ def delete_bookmark(self, *, auth_token: str, tweet_id: str, proxy: Optional[str] = None) -> ActionResponse:
49
+ """Remove a bookmark."""
50
+ return self._client._post("/tw-v2/interaction/delete-bookmark", {
51
+ "authToken": auth_token, "tweetId": tweet_id, "proxy": proxy,
52
+ })
53
+
54
+ def follow(self, *, auth_token: str, user_id: str, proxy: Optional[str] = None) -> ActionResponse:
55
+ """Follow a user."""
56
+ return self._client._post("/tw-v2/interaction/follow", {
57
+ "authToken": auth_token, "userId": user_id, "proxy": proxy,
58
+ })
59
+
60
+ def unfollow(self, *, auth_token: str, user_id: str, proxy: Optional[str] = None) -> ActionResponse:
61
+ """Unfollow a user."""
62
+ return self._client._post("/tw-v2/interaction/unfollow", {
63
+ "authToken": auth_token, "userId": user_id, "proxy": proxy,
64
+ })
65
+
66
+ def add_member_to_list(self, *, auth_token: str, list_id: str, user_id: str, proxy: Optional[str] = None) -> ActionResponse:
67
+ """Add a user to a list."""
68
+ return self._client._post("/tw-v2/interaction/add-to-list", {
69
+ "authToken": auth_token, "listId": list_id, "userId": user_id, "proxy": proxy,
70
+ })
71
+
72
+ def remove_member_from_list(self, *, auth_token: str, list_id: str, user_id: str, proxy: Optional[str] = None) -> ActionResponse:
73
+ """Remove a user from a list."""
74
+ return self._client._post("/tw-v2/interaction/remove-from-list", {
75
+ "authToken": auth_token, "listId": list_id, "userId": user_id, "proxy": proxy,
76
+ })
77
+
78
+ def get_notifications(self, *, auth_token: str, timeline_type: Optional[str] = None, count: Optional[int] = None, cursor: Optional[str] = None, proxy: Optional[str] = None) -> NotificationPaginatedResponse:
79
+ """Get your notifications."""
80
+ return self._client._get("/tw-v2/interaction/notifications", {
81
+ "authToken": auth_token, "timelineType": timeline_type,
82
+ "count": count, "cursor": cursor, "proxy": proxy,
83
+ })
84
+
85
+ def get_user_analytics(self, *, auth_token: str, to_time: Optional[str] = None, from_time: Optional[str] = None, granularity: Optional[str] = None, show_verified_followers: Optional[bool] = None, proxy: Optional[str] = None) -> UserAnalyticsResponse:
86
+ """Get your account analytics."""
87
+ return self._client._get("/tw-v2/interaction/user-analytics", {
88
+ "authToken": auth_token, "toTime": to_time, "fromTime": from_time,
89
+ "granularity": granularity, "showVerifiedFollowers": show_verified_followers,
90
+ "proxy": proxy,
91
+ })