arcade-x 1.1.1__py3-none-any.whl → 1.3.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.
arcade_x/tools/tweets.py CHANGED
@@ -26,7 +26,24 @@ async def _post_tweet(context: ToolContext, payload: dict) -> str:
26
26
  async with httpx.AsyncClient() as client:
27
27
  response = await client.post(TWEETS_URL, headers=headers, json=payload, timeout=10)
28
28
  response.raise_for_status()
29
- tweet_id = response.json()["data"]["id"]
29
+
30
+ response_json = response.json()
31
+
32
+ # Check if data exists in response
33
+ if "data" not in response_json or response_json.get("data") is None:
34
+ return (
35
+ "The post was successfully created, but the X API returned an unexpected response. "
36
+ f"The X API returned:\n {response_json}"
37
+ )
38
+
39
+ tweet_data = response_json["data"]
40
+ if "id" not in tweet_data:
41
+ return (
42
+ "The post was successfully created, but the X API returned an unexpected response. "
43
+ f"The X API returned:\n {response_json}"
44
+ )
45
+
46
+ tweet_id = tweet_data["id"]
30
47
  return f"Tweet with id {tweet_id} posted successfully. URL: {get_tweet_url(tweet_id)}"
31
48
 
32
49
 
@@ -40,8 +57,8 @@ async def post_tweet(
40
57
  tweet_text: Annotated[str, "The text content of the tweet you want to post"],
41
58
  quote_tweet_id: Annotated[
42
59
  str | None,
43
- "The ID of the tweet you want to quote."
44
- " It must be a valid integer as a string. Optional.",
60
+ "The ID of the tweet you want to quote. "
61
+ "It must be a valid integer as a string. Default is None.",
45
62
  ] = None,
46
63
  ) -> Annotated[str, "Success string and the URL of the tweet"]:
47
64
  """Post a tweet to X (Twitter).
@@ -0,0 +1,76 @@
1
+ """User context tools for X (Twitter) toolkit."""
2
+
3
+ from typing import Annotated
4
+
5
+ import httpx
6
+ from arcade_tdk import ToolContext, tool
7
+ from arcade_tdk.auth import X
8
+ from arcade_tdk.errors import ToolExecutionError
9
+
10
+ from arcade_x.tools.utils import (
11
+ expand_urls_in_user_description,
12
+ expand_urls_in_user_url,
13
+ get_headers_with_token,
14
+ )
15
+
16
+
17
+ @tool(requires_auth=X(scopes=["users.read", "tweet.read"]))
18
+ async def who_am_i(
19
+ context: ToolContext,
20
+ ) -> Annotated[dict, "Authenticated user's profile information"]:
21
+ """
22
+ Get information about the authenticated X (Twitter) user.
23
+
24
+ Returns the current user's profile including their username, name, description,
25
+ follower counts, and other account information.
26
+ """
27
+ headers = get_headers_with_token(context)
28
+
29
+ # User fields to retrieve
30
+ user_fields = ",".join([
31
+ "created_at",
32
+ "description",
33
+ "id",
34
+ "location",
35
+ "most_recent_tweet_id",
36
+ "name",
37
+ "pinned_tweet_id",
38
+ "profile_image_url",
39
+ "protected",
40
+ "public_metrics",
41
+ "url",
42
+ "username",
43
+ "verified",
44
+ "verified_type",
45
+ "withheld",
46
+ "entities",
47
+ ])
48
+
49
+ # Use the /2/users/me endpoint to get authenticated user's information
50
+ # API Reference: https://developer.x.com/en/docs/x-api/users/lookup/api-reference/get-users-me
51
+ # Returns user object with fields: id, name, username, and optional expansions
52
+ url = f"https://api.x.com/2/users/me?user.fields={user_fields}"
53
+
54
+ try:
55
+ async with httpx.AsyncClient() as client:
56
+ response = await client.get(url, headers=headers, timeout=10)
57
+ response.raise_for_status()
58
+ except httpx.HTTPStatusError as e:
59
+ raise ToolExecutionError(
60
+ f"X API returned an error: {e.response.status_code} {e.response.reason_phrase}"
61
+ ) from e
62
+ except httpx.RequestError as e:
63
+ raise ToolExecutionError(f"Failed to connect to X API: {e}") from e
64
+
65
+ # Parse the response JSON
66
+ response_data = response.json()
67
+ user_data = response_data.get("data")
68
+
69
+ if not user_data:
70
+ raise ToolExecutionError(f"X API response missing 'data' field. Response: {response_data}")
71
+
72
+ # Expand URLs in the user description and profile URL
73
+ user_data = expand_urls_in_user_description(user_data, delete_entities=False)
74
+ user_data = expand_urls_in_user_url(user_data, delete_entities=True)
75
+
76
+ return {"data": user_data}
arcade_x/tools/users.py CHANGED
@@ -3,7 +3,6 @@ from typing import Annotated
3
3
  import httpx
4
4
  from arcade_tdk import ToolContext, tool
5
5
  from arcade_tdk.auth import X
6
- from arcade_tdk.errors import RetryableToolError
7
6
 
8
7
  from arcade_x.tools.utils import (
9
8
  expand_urls_in_user_description,
@@ -46,18 +45,22 @@ async def lookup_single_user_by_username(
46
45
 
47
46
  async with httpx.AsyncClient() as client:
48
47
  response = await client.get(url, headers=headers, timeout=10)
49
- if response.status_code == 404:
50
- # User not found
51
- raise RetryableToolError(
52
- "User not found",
53
- developer_message=f"User with username '{username}' not found.",
54
- additional_prompt_content="Please check the username and try again.",
55
- retry_after_ms=500, # Play nice with X API rate limits
56
- )
57
48
  response.raise_for_status()
49
+
58
50
  # Parse the response JSON
59
- user_data = response.json()["data"]
51
+ response_json = response.json()
52
+
53
+ # Check if data exists in response
54
+ if response_json.get("data") is None:
55
+ return {
56
+ "data": None,
57
+ "message": (
58
+ f"No user found with username '{username}'. "
59
+ "The account may not exist or may have been suspended."
60
+ ),
61
+ }
60
62
 
63
+ user_data = response_json["data"]
61
64
  user_data = expand_urls_in_user_description(user_data, delete_entities=False)
62
65
  user_data = expand_urls_in_user_url(user_data, delete_entities=True)
63
66
 
arcade_x/tools/utils.py CHANGED
@@ -35,14 +35,20 @@ def parse_search_recent_tweets_response(response_data: dict[str, Any]) -> dict[s
35
35
 
36
36
  # Add 'tweet_url' to each tweet
37
37
  for tweet in response_data["data"]:
38
+ # Skip tweets without an id
39
+ if "id" not in tweet:
40
+ continue
38
41
  tweet["tweet_url"] = get_tweet_url(tweet["id"])
39
42
 
40
43
  # Add 'author_username' and 'author_name' to each tweet
41
44
  for tweet_data, user_data in zip(
42
45
  response_data["data"], response_data["includes"]["users"], strict=False
43
46
  ):
44
- tweet_data["author_username"] = user_data["username"]
45
- tweet_data["author_name"] = user_data["name"]
47
+ # Skip if user data is missing required fields
48
+ if "username" in user_data:
49
+ tweet_data["author_username"] = user_data["username"]
50
+ if "name" in user_data:
51
+ tweet_data["author_name"] = user_data["name"]
46
52
 
47
53
  return response_data
48
54
 
@@ -101,6 +107,9 @@ def expand_urls_in_user_description(user_data: dict, delete_entities: bool = Tru
101
107
  description_urls = new_user_data.get("entities", {}).get("description", {}).get("urls", [])
102
108
  description = new_user_data.get("description", "")
103
109
  for url_info in description_urls:
110
+ # Skip URL entities that don't have both url and expanded_url
111
+ if "url" not in url_info or "expanded_url" not in url_info:
112
+ continue
104
113
  t_co_link = url_info["url"]
105
114
  expanded_url = url_info["expanded_url"]
106
115
  description = description.replace(t_co_link, expanded_url)
@@ -119,6 +128,9 @@ def expand_urls_in_user_url(user_data: dict, delete_entities: bool = True) -> di
119
128
  url_urls = new_user_data.get("entities", {}).get("url", {}).get("urls", [])
120
129
  url = new_user_data.get("url", "")
121
130
  for url_info in url_urls:
131
+ # Skip URL entities that don't have both url and expanded_url
132
+ if "url" not in url_info or "expanded_url" not in url_info:
133
+ continue
122
134
  t_co_link = url_info["url"]
123
135
  expanded_url = url_info["expanded_url"]
124
136
  url = url.replace(t_co_link, expanded_url)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arcade_x
3
- Version: 1.1.1
3
+ Version: 1.3.0
4
4
  Summary: Arcade.dev LLM tools for X (Twitter)
5
5
  Author-email: Arcade <dev@arcade.dev>
6
6
  License: Proprietary - Arcade Software License Agreement v1.0
@@ -0,0 +1,11 @@
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=KD93AY9EteE22txQGeVTK6N9_saJNZvBxJ8U1FOFLEQ,9986
5
+ arcade_x/tools/user_context.py,sha256=nKlsM2LGdoIPl8FQ7FKqd6hkbPHVSJUiZdShZ7ZCsIk,2470
6
+ arcade_x/tools/users.py,sha256=YObl3_3aBZpW-GqdSrR2Gmw3BHzdq075GWSxDd1Ery0,2033
7
+ arcade_x/tools/utils.py,sha256=2s3hxpXplxIQs4rO1IJdO-1KbUj-Tm3f9P65n4drw1s,6259
8
+ arcade_x-1.3.0.dist-info/METADATA,sha256=3dQr1nSmukSCVpxSj9xpqU_JeqxEKtuOqrsDI3Qi6l0,897
9
+ arcade_x-1.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
+ arcade_x-1.3.0.dist-info/licenses/LICENSE,sha256=ixeE7aL9b2B-_ZYHTY1vQcJB4NufKeo-LWwKNObGDN0,1960
11
+ arcade_x-1.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,10 +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/constants.py,sha256=d-OJK5Qx05JRUcpK5G1DcYB91wT37hFSzGmIYfwlEtA,42
4
- arcade_x/tools/tweets.py,sha256=xfg31_Jh7sUuBfFjRIY_TwbUFCcYbm_xyUE_s35HB-8,9416
5
- arcade_x/tools/users.py,sha256=gopYKTBOwbZAeVdL_T3vwkepbpv1bdPbqcU_2oPC3SY,2132
6
- arcade_x/tools/utils.py,sha256=lR_shws08LWQD-4XU9r3xYmasZ2j2i-cUaCCDnuN8UE,5723
7
- arcade_x-1.1.1.dist-info/METADATA,sha256=Sy3KedtCp4On90A6gekRMTaOoeSp3G7wvLXxj9UlR4Y,897
8
- arcade_x-1.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- arcade_x-1.1.1.dist-info/licenses/LICENSE,sha256=ixeE7aL9b2B-_ZYHTY1vQcJB4NufKeo-LWwKNObGDN0,1960
10
- arcade_x-1.1.1.dist-info/RECORD,,