arcade-x 0.1.6__py3-none-any.whl → 0.1.16__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,11 +1,14 @@
1
- from typing import Annotated, Any, Optional
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,
@@ -13,9 +16,6 @@ from arcade_x.tools.utils import (
13
16
  remove_none_values,
14
17
  )
15
18
 
16
- TWEETS_URL = "https://api.x.com/2/tweets"
17
-
18
-
19
19
  # Manage Tweets Tools. See developer docs for additional available parameters:
20
20
  # https://developer.x.com/en/docs/x-api/tweets/manage-tweets/api-reference
21
21
 
@@ -67,26 +67,26 @@ async def search_recent_tweets_by_username(
67
67
  int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
68
68
  ] = 10,
69
69
  next_token: Annotated[
70
- Optional[str], "The pagination token starting from which to return results"
70
+ str | None, "The pagination token starting from which to return results"
71
71
  ] = None,
72
72
  ) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
73
73
  """Search for recent tweets (last 7 days) on X (Twitter) by username.
74
74
  Includes replies and reposts."""
75
75
 
76
76
  headers = get_headers_with_token(context)
77
- params: dict[str, int | str] = {
77
+ params: dict[str, Any] = {
78
78
  "query": f"from:{username}",
79
79
  "max_results": min(
80
80
  max(max_results, 10), 100
81
81
  ), # X API does not allow 'max_results' less than 10 or greater than 100
82
82
  "next_token": next_token,
83
+ "expansions": "author_id",
84
+ "user.fields": "id,name,username,entities",
85
+ "tweet.fields": "entities,note_tweet",
83
86
  }
84
- params = remove_none_values(params)
87
+ params = expand_attached_media(remove_none_values(params))
85
88
 
86
- url = (
87
- "https://api.x.com/2/tweets/search/recent?"
88
- "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
89
- )
89
+ url = f"{TWEETS_URL}/search/recent"
90
90
 
91
91
  async with httpx.AsyncClient() as client:
92
92
  response = await client.get(url, headers=headers, params=params, timeout=10)
@@ -94,6 +94,9 @@ async def search_recent_tweets_by_username(
94
94
 
95
95
  response_data: dict[str, Any] = response.json()
96
96
 
97
+ for tweet in response_data.get("data", []):
98
+ expand_long_tweet(tweet)
99
+
97
100
  # Expand the URLs that are in the tweets
98
101
  response_data["data"] = expand_urls_in_tweets(
99
102
  response_data.get("data", []), delete_entities=True
@@ -118,7 +121,7 @@ async def search_recent_tweets_by_keywords(
118
121
  int, "The maximum number of results to return. Must be in range [1, 100] inclusive"
119
122
  ] = 10,
120
123
  next_token: Annotated[
121
- Optional[str], "The pagination token starting from which to return results"
124
+ str | None, "The pagination token starting from which to return results"
122
125
  ] = None,
123
126
  ) -> Annotated[dict[str, Any], "Dictionary containing the search results"]:
124
127
  """
@@ -128,7 +131,7 @@ async def search_recent_tweets_by_keywords(
128
131
  """
129
132
 
130
133
  if not any([keywords, phrases]):
131
- raise RetryableToolError( # noqa: TRY003
134
+ raise RetryableToolError(
132
135
  "No keywords or phrases provided",
133
136
  developer_message="Predicted inputs didn't contain any keywords or phrases",
134
137
  additional_prompt_content="Please provide at least one keyword or phrase for search",
@@ -141,19 +144,19 @@ async def search_recent_tweets_by_keywords(
141
144
  if keywords:
142
145
  query += " ".join(keywords or [])
143
146
 
144
- params: dict[str, int | str] = {
147
+ params: dict[str, Any] = {
145
148
  "query": query.strip(),
146
149
  "max_results": min(
147
150
  max(max_results, 10), 100
148
151
  ), # X API does not allow 'max_results' less than 10 or greater than 100
149
152
  "next_token": next_token,
153
+ "expansions": "author_id",
154
+ "user.fields": "id,name,username,entities",
155
+ "tweet.fields": "entities,note_tweet",
150
156
  }
151
- params = remove_none_values(params)
157
+ params = expand_attached_media(remove_none_values(params))
152
158
 
153
- url = (
154
- "https://api.x.com/2/tweets/search/recent?"
155
- "expansions=author_id&user.fields=id,name,username,entities&tweet.fields=entities"
156
- )
159
+ url = f"{TWEETS_URL}/search/recent"
157
160
 
158
161
  async with httpx.AsyncClient() as client:
159
162
  response = await client.get(url, headers=headers, params=params, timeout=10)
@@ -161,6 +164,9 @@ async def search_recent_tweets_by_keywords(
161
164
 
162
165
  response_data: dict[str, Any] = response.json()
163
166
 
167
+ for tweet in response_data.get("data", []):
168
+ expand_long_tweet(tweet)
169
+
164
170
  # Expand the URLs that are in the tweets
165
171
  response_data["data"] = expand_urls_in_tweets(
166
172
  response_data.get("data", []), delete_entities=True
@@ -183,8 +189,10 @@ async def lookup_tweet_by_id(
183
189
  params = {
184
190
  "expansions": "author_id",
185
191
  "user.fields": "id,name,username,entities",
186
- "tweet.fields": "entities",
192
+ "tweet.fields": "entities,note_tweet",
187
193
  }
194
+ params = expand_attached_media(params)
195
+
188
196
  url = f"{TWEETS_URL}/{tweet_id}"
189
197
 
190
198
  async with httpx.AsyncClient() as client:
@@ -196,6 +204,8 @@ async def lookup_tweet_by_id(
196
204
  # Get the tweet data
197
205
  tweet_data = response_data.get("data")
198
206
  if tweet_data:
207
+ expand_long_tweet(tweet_data)
208
+
199
209
  # Expand the URLs that are in the tweet
200
210
  expanded_tweet_list = expand_urls_in_tweets([tweet_data], delete_entities=True)
201
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
 
@@ -35,7 +38,9 @@ def parse_search_recent_tweets_response(response_data: dict[str, Any]) -> dict[s
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]]:
@@ -66,9 +82,10 @@ def expand_urls_in_tweets(
66
82
  new_tweet = tweet_data.copy()
67
83
  if "entities" in new_tweet and "urls" in new_tweet["entities"]:
68
84
  for url_entity in new_tweet["entities"]["urls"]:
69
- short_url = url_entity["url"]
70
- expanded_url = url_entity["expanded_url"]
71
- new_tweet["text"] = new_tweet["text"].replace(short_url, expanded_url)
85
+ if "url" in url_entity and "expanded_url" in url_entity:
86
+ short_url = url_entity["url"]
87
+ expanded_url = url_entity["expanded_url"]
88
+ new_tweet["text"] = new_tweet["text"].replace(short_url, expanded_url)
72
89
 
73
90
  if delete_entities:
74
91
  new_tweet.pop("entities", None)
@@ -123,3 +140,24 @@ def remove_none_values(params: dict) -> dict:
123
140
  A new dictionary with None values removed
124
141
  """
125
142
  return {k: v for k, v in params.items() if v is not None}
143
+
144
+
145
+ def expand_attached_media(params: dict) -> dict:
146
+ """
147
+ Include attached media metadata in the request parameters.
148
+ """
149
+ params["expansions"] += ",attachments.media_keys"
150
+ params["tweet.fields"] += ",attachments"
151
+ params["media.fields"] = ",".join([
152
+ # media_key, url and type are returned by default, added here for clarity
153
+ "media_key",
154
+ "url",
155
+ "type",
156
+ "duration_ms",
157
+ "height",
158
+ "width",
159
+ "preview_image_url",
160
+ "alt_text",
161
+ "public_metrics",
162
+ ])
163
+ return params
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcade_x
3
+ Version: 0.1.16
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=wxPdp3JDG-gEDM7XUNiZSgL61isVE3SRqaLiwYP-K40,5461
7
+ arcade_x-0.1.16.dist-info/METADATA,sha256=7ox9l5R61ZAaxf46r2Bwib5vxmr1OrWB19LSldl_k2Q,835
8
+ arcade_x-0.1.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ arcade_x-0.1.16.dist-info/licenses/LICENSE,sha256=f4Q0XUZJ2MqZBO1XsqqHhuZfSs2ar1cZEJ45150zERo,1067
10
+ arcade_x-0.1.16.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.6
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.6)
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=VchdglDVI15rsEyZuBLIj6qjGql0-UO8TdY6ylGl4yA,7158
4
- arcade_x/tools/users.py,sha256=zNACU9UhQJkGNnrei-E6GWvFnbDzg0YG9pkdplu8TzM,2148
5
- arcade_x/tools/utils.py,sha256=YKRGXcUPKcPnIvmiNF2-0jB4edxFmFZLJ90OQNR-DGk,4366
6
- arcade_x-0.1.6.dist-info/LICENSE,sha256=SphQPbiNmBD1J6yJ7oxG9bIZDbj8lNHKSv5Kl86zA40,1066
7
- arcade_x-0.1.6.dist-info/METADATA,sha256=5eq4z7KaY2GSDY8J-KLUDRdibTsCsHpt29AgW7qlSac,510
8
- arcade_x-0.1.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
9
- arcade_x-0.1.6.dist-info/RECORD,,