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.
@@ -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,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: xapiweb
3
+ Version: 1.5.0
4
+ Summary: Unified Python library for the X web API (stdlib only, 157 proven endpoints)
5
+ Requires-Python: >=3.8
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ xapiweb