arcade-x 0.1.5__py3-none-any.whl → 0.1.15__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 @@
1
+ TWEETS_URL = "https://api.x.com/2/tweets"
arcade_x/tools/tweets.py CHANGED
@@ -1,20 +1,21 @@
1
1
  from typing import Annotated, Any
2
2
 
3
3
  import httpx
4
- from arcade.sdk import ToolContext, tool
5
- from arcade.sdk.auth import X
6
- from arcade.sdk.errors import RetryableToolError
4
+ from arcade_tdk import ToolContext, tool
5
+ from arcade_tdk.auth import X
6
+ from arcade_tdk.errors import RetryableToolError
7
7
 
8
+ from arcade_x.tools.constants import TWEETS_URL
8
9
  from arcade_x.tools.utils import (
10
+ expand_attached_media,
11
+ expand_long_tweet,
9
12
  expand_urls_in_tweets,
10
13
  get_headers_with_token,
11
14
  get_tweet_url,
12
15
  parse_search_recent_tweets_response,
16
+ remove_none_values,
13
17
  )
14
18
 
15
- TWEETS_URL = "https://api.x.com/2/tweets"
16
-
17
-
18
19
  # Manage Tweets Tools. See developer docs for additional available parameters:
19
20
  # https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference
20
21
 
@@ -63,21 +64,29 @@ async def search_recent_tweets_by_username(
63
64
  context: ToolContext,
64
65
  username: Annotated[str, "The username of the X (Twitter) user to look up"],
65
66
  max_results: Annotated[
66
- int, "The maximum number of results to return. Cannot be less than 10"
67
+ int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
67
68
  ] = 10,
69
+ next_token: Annotated[
70
+ str | None, "The pagination token starting from which to return results"
71
+ ] = None,
68
72
  ) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
69
73
  """Search for recent tweets (last 7 days) on X (Twitter) by username.
70
74
  Includes replies and reposts."""
71
75
 
72
76
  headers = get_headers_with_token(context)
73
- params: dict[str, int | str] = {
77
+ params: dict[str, Any] = {
74
78
  "query": f"from:{username}",
75
- "max_results": max(max_results, 10), # X API does not allow 'max_results' less than 10
79
+ "max_results": min(
80
+ max(max_results, 10), 100
81
+ ), # X API does not allow 'max_results' less than 10 or greater than 100
82
+ "next_token": next_token,
83
+ "expansions": "author_id",
84
+ "user.fields": "id,name,username,entities",
85
+ "tweet.fields": "entities,note_tweet",
76
86
  }
77
- url = (
78
- "https://api.x.com/2/tweets/search/recent?"
79
- "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
80
- )
87
+ params = expand_attached_media(remove_none_values(params))
88
+
89
+ url = f"{TWEETS_URL}/search/recent"
81
90
 
82
91
  async with httpx.AsyncClient() as client:
83
92
  response = await client.get(url, headers=headers, params=params, timeout=10)
@@ -85,6 +94,9 @@ async def search_recent_tweets_by_username(
85
94
 
86
95
  response_data: dict[str, Any] = response.json()
87
96
 
97
+ for tweet in response_data.get("data", []):
98
+ expand_long_tweet(tweet)
99
+
88
100
  # Expand the URLs that are in the tweets
89
101
  response_data["data"] = expand_urls_in_tweets(
90
102
  response_data.get("data", []), delete_entities=True
@@ -106,8 +118,11 @@ async def search_recent_tweets_by_keywords(
106
118
  list[str] | None, "List of phrases that must be present in the tweet"
107
119
  ] = None,
108
120
  max_results: Annotated[
109
- int, "The maximum number of results to return. Cannot be less than 10"
121
+ int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
110
122
  ] = 10,
123
+ next_token: Annotated[
124
+ str | None, "The pagination token starting from which to return results"
125
+ ] = None,
111
126
  ) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
112
127
  """
113
128
  Search for recent tweets (last 7 days) on X (Twitter) by required keywords and phrases.
@@ -116,7 +131,7 @@ async def search_recent_tweets_by_keywords(
116
131
  """
117
132
 
118
133
  if not any([keywords, phrases]):
119
- raise RetryableToolError( # noqa: TRY003
134
+ raise RetryableToolError(
120
135
  "No keywords or phrases provided",
121
136
  developer_message="Predicted inputs didn't contain any keywords or phrases",
122
137
  additional_prompt_content="Please provide at least one keyword or phrase for search",
@@ -129,14 +144,19 @@ async def search_recent_tweets_by_keywords(
129
144
  if keywords:
130
145
  query += " ".join(keywords or [])
131
146
 
132
- params: dict[str, int | str] = {
147
+ params: dict[str, Any] = {
133
148
  "query": query.strip(),
134
- "max_results": max(max_results, 10), # X API does not allow 'max_results' less than 10
149
+ "max_results": min(
150
+ max(max_results, 10), 100
151
+ ), # X API does not allow 'max_results' less than 10 or greater than 100
152
+ "next_token": next_token,
153
+ "expansions": "author_id",
154
+ "user.fields": "id,name,username,entities",
155
+ "tweet.fields": "entities,note_tweet",
135
156
  }
136
- url = (
137
- "https://api.x.com/2/tweets/search/recent?"
138
- "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
139
- )
157
+ params = expand_attached_media(remove_none_values(params))
158
+
159
+ url = f"{TWEETS_URL}/search/recent"
140
160
 
141
161
  async with httpx.AsyncClient() as client:
142
162
  response = await client.get(url, headers=headers, params=params, timeout=10)
@@ -144,6 +164,9 @@ async def search_recent_tweets_by_keywords(
144
164
 
145
165
  response_data: dict[str, Any] = response.json()
146
166
 
167
+ for tweet in response_data.get("data", []):
168
+ expand_long_tweet(tweet)
169
+
147
170
  # Expand the URLs that are in the tweets
148
171
  response_data["data"] = expand_urls_in_tweets(
149
172
  response_data.get("data", []), delete_entities=True
@@ -166,8 +189,10 @@ async def lookup_tweet_by_id(
166
189
  params = {
167
190
  "expansions": "author_id",
168
191
  "user.fields": "id,name,username,entities",
169
- "tweet.fields": "entities",
192
+ "tweet.fields": "entities,note_tweet",
170
193
  }
194
+ params = expand_attached_media(params)
195
+
171
196
  url = f"{TWEETS_URL}/{tweet_id}"
172
197
 
173
198
  async with httpx.AsyncClient() as client:
@@ -179,6 +204,8 @@ async def lookup_tweet_by_id(
179
204
  # Get the tweet data
180
205
  tweet_data = response_data.get("data")
181
206
  if tweet_data:
207
+ expand_long_tweet(tweet_data)
208
+
182
209
  # Expand the URLs that are in the tweet
183
210
  expanded_tweet_list = expand_urls_in_tweets([tweet_data], delete_entities=True)
184
211
  response_data["data"] = expanded_tweet_list[0]
arcade_x/tools/users.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from typing import Annotated
2
2
 
3
3
  import httpx
4
- from arcade.sdk import ToolContext, tool
5
- from arcade.sdk.auth import X
6
- from arcade.sdk.errors import RetryableToolError
4
+ from arcade_tdk import ToolContext, tool
5
+ from arcade_tdk.auth import X
6
+ from arcade_tdk.errors import RetryableToolError
7
7
 
8
8
  from arcade_x.tools.utils import (
9
9
  expand_urls_in_user_description,
@@ -48,7 +48,7 @@ async def lookup_single_user_by_username(
48
48
  response = await client.get(url, headers=headers, timeout=10)
49
49
  if response.status_code == 404:
50
50
  # User not found
51
- raise RetryableToolError( # noqa: TRY003
51
+ raise RetryableToolError(
52
52
  "User not found",
53
53
  developer_message=f"User with username '{username}' not found.",
54
54
  additional_prompt_content="Please check the username and try again.",
arcade_x/tools/utils.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import Any
2
2
 
3
- from arcade.sdk import ToolContext
4
- from arcade.sdk.errors import ToolExecutionError
3
+ from arcade_tdk import ToolContext
4
+ from arcade_tdk.errors import ToolExecutionError
5
5
 
6
6
 
7
7
  def get_tweet_url(tweet_id: str) -> str:
@@ -12,12 +12,15 @@ def get_tweet_url(tweet_id: str) -> str:
12
12
  def get_headers_with_token(context: ToolContext) -> dict[str, str]:
13
13
  """Get the headers for a request to the X API."""
14
14
  if context.authorization is None or context.authorization.token is None:
15
- raise ToolExecutionError( # noqa: TRY003
15
+ raise ToolExecutionError(
16
16
  "Missing Token. Authorization is required to post a tweet.",
17
17
  developer_message="Token is not set in the ToolContext.",
18
18
  )
19
+ token = (
20
+ context.authorization.token if context.authorization and context.authorization.token else ""
21
+ )
19
22
  return {
20
- "Authorization": f"Bearer {context.authorization.token}",
23
+ "Authorization": f"Bearer {token}",
21
24
  "Content-Type": "application/json",
22
25
  }
23
26
 
@@ -28,14 +31,16 @@ def parse_search_recent_tweets_response(response_data: dict[str, Any]) -> dict[s
28
31
  Returns the modified response data with added 'tweet_url', 'author_username', and 'author_name'.
29
32
  """
30
33
  if not sanity_check_tweets_data(response_data):
31
- return {"data": []}
34
+ return {"data": [], "next_token": ""}
32
35
 
33
36
  # Add 'tweet_url' to each tweet
34
37
  for tweet in response_data["data"]:
35
38
  tweet["tweet_url"] = get_tweet_url(tweet["id"])
36
39
 
37
40
  # Add 'author_username' and 'author_name' to each tweet
38
- for tweet_data, user_data in zip(response_data["data"], response_data["includes"]["users"]):
41
+ for tweet_data, user_data in zip(
42
+ response_data["data"], response_data["includes"]["users"], strict=False
43
+ ):
39
44
  tweet_data["author_username"] = user_data["username"]
40
45
  tweet_data["author_name"] = user_data["name"]
41
46
 
@@ -55,6 +60,17 @@ def sanity_check_tweets_data(tweets_data: dict[str, Any]) -> bool:
55
60
  return True
56
61
 
57
62
 
63
+ def expand_long_tweet(tweet_data: dict[str, Any]) -> None:
64
+ """Expand a long tweet.
65
+
66
+ For tweets exceeding 280 characters,
67
+ replace the truncated tweet text with the full tweet text.
68
+ """
69
+ if tweet_data.get("note_tweet"):
70
+ tweet_data["text"] = tweet_data["note_tweet"]["text"]
71
+ del tweet_data["note_tweet"]
72
+
73
+
58
74
  def expand_urls_in_tweets(
59
75
  tweets_data: list[dict[str, Any]], delete_entities: bool = True
60
76
  ) -> list[dict[str, Any]]:
@@ -110,3 +126,37 @@ def expand_urls_in_user_url(user_data: dict, delete_entities: bool = True) -> di
110
126
  if delete_entities:
111
127
  new_user_data.pop("entities", None)
112
128
  return new_user_data
129
+
130
+
131
+ def remove_none_values(params: dict) -> dict:
132
+ """
133
+ Remove key/value pairs with None values from a dictionary.
134
+
135
+ Args:
136
+ params: The dictionary to clean
137
+
138
+ Returns:
139
+ A new dictionary with None values removed
140
+ """
141
+ return {k: v for k, v in params.items() if v is not None}
142
+
143
+
144
+ def expand_attached_media(params: dict) -> dict:
145
+ """
146
+ Include attached media metadata in the request parameters.
147
+ """
148
+ params["expansions"] += ",attachments.media_keys"
149
+ params["tweet.fields"] += ",attachments"
150
+ params["media.fields"] = ",".join([
151
+ # media_key, url and type are returned by default, added here for clarity
152
+ "media_key",
153
+ "url",
154
+ "type",
155
+ "duration_ms",
156
+ "height",
157
+ "width",
158
+ "preview_image_url",
159
+ "alt_text",
160
+ "public_metrics",
161
+ ])
162
+ return params
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcade_x
3
+ Version: 0.1.15
4
+ Summary: Arcade.dev LLM tools for X (Twitter)
5
+ Author-email: Arcade <dev@arcade.dev>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: arcade-tdk<3.0.0,>=2.0.0
9
+ Requires-Dist: httpx<1.0.0,>=0.27.2
10
+ Provides-Extra: dev
11
+ Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.0; extra == 'dev'
12
+ Requires-Dist: arcade-serve<3.0.0,>=2.0.0; extra == 'dev'
13
+ Requires-Dist: mypy<1.6.0,>=1.5.1; extra == 'dev'
14
+ Requires-Dist: pre-commit<3.5.0,>=3.4.0; extra == 'dev'
15
+ Requires-Dist: pytest-asyncio<0.25.0,>=0.24.0; extra == 'dev'
16
+ Requires-Dist: pytest-cov<4.1.0,>=4.0.0; extra == 'dev'
17
+ Requires-Dist: pytest-mock<3.12.0,>=3.11.1; extra == 'dev'
18
+ Requires-Dist: pytest<8.4.0,>=8.3.0; extra == 'dev'
19
+ Requires-Dist: ruff<0.8.0,>=0.7.4; extra == 'dev'
20
+ Requires-Dist: tox<4.12.0,>=4.11.1; extra == 'dev'
@@ -0,0 +1,10 @@
1
+ arcade_x/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ arcade_x/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ arcade_x/tools/constants.py,sha256=d-OJK5Qx05JRUcpK5G1DcYB91wT37hFSzGmIYfwlEtA,42
4
+ arcade_x/tools/tweets.py,sha256=S1Psv-gFePz-8ibnp7rY-GCdogkAm-hJQZpHf7hUusk,7498
5
+ arcade_x/tools/users.py,sha256=gopYKTBOwbZAeVdL_T3vwkepbpv1bdPbqcU_2oPC3SY,2132
6
+ arcade_x/tools/utils.py,sha256=Xx4S5bq0udUwSiodf5cs19ftgWHROO-scpY3RY-Fam8,5376
7
+ arcade_x-0.1.15.dist-info/METADATA,sha256=4Y_NCBB8UOHDcKEdJMGGWVV1FfsEMkluR3ebobt0OwE,835
8
+ arcade_x-0.1.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ arcade_x-0.1.15.dist-info/licenses/LICENSE,sha256=f4Q0XUZJ2MqZBO1XsqqHhuZfSs2ar1cZEJ45150zERo,1067
10
+ arcade_x-0.1.15.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.1
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024, arcadeai
3
+ Copyright (c) 2025, Arcade AI
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,14 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: arcade_x
3
- Version: 0.1.5
4
- Summary: LLM tools for interacting with X (Twitter)
5
- Author: Arcade AI
6
- Author-email: dev@arcade-ai.com
7
- Requires-Python: >=3.10,<4.0
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.10
10
- Classifier: Programming Language :: Python :: 3.11
11
- Classifier: Programming Language :: Python :: 3.12
12
- Classifier: Programming Language :: Python :: 3.13
13
- Requires-Dist: arcade-ai (==0.1.5)
14
- Requires-Dist: httpx (>=0.27.2,<0.28.0)
@@ -1,9 +0,0 @@
1
- arcade_x/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- arcade_x/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- arcade_x/tools/tweets.py,sha256=IDAjx1j_XLoc7ILyuRKVyjTPwQeNtD47lnyKUMO-WpU,6594
4
- arcade_x/tools/users.py,sha256=zNACU9UhQJkGNnrei-E6GWvFnbDzg0YG9pkdplu8TzM,2148
5
- arcade_x/tools/utils.py,sha256=VEeUo715m6l6X9Syszy2E72rdvv4h_tc-P7wMaQEbGo,4044
6
- arcade_x-0.1.5.dist-info/LICENSE,sha256=SphQPbiNmBD1J6yJ7oxG9bIZDbj8lNHKSv5Kl86zA40,1066
7
- arcade_x-0.1.5.dist-info/METADATA,sha256=pa4GiYU352AShVYdd2rDjVa3AP5b1sFtJpi1gUihQmM,510
8
- arcade_x-0.1.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
9
- arcade_x-0.1.5.dist-info/RECORD,,