universal-mcp-applications 0.1.30rc2__py3-none-any.whl → 0.1.36rc2__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.
- universal_mcp/applications/ahrefs/app.py +52 -198
- universal_mcp/applications/airtable/app.py +23 -122
- universal_mcp/applications/apollo/app.py +111 -464
- universal_mcp/applications/asana/app.py +417 -1567
- universal_mcp/applications/aws_s3/app.py +36 -103
- universal_mcp/applications/bill/app.py +546 -1957
- universal_mcp/applications/box/app.py +1068 -3981
- universal_mcp/applications/braze/app.py +364 -1430
- universal_mcp/applications/browser_use/app.py +2 -8
- universal_mcp/applications/cal_com_v2/app.py +207 -625
- universal_mcp/applications/calendly/app.py +61 -200
- universal_mcp/applications/canva/app.py +45 -110
- universal_mcp/applications/clickup/app.py +207 -674
- universal_mcp/applications/coda/app.py +146 -426
- universal_mcp/applications/confluence/app.py +310 -1098
- universal_mcp/applications/contentful/app.py +36 -151
- universal_mcp/applications/crustdata/app.py +28 -107
- universal_mcp/applications/dialpad/app.py +283 -756
- universal_mcp/applications/digitalocean/app.py +1766 -5777
- universal_mcp/applications/domain_checker/app.py +3 -54
- universal_mcp/applications/e2b/app.py +14 -64
- universal_mcp/applications/elevenlabs/app.py +9 -47
- universal_mcp/applications/exa/app.py +6 -17
- universal_mcp/applications/falai/app.py +24 -101
- universal_mcp/applications/figma/app.py +53 -137
- universal_mcp/applications/file_system/app.py +2 -13
- universal_mcp/applications/firecrawl/app.py +51 -152
- universal_mcp/applications/fireflies/app.py +59 -281
- universal_mcp/applications/fpl/app.py +91 -528
- universal_mcp/applications/fpl/utils/fixtures.py +15 -49
- universal_mcp/applications/fpl/utils/helper.py +25 -89
- universal_mcp/applications/fpl/utils/league_utils.py +20 -64
- universal_mcp/applications/ghost_content/app.py +52 -161
- universal_mcp/applications/github/app.py +19 -56
- universal_mcp/applications/gong/app.py +88 -248
- universal_mcp/applications/google_calendar/app.py +16 -68
- universal_mcp/applications/google_docs/app.py +88 -188
- universal_mcp/applications/google_drive/app.py +141 -463
- universal_mcp/applications/google_gemini/app.py +12 -64
- universal_mcp/applications/google_mail/app.py +28 -157
- universal_mcp/applications/google_searchconsole/app.py +15 -48
- universal_mcp/applications/google_sheet/app.py +103 -580
- universal_mcp/applications/google_sheet/helper.py +10 -37
- universal_mcp/applications/hashnode/app.py +57 -269
- universal_mcp/applications/heygen/app.py +44 -122
- universal_mcp/applications/http_tools/app.py +10 -32
- universal_mcp/applications/hubspot/api_segments/crm_api.py +460 -1573
- universal_mcp/applications/hubspot/api_segments/marketing_api.py +74 -262
- universal_mcp/applications/hubspot/app.py +23 -87
- universal_mcp/applications/jira/app.py +2071 -7986
- universal_mcp/applications/klaviyo/app.py +494 -1376
- universal_mcp/applications/linkedin/README.md +9 -2
- universal_mcp/applications/linkedin/app.py +392 -212
- universal_mcp/applications/mailchimp/app.py +450 -1605
- universal_mcp/applications/markitdown/app.py +8 -20
- universal_mcp/applications/miro/app.py +217 -699
- universal_mcp/applications/ms_teams/app.py +64 -186
- universal_mcp/applications/neon/app.py +86 -192
- universal_mcp/applications/notion/app.py +21 -36
- universal_mcp/applications/onedrive/app.py +16 -38
- universal_mcp/applications/openai/app.py +42 -165
- universal_mcp/applications/outlook/app.py +24 -84
- universal_mcp/applications/perplexity/app.py +4 -19
- universal_mcp/applications/pipedrive/app.py +832 -3142
- universal_mcp/applications/posthog/app.py +163 -432
- universal_mcp/applications/reddit/app.py +40 -139
- universal_mcp/applications/resend/app.py +41 -107
- universal_mcp/applications/retell/app.py +14 -41
- universal_mcp/applications/rocketlane/app.py +221 -934
- universal_mcp/applications/scraper/README.md +7 -4
- universal_mcp/applications/scraper/app.py +216 -102
- universal_mcp/applications/semanticscholar/app.py +22 -64
- universal_mcp/applications/semrush/app.py +43 -77
- universal_mcp/applications/sendgrid/app.py +512 -1262
- universal_mcp/applications/sentry/app.py +271 -906
- universal_mcp/applications/serpapi/app.py +40 -143
- universal_mcp/applications/sharepoint/app.py +17 -39
- universal_mcp/applications/shopify/app.py +1551 -4287
- universal_mcp/applications/shortcut/app.py +155 -417
- universal_mcp/applications/slack/app.py +50 -101
- universal_mcp/applications/spotify/app.py +126 -325
- universal_mcp/applications/supabase/app.py +104 -213
- universal_mcp/applications/tavily/app.py +1 -1
- universal_mcp/applications/trello/app.py +693 -2656
- universal_mcp/applications/twilio/app.py +14 -50
- universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
- universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
- universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
- universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
- universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
- universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
- universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
- universal_mcp/applications/whatsapp/app.py +35 -186
- universal_mcp/applications/whatsapp/audio.py +2 -6
- universal_mcp/applications/whatsapp/whatsapp.py +17 -51
- universal_mcp/applications/whatsapp_business/app.py +70 -283
- universal_mcp/applications/wrike/app.py +45 -118
- universal_mcp/applications/yahoo_finance/app.py +19 -65
- universal_mcp/applications/youtube/app.py +75 -261
- universal_mcp/applications/zenquotes/app.py +2 -2
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/METADATA +2 -2
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/RECORD +105 -105
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/WHEEL +0 -0
- {universal_mcp_applications-0.1.30rc2.dist-info → universal_mcp_applications-0.1.36rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
1
|
+
from typing import Any
|
|
3
2
|
import httpx
|
|
4
3
|
from loguru import logger
|
|
5
4
|
from universal_mcp.applications.application import APIApplication
|
|
6
|
-
from universal_mcp.exceptions import NotAuthorizedError
|
|
7
|
-
from universal_mcp.integrations import Integration
|
|
8
|
-
|
|
9
|
-
from serpapi import SerpApiClient as SerpApiSearch # Added SerpApiError
|
|
5
|
+
from universal_mcp.exceptions import NotAuthorizedError
|
|
6
|
+
from universal_mcp.integrations import Integration
|
|
7
|
+
from serpapi import SerpApiClient as SerpApiSearch
|
|
10
8
|
|
|
11
9
|
|
|
12
10
|
class SerpapiApp(APIApplication):
|
|
13
11
|
def __init__(self, integration: Integration | None = None, **kwargs: Any) -> None:
|
|
14
12
|
super().__init__(name="serpapi", integration=integration, **kwargs)
|
|
15
|
-
self._serpapi_api_key: str | None = None
|
|
13
|
+
self._serpapi_api_key: str | None = None
|
|
16
14
|
self.base_url = "https://serpapi.com/search"
|
|
17
15
|
|
|
18
16
|
@property
|
|
@@ -23,54 +21,33 @@ class SerpapiApp(APIApplication):
|
|
|
23
21
|
if self._serpapi_api_key is None:
|
|
24
22
|
if not self.integration:
|
|
25
23
|
logger.error("SerpApi App: Integration not configured.")
|
|
26
|
-
raise NotAuthorizedError(
|
|
27
|
-
"Integration not configured for SerpApi App. Cannot retrieve API key."
|
|
28
|
-
)
|
|
29
|
-
|
|
24
|
+
raise NotAuthorizedError("Integration not configured for SerpApi App. Cannot retrieve API key.")
|
|
30
25
|
try:
|
|
31
26
|
credentials = self.integration.get_credentials()
|
|
32
27
|
except NotAuthorizedError as e:
|
|
33
|
-
logger.error(
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
raise # Re-raise the original NotAuthorizedError
|
|
28
|
+
logger.error(f"SerpApi App: Authorization error when fetching credentials: {e.message}")
|
|
29
|
+
raise
|
|
37
30
|
except Exception as e:
|
|
38
|
-
logger.error(
|
|
39
|
-
f"SerpApi App: Unexpected error when fetching credentials: {e}",
|
|
40
|
-
exc_info=True,
|
|
41
|
-
)
|
|
31
|
+
logger.error(f"SerpApi App: Unexpected error when fetching credentials: {e}", exc_info=True)
|
|
42
32
|
raise NotAuthorizedError(f"Failed to get SerpApi credentials: {e}")
|
|
43
|
-
|
|
44
|
-
api_key = (
|
|
45
|
-
credentials.get("api_key")
|
|
46
|
-
or credentials.get("API_KEY") # Check common variations
|
|
47
|
-
or credentials.get("apiKey")
|
|
48
|
-
)
|
|
49
|
-
|
|
33
|
+
api_key = credentials.get("api_key") or credentials.get("API_KEY") or credentials.get("apiKey")
|
|
50
34
|
if not api_key:
|
|
51
35
|
logger.error("SerpApi App: API key not found in credentials.")
|
|
52
|
-
action_message =
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
):
|
|
36
|
+
action_message = (
|
|
37
|
+
"API key for SerpApi is missing. Please ensure it's set in the store (e.g., SERPAPI_API_KEY in credentials)."
|
|
38
|
+
)
|
|
39
|
+
if hasattr(self.integration, "authorize") and callable(self.integration.authorize):
|
|
56
40
|
try:
|
|
57
41
|
auth_details = self.integration.authorize()
|
|
58
42
|
if isinstance(auth_details, str):
|
|
59
43
|
action_message = auth_details
|
|
60
44
|
elif isinstance(auth_details, dict) and "url" in auth_details:
|
|
61
|
-
action_message =
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
elif (
|
|
65
|
-
isinstance(auth_details, dict) and "message" in auth_details
|
|
66
|
-
):
|
|
45
|
+
action_message = f"Please authorize via: {auth_details['url']}"
|
|
46
|
+
elif isinstance(auth_details, dict) and "message" in auth_details:
|
|
67
47
|
action_message = auth_details["message"]
|
|
68
48
|
except Exception as auth_e:
|
|
69
|
-
logger.warning(
|
|
70
|
-
f"Could not retrieve specific authorization action for SerpApi: {auth_e}"
|
|
71
|
-
)
|
|
49
|
+
logger.warning(f"Could not retrieve specific authorization action for SerpApi: {auth_e}")
|
|
72
50
|
raise NotAuthorizedError(action_message)
|
|
73
|
-
|
|
74
51
|
self._serpapi_api_key = api_key
|
|
75
52
|
logger.info("SerpApi API Key successfully retrieved and cached.")
|
|
76
53
|
return self._serpapi_api_key
|
|
@@ -93,27 +70,15 @@ class SerpapiApp(APIApplication):
|
|
|
93
70
|
search, async, web-scraping, api, serpapi, important
|
|
94
71
|
"""
|
|
95
72
|
request_params = params or {}
|
|
96
|
-
|
|
97
73
|
try:
|
|
98
|
-
current_api_key = self.serpapi_api_key
|
|
74
|
+
current_api_key = self.serpapi_api_key
|
|
99
75
|
logger.info("Attempting SerpApi search.")
|
|
100
|
-
|
|
101
|
-
serpapi_call_params = {
|
|
102
|
-
"api_key": current_api_key,
|
|
103
|
-
"engine": "google_light", # Fastest engine by default
|
|
104
|
-
**request_params, # Include any additional parameters from the user
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
# SerpApiSearch (SerpApiClient) uses the 'requests' library and its get_dict() is synchronous.
|
|
108
|
-
# If true async behavior is needed, this call should be wrapped with asyncio.to_thread.
|
|
76
|
+
serpapi_call_params = {"api_key": current_api_key, "engine": "google_light", **request_params}
|
|
109
77
|
search_client = SerpApiSearch(serpapi_call_params)
|
|
110
78
|
data = search_client.get_dict()
|
|
111
|
-
|
|
112
|
-
# Check for errors returned in the API response body
|
|
113
79
|
if "error" in data:
|
|
114
80
|
error_message = data["error"]
|
|
115
81
|
logger.error(f"SerpApi API returned an error: {error_message}")
|
|
116
|
-
# Keywords indicating authorization/authentication issues
|
|
117
82
|
auth_error_keywords = [
|
|
118
83
|
"invalid api key",
|
|
119
84
|
"authorization failed",
|
|
@@ -122,76 +87,42 @@ class SerpapiApp(APIApplication):
|
|
|
122
87
|
"account disabled",
|
|
123
88
|
"private api key is missing",
|
|
124
89
|
]
|
|
125
|
-
if any(
|
|
126
|
-
keyword in error_message.lower() for keyword in auth_error_keywords
|
|
127
|
-
):
|
|
90
|
+
if any((keyword in error_message.lower() for keyword in auth_error_keywords)):
|
|
128
91
|
raise NotAuthorizedError(f"SerpApi Error: {error_message}")
|
|
129
|
-
return f"SerpApi API Error: {error_message}"
|
|
130
|
-
|
|
131
|
-
# Process organic search results if available
|
|
92
|
+
return f"SerpApi API Error: {error_message}"
|
|
132
93
|
if "organic_results" in data:
|
|
133
94
|
formatted_results = []
|
|
134
95
|
for result in data.get("organic_results", []):
|
|
135
96
|
title = result.get("title", "No title")
|
|
136
97
|
link = result.get("link", "No link")
|
|
137
98
|
snippet = result.get("snippet", "No snippet")
|
|
138
|
-
formatted_results.append(
|
|
139
|
-
|
|
140
|
-
)
|
|
141
|
-
return (
|
|
142
|
-
"\n".join(formatted_results)
|
|
143
|
-
if formatted_results
|
|
144
|
-
else "No organic results found."
|
|
145
|
-
)
|
|
99
|
+
formatted_results.append(f"Title: {title}\nLink: {link}\nSnippet: {snippet}\n")
|
|
100
|
+
return "\n".join(formatted_results) if formatted_results else "No organic results found."
|
|
146
101
|
else:
|
|
147
102
|
return "No organic results found."
|
|
148
|
-
|
|
149
|
-
except (
|
|
150
|
-
NotAuthorizedError
|
|
151
|
-
): # Catches from self.serpapi_api_key or explicit raise above
|
|
103
|
+
except NotAuthorizedError:
|
|
152
104
|
logger.error("SerpApi search failed due to an authorization error.")
|
|
153
|
-
raise
|
|
154
|
-
|
|
155
|
-
except httpx.HTTPStatusError as e: # Kept from original for robustness, though SerpApiClient uses 'requests'
|
|
105
|
+
raise
|
|
106
|
+
except httpx.HTTPStatusError as e:
|
|
156
107
|
logger.warning(
|
|
157
108
|
f"SerpApi search encountered httpx.HTTPStatusError (unexpected with default SerpApiClient): {e.response.status_code}",
|
|
158
109
|
exc_info=True,
|
|
159
110
|
)
|
|
160
111
|
if e.response.status_code == 429:
|
|
161
112
|
return "Error: Rate limit exceeded (HTTP 429). Please try again later."
|
|
162
|
-
elif
|
|
163
|
-
|
|
164
|
-
): # Key was fetched but rejected by API with HTTP 401
|
|
165
|
-
raise NotAuthorizedError(
|
|
166
|
-
"Error: Invalid API key (HTTP 401). Please check your SERPAPI_API_KEY."
|
|
167
|
-
)
|
|
113
|
+
elif e.response.status_code == 401:
|
|
114
|
+
raise NotAuthorizedError("Error: Invalid API key (HTTP 401). Please check your SERPAPI_API_KEY.")
|
|
168
115
|
else:
|
|
169
116
|
return f"HTTP Error: {e.response.status_code} - {e.response.text}"
|
|
170
|
-
|
|
171
|
-
except Exception as e: # General catch-all, similar to E2B's final catch
|
|
117
|
+
except Exception as e:
|
|
172
118
|
error_message_lower = str(e).lower()
|
|
173
119
|
logger.error(f"Unexpected error during SerpApi search: {e}", exc_info=True)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"authentication"
|
|
177
|
-
"api key",
|
|
178
|
-
"unauthorized",
|
|
179
|
-
"401",
|
|
180
|
-
"forbidden",
|
|
181
|
-
"invalid key",
|
|
182
|
-
]
|
|
183
|
-
if any(keyword in error_message_lower for keyword in auth_error_keywords):
|
|
184
|
-
raise NotAuthorizedError(
|
|
185
|
-
f"SerpApi authentication/authorization failed: {str(e)}"
|
|
186
|
-
)
|
|
120
|
+
auth_error_keywords = ["authentication", "api key", "unauthorized", "401", "forbidden", "invalid key"]
|
|
121
|
+
if any((keyword in error_message_lower for keyword in auth_error_keywords)):
|
|
122
|
+
raise NotAuthorizedError(f"SerpApi authentication/authorization failed: {str(e)}")
|
|
187
123
|
return f"An unexpected error occurred during search: {str(e)}"
|
|
188
124
|
|
|
189
|
-
async def google_maps_search(
|
|
190
|
-
self,
|
|
191
|
-
q: str | None = None,
|
|
192
|
-
ll: str | None = None,
|
|
193
|
-
place_id: str | None = None,
|
|
194
|
-
) -> dict[str, Any]:
|
|
125
|
+
async def google_maps_search(self, q: str | None = None, ll: str | None = None, place_id: str | None = None) -> dict[str, Any]:
|
|
195
126
|
"""
|
|
196
127
|
Executes a Google Maps search via SerpApi using a query, coordinates, or place ID. It enhances the results by adding a `google_maps_url` to each location, distinguishing it from `get_google_maps_reviews` which retrieves reviews for a known place.
|
|
197
128
|
|
|
@@ -210,43 +141,23 @@ class SerpapiApp(APIApplication):
|
|
|
210
141
|
Tags:
|
|
211
142
|
google-maps, search, location, places, important
|
|
212
143
|
"""
|
|
213
|
-
|
|
214
144
|
query_params = {}
|
|
215
|
-
query_params = {
|
|
216
|
-
"engine": "google_maps",
|
|
217
|
-
"api_key": self.serpapi_api_key,
|
|
218
|
-
}
|
|
219
|
-
|
|
145
|
+
query_params = {"engine": "google_maps", "api_key": self.serpapi_api_key}
|
|
220
146
|
if q is not None:
|
|
221
147
|
query_params["q"] = q
|
|
222
|
-
|
|
223
148
|
if ll is not None:
|
|
224
149
|
query_params["ll"] = ll
|
|
225
|
-
|
|
226
150
|
if place_id is not None:
|
|
227
151
|
query_params["place_id"] = place_id
|
|
228
|
-
|
|
229
|
-
response = self._get(
|
|
230
|
-
self.base_url,
|
|
231
|
-
params=query_params,
|
|
232
|
-
)
|
|
152
|
+
response = self._get(self.base_url, params=query_params)
|
|
233
153
|
data = self._handle_response(response)
|
|
234
|
-
|
|
235
|
-
# Add Google Maps URLs for each place in the results
|
|
236
154
|
if "local_results" in data:
|
|
237
155
|
for place in data["local_results"]:
|
|
238
156
|
if "place_id" in place:
|
|
239
|
-
place["google_maps_url"] =
|
|
240
|
-
f"https://www.google.com/maps/place/?q=place_id:{place['place_id']}"
|
|
241
|
-
)
|
|
242
|
-
|
|
157
|
+
place["google_maps_url"] = f"https://www.google.com/maps/place/?q=place_id:{place['place_id']}"
|
|
243
158
|
return data
|
|
244
159
|
|
|
245
|
-
async def get_google_maps_reviews(
|
|
246
|
-
self,
|
|
247
|
-
data_id: str,
|
|
248
|
-
hl: str | None = None,
|
|
249
|
-
) -> dict[str, Any]:
|
|
160
|
+
async def get_google_maps_reviews(self, data_id: str, hl: str | None = None) -> dict[str, Any]:
|
|
250
161
|
"""
|
|
251
162
|
Fetches Google Maps reviews for a specific location via SerpApi using its unique `data_id`. This function uses the `google_maps_reviews` engine, unlike `google_maps_search` which finds locations. Results can be returned in a specified language, defaulting to English.
|
|
252
163
|
|
|
@@ -264,28 +175,14 @@ class SerpapiApp(APIApplication):
|
|
|
264
175
|
Tags:
|
|
265
176
|
google-maps, reviews, ratings, places, important
|
|
266
177
|
"""
|
|
267
|
-
|
|
268
178
|
query_params = {}
|
|
269
|
-
query_params = {
|
|
270
|
-
"engine": "google_maps_reviews",
|
|
271
|
-
"data_id": data_id,
|
|
272
|
-
"api_key": self.serpapi_api_key,
|
|
273
|
-
}
|
|
274
|
-
|
|
179
|
+
query_params = {"engine": "google_maps_reviews", "data_id": data_id, "api_key": self.serpapi_api_key}
|
|
275
180
|
if hl is not None:
|
|
276
181
|
query_params["hl"] = hl
|
|
277
182
|
else:
|
|
278
183
|
query_params["hl"] = "en"
|
|
279
|
-
|
|
280
|
-
response = self._get(
|
|
281
|
-
self.base_url,
|
|
282
|
-
params=query_params,
|
|
283
|
-
)
|
|
184
|
+
response = self._get(self.base_url, params=query_params)
|
|
284
185
|
return self._handle_response(response)
|
|
285
186
|
|
|
286
187
|
def list_tools(self) -> list[callable]:
|
|
287
|
-
return [
|
|
288
|
-
self.web_search,
|
|
289
|
-
self.google_maps_search,
|
|
290
|
-
self.get_google_maps_reviews,
|
|
291
|
-
]
|
|
188
|
+
return [self.web_search, self.google_maps_search, self.get_google_maps_reviews]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import os
|
|
3
3
|
from typing import Any
|
|
4
|
-
|
|
5
4
|
from loguru import logger
|
|
6
5
|
from universal_mcp.applications.application import APIApplication
|
|
7
6
|
from universal_mcp.integrations import Integration
|
|
@@ -17,7 +16,7 @@ class SharepointApp(APIApplication):
|
|
|
17
16
|
super().__init__(name="sharepoint", integration=integration, **kwargs)
|
|
18
17
|
self.base_url = "https://graph.microsoft.com/v1.0"
|
|
19
18
|
|
|
20
|
-
def get_my_profile(self) -> dict[str, Any]:
|
|
19
|
+
async def get_my_profile(self) -> dict[str, Any]:
|
|
21
20
|
"""
|
|
22
21
|
Fetches the profile for the currently authenticated user, specifically retrieving their ID and user principal name. This function confirms user identity, distinguishing it from `get_drive_info`, which returns details about the SharePoint storage space (e.g., quota) rather than the user's personal profile.
|
|
23
22
|
|
|
@@ -35,7 +34,7 @@ class SharepointApp(APIApplication):
|
|
|
35
34
|
response = self._get(url, params=query_params)
|
|
36
35
|
return self._handle_response(response)
|
|
37
36
|
|
|
38
|
-
def get_drive_info(self) -> dict[str, Any]:
|
|
37
|
+
async def get_drive_info(self) -> dict[str, Any]:
|
|
39
38
|
"""
|
|
40
39
|
Fetches high-level information about the user's entire SharePoint. It returns drive-wide details like the owner and storage quota, differing from `get_item_metadata` which describes a specific item, and `get_my_profile` which retrieves general user account information.
|
|
41
40
|
|
|
@@ -63,7 +62,7 @@ class SharepointApp(APIApplication):
|
|
|
63
62
|
response = self._get(url)
|
|
64
63
|
return self._handle_response(response)
|
|
65
64
|
|
|
66
|
-
def search_files(self, query: str) -> dict[str, Any]:
|
|
65
|
+
async def search_files(self, query: str) -> dict[str, Any]:
|
|
67
66
|
"""
|
|
68
67
|
Searches the user's entire SharePoint for files and folders matching a specified text query. This function performs a comprehensive search from the drive's root, distinguishing it from `list_files` or `list_folders` which only browse the contents of a single directory.
|
|
69
68
|
|
|
@@ -78,12 +77,11 @@ class SharepointApp(APIApplication):
|
|
|
78
77
|
"""
|
|
79
78
|
if not query:
|
|
80
79
|
raise ValueError("Search query cannot be empty.")
|
|
81
|
-
|
|
82
80
|
url = f"{self.base_url}/me/drive/root/search(q='{query}')"
|
|
83
81
|
response = self._get(url)
|
|
84
82
|
return self._handle_response(response)
|
|
85
83
|
|
|
86
|
-
def get_item_metadata(self, item_id: str) -> dict[str, Any]:
|
|
84
|
+
async def get_item_metadata(self, item_id: str) -> dict[str, Any]:
|
|
87
85
|
"""
|
|
88
86
|
Fetches detailed metadata for a specific file or folder using its unique ID. It returns properties like name, size, and type. Unlike `get_document_content`, it doesn't retrieve the file's actual content, focusing solely on the item's attributes for quick inspection without a full download.
|
|
89
87
|
|
|
@@ -98,12 +96,11 @@ class SharepointApp(APIApplication):
|
|
|
98
96
|
"""
|
|
99
97
|
if not item_id:
|
|
100
98
|
raise ValueError("Missing required parameter 'item_id'.")
|
|
101
|
-
|
|
102
99
|
url = f"{self.base_url}/me/drive/items/{item_id}"
|
|
103
100
|
response = self._get(url)
|
|
104
101
|
return self._handle_response(response)
|
|
105
102
|
|
|
106
|
-
def create_folder(self, name: str, parent_id: str = "root") -> dict[str, Any]:
|
|
103
|
+
async def create_folder(self, name: str, parent_id: str = "root") -> dict[str, Any]:
|
|
107
104
|
"""
|
|
108
105
|
Creates a new folder with a specified name within a parent directory, which defaults to the root. Returns metadata for the new folder. Unlike `create_folder_and_list`, this function only creates the folder and returns its specific metadata, not the parent directory's contents.
|
|
109
106
|
|
|
@@ -119,13 +116,12 @@ class SharepointApp(APIApplication):
|
|
|
119
116
|
"""
|
|
120
117
|
if not name:
|
|
121
118
|
raise ValueError("Folder name cannot be empty.")
|
|
122
|
-
|
|
123
119
|
url = f"{self.base_url}/me/drive/items/{parent_id}/children"
|
|
124
120
|
data = {"name": name, "folder": {}, "@microsoft.graph.conflictBehavior": "rename"}
|
|
125
121
|
response = self._post(url, data=data)
|
|
126
122
|
return self._handle_response(response)
|
|
127
123
|
|
|
128
|
-
def delete_item(self, item_id: str) -> dict[str, Any]:
|
|
124
|
+
async def delete_item(self, item_id: str) -> dict[str, Any]:
|
|
129
125
|
"""
|
|
130
126
|
Permanently deletes a specified file or folder from SharePoint using its unique item ID. This versatile function can remove any type of drive item, distinguished from functions that only list or create specific types. A successful deletion returns an empty response, confirming the item's removal.
|
|
131
127
|
|
|
@@ -140,12 +136,11 @@ class SharepointApp(APIApplication):
|
|
|
140
136
|
"""
|
|
141
137
|
if not item_id:
|
|
142
138
|
raise ValueError("Missing required parameter 'item_id'.")
|
|
143
|
-
|
|
144
139
|
url = f"{self.base_url}/me/drive/items/{item_id}"
|
|
145
140
|
response = self._delete(url)
|
|
146
141
|
return self._handle_response(response)
|
|
147
142
|
|
|
148
|
-
def download_file(self, item_id: str) -> dict[str, Any]:
|
|
143
|
+
async def download_file(self, item_id: str) -> dict[str, Any]:
|
|
149
144
|
"""
|
|
150
145
|
Retrieves a temporary, pre-authenticated download URL for a specific file using its item ID. This function provides a link for subsequent download, differing from `get_document_content` which directly fetches the file's raw content. The URL is returned within a dictionary.
|
|
151
146
|
|
|
@@ -160,7 +155,6 @@ class SharepointApp(APIApplication):
|
|
|
160
155
|
"""
|
|
161
156
|
if not item_id:
|
|
162
157
|
raise ValueError("Missing required parameter 'item_id'.")
|
|
163
|
-
|
|
164
158
|
url = f"{self.base_url}/me/drive/items/{item_id}"
|
|
165
159
|
response = self._get(url)
|
|
166
160
|
metadata = self._handle_response(response)
|
|
@@ -169,7 +163,7 @@ class SharepointApp(APIApplication):
|
|
|
169
163
|
raise ValueError("Could not retrieve download URL for the item.")
|
|
170
164
|
return {"download_url": download_url}
|
|
171
165
|
|
|
172
|
-
def upload_file(self, file_path: str, parent_id: str = "root", file_name: str | None = None) -> dict[str, Any]:
|
|
166
|
+
async def upload_file(self, file_path: str, parent_id: str = "root", file_name: str | None = None) -> dict[str, Any]:
|
|
173
167
|
"""
|
|
174
168
|
Uploads a local binary file (under 4MB) from a given path to a specified SharePoint folder. Unlike `upload_text_file`, which uploads string content, this function reads from the filesystem. The destination filename can be customized, and it returns the new file's metadata upon completion.
|
|
175
169
|
|
|
@@ -186,17 +180,15 @@ class SharepointApp(APIApplication):
|
|
|
186
180
|
"""
|
|
187
181
|
if not os.path.exists(file_path):
|
|
188
182
|
raise FileNotFoundError(f"The file was not found at path: {file_path}")
|
|
189
|
-
|
|
190
183
|
if not file_name:
|
|
191
184
|
file_name = os.path.basename(file_path)
|
|
192
|
-
|
|
193
185
|
url = f"{self.base_url}/me/drive/items/{parent_id}:/{file_name}:/content"
|
|
194
186
|
with open(file_path, "rb") as f:
|
|
195
187
|
data = f.read()
|
|
196
188
|
response = self._put(url, data=data, content_type="application/octet-stream")
|
|
197
189
|
return self._handle_response(response)
|
|
198
190
|
|
|
199
|
-
def list_folders(self, item_id: str = "root") -> dict[str, Any]:
|
|
191
|
+
async def list_folders(self, item_id: str = "root") -> dict[str, Any]:
|
|
200
192
|
"""
|
|
201
193
|
Retrieves a list of only the folders within a specified parent directory in SharePoint. Unlike `_list_drive_items` which returns all items, this function filters the results to exclude files. Defaults to the root directory if no parent `item_id` is provided.
|
|
202
194
|
|
|
@@ -213,7 +205,7 @@ class SharepointApp(APIApplication):
|
|
|
213
205
|
folders = [item for item in all_items.get("value", []) if "folder" in item]
|
|
214
206
|
return {"value": folders}
|
|
215
207
|
|
|
216
|
-
def list_files(self, item_id: str = "root") -> dict[str, Any]:
|
|
208
|
+
async def list_files(self, item_id: str = "root") -> dict[str, Any]:
|
|
217
209
|
"""
|
|
218
210
|
Retrieves a list of files within a specified SharePoint folder, defaulting to the root. Unlike `_list_drive_items` which fetches all items, this function filters the results to exclusively return items identified as files, excluding any subdirectories.
|
|
219
211
|
|
|
@@ -230,7 +222,7 @@ class SharepointApp(APIApplication):
|
|
|
230
222
|
files = [item for item in all_items.get("value", []) if "file" in item]
|
|
231
223
|
return {"value": files}
|
|
232
224
|
|
|
233
|
-
def create_folder_and_list(self, name: str, parent_id: str = "root") -> dict[str, Any]:
|
|
225
|
+
async def create_folder_and_list(self, name: str, parent_id: str = "root") -> dict[str, Any]:
|
|
234
226
|
"""
|
|
235
227
|
Performs a composite action: creates a new folder, then lists all items (files and folders) within that parent directory. This confirms creation by returning the parent's updated contents, distinct from `create_folder` which only returns the new folder's metadata.
|
|
236
228
|
|
|
@@ -244,10 +236,10 @@ class SharepointApp(APIApplication):
|
|
|
244
236
|
Tags:
|
|
245
237
|
create, folder, list, important
|
|
246
238
|
"""
|
|
247
|
-
self.create_folder(name=name, parent_id=parent_id)
|
|
239
|
+
await self.create_folder(name=name, parent_id=parent_id)
|
|
248
240
|
return self._list_drive_items(item_id=parent_id)
|
|
249
241
|
|
|
250
|
-
def upload_text_file(self, content: str, parent_id: str = "root", file_name: str = "new_file.txt") -> dict[str, Any]:
|
|
242
|
+
async def upload_text_file(self, content: str, parent_id: str = "root", file_name: str = "new_file.txt") -> dict[str, Any]:
|
|
251
243
|
"""
|
|
252
244
|
Creates and uploads a new file to SharePoint directly from a string of text content. Unlike `upload_file`, which requires a local file path, this function is specifically for creating a text file from in-memory string data, with a customizable name and destination folder.
|
|
253
245
|
|
|
@@ -264,13 +256,12 @@ class SharepointApp(APIApplication):
|
|
|
264
256
|
"""
|
|
265
257
|
if not file_name:
|
|
266
258
|
raise ValueError("File name cannot be empty.")
|
|
267
|
-
|
|
268
259
|
url = f"{self.base_url}/me/drive/items/{parent_id}:/{file_name}:/content"
|
|
269
260
|
data = content.encode("utf-8")
|
|
270
261
|
response = self._put(url, data=data, content_type="text/plain")
|
|
271
262
|
return self._handle_response(response)
|
|
272
263
|
|
|
273
|
-
def get_document_content(self, item_id: str) -> dict[str, Any]:
|
|
264
|
+
async def get_document_content(self, item_id: str) -> dict[str, Any]:
|
|
274
265
|
"""
|
|
275
266
|
Retrieves the content of a specific file by its item ID and returns it directly as base64-encoded data. This function is distinct from `download_file`, which only provides a temporary URL for the content, and from `get_item_metadata`, which returns file attributes without the content itself. The function fetches the content by following the file's pre-authenticated download URL.
|
|
276
267
|
|
|
@@ -289,36 +280,23 @@ class SharepointApp(APIApplication):
|
|
|
289
280
|
"""
|
|
290
281
|
if not item_id:
|
|
291
282
|
raise ValueError("Missing required parameter 'item_id'.")
|
|
292
|
-
|
|
293
|
-
metadata = self.get_item_metadata(item_id=item_id)
|
|
283
|
+
metadata = await self.get_item_metadata(item_id=item_id)
|
|
294
284
|
file_metadata = metadata.get("file")
|
|
295
285
|
if not file_metadata:
|
|
296
286
|
raise ValueError(f"Item with ID '{item_id}' is not a file.")
|
|
297
|
-
|
|
298
287
|
file_mime_type = file_metadata.get("mimeType", "application/octet-stream")
|
|
299
288
|
file_name = metadata.get("name")
|
|
300
|
-
|
|
301
289
|
download_url = metadata.get("@microsoft.graph.downloadUrl")
|
|
302
290
|
if not download_url:
|
|
303
291
|
logger.error(f"Could not find @microsoft.graph.downloadUrl in metadata for item {item_id}")
|
|
304
292
|
raise ValueError("Could not retrieve download URL for the item.")
|
|
305
|
-
|
|
306
293
|
response = self._get(download_url)
|
|
307
|
-
|
|
308
294
|
response.raise_for_status()
|
|
309
|
-
|
|
310
295
|
content = response.content
|
|
311
|
-
|
|
312
296
|
attachment_type = file_mime_type.split("/")[0] if "/" in file_mime_type else "file"
|
|
313
297
|
if attachment_type not in ["image", "audio", "video", "text"]:
|
|
314
298
|
attachment_type = "file"
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
"type": attachment_type,
|
|
318
|
-
"data": content,
|
|
319
|
-
"mime_type": file_mime_type,
|
|
320
|
-
"file_name": file_name,
|
|
321
|
-
}
|
|
299
|
+
return {"type": attachment_type, "data": content, "mime_type": file_mime_type, "file_name": file_name}
|
|
322
300
|
|
|
323
301
|
def list_tools(self):
|
|
324
302
|
return [
|
|
@@ -335,4 +313,4 @@ class SharepointApp(APIApplication):
|
|
|
335
313
|
self.create_folder_and_list,
|
|
336
314
|
self.upload_text_file,
|
|
337
315
|
self.get_document_content,
|
|
338
|
-
]
|
|
316
|
+
]
|