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 +40 -0
- tweetapi/client.py +232 -0
- tweetapi/errors.py +122 -0
- tweetapi/pagination.py +64 -0
- tweetapi/py.typed +0 -0
- tweetapi/resources/__init__.py +0 -0
- tweetapi/resources/auth.py +19 -0
- tweetapi/resources/community.py +75 -0
- tweetapi/resources/explore.py +22 -0
- tweetapi/resources/interaction.py +91 -0
- tweetapi/resources/list_.py +32 -0
- tweetapi/resources/post.py +53 -0
- tweetapi/resources/space.py +20 -0
- tweetapi/resources/tweet.py +37 -0
- tweetapi/resources/unencrypted_dm.py +64 -0
- tweetapi/resources/user.py +84 -0
- tweetapi/resources/xchat.py +45 -0
- tweetapi/types.py +563 -0
- tweetapi-1.1.0.dist-info/METADATA +345 -0
- tweetapi-1.1.0.dist-info/RECORD +21 -0
- tweetapi-1.1.0.dist-info/WHEEL +4 -0
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
|
+
})
|