alita-sdk 0.3.497__py3-none-any.whl → 0.3.515__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.

Potentially problematic release.


This version of alita-sdk might be problematic. Click here for more details.

Files changed (108) hide show
  1. alita_sdk/cli/inventory.py +12 -195
  2. alita_sdk/community/inventory/__init__.py +12 -0
  3. alita_sdk/community/inventory/toolkit.py +9 -5
  4. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  5. alita_sdk/configurations/ado.py +144 -0
  6. alita_sdk/configurations/confluence.py +76 -42
  7. alita_sdk/configurations/figma.py +76 -0
  8. alita_sdk/configurations/gitlab.py +2 -0
  9. alita_sdk/configurations/qtest.py +72 -1
  10. alita_sdk/configurations/report_portal.py +96 -0
  11. alita_sdk/configurations/sharepoint.py +148 -0
  12. alita_sdk/configurations/testio.py +83 -0
  13. alita_sdk/runtime/clients/artifact.py +2 -2
  14. alita_sdk/runtime/clients/client.py +24 -19
  15. alita_sdk/runtime/clients/sandbox_client.py +14 -0
  16. alita_sdk/runtime/langchain/assistant.py +48 -2
  17. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  18. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +2 -1
  19. alita_sdk/runtime/langchain/document_loaders/constants.py +2 -1
  20. alita_sdk/runtime/langchain/langraph_agent.py +8 -9
  21. alita_sdk/runtime/langchain/utils.py +6 -1
  22. alita_sdk/runtime/toolkits/artifact.py +14 -5
  23. alita_sdk/runtime/toolkits/datasource.py +13 -6
  24. alita_sdk/runtime/toolkits/mcp.py +26 -157
  25. alita_sdk/runtime/toolkits/planning.py +10 -5
  26. alita_sdk/runtime/toolkits/tools.py +23 -7
  27. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  28. alita_sdk/runtime/tools/artifact.py +139 -6
  29. alita_sdk/runtime/tools/llm.py +20 -10
  30. alita_sdk/runtime/tools/mcp_remote_tool.py +2 -3
  31. alita_sdk/runtime/tools/mcp_server_tool.py +2 -4
  32. alita_sdk/runtime/utils/AlitaCallback.py +30 -1
  33. alita_sdk/runtime/utils/mcp_client.py +33 -6
  34. alita_sdk/runtime/utils/mcp_oauth.py +125 -8
  35. alita_sdk/runtime/utils/mcp_sse_client.py +35 -6
  36. alita_sdk/runtime/utils/utils.py +2 -0
  37. alita_sdk/tools/__init__.py +15 -0
  38. alita_sdk/tools/ado/repos/__init__.py +10 -12
  39. alita_sdk/tools/ado/test_plan/__init__.py +23 -8
  40. alita_sdk/tools/ado/wiki/__init__.py +24 -8
  41. alita_sdk/tools/ado/wiki/ado_wrapper.py +21 -7
  42. alita_sdk/tools/ado/work_item/__init__.py +24 -8
  43. alita_sdk/tools/advanced_jira_mining/__init__.py +10 -8
  44. alita_sdk/tools/aws/delta_lake/__init__.py +12 -9
  45. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  46. alita_sdk/tools/azure_ai/search/__init__.py +9 -7
  47. alita_sdk/tools/base/tool.py +5 -1
  48. alita_sdk/tools/base_indexer_toolkit.py +25 -0
  49. alita_sdk/tools/bitbucket/__init__.py +14 -10
  50. alita_sdk/tools/bitbucket/api_wrapper.py +50 -2
  51. alita_sdk/tools/browser/__init__.py +5 -4
  52. alita_sdk/tools/carrier/__init__.py +5 -6
  53. alita_sdk/tools/cloud/aws/__init__.py +9 -7
  54. alita_sdk/tools/cloud/azure/__init__.py +9 -7
  55. alita_sdk/tools/cloud/gcp/__init__.py +9 -7
  56. alita_sdk/tools/cloud/k8s/__init__.py +9 -7
  57. alita_sdk/tools/code/linter/__init__.py +9 -8
  58. alita_sdk/tools/code/sonar/__init__.py +9 -7
  59. alita_sdk/tools/confluence/__init__.py +15 -10
  60. alita_sdk/tools/custom_open_api/__init__.py +11 -5
  61. alita_sdk/tools/elastic/__init__.py +10 -8
  62. alita_sdk/tools/elitea_base.py +387 -9
  63. alita_sdk/tools/figma/__init__.py +8 -7
  64. alita_sdk/tools/github/__init__.py +12 -14
  65. alita_sdk/tools/github/github_client.py +68 -2
  66. alita_sdk/tools/github/tool.py +5 -1
  67. alita_sdk/tools/gitlab/__init__.py +14 -11
  68. alita_sdk/tools/gitlab/api_wrapper.py +81 -1
  69. alita_sdk/tools/gitlab_org/__init__.py +9 -8
  70. alita_sdk/tools/google/bigquery/__init__.py +12 -12
  71. alita_sdk/tools/google/bigquery/tool.py +5 -1
  72. alita_sdk/tools/google_places/__init__.py +9 -8
  73. alita_sdk/tools/jira/__init__.py +15 -10
  74. alita_sdk/tools/keycloak/__init__.py +10 -8
  75. alita_sdk/tools/localgit/__init__.py +8 -3
  76. alita_sdk/tools/localgit/local_git.py +62 -54
  77. alita_sdk/tools/localgit/tool.py +5 -1
  78. alita_sdk/tools/memory/__init__.py +11 -3
  79. alita_sdk/tools/ocr/__init__.py +10 -8
  80. alita_sdk/tools/openapi/__init__.py +6 -2
  81. alita_sdk/tools/pandas/__init__.py +9 -7
  82. alita_sdk/tools/postman/__init__.py +10 -11
  83. alita_sdk/tools/pptx/__init__.py +9 -9
  84. alita_sdk/tools/qtest/__init__.py +9 -8
  85. alita_sdk/tools/rally/__init__.py +9 -8
  86. alita_sdk/tools/report_portal/__init__.py +11 -9
  87. alita_sdk/tools/salesforce/__init__.py +9 -9
  88. alita_sdk/tools/servicenow/__init__.py +10 -8
  89. alita_sdk/tools/sharepoint/__init__.py +9 -8
  90. alita_sdk/tools/slack/__init__.py +8 -7
  91. alita_sdk/tools/sql/__init__.py +9 -8
  92. alita_sdk/tools/testio/__init__.py +9 -8
  93. alita_sdk/tools/testrail/__init__.py +10 -8
  94. alita_sdk/tools/utils/__init__.py +9 -4
  95. alita_sdk/tools/utils/text_operations.py +254 -0
  96. alita_sdk/tools/xray/__init__.py +10 -8
  97. alita_sdk/tools/yagmail/__init__.py +8 -3
  98. alita_sdk/tools/zephyr/__init__.py +8 -7
  99. alita_sdk/tools/zephyr_enterprise/__init__.py +10 -8
  100. alita_sdk/tools/zephyr_essential/__init__.py +9 -8
  101. alita_sdk/tools/zephyr_scale/__init__.py +9 -8
  102. alita_sdk/tools/zephyr_squad/__init__.py +9 -8
  103. {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.515.dist-info}/METADATA +1 -1
  104. {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.515.dist-info}/RECORD +108 -105
  105. {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.515.dist-info}/WHEEL +0 -0
  106. {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.515.dist-info}/entry_points.txt +0 -0
  107. {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.515.dist-info}/licenses/LICENSE +0 -0
  108. {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.515.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,7 @@
1
1
  from typing import Optional
2
+ from urllib.parse import urlparse, urlunparse
2
3
 
4
+ from atlassian import Confluence
3
5
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
4
6
 
5
7
 
@@ -55,25 +57,53 @@ class ConfluenceConfiguration(BaseModel):
55
57
  from requests.auth import HTTPBasicAuth
56
58
 
57
59
  # Validate base_url
58
- base_url = settings.get("base_url", "").strip()
60
+ base_url_input = settings.get("base_url", "")
61
+ base_url = base_url_input.strip() if isinstance(base_url_input, str) else ""
59
62
  if not base_url:
60
63
  return "Confluence URL is required"
61
64
 
62
- # Normalize URL - remove trailing slashes
63
- base_url = base_url.rstrip("/")
64
-
65
65
  # Basic URL validation
66
66
  if not base_url.startswith(("http://", "https://")):
67
67
  return "Confluence URL must start with http:// or https://"
68
+
69
+ # Normalize URL - remove trailing slashes
70
+ base_url = base_url.rstrip("/")
71
+
72
+ # Build candidate base URLs.
73
+ # Confluence Cloud REST API is typically under /wiki. Users often paste
74
+ # https://<site>.atlassian.net and shouldn't be forced to know about /wiki.
75
+ parsed = urlparse(base_url)
76
+ host = (parsed.hostname or "").lower()
77
+ path = parsed.path or ""
78
+
79
+ def with_wiki_path(url: str) -> str:
80
+ p = urlparse(url)
81
+ # Keep existing path if it already starts with /wiki
82
+ if (p.path or "").startswith("/wiki"):
83
+ return url
84
+ # Append /wiki, preserving any existing path (rare but safe)
85
+ new_path = (p.path or "") + "/wiki"
86
+ return urlunparse(p._replace(path=new_path.rstrip("/")))
87
+
88
+ candidate_base_urls: list[str] = []
89
+ if host.endswith(".atlassian.net"):
90
+ # For Atlassian Cloud, prefer the /wiki variant first
91
+ candidate_base_urls.append(with_wiki_path(base_url))
92
+ candidate_base_urls.append(base_url)
93
+ # De-duplicate while preserving order
94
+ candidate_base_urls = list(dict.fromkeys(candidate_base_urls))
68
95
 
69
96
  # Check authentication credentials
70
97
  username = settings.get("username")
71
98
  api_key = settings.get("api_key")
72
99
  token = settings.get("token")
73
100
 
101
+ api_key_value = api_key.get_secret_value() if hasattr(api_key, 'get_secret_value') else api_key
102
+ token_value = token.get_secret_value() if hasattr(token, 'get_secret_value') else token
103
+
74
104
  # Validate authentication - at least one method must be provided
75
- has_basic_auth = bool(username and api_key)
76
- has_token = bool(token and str(token).strip())
105
+ has_basic_auth = bool(username and api_key_value and str(api_key_value).strip())
106
+ has_token = bool(token_value and str(token_value).strip())
77
107
 
78
108
  # Determine authentication method
79
109
  auth_headers = {}
@@ -81,52 +111,56 @@ class ConfluenceConfiguration(BaseModel):
81
111
 
82
112
  if has_token:
83
113
  # Bearer token authentication
84
- token_value = token.get_secret_value() if hasattr(token, 'get_secret_value') else token
85
114
  auth_headers["Authorization"] = f"Bearer {token_value}"
86
115
  elif has_basic_auth:
87
116
  # Basic authentication
88
- api_key_value = api_key.get_secret_value() if hasattr(api_key, 'get_secret_value') else api_key
89
117
  auth = HTTPBasicAuth(username, api_key_value)
90
118
  else:
91
119
  return "Authentication required: provide either token or both username and api_key"
92
120
 
93
- # Test connection using /rest/api/user/current endpoint
94
- # This endpoint returns current user info and validates authentication
95
- test_url = f"{base_url}/rest/api/user/current"
96
-
97
121
  try:
98
- response = requests.get(
99
- test_url,
100
- auth=auth,
101
- headers=auth_headers,
102
- timeout=10
103
- )
104
-
105
- # Check response status
106
- if response.status_code == 200:
107
- # Successfully connected and authenticated
108
- return None
109
- elif response.status_code == 401:
110
- # Authentication failed
111
- if has_token:
112
- return "Authentication failed: Invalid token"
113
- else:
114
- return "Authentication failed: Invalid username or API key"
115
- elif response.status_code == 403:
116
- return """Access forbidden: check permissions and verify the credentials you provided.
117
- Most probably you provided incorrect credentials (user name and api key or token)"""
118
- elif response.status_code == 404:
119
- return "Confluence API endpoint not found: verify the Confluence URL"
120
- else:
121
- return f"Confluence API returned status code {response.status_code}"
122
+ # Test connection using /rest/api/user/current endpoint
123
+ # This endpoint returns current user info and validates authentication
124
+ last_status = None
125
+ for candidate_base in candidate_base_urls:
126
+ test_url = f"{candidate_base}/rest/api/user/current"
127
+ response = requests.get(
128
+ test_url,
129
+ auth=auth,
130
+ headers=auth_headers,
131
+ timeout=10
132
+ )
133
+ last_status = response.status_code
134
+
135
+ if response.status_code == 200:
136
+ return None
137
+
138
+ # If we get 404 on the first candidate, try the next one
139
+ if response.status_code == 404:
140
+ continue
141
+
142
+ if response.status_code == 401:
143
+ return "Invalid credentials (401) - check token or username/api_key"
144
+ if response.status_code == 403:
145
+ return "Access forbidden (403) - credentials lack Confluence permissions"
146
+ if response.status_code == 429:
147
+ return "Rate limited (429) - please try again later"
148
+ if 500 <= response.status_code <= 599:
149
+ return f"Confluence service error (HTTP {response.status_code})"
150
+ return f"Confluence request failed (HTTP {response.status_code})"
151
+
152
+ # All candidates returned 404
153
+ return "Confluence API endpoint not found (404) - verify the Confluence URL"
122
154
 
123
155
  except requests.exceptions.SSLError as e:
124
- return f"SSL certificate verification failed: {str(e)}"
156
+ if 'Hostname mismatch' in str(e):
157
+ return "SSL error - hostname mismatch. Verify the Confluence URL"
158
+ return "SSL error - certificate verification failed"
125
159
  except requests.exceptions.ConnectionError:
126
- return f"Cannot connect to Confluence at {base_url}: connection refused"
160
+ return "Connection error - unable to reach Confluence. Check URL and network."
127
161
  except requests.exceptions.Timeout:
128
- return f"Connection to Confluence at {base_url} timed out"
162
+ return "Connection timeout - Confluence did not respond within 10 seconds. Check URL and network."
129
163
  except requests.exceptions.RequestException as e:
130
- return f"Error connecting to Confluence: {str(e)}"
131
- except Exception as e:
132
- return f"Unexpected error: {str(e)}"
164
+ return f"Request failed: {str(e)}"
165
+ except Exception:
166
+ return "Unexpected error during Confluence connection check"
@@ -1,8 +1,30 @@
1
+ from json import JSONDecodeError
1
2
  from typing import Optional
2
3
 
4
+ import requests
3
5
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
4
6
 
5
7
 
8
+ def _parse_error_response(response: requests.Response) -> Optional[str]:
9
+ """
10
+ Parse error response from Figma API to extract detailed error message.
11
+
12
+ Args:
13
+ response: Response object from requests
14
+
15
+ Returns:
16
+ Detailed error message if found, None otherwise
17
+ """
18
+ try:
19
+ json_response = response.json()
20
+ error = json_response.get("err") or json_response.get("error")
21
+ if error and 'Invalid token' in str(error):
22
+ return "Invalid token. Please verify the Figma token and try again."
23
+ except (JSONDecodeError, KeyError, AttributeError):
24
+ pass
25
+ return None
26
+
27
+
6
28
  class FigmaConfiguration(BaseModel):
7
29
  model_config = ConfigDict(
8
30
  json_schema_extra={
@@ -28,3 +50,57 @@ class FigmaConfiguration(BaseModel):
28
50
  }
29
51
  )
30
52
  token: Optional[SecretStr] = Field(description="Figma Token", json_schema_extra={"secret": True}, default=None)
53
+
54
+ @staticmethod
55
+ def check_connection(settings: dict) -> str | None:
56
+ """
57
+ Test the connection to Figma API.
58
+
59
+ Args:
60
+ settings: Dictionary containing 'token' (required)
61
+
62
+ Returns:
63
+ None if connection is successful, error message string otherwise
64
+ """
65
+ token = settings.get("token")
66
+ if token is None:
67
+ return "Token is required"
68
+
69
+ # Extract secret value if it's a SecretStr
70
+ if hasattr(token, "get_secret_value"):
71
+ token = token.get_secret_value()
72
+
73
+ # Validate token is not empty
74
+ if not token or not token.strip():
75
+ return "Token cannot be empty"
76
+
77
+ # Figma API endpoint
78
+ base_url = "https://api.figma.com"
79
+ endpoint = f"{base_url}/v1/me"
80
+
81
+ try:
82
+ response = requests.get(
83
+ endpoint,
84
+ headers={"X-Figma-Token": token},
85
+ timeout=10,
86
+ )
87
+
88
+ if response.status_code == 200:
89
+ return None # Connection successful
90
+ elif response.status_code == 401:
91
+ detailed_error = _parse_error_response(response)
92
+ return detailed_error if detailed_error else "Invalid token"
93
+ elif response.status_code == 403:
94
+ detailed_error = _parse_error_response(response)
95
+ return detailed_error if detailed_error else "Access forbidden - token may lack required permissions"
96
+ else:
97
+ return f"Connection failed with status {response.status_code}"
98
+
99
+ except requests.exceptions.Timeout:
100
+ return "Connection timeout - Figma API is not responding"
101
+ except requests.exceptions.ConnectionError:
102
+ return "Connection error - unable to reach Figma API"
103
+ except requests.exceptions.RequestException as e:
104
+ return f"Request failed: {str(e)}"
105
+ except Exception as e:
106
+ return f"Unexpected error: {str(e)}"
@@ -99,6 +99,8 @@ class GitlabConfiguration(BaseModel):
99
99
  return f"GitLab API returned status code {response.status_code}"
100
100
 
101
101
  except requests.exceptions.SSLError as e:
102
+ if 'Hostname mismatch' in str(e):
103
+ return "GitLab API endpoint not found: verify the GitLab URL"
102
104
  return f"SSL certificate verification failed: {str(e)}"
103
105
  except requests.exceptions.ConnectionError:
104
106
  return f"Cannot connect to GitLab at {url}: connection refused"
@@ -1,3 +1,6 @@
1
+ import re
2
+
3
+ import requests
1
4
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
2
5
 
3
6
 
@@ -14,6 +17,74 @@ class QtestConfiguration(BaseModel):
14
17
  }
15
18
  }
16
19
  )
17
- base_url: str = Field(description="QTest base url")
20
+ base_url: str = Field(description="QTest base URL")
18
21
  qtest_api_token: SecretStr = Field(description="QTest API token")
19
22
 
23
+ @staticmethod
24
+ def check_connection(settings: dict) -> str | None:
25
+ """Check connectivity and credentials for qTest.
26
+
27
+ Strategy:
28
+ - Validate token against an auth-required endpoint (so an incorrect token is detected).
29
+
30
+ Returns:
31
+ None if successful, otherwise a short actionable error message.
32
+ """
33
+ base_url_input = settings.get("base_url")
34
+ base_url = base_url_input.strip() if isinstance(base_url_input, str) else ""
35
+ if not base_url:
36
+ return "QTest base URL is required"
37
+
38
+ if not base_url.startswith(("http://", "https://")):
39
+ return "QTest base URL must start with http:// or https://"
40
+
41
+ base_url = base_url.rstrip("/")
42
+ # If user pasted /api/v3 (or similar), strip it so we can build canonical API URLs.
43
+ base_url = re.sub(r"/api/v\d+/?$", "", base_url, flags=re.IGNORECASE)
44
+
45
+ token = settings.get("qtest_api_token")
46
+ if token is None:
47
+ return "QTest API token is required"
48
+ token_value = token.get_secret_value() if hasattr(token, "get_secret_value") else str(token)
49
+ if not token_value or not token_value.strip():
50
+ return "QTest API token cannot be empty"
51
+
52
+ headers = {
53
+ "Authorization": f"Bearer {token_value}",
54
+ "Content-Type": "application/json",
55
+ }
56
+
57
+ # Auth-required endpoint to validate the token.
58
+ # /projects works on v3 and requires auth in typical qTest deployments.
59
+ token_check_url = f"{base_url}/api/v3/projects?pageSize=1&page=1"
60
+
61
+ try:
62
+ resp = requests.get(token_check_url, headers=headers, timeout=10)
63
+ if resp.status_code == 200:
64
+ return None
65
+ elif resp.status_code == 401:
66
+ return "Invalid or expired QTest API token"
67
+ elif resp.status_code == 403:
68
+ return "Access forbidden - token lacks required permissions"
69
+ elif resp.status_code == 404:
70
+ return "QTest API not found (404) - verify base URL (do not include /api/v3)"
71
+ elif resp.status_code == 429:
72
+ return "Rate limited (429) - please try again later"
73
+ elif 500 <= resp.status_code <= 599:
74
+ return f"QTest service error (HTTP {resp.status_code})"
75
+ else:
76
+ return f"QTest connection failed (HTTP {resp.status_code})"
77
+
78
+ except requests.exceptions.Timeout:
79
+ return "Connection timeout - qTest did not respond within 10 seconds"
80
+ except requests.exceptions.ConnectionError:
81
+ return "Connection error - unable to reach qTest. Check base URL and network."
82
+ except requests.exceptions.SSLError:
83
+ return "SSL error - certificate verification failed"
84
+ except requests.exceptions.RequestException as e:
85
+ return f"Request failed: {str(e)}"
86
+ except Exception:
87
+ return "Unexpected error during qTest connection check"
88
+
89
+
90
+
@@ -1,3 +1,6 @@
1
+ from urllib.parse import quote, urlparse, urlunparse
2
+
3
+ import requests
1
4
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
2
5
 
3
6
 
@@ -17,3 +20,96 @@ class ReportPortalConfiguration(BaseModel):
17
20
  project: str = Field(description="Report Portal Project Name")
18
21
  endpoint: str = Field(description="Report Portal Endpoint URL")
19
22
  api_key: SecretStr = Field(description="Report Portal API Key")
23
+
24
+ @staticmethod
25
+ def check_connection(settings: dict) -> str | None:
26
+ """Check the connection to ReportPortal.
27
+
28
+ Validates:
29
+ - endpoint URL format and reachability
30
+ - API key (token) via an auth-required endpoint
31
+ - project access (because most ReportPortal APIs are scoped to a project)
32
+
33
+ Returns:
34
+ None if connection successful, error message string otherwise
35
+ """
36
+ endpoint_in = settings.get("endpoint")
37
+ endpoint = endpoint_in.strip() if isinstance(endpoint_in, str) else ""
38
+ if not endpoint:
39
+ return "Endpoint is required"
40
+
41
+ if not endpoint.startswith(("http://", "https://")):
42
+ return "Endpoint must start with http:// or https://"
43
+
44
+ # Normalize: remove query/fragment and trailing slash.
45
+ parsed = urlparse(endpoint)
46
+ endpoint = urlunparse(parsed._replace(query="", fragment="")).rstrip("/")
47
+
48
+ # If user pasted an API URL, normalize back to base endpoint.
49
+ # Common pastes: .../api/v1 or .../api/v1/<project>
50
+ # lowered = endpoint.lower()
51
+ # for suffix in ("/api/v1", "/api"):
52
+ # if lowered.endswith(suffix):
53
+ # endpoint = endpoint[: -len(suffix)].rstrip("/")
54
+ # lowered = endpoint.lower()
55
+ # break
56
+
57
+ project_in = settings.get("project")
58
+ project = project_in.strip() if isinstance(project_in, str) else ""
59
+ if not project:
60
+ return "Project is required"
61
+
62
+ api_key = settings.get("api_key")
63
+ if api_key is None:
64
+ return "API key is required"
65
+ api_key_value = api_key.get_secret_value() if hasattr(api_key, "get_secret_value") else str(api_key)
66
+ if not api_key_value or not api_key_value.strip():
67
+ return "API key cannot be empty"
68
+
69
+ # Auth-required endpoint for verification.
70
+ # /user endpoint validates the token and project context.
71
+ project_encoded = quote(project, safe="")
72
+ test_url = f"{endpoint}/api/v1/project/{project_encoded}"
73
+
74
+ try:
75
+ resp = requests.get(
76
+ test_url,
77
+ headers={"Authorization": f"Bearer {api_key_value}"},
78
+ timeout=10,
79
+ )
80
+
81
+ if resp.status_code == 200:
82
+ return None
83
+ if resp.status_code == 401:
84
+ return "Invalid API key"
85
+ if resp.status_code == 403:
86
+ return "Access forbidden - API key has no access to this project"
87
+ if resp.status_code == 404:
88
+ return "API endpoint not found (404) - verify endpoint URL and project name"
89
+ if resp.status_code == 429:
90
+ return "Rate limited (429) - please try again later"
91
+ if 500 <= resp.status_code <= 599:
92
+ return f"ReportPortal service error (HTTP {resp.status_code})"
93
+ return f"Connection failed (HTTP {resp.status_code})"
94
+
95
+ except requests.exceptions.Timeout:
96
+ return "Connection timeout - ReportPortal did not respond within 10 seconds"
97
+ except requests.exceptions.SSLError as e:
98
+ if "Hostname mismatch" in str(e):
99
+ return "API endpoint not found - verify endpoint URL and project name"
100
+ return "SSL error - certificate verification failed"
101
+ except requests.exceptions.ConnectionError:
102
+ return "Connection error - unable to reach ReportPortal. Check endpoint URL and network."
103
+ except requests.exceptions.RequestException as e:
104
+ return f"Request failed: {str(e)}"
105
+ except Exception:
106
+ return "Unexpected error during ReportPortal connection check"
107
+
108
+ if __name__ == '__main__':
109
+ settings = {
110
+ "endpoint": "https://reportportal.epam.com",
111
+ "project": "epm-alta",
112
+ "api_key": "my-api-key_U1kF0zFvToqcweG3x552cI8pnYkMqszgtht_LHGZhpxwrxDl5nXlmrSf_JLEE8jy",
113
+ }
114
+
115
+ print(ReportPortalConfiguration.check_connection(settings))
@@ -1,3 +1,5 @@
1
+ import requests
2
+ from office365.onedrive.sharepoint_settings import SharepointSettings
1
3
  from pydantic import BaseModel, ConfigDict, Field, SecretStr
2
4
 
3
5
 
@@ -17,3 +19,149 @@ class SharepointConfiguration(BaseModel):
17
19
  client_id: str = Field(description="SharePoint Client ID")
18
20
  client_secret: SecretStr = Field(description="SharePoint Client Secret")
19
21
  site_url: str = Field(description="SharePoint Site URL")
22
+
23
+ @staticmethod
24
+ def check_connection(settings: dict) -> str | None:
25
+ """
26
+ Test the connection to SharePoint API using OAuth2 client credentials.
27
+
28
+ Args:
29
+ settings: Dictionary containing 'client_id', 'client_secret', and 'site_url' (all required)
30
+
31
+ Returns:
32
+ None if connection is successful, error message string otherwise
33
+ """
34
+ # Validate client_id
35
+ client_id = settings.get("client_id")
36
+ if client_id is None or client_id == "":
37
+ if client_id == "":
38
+ return "Client ID cannot be empty"
39
+ return "Client ID is required"
40
+
41
+ if not isinstance(client_id, str):
42
+ return "Client ID must be a string"
43
+
44
+ client_id = client_id.strip()
45
+ if not client_id:
46
+ return "Client ID cannot be empty"
47
+
48
+ # Validate client_secret
49
+ client_secret = settings.get("client_secret")
50
+ if client_secret is None:
51
+ return "Client secret is required"
52
+
53
+ # Extract secret value if it's a SecretStr
54
+ if hasattr(client_secret, "get_secret_value"):
55
+ client_secret = client_secret.get_secret_value()
56
+
57
+ if not client_secret or not client_secret.strip():
58
+ return "Client secret cannot be empty"
59
+
60
+ # Validate site_url
61
+ site_url = settings.get("site_url")
62
+ if site_url is None or site_url == "":
63
+ if site_url == "":
64
+ return "Site URL cannot be empty"
65
+ return "Site URL is required"
66
+
67
+ if not isinstance(site_url, str):
68
+ return "Site URL must be a string"
69
+
70
+ site_url = site_url.strip()
71
+ if not site_url:
72
+ return "Site URL cannot be empty"
73
+
74
+ if not site_url.startswith(("http://", "https://")):
75
+ return "Site URL must start with http:// or https://"
76
+
77
+ # Remove trailing slash for consistency
78
+ site_url = site_url.rstrip("/")
79
+
80
+ # Extract tenant and resource from site URL
81
+ # Expected format: https://<tenant>.sharepoint.com/sites/<site>
82
+ try:
83
+ if ".sharepoint.com" not in site_url:
84
+ return "Site URL must be a valid SharePoint URL (*.sharepoint.com)"
85
+
86
+ # Extract tenant (e.g., "contoso" from "contoso.sharepoint.com")
87
+ parts = site_url.split("//")[1].split(".")
88
+ if len(parts) < 3:
89
+ return "Invalid SharePoint URL format"
90
+ tenant = parts[0]
91
+
92
+ # Build token endpoint
93
+ token_url = f"https://accounts.accesscontrol.windows.net/{tenant}.onmicrosoft.com/tokens/OAuth/2"
94
+
95
+ # Build resource (the site URL with /_api appended)
96
+ resource = f"{site_url.split('/sites/')[0]}@{site_url.split('//')[1].split('/')[0].split('.')[0]}"
97
+
98
+ except Exception:
99
+ return "Failed to parse SharePoint URL - ensure it's in format: https://<tenant>.sharepoint.com/sites/<site>"
100
+
101
+ try:
102
+ # Step 1: Get OAuth2 access token using client credentials
103
+ token_response = requests.post(
104
+ token_url,
105
+ data={
106
+ "grant_type": "client_credentials",
107
+ "client_id": f"{client_id}@{tenant}.onmicrosoft.com",
108
+ "client_secret": client_secret,
109
+ "resource": f"00000003-0000-0ff1-ce00-000000000000/{site_url.split('//')[1].split('/')[0]}@{tenant}.onmicrosoft.com"
110
+ },
111
+ timeout=10,
112
+ )
113
+
114
+ if token_response.status_code == 400:
115
+ try:
116
+ error_data = token_response.json()
117
+ error_desc = error_data.get("error_description", "")
118
+ if "not found in the directory" in error_desc.lower():
119
+ return "Invalid client ID. Please check if you provide a correct client ID and try again."
120
+ elif "client_secret" in error_desc.lower():
121
+ return "Invalid client secret"
122
+ else:
123
+ return f"OAuth2 authentication failed: {error_desc}"
124
+ except Exception:
125
+ return "Invalid client credentials"
126
+
127
+ elif token_response.status_code == 401:
128
+ return "Invalid client secret provided. Please check if you provide a correct client secret and try again."
129
+ elif token_response.status_code != 200:
130
+ return f"Failed to obtain access token (status {token_response.status_code})"
131
+
132
+ # Extract access token
133
+ try:
134
+ token_data = token_response.json()
135
+ access_token = token_data.get("access_token")
136
+ if not access_token:
137
+ return "No access token received from SharePoint"
138
+ except Exception:
139
+ return "Failed to parse token response"
140
+
141
+ # Step 2: Test the access token by calling SharePoint API
142
+ api_url = f"{site_url}/_api/web"
143
+ api_response = requests.get(
144
+ api_url,
145
+ headers={"Authorization": f"Bearer {access_token}"},
146
+ timeout=10,
147
+ )
148
+
149
+ if api_response.status_code == 200:
150
+ return None # Connection successful
151
+ elif api_response.status_code == 401:
152
+ return "Access token is invalid or expired"
153
+ elif api_response.status_code == 403:
154
+ return "Access forbidden - client may lack required permissions for this site"
155
+ elif api_response.status_code == 404:
156
+ return f"Site not found or not accessible: {site_url}"
157
+ else:
158
+ return f"SharePoint API request failed with status {api_response.status_code}"
159
+
160
+ except requests.exceptions.Timeout:
161
+ return "Connection timeout - SharePoint is not responding"
162
+ except requests.exceptions.ConnectionError:
163
+ return "Connection error - unable to reach SharePoint"
164
+ except requests.exceptions.RequestException as e:
165
+ return f"Request failed: {str(e)}"
166
+ except Exception as e:
167
+ return f"Unexpected error: {str(e)}"