xapiweb 1.5.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.
- xapiweb/__init__.py +13 -0
- xapiweb/_txn.py +224 -0
- xapiweb/client.py +52 -0
- xapiweb/errors.py +114 -0
- xapiweb/qids.py +126 -0
- xapiweb/resources/__init__.py +20 -0
- xapiweb/resources/_base.py +13 -0
- xapiweb/resources/communities.py +34 -0
- xapiweb/resources/dms.py +48 -0
- xapiweb/resources/engagement.py +83 -0
- xapiweb/resources/follows.py +115 -0
- xapiweb/resources/lists.py +75 -0
- xapiweb/resources/media.py +150 -0
- xapiweb/resources/misc.py +164 -0
- xapiweb/resources/moderation.py +84 -0
- xapiweb/resources/notifications.py +84 -0
- xapiweb/resources/settings.py +102 -0
- xapiweb/resources/timelines.py +228 -0
- xapiweb/resources/trends.py +49 -0
- xapiweb/resources/tweets.py +288 -0
- xapiweb/resources/users.py +85 -0
- xapiweb/response.py +39 -0
- xapiweb/session.py +213 -0
- xapiweb/tests/test_offline.py +162 -0
- xapiweb-1.5.0.dist-info/METADATA +5 -0
- xapiweb-1.5.0.dist-info/RECORD +28 -0
- xapiweb-1.5.0.dist-info/WHEEL +5 -0
- xapiweb-1.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Offline self-test for xapiweb (no network). Run: python3 xapiweb/tests/test_offline.py"""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
9
|
+
|
|
10
|
+
from xapiweb import XClient, Session, Response, errors, QIDS
|
|
11
|
+
from xapiweb.resources.timelines import extract_tweets, cursors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestPack(unittest.TestCase):
|
|
15
|
+
def test_qids(self):
|
|
16
|
+
self.assertGreaterEqual(len(QIDS), 115, f"only {len(QIDS)} qids")
|
|
17
|
+
for op in ("CreateTweet", "HomeTimeline", "Followers", "SearchTimeline",
|
|
18
|
+
"DeleteRetweet", "dmBlockUser", "ViewerBadgeCounts"):
|
|
19
|
+
self.assertIn(op, QIDS)
|
|
20
|
+
|
|
21
|
+
def test_features(self):
|
|
22
|
+
s = Session(cookie="c", csrf_token="t")
|
|
23
|
+
self.assertEqual(len(s.features), 40)
|
|
24
|
+
self.assertIn("withPayments", s.field_toggles)
|
|
25
|
+
self.assertEqual(s.min_interval, 1.0)
|
|
26
|
+
|
|
27
|
+
def test_session_file(self):
|
|
28
|
+
d = {"cookie": "abc", "csrf_token": "ct0", "bearer": "B", "user_agent": "U",
|
|
29
|
+
"reference_ids": {"self_id": "123", "self_screen_name": "me"}}
|
|
30
|
+
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
|
|
31
|
+
json.dump(d, f)
|
|
32
|
+
path = f.name
|
|
33
|
+
try:
|
|
34
|
+
s = Session.from_session_file(path)
|
|
35
|
+
self.assertEqual(s.cookie, "abc")
|
|
36
|
+
self.assertEqual(s.refs["self_id"], "123")
|
|
37
|
+
c = XClient.from_session_file(path)
|
|
38
|
+
self.assertEqual(c.me_id, "123") # no network: from refs
|
|
39
|
+
finally:
|
|
40
|
+
os.unlink(path)
|
|
41
|
+
cache = os.path.join(os.path.dirname(path), ".txn_cache.json")
|
|
42
|
+
if os.path.exists(cache):
|
|
43
|
+
os.unlink(cache)
|
|
44
|
+
|
|
45
|
+
def test_response(self):
|
|
46
|
+
r = Response(url="u", status=200, headers={}, raw='{"a":{"b":[1,2]}}')
|
|
47
|
+
self.assertTrue(r.ok)
|
|
48
|
+
self.assertEqual(r.get("a", "b", 1), 2)
|
|
49
|
+
self.assertIsNone(r.get("x", "y"))
|
|
50
|
+
self.assertEqual(r.get("x", default="d"), "d")
|
|
51
|
+
self.assertIn("a", r.summary(10))
|
|
52
|
+
|
|
53
|
+
def test_http_error_map(self):
|
|
54
|
+
self.assertIsInstance(errors.http_error("u", 401, ""), errors.XAuthError)
|
|
55
|
+
self.assertIsInstance(errors.http_error("u", 429, ""), errors.XRateLimitError)
|
|
56
|
+
self.assertIsInstance(errors.http_error("u", 406, ""), errors.XMethodError)
|
|
57
|
+
self.assertIsInstance(errors.http_error("u", 422, ""), errors.XValidationError)
|
|
58
|
+
e = errors.http_error("u", 404, '{"code":34,"message":"Sorry"}')
|
|
59
|
+
self.assertIsInstance(e, errors.XTxnError)
|
|
60
|
+
self.assertIsInstance(errors.http_error("u", 404, "other"), errors.XHttpError)
|
|
61
|
+
|
|
62
|
+
def test_guard(self):
|
|
63
|
+
def resp(code, msg="m"):
|
|
64
|
+
return Response(url="u", status=200, headers={},
|
|
65
|
+
raw=json.dumps({"errors": [{"code": code, "message": msg}]}))
|
|
66
|
+
self.assertIs(errors.guard(Response(url="u", status=200, headers={}, raw='{"data":{}}'), "op").ok, True)
|
|
67
|
+
for code, exc in ((226, errors.XAutomationFlag), (37, errors.XGateError),
|
|
68
|
+
(214, errors.XBadRequest), (327, errors.XAlreadyError),
|
|
69
|
+
(344, errors.XDailyLimit), (999, errors.XApiError)):
|
|
70
|
+
with self.assertRaises(exc, msg=code):
|
|
71
|
+
errors.guard(resp(code), "op")
|
|
72
|
+
|
|
73
|
+
def test_client_composition(self):
|
|
74
|
+
c = XClient(cookie="c", csrf_token="t")
|
|
75
|
+
for name in ("tweets", "engagement", "timelines", "users", "follows", "moderation",
|
|
76
|
+
"lists", "dms", "communities", "trends", "notifications", "settings",
|
|
77
|
+
"media", "misc"):
|
|
78
|
+
self.assertTrue(hasattr(c, name), name)
|
|
79
|
+
# spot-check method surface
|
|
80
|
+
self.assertTrue(callable(c.tweets.post) and callable(c.tweets.set_reply_control))
|
|
81
|
+
self.assertTrue(callable(c.engagement.unretweet) and callable(c.dms.inbox))
|
|
82
|
+
self.assertTrue(callable(c.media.upload_image))
|
|
83
|
+
with self.assertRaises(errors.XError):
|
|
84
|
+
c.dms.send("1", "hi") # confirm=True required
|
|
85
|
+
# pure-client validation (no network: raises before any call)
|
|
86
|
+
with self.assertRaises(ValueError):
|
|
87
|
+
c.tweets.create_poll_card(["only-one"])
|
|
88
|
+
with self.assertRaises(ValueError):
|
|
89
|
+
c.tweets.create_poll_card(["a", "b", "c", "d", "e"])
|
|
90
|
+
with self.assertRaises(ValueError):
|
|
91
|
+
c.tweets.create_poll_card(["ok", "x" * 26])
|
|
92
|
+
with self.assertRaises(ValueError):
|
|
93
|
+
c.tweets.create_poll_card(["a", "b"], duration_minutes=2)
|
|
94
|
+
with self.assertRaises(ValueError):
|
|
95
|
+
c.tweets.post_thread([])
|
|
96
|
+
with self.assertRaises(errors.XError):
|
|
97
|
+
c.dms.set_nsfw_filter(confirm=False)
|
|
98
|
+
with self.assertRaises(errors.XError):
|
|
99
|
+
c.follows.remove_follower("1")
|
|
100
|
+
|
|
101
|
+
def test_extract_tweets(self):
|
|
102
|
+
data = {"data": {"home": {"home_timeline_urt": {"instructions": [
|
|
103
|
+
{"entries": [
|
|
104
|
+
{"content": {"itemContent": {"tweet_results": {"result": {
|
|
105
|
+
"rest_id": "1",
|
|
106
|
+
"views": {"count": "99", "state": "EnabledWithCount"},
|
|
107
|
+
"legacy": {"full_text": "hi", "retweeted": False, "favorited": True,
|
|
108
|
+
"retweet_count": 2, "favorite_count": 3},
|
|
109
|
+
"core": {"user_results": {"result": {"legacy": {"screen_name": "bob"}}}}}}}}},
|
|
110
|
+
{"content": {"cursorType": "Bottom", "value": "CUR123"}}]}]}}}}
|
|
111
|
+
tw = extract_tweets(data)
|
|
112
|
+
self.assertEqual(len(tw), 1)
|
|
113
|
+
self.assertEqual(tw[0]["author"], "bob")
|
|
114
|
+
self.assertEqual(tw[0]["text"], "hi")
|
|
115
|
+
self.assertEqual(tw[0]["views"], 99)
|
|
116
|
+
self.assertEqual(cursors(data)["bottom"], "CUR123")
|
|
117
|
+
|
|
118
|
+
def test_parse_media(self):
|
|
119
|
+
from xapiweb.resources.media import parse_media_entities
|
|
120
|
+
leg = {"extended_entities": {"media": [
|
|
121
|
+
{"type": "photo", "id_str": "1",
|
|
122
|
+
"media_url_https": "https://pbs.twimg.com/media/a.jpg",
|
|
123
|
+
"original_info": {"width": 100, "height": 50}},
|
|
124
|
+
{"type": "video", "id_str": "2",
|
|
125
|
+
"media_url_https": "https://pbs.twimg.com/thumb.jpg",
|
|
126
|
+
"video_info": {"duration_millis": 5000, "variants": [
|
|
127
|
+
{"content_type": "application/x-mpegURL", "url": "https://x.m3u8"},
|
|
128
|
+
{"content_type": "video/mp4", "bitrate": 100, "url": "https://low.mp4"},
|
|
129
|
+
{"content_type": "video/mp4", "bitrate": 900, "url": "https://hi.mp4"}]}}]}}
|
|
130
|
+
m = parse_media_entities(leg)
|
|
131
|
+
self.assertEqual(len(m), 2)
|
|
132
|
+
self.assertEqual(m[0]["url"], "https://pbs.twimg.com/media/a.jpg")
|
|
133
|
+
self.assertEqual(m[1]["url"], "https://hi.mp4") # max bitrate mp4
|
|
134
|
+
self.assertEqual(m[1]["bitrate"], 900)
|
|
135
|
+
self.assertEqual(parse_media_entities({}), [])
|
|
136
|
+
|
|
137
|
+
def test_extract_notifications(self):
|
|
138
|
+
from xapiweb.resources.notifications import extract_notifications
|
|
139
|
+
data = {"data": {"viewer_v2": {"user_results": {"result": {"notification_timeline": {
|
|
140
|
+
"timeline": {"instructions": [{"entries": [
|
|
141
|
+
{"entryId": "notification-1", "content": {
|
|
142
|
+
"clientEventInfo": {"element": "users_liked_your_tweet"},
|
|
143
|
+
"itemContent": {
|
|
144
|
+
"__typename": "TimelineNotification", "id": "n1",
|
|
145
|
+
"notification_icon": "heart_icon",
|
|
146
|
+
"notification_url": {"url": "https://twitter.com/me/status/123"},
|
|
147
|
+
"timestamp_ms": "2026-09-20T01:02:03.000Z",
|
|
148
|
+
"rich_message": {"text": "Bob liked your post", "entities": [
|
|
149
|
+
{"ref": {"user_results": {"result": {
|
|
150
|
+
"core": {"screen_name": "bob", "name": "Bob"}}}}}]}}}},
|
|
151
|
+
{"entryId": "cursor-bottom", "content": {"itemContent": {
|
|
152
|
+
"__typename": "TimelineTimelineCursor", "cursorType": "Bottom",
|
|
153
|
+
"value": "BOT123"}}}]}]}}}}}}}
|
|
154
|
+
n = extract_notifications(data)
|
|
155
|
+
self.assertEqual(len(n), 1)
|
|
156
|
+
self.assertEqual(n[0]["type"], "users_liked_your_tweet")
|
|
157
|
+
self.assertEqual(n[0]["tweet_id"], "123")
|
|
158
|
+
self.assertEqual(n[0]["users"][0]["screen_name"], "bob")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
unittest.main(verbosity=2)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
xapiweb/__init__.py,sha256=fI8t23chKJS6qETWoP4B3UVKsAGSSdAlnkEFd4ND7e0,434
|
|
2
|
+
xapiweb/_txn.py,sha256=Ouk55PToRXqWSqmLe7NMa1TTG3hvWISFg12p9N86cN0,9413
|
|
3
|
+
xapiweb/client.py,sha256=MCvL9_MeNEv7ODJM96I3ijiXy02rslpZw1YPMqpT8QY,1942
|
|
4
|
+
xapiweb/errors.py,sha256=uitoh_TMF4DIEHdHiYuOvxCdreBasx251UDzIWH2JB0,3590
|
|
5
|
+
xapiweb/qids.py,sha256=AbZ21U5NIfUcxYWU0WiALPtTfdgo0lZQxxO63BATANM,6671
|
|
6
|
+
xapiweb/response.py,sha256=1kBGFq2cRdNkhiuIJuZSZFU9bYHTuq7BgF-bkiw2jMY,1041
|
|
7
|
+
xapiweb/session.py,sha256=JjzRYVl8tV-zMV4kp54-GZL3oYeWIz1_2wPjZuTLI1w,9375
|
|
8
|
+
xapiweb/resources/__init__.py,sha256=b9mPa78mjB5dbiQ6uKajv2Ca11jTdBSTGfWPToliNNA,700
|
|
9
|
+
xapiweb/resources/_base.py,sha256=nnl_IrsyXDWauetGw4tNXX9oVmUdDZ6esrSZnk7Md4M,244
|
|
10
|
+
xapiweb/resources/communities.py,sha256=GUwLtHs-59hsx8U84VboPJ7ACm90MlO8GubEYN85d28,1658
|
|
11
|
+
xapiweb/resources/dms.py,sha256=_9MOx53IN2iA1w0jGhsT5St8MfLvGPWdwV-jVtzWASA,2236
|
|
12
|
+
xapiweb/resources/engagement.py,sha256=QvJu6PkdhqlZ4Sw28rWGC1rQQwXsT3xrP2scck_QzJ8,3993
|
|
13
|
+
xapiweb/resources/follows.py,sha256=LXQu5LyzS6AFTWY5AG47OgHCki8cya3FdDUlX-2x4QE,5905
|
|
14
|
+
xapiweb/resources/lists.py,sha256=ySTSX9iei_pKVgV7Dar6zvg7tJ5rUWFhP9vhYpYix18,3640
|
|
15
|
+
xapiweb/resources/media.py,sha256=1zMXQOZuRGo35f6ULAHUnVo3KbZh8MxNYhWPe-JhR8I,7664
|
|
16
|
+
xapiweb/resources/misc.py,sha256=hziFS8xvLz06ZkmhJhanBQgzShUoBoBKAEbTi2_DFx8,7944
|
|
17
|
+
xapiweb/resources/moderation.py,sha256=dQ2w5_Kb8zoCqqYDu1Rt97GCHlP1RpHOav641ph6wpQ,3654
|
|
18
|
+
xapiweb/resources/notifications.py,sha256=OzV480z0HbHeEl7ifCiU1MFpgJWpWrMIyplab8F-Mx0,3925
|
|
19
|
+
xapiweb/resources/settings.py,sha256=8jM7LJmJyIuvTCk7oziFAjFS-FuSQJ-qJrFyR5tAivE,4786
|
|
20
|
+
xapiweb/resources/timelines.py,sha256=VGcV66VBOPboTRGqTc7vFGFDahTl8Rz2i434dy3oGWU,10365
|
|
21
|
+
xapiweb/resources/trends.py,sha256=NsmKTdVdyalmIOp08RbV-eUrCTsF8SwRTfwvaMF0iVc,1947
|
|
22
|
+
xapiweb/resources/tweets.py,sha256=ADNID5_kZLLRZork-QY9z1BUct4L_iGo3F-KKkWumyg,14049
|
|
23
|
+
xapiweb/resources/users.py,sha256=wTgO7S9jUmXezC7YBn7ZU6i7z1csk9WgmRWvblcN4qc,3794
|
|
24
|
+
xapiweb/tests/test_offline.py,sha256=h4MX7MNZZpCbDyryTuCQ4Dmmjk6TDa_WyChs-cYSQwE,8331
|
|
25
|
+
xapiweb-1.5.0.dist-info/METADATA,sha256=sowAMclIo5ty7lb5WOAggnLkgl5qCsGtyByXMrkao8A,160
|
|
26
|
+
xapiweb-1.5.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
27
|
+
xapiweb-1.5.0.dist-info/top_level.txt,sha256=gWk9igsaBanNnfSbWoi7VUQ30oBFiL7pfjl-bvHUicQ,8
|
|
28
|
+
xapiweb-1.5.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xapiweb
|