alita-sdk 0.3.462__py3-none-any.whl → 0.3.627__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.
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +15 -3
- alita_sdk/cli/agent_loader.py +56 -8
- alita_sdk/cli/agent_ui.py +93 -31
- alita_sdk/cli/agents.py +2274 -230
- alita_sdk/cli/callbacks.py +96 -25
- alita_sdk/cli/cli.py +10 -1
- alita_sdk/cli/config.py +162 -9
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/testcases/__init__.py +94 -0
- alita_sdk/cli/testcases/data_generation.py +119 -0
- alita_sdk/cli/testcases/discovery.py +96 -0
- alita_sdk/cli/testcases/executor.py +84 -0
- alita_sdk/cli/testcases/logger.py +85 -0
- alita_sdk/cli/testcases/parser.py +172 -0
- alita_sdk/cli/testcases/prompts.py +91 -0
- alita_sdk/cli/testcases/reporting.py +125 -0
- alita_sdk/cli/testcases/setup.py +108 -0
- alita_sdk/cli/testcases/test_runner.py +282 -0
- alita_sdk/cli/testcases/utils.py +39 -0
- alita_sdk/cli/testcases/validation.py +90 -0
- alita_sdk/cli/testcases/workflow.py +196 -0
- alita_sdk/cli/toolkit.py +14 -17
- alita_sdk/cli/toolkit_loader.py +35 -5
- alita_sdk/cli/tools/__init__.py +36 -2
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +910 -64
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +1 -1
- alita_sdk/configurations/ado.py +141 -20
- alita_sdk/configurations/bitbucket.py +0 -3
- alita_sdk/configurations/confluence.py +76 -42
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +17 -5
- alita_sdk/configurations/openapi.py +329 -0
- alita_sdk/configurations/qtest.py +72 -1
- alita_sdk/configurations/report_portal.py +96 -0
- alita_sdk/configurations/sharepoint.py +148 -0
- alita_sdk/configurations/testio.py +83 -0
- alita_sdk/runtime/clients/artifact.py +3 -3
- alita_sdk/runtime/clients/client.py +353 -48
- alita_sdk/runtime/clients/sandbox_client.py +0 -21
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +123 -26
- alita_sdk/runtime/langchain/constants.py +642 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +6 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +226 -7
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
- alita_sdk/runtime/langchain/langraph_agent.py +279 -73
- alita_sdk/runtime/langchain/utils.py +82 -15
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +7 -0
- alita_sdk/runtime/toolkits/application.py +21 -9
- alita_sdk/runtime/toolkits/artifact.py +15 -5
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +139 -251
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +251 -6
- alita_sdk/runtime/toolkits/tools.py +238 -32
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +3 -1
- alita_sdk/runtime/tools/application.py +20 -6
- alita_sdk/runtime/tools/artifact.py +511 -28
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +43 -15
- alita_sdk/runtime/tools/image_generation.py +50 -44
- alita_sdk/runtime/tools/llm.py +852 -67
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
- alita_sdk/runtime/tools/mcp_server_tool.py +7 -6
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -4
- alita_sdk/runtime/tools/sandbox.py +9 -6
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +7 -2
- alita_sdk/runtime/tools/vectorstore_base.py +51 -11
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +202 -5
- alita_sdk/runtime/utils/mcp_sse_client.py +36 -7
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/serialization.py +155 -0
- alita_sdk/runtime/utils/streamlit.py +6 -10
- alita_sdk/runtime/utils/toolkit_utils.py +16 -5
- alita_sdk/runtime/utils/utils.py +36 -0
- alita_sdk/tools/__init__.py +113 -29
- alita_sdk/tools/ado/repos/__init__.py +51 -33
- alita_sdk/tools/ado/repos/repos_wrapper.py +148 -89
- alita_sdk/tools/ado/test_plan/__init__.py +25 -9
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
- alita_sdk/tools/ado/utils.py +1 -18
- alita_sdk/tools/ado/wiki/__init__.py +25 -8
- alita_sdk/tools/ado/wiki/ado_wrapper.py +291 -22
- alita_sdk/tools/ado/work_item/__init__.py +26 -9
- alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
- alita_sdk/tools/advanced_jira_mining/__init__.py +11 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +13 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +11 -8
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +170 -45
- alita_sdk/tools/bitbucket/__init__.py +17 -12
- alita_sdk/tools/bitbucket/api_wrapper.py +59 -11
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/carrier/backend_reports_tool.py +6 -6
- alita_sdk/tools/carrier/run_ui_test_tool.py +6 -6
- alita_sdk/tools/carrier/ui_reports_tool.py +5 -5
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +10 -7
- alita_sdk/tools/cloud/azure/__init__.py +10 -7
- alita_sdk/tools/cloud/gcp/__init__.py +10 -7
- alita_sdk/tools/cloud/k8s/__init__.py +10 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +10 -7
- alita_sdk/tools/code_indexer_toolkit.py +73 -23
- alita_sdk/tools/confluence/__init__.py +21 -15
- alita_sdk/tools/confluence/api_wrapper.py +78 -23
- alita_sdk/tools/confluence/loader.py +4 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +493 -30
- alita_sdk/tools/figma/__init__.py +58 -11
- alita_sdk/tools/figma/api_wrapper.py +1235 -143
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +13 -14
- alita_sdk/tools/github/github_client.py +224 -100
- alita_sdk/tools/github/graphql_client_wrapper.py +119 -33
- alita_sdk/tools/github/schemas.py +14 -5
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/github/tool_prompts.py +9 -22
- alita_sdk/tools/gitlab/__init__.py +15 -11
- alita_sdk/tools/gitlab/api_wrapper.py +207 -41
- alita_sdk/tools/gitlab_org/__init__.py +10 -8
- alita_sdk/tools/gitlab_org/api_wrapper.py +63 -64
- alita_sdk/tools/google/bigquery/__init__.py +13 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +10 -8
- alita_sdk/tools/google_places/api_wrapper.py +1 -1
- alita_sdk/tools/jira/__init__.py +17 -11
- alita_sdk/tools/jira/api_wrapper.py +91 -40
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +11 -3
- alita_sdk/tools/non_code_indexer_toolkit.py +1 -0
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +490 -114
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +38 -25
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +11 -11
- alita_sdk/tools/pptx/__init__.py +10 -9
- alita_sdk/tools/pptx/pptx_wrapper.py +1 -1
- alita_sdk/tools/qtest/__init__.py +30 -10
- alita_sdk/tools/qtest/api_wrapper.py +430 -13
- alita_sdk/tools/rally/__init__.py +10 -8
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +12 -9
- alita_sdk/tools/salesforce/__init__.py +10 -9
- alita_sdk/tools/servicenow/__init__.py +17 -14
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +10 -8
- alita_sdk/tools/sharepoint/api_wrapper.py +4 -4
- alita_sdk/tools/slack/__init__.py +10 -8
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +11 -9
- alita_sdk/tools/testio/__init__.py +10 -8
- alita_sdk/tools/testrail/__init__.py +11 -8
- alita_sdk/tools/testrail/api_wrapper.py +1 -1
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/content_parser.py +77 -3
- alita_sdk/tools/utils/text_operations.py +410 -0
- alita_sdk/tools/utils/tool_prompts.py +79 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +17 -13
- alita_sdk/tools/xray/__init__.py +12 -9
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +9 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +11 -8
- alita_sdk/tools/zephyr_essential/__init__.py +10 -8
- alita_sdk/tools/zephyr_essential/api_wrapper.py +30 -13
- alita_sdk/tools/zephyr_essential/client.py +2 -2
- alita_sdk/tools/zephyr_scale/__init__.py +11 -9
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +10 -8
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/METADATA +147 -7
- alita_sdk-0.3.627.dist-info/RECORD +468 -0
- alita_sdk-0.3.627.dist-info/entry_points.txt +2 -0
- alita_sdk-0.3.462.dist-info/RECORD +0 -384
- alita_sdk-0.3.462.dist-info/entry_points.txt +0 -2
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.462.dist-info → alita_sdk-0.3.627.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
from typing import Any, Literal, Optional
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OpenApiConfiguration(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
OpenAPI configuration for authentication.
|
|
11
|
+
|
|
12
|
+
Supports three authentication modes:
|
|
13
|
+
- Anonymous: No authentication (all fields empty)
|
|
14
|
+
- API Key: Static key sent via header (Bearer, Basic, or Custom)
|
|
15
|
+
- OAuth2 Client Credentials: Machine-to-machine authentication flow
|
|
16
|
+
|
|
17
|
+
Note: Only OAuth2 Client Credentials flow is supported. Authorization Code flow
|
|
18
|
+
is not supported as it requires user interaction and pre-registered redirect URLs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(
|
|
22
|
+
extra='allow',
|
|
23
|
+
json_schema_extra={
|
|
24
|
+
"metadata": {
|
|
25
|
+
"label": "OpenAPI",
|
|
26
|
+
"icon_url": "openapi.svg",
|
|
27
|
+
"categories": ["integrations"],
|
|
28
|
+
"type": "openapi",
|
|
29
|
+
"extra_categories": ["api", "openapi", "swagger"],
|
|
30
|
+
"check_connection": {
|
|
31
|
+
"enabled_when": {
|
|
32
|
+
"all_fields_set": ["client_id", "client_secret", "method", "token_url"],
|
|
33
|
+
},
|
|
34
|
+
"disabled_tooltip": "Available only for OAuth (Client Credentials). Please setup client_id, client_secret, method and token_url to activate it on setup.",
|
|
35
|
+
},
|
|
36
|
+
"sections": {
|
|
37
|
+
"auth": {
|
|
38
|
+
"required": False,
|
|
39
|
+
"subsections": [
|
|
40
|
+
{
|
|
41
|
+
"name": "API Key",
|
|
42
|
+
"fields": ["api_key", "auth_type", "custom_header_name"],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "OAuth",
|
|
46
|
+
"fields": [
|
|
47
|
+
"client_id",
|
|
48
|
+
"client_secret",
|
|
49
|
+
"token_url",
|
|
50
|
+
"scope",
|
|
51
|
+
"method",
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"section": "credentials",
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# =========================================================================
|
|
63
|
+
# API Key Authentication Fields
|
|
64
|
+
# =========================================================================
|
|
65
|
+
|
|
66
|
+
api_key: Optional[SecretStr] = Field(
|
|
67
|
+
default=None,
|
|
68
|
+
description=(
|
|
69
|
+
"API key value (stored as a secret). Used when selecting 'API Key' authentication subsection."
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
auth_type: Optional[Literal['Basic', 'Bearer', 'Custom']] = Field(
|
|
73
|
+
default=None,
|
|
74
|
+
description=(
|
|
75
|
+
"How to apply the API key. "
|
|
76
|
+
"- 'Bearer': sets 'Authorization: Bearer <api_key>' "
|
|
77
|
+
"- 'Basic': sets 'Authorization: Basic <api_key>' "
|
|
78
|
+
"- 'custom': sets '<custom_header_name>: <api_key>'"
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
custom_header_name: Optional[str] = Field(
|
|
82
|
+
default=None,
|
|
83
|
+
description="Custom header name to use when auth_type='custom' (e.g. 'X-Api-Key').",
|
|
84
|
+
json_schema_extra={'visible_when': {'field': 'auth_type', 'value': 'custom'}},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# =========================================================================
|
|
88
|
+
# OAuth2 Client Credentials Flow Fields
|
|
89
|
+
# =========================================================================
|
|
90
|
+
|
|
91
|
+
client_id: Optional[str] = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description='OAuth2 client ID (also known as Application ID or App ID)'
|
|
94
|
+
)
|
|
95
|
+
client_secret: Optional[SecretStr] = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
description='OAuth2 client secret (stored securely)'
|
|
98
|
+
)
|
|
99
|
+
token_url: Optional[str] = Field(
|
|
100
|
+
default=None,
|
|
101
|
+
description=(
|
|
102
|
+
'OAuth2 token endpoint URL for obtaining access tokens. '
|
|
103
|
+
'Examples: '
|
|
104
|
+
'Azure AD: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token, '
|
|
105
|
+
'Google: https://oauth2.googleapis.com/token, '
|
|
106
|
+
'Auth0: https://{domain}/oauth/token, '
|
|
107
|
+
'Spotify: https://accounts.spotify.com/api/token'
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
scope: Optional[str] = Field(
|
|
111
|
+
default=None,
|
|
112
|
+
description=(
|
|
113
|
+
'OAuth2 scope(s), space-separated if multiple (per OAuth2 RFC 6749). '
|
|
114
|
+
'Examples: "user-read-private user-read-email" (Spotify), '
|
|
115
|
+
'"api://app-id/.default" (Azure), '
|
|
116
|
+
'"https://www.googleapis.com/auth/cloud-platform" (Google)'
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
method: Optional[Literal['default', 'Basic']] = Field(
|
|
120
|
+
default=None,
|
|
121
|
+
description=(
|
|
122
|
+
"Token exchange method for client credentials flow. "
|
|
123
|
+
"'default': Sends client_id and client_secret in POST body (Azure AD, Auth0, most providers). "
|
|
124
|
+
"'Basic': Sends credentials via HTTP Basic auth header - required by Spotify, some AWS services, and certain OAuth providers."
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
@model_validator(mode='before')
|
|
129
|
+
@classmethod
|
|
130
|
+
def _validate_auth_consistency(cls, values):
|
|
131
|
+
if not isinstance(values, dict):
|
|
132
|
+
return values
|
|
133
|
+
|
|
134
|
+
# OAuth: if any OAuth field is provided, require the essential ones
|
|
135
|
+
has_any_oauth = any(
|
|
136
|
+
(values.get('client_id'), values.get('client_secret'), values.get('token_url'))
|
|
137
|
+
)
|
|
138
|
+
if has_any_oauth:
|
|
139
|
+
missing = []
|
|
140
|
+
if not values.get('client_id'):
|
|
141
|
+
missing.append('client_id')
|
|
142
|
+
if not values.get('client_secret'):
|
|
143
|
+
missing.append('client_secret')
|
|
144
|
+
if not values.get('token_url'):
|
|
145
|
+
missing.append('token_url')
|
|
146
|
+
if missing:
|
|
147
|
+
raise ValueError(f"OAuth is misconfigured; missing: {', '.join(missing)}")
|
|
148
|
+
|
|
149
|
+
# API key: if auth_type is custom, custom_header_name must be present
|
|
150
|
+
auth_type = values.get('auth_type')
|
|
151
|
+
if isinstance(auth_type, str) and auth_type.strip().lower() == 'custom' and values.get('api_key'):
|
|
152
|
+
if not values.get('custom_header_name'):
|
|
153
|
+
raise ValueError("custom_header_name is required when auth_type='custom'")
|
|
154
|
+
|
|
155
|
+
return values
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def check_connection(settings: dict) -> str | None:
|
|
159
|
+
"""
|
|
160
|
+
Validate the OpenAPI configuration by testing connectivity where possible.
|
|
161
|
+
|
|
162
|
+
Validation behavior by authentication type:
|
|
163
|
+
|
|
164
|
+
1. ANONYMOUS (no auth fields configured):
|
|
165
|
+
- Cannot validate without making actual API calls
|
|
166
|
+
- Returns None (success) - validation skipped
|
|
167
|
+
|
|
168
|
+
2. API KEY (api_key field configured):
|
|
169
|
+
- Cannot validate without knowing which endpoint to call
|
|
170
|
+
- The OpenAPI spec is not available at configuration time
|
|
171
|
+
- Returns None (success) - validation skipped
|
|
172
|
+
|
|
173
|
+
3. OAUTH2 CLIENT CREDENTIALS (client_id, client_secret, token_url configured):
|
|
174
|
+
- CAN validate by attempting token exchange with the OAuth provider
|
|
175
|
+
- Makes a real HTTP request to token_url
|
|
176
|
+
- Returns None on success, error message on failure
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
settings: Dictionary containing OpenAPI configuration fields
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
None: Configuration is valid (or cannot be validated for this auth type)
|
|
183
|
+
str: Error message describing the validation failure
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
# =====================================================================
|
|
187
|
+
# Determine authentication type from configured fields
|
|
188
|
+
# =====================================================================
|
|
189
|
+
|
|
190
|
+
client_id = settings.get('client_id')
|
|
191
|
+
client_secret = settings.get('client_secret')
|
|
192
|
+
token_url = settings.get('token_url')
|
|
193
|
+
|
|
194
|
+
has_oauth_fields = client_id or client_secret or token_url
|
|
195
|
+
|
|
196
|
+
# =====================================================================
|
|
197
|
+
# ANONYMOUS or API KEY: Cannot validate, return success
|
|
198
|
+
# =====================================================================
|
|
199
|
+
|
|
200
|
+
if not has_oauth_fields:
|
|
201
|
+
# No OAuth fields configured - this is either:
|
|
202
|
+
# - Anonymous authentication (no auth at all)
|
|
203
|
+
# - API Key authentication (api_key field may be set)
|
|
204
|
+
#
|
|
205
|
+
# Neither can be validated without making actual API calls to the
|
|
206
|
+
# target service, and we don't have the OpenAPI spec available here.
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
# =====================================================================
|
|
210
|
+
# OAUTH2: Validate by attempting token exchange
|
|
211
|
+
# =====================================================================
|
|
212
|
+
|
|
213
|
+
# Check for required OAuth fields
|
|
214
|
+
if not client_id:
|
|
215
|
+
return "OAuth client_id is required when using OAuth authentication"
|
|
216
|
+
if not client_secret:
|
|
217
|
+
return "OAuth client_secret is required when using OAuth authentication"
|
|
218
|
+
if not token_url:
|
|
219
|
+
return "OAuth token_url is required when using OAuth authentication"
|
|
220
|
+
|
|
221
|
+
# Extract secret value if it's a SecretStr
|
|
222
|
+
if hasattr(client_secret, 'get_secret_value'):
|
|
223
|
+
client_secret = client_secret.get_secret_value()
|
|
224
|
+
|
|
225
|
+
if not client_secret or not str(client_secret).strip():
|
|
226
|
+
return "OAuth client_secret cannot be empty"
|
|
227
|
+
|
|
228
|
+
# Validate token_url format
|
|
229
|
+
token_url = token_url.strip()
|
|
230
|
+
if not token_url.startswith(('http://', 'https://')):
|
|
231
|
+
return "OAuth token_url must start with http:// or https://"
|
|
232
|
+
|
|
233
|
+
# Get optional OAuth settings
|
|
234
|
+
scope = settings.get('scope')
|
|
235
|
+
method = settings.get('method', 'default') or 'default'
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------
|
|
238
|
+
# Attempt OAuth2 Client Credentials token exchange
|
|
239
|
+
# ---------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
headers = {
|
|
243
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
244
|
+
'Accept': 'application/json',
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
data = {
|
|
248
|
+
'grant_type': 'client_credentials',
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Apply credentials based on method
|
|
252
|
+
if method == 'Basic':
|
|
253
|
+
# Basic method: credentials in Authorization header (Spotify, some AWS)
|
|
254
|
+
credentials = f"{client_id}:{client_secret}"
|
|
255
|
+
encoded = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
|
|
256
|
+
headers['Authorization'] = f'Basic {encoded}'
|
|
257
|
+
else:
|
|
258
|
+
# Default method: credentials in POST body (Azure AD, Auth0, most providers)
|
|
259
|
+
data['client_id'] = client_id
|
|
260
|
+
data['client_secret'] = str(client_secret)
|
|
261
|
+
|
|
262
|
+
if scope:
|
|
263
|
+
data['scope'] = scope
|
|
264
|
+
|
|
265
|
+
response = requests.post(
|
|
266
|
+
token_url,
|
|
267
|
+
headers=headers,
|
|
268
|
+
data=data,
|
|
269
|
+
timeout=30,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------
|
|
273
|
+
# Handle response
|
|
274
|
+
# ---------------------------------------------------------------------
|
|
275
|
+
|
|
276
|
+
if response.status_code == 200:
|
|
277
|
+
try:
|
|
278
|
+
token_data = response.json()
|
|
279
|
+
if 'access_token' in token_data:
|
|
280
|
+
return None # Success - token obtained
|
|
281
|
+
return "OAuth response did not contain 'access_token'"
|
|
282
|
+
except Exception:
|
|
283
|
+
return "Failed to parse OAuth token response"
|
|
284
|
+
|
|
285
|
+
# Handle common error status codes with helpful messages
|
|
286
|
+
if response.status_code == 400:
|
|
287
|
+
try:
|
|
288
|
+
error_data = response.json()
|
|
289
|
+
error = error_data.get('error', 'bad_request')
|
|
290
|
+
error_desc = error_data.get('error_description', '')
|
|
291
|
+
if error_desc:
|
|
292
|
+
return f"OAuth error: {error} - {error_desc}"
|
|
293
|
+
return f"OAuth error: {error}"
|
|
294
|
+
except Exception:
|
|
295
|
+
return "OAuth request failed: bad request (400)"
|
|
296
|
+
|
|
297
|
+
if response.status_code == 401:
|
|
298
|
+
return "OAuth authentication failed: invalid client_id or client_secret"
|
|
299
|
+
|
|
300
|
+
if response.status_code == 403:
|
|
301
|
+
return "OAuth access forbidden: client may lack required permissions"
|
|
302
|
+
|
|
303
|
+
if response.status_code == 404:
|
|
304
|
+
return f"OAuth token endpoint not found: {token_url}"
|
|
305
|
+
|
|
306
|
+
return f"OAuth token request failed with status {response.status_code}"
|
|
307
|
+
|
|
308
|
+
except requests.exceptions.SSLError as e:
|
|
309
|
+
error_str = str(e).lower()
|
|
310
|
+
if 'hostname mismatch' in error_str:
|
|
311
|
+
return "OAuth token_url hostname does not match SSL certificate - verify the URL is correct"
|
|
312
|
+
if 'certificate verify failed' in error_str:
|
|
313
|
+
return "SSL certificate verification failed for OAuth endpoint - the server may have an invalid or self-signed certificate"
|
|
314
|
+
if 'certificate has expired' in error_str:
|
|
315
|
+
return "SSL certificate has expired for OAuth endpoint"
|
|
316
|
+
return "SSL error connecting to OAuth endpoint - verify the token_url is correct"
|
|
317
|
+
except requests.exceptions.ConnectionError as e:
|
|
318
|
+
error_str = str(e).lower()
|
|
319
|
+
if 'name or service not known' in error_str or 'nodename nor servname provided' in error_str:
|
|
320
|
+
return "OAuth token_url hostname could not be resolved - verify the URL is correct"
|
|
321
|
+
if 'connection refused' in error_str:
|
|
322
|
+
return "Connection refused by OAuth endpoint - verify the token_url and port are correct"
|
|
323
|
+
return "Cannot connect to OAuth token endpoint - verify the token_url is correct"
|
|
324
|
+
except requests.exceptions.Timeout:
|
|
325
|
+
return "OAuth token request timed out - the endpoint may be unreachable"
|
|
326
|
+
except requests.exceptions.RequestException:
|
|
327
|
+
return "OAuth request failed - verify the token_url is correct and accessible"
|
|
328
|
+
except Exception:
|
|
329
|
+
return "Unexpected error during OAuth configuration validation"
|
|
@@ -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
|
|
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)}"
|