mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-01 18:19:08 +00:00
update auth type
This commit is contained in:
parent
7af65a5a66
commit
4984aab3f1
6 changed files with 245 additions and 140 deletions
|
@ -5,8 +5,10 @@ A module for retrieving data from Jira.
|
|||
Allows fetching issue lists and their comments, projects and more.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
import base64
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
|
@ -17,55 +19,76 @@ class JiraConnector:
|
|||
def __init__(
|
||||
self,
|
||||
base_url: Optional[str] = None,
|
||||
personal_access_token: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
api_token: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the JiraConnector class.
|
||||
|
||||
Args:
|
||||
base_url: Jira instance base URL (e.g., 'https://yourcompany.atlassian.net') (optional)
|
||||
personal_access_token: Jira personal access token (optional)
|
||||
email: Jira account email address (optional)
|
||||
api_token: Jira API token (optional)
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/") if base_url else None
|
||||
self.personal_access_token = personal_access_token
|
||||
self.email = email
|
||||
self.api_token = api_token
|
||||
self.api_version = "3" # Jira Cloud API version
|
||||
|
||||
def set_credentials(self, base_url: str, personal_access_token: str) -> None:
|
||||
def set_credentials(self, base_url: str, email: str, api_token: str) -> None:
|
||||
"""
|
||||
Set the Jira credentials.
|
||||
|
||||
Args:
|
||||
base_url: Jira instance base URL
|
||||
personal_access_token: Jira personal access token
|
||||
email: Jira account email address
|
||||
api_token: Jira API token
|
||||
"""
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.personal_access_token = personal_access_token
|
||||
self.email = email
|
||||
self.api_token = api_token
|
||||
|
||||
def set_personal_access_token(self, personal_access_token: str) -> None:
|
||||
def set_email(self, email: str) -> None:
|
||||
"""
|
||||
Set the Jira personal access token.
|
||||
Set the Jira account email.
|
||||
|
||||
Args:
|
||||
personal_access_token: Jira personal access token
|
||||
email: Jira account email address
|
||||
"""
|
||||
self.personal_access_token = personal_access_token
|
||||
self.email = email
|
||||
|
||||
def set_api_token(self, api_token: str) -> None:
|
||||
"""
|
||||
Set the Jira API token.
|
||||
|
||||
Args:
|
||||
api_token: Jira API token
|
||||
"""
|
||||
self.api_token = api_token
|
||||
|
||||
def get_headers(self) -> Dict[str, str]:
|
||||
"""
|
||||
Get headers for Jira API requests.
|
||||
Get headers for Jira API requests using Basic Authentication.
|
||||
|
||||
Returns:
|
||||
Dictionary of headers
|
||||
|
||||
Raises:
|
||||
ValueError: If personal_access_token or base_url have not been set
|
||||
ValueError: If email, api_token, or base_url have not been set
|
||||
"""
|
||||
if not all([self.base_url, self.personal_access_token]):
|
||||
raise ValueError("Jira personal access token or base URL not initialized.")
|
||||
if not all([self.base_url, self.email, self.api_token]):
|
||||
raise ValueError(
|
||||
"Jira credentials not initialized. Call set_credentials() first."
|
||||
)
|
||||
|
||||
# Create Basic Auth header using email:api_token
|
||||
auth_str = f"{self.email}:{self.api_token}"
|
||||
auth_bytes = auth_str.encode("utf-8")
|
||||
auth_header = "Basic " + base64.b64encode(auth_bytes).decode("ascii")
|
||||
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.personal_access_token}",
|
||||
"Authorization": auth_header,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
|
@ -83,17 +106,21 @@ class JiraConnector:
|
|||
Response data from the API
|
||||
|
||||
Raises:
|
||||
ValueError: If personal_access_token or base_url have not been set
|
||||
ValueError: If email, api_token, or base_url have not been set
|
||||
Exception: If the API request fails
|
||||
"""
|
||||
if not all([self.base_url, self.personal_access_token]):
|
||||
raise ValueError("Jira personal access token or base URL not initialized.")
|
||||
if not all([self.base_url, self.email, self.api_token]):
|
||||
raise ValueError(
|
||||
"Jira credentials not initialized. Call set_credentials() first."
|
||||
)
|
||||
|
||||
url = f"{self.base_url}/rest/api/{self.api_version}/{endpoint}"
|
||||
headers = self.get_headers()
|
||||
|
||||
response = requests.get(url, headers=headers, params=params, timeout=500)
|
||||
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
|
@ -197,9 +224,11 @@ class JiraConnector:
|
|||
try:
|
||||
# Build JQL query for date range
|
||||
# Query issues that were either created OR updated within the date range
|
||||
date_filter = f"(created >= '{start_date}' AND created <= '{end_date}') OR (updated >= '{start_date}' AND updated <= '{end_date}')"
|
||||
date_filter = (
|
||||
f"(createdDate >= '{start_date}' AND createdDate <= '{end_date}')"
|
||||
)
|
||||
|
||||
jql = f"{date_filter} ORDER BY created DESC"
|
||||
jql = f"{date_filter}"
|
||||
if project_key:
|
||||
jql = (
|
||||
f'project = "{project_key}" AND {date_filter} ORDER BY created DESC'
|
||||
|
@ -234,8 +263,11 @@ class JiraConnector:
|
|||
|
||||
while True:
|
||||
params["startAt"] = start_at
|
||||
print(json.dumps(params, indent=2))
|
||||
result = self.make_api_request("search", params)
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
if not isinstance(result, dict) or "issues" not in result:
|
||||
return [], "Invalid response from Jira API"
|
||||
|
||||
|
|
|
@ -1,104 +1,112 @@
|
|||
import unittest
|
||||
from unittest.mock import patch, Mock
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
# Import the JiraConnector
|
||||
from .jira_connector import JiraConnector
|
||||
|
||||
|
||||
class TestJiraConnector(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.base_url = "https://test.atlassian.net"
|
||||
self.token = "test_token"
|
||||
self.connector = JiraConnector(base_url=self.base_url, personal_access_token=self.token)
|
||||
self.email = "test@example.com"
|
||||
self.api_token = "test_api_token"
|
||||
self.connector = JiraConnector(
|
||||
base_url=self.base_url, email=self.email, api_token=self.api_token
|
||||
)
|
||||
|
||||
def test_init(self):
|
||||
"""Test JiraConnector initialization."""
|
||||
self.assertEqual(self.connector.base_url, self.base_url)
|
||||
self.assertEqual(self.connector.personal_access_token, self.token)
|
||||
self.assertEqual(self.connector.email, self.email)
|
||||
self.assertEqual(self.connector.api_token, self.api_token)
|
||||
self.assertEqual(self.connector.api_version, "3")
|
||||
|
||||
def test_init_with_trailing_slash(self):
|
||||
"""Test JiraConnector initialization with trailing slash in URL."""
|
||||
connector = JiraConnector(base_url="https://test.atlassian.net/", personal_access_token=self.token)
|
||||
connector = JiraConnector(
|
||||
base_url="https://test.atlassian.net/",
|
||||
email=self.email,
|
||||
api_token=self.api_token,
|
||||
)
|
||||
self.assertEqual(connector.base_url, "https://test.atlassian.net")
|
||||
|
||||
def test_set_credentials(self):
|
||||
"""Test setting credentials."""
|
||||
new_url = "https://newtest.atlassian.net/"
|
||||
new_token = "new_token"
|
||||
|
||||
self.connector.set_credentials(new_url, new_token)
|
||||
|
||||
new_email = "new@example.com"
|
||||
new_token = "new_api_token"
|
||||
|
||||
self.connector.set_credentials(new_url, new_email, new_token)
|
||||
|
||||
self.assertEqual(self.connector.base_url, "https://newtest.atlassian.net")
|
||||
self.assertEqual(self.connector.personal_access_token, new_token)
|
||||
self.assertEqual(self.connector.email, new_email)
|
||||
self.assertEqual(self.connector.api_token, new_token)
|
||||
|
||||
def test_get_headers(self):
|
||||
"""Test header generation."""
|
||||
headers = self.connector.get_headers()
|
||||
|
||||
self.assertIn('Content-Type', headers)
|
||||
self.assertIn('Authorization', headers)
|
||||
self.assertIn('Accept', headers)
|
||||
self.assertEqual(headers['Content-Type'], 'application/json')
|
||||
self.assertEqual(headers['Accept'], 'application/json')
|
||||
self.assertTrue(headers['Authorization'].startswith('Bearer '))
|
||||
|
||||
self.assertIn("Content-Type", headers)
|
||||
self.assertIn("Authorization", headers)
|
||||
self.assertIn("Accept", headers)
|
||||
self.assertEqual(headers["Content-Type"], "application/json")
|
||||
self.assertEqual(headers["Accept"], "application/json")
|
||||
self.assertTrue(headers["Authorization"].startswith("Basic "))
|
||||
|
||||
def test_get_headers_no_credentials(self):
|
||||
"""Test header generation without credentials."""
|
||||
connector = JiraConnector()
|
||||
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
connector.get_headers()
|
||||
|
||||
|
||||
self.assertIn("Jira credentials not initialized", str(context.exception))
|
||||
|
||||
@patch('requests.get')
|
||||
@patch("requests.get")
|
||||
def test_make_api_request_success(self, mock_get):
|
||||
"""Test successful API request."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"test": "data"}
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
|
||||
result = self.connector.make_api_request("test/endpoint")
|
||||
|
||||
|
||||
self.assertEqual(result, {"test": "data"})
|
||||
mock_get.assert_called_once()
|
||||
|
||||
@patch('requests.get')
|
||||
@patch("requests.get")
|
||||
def test_make_api_request_failure(self, mock_get):
|
||||
"""Test failed API request."""
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 401
|
||||
mock_response.text = "Unauthorized"
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
self.connector.make_api_request("test/endpoint")
|
||||
|
||||
|
||||
self.assertIn("API request failed with status code 401", str(context.exception))
|
||||
|
||||
@patch.object(JiraConnector, 'make_api_request')
|
||||
@patch.object(JiraConnector, "make_api_request")
|
||||
def test_get_all_projects(self, mock_api_request):
|
||||
"""Test getting all projects."""
|
||||
mock_api_request.return_value = {
|
||||
"values": [
|
||||
{"id": "1", "key": "TEST", "name": "Test Project"},
|
||||
{"id": "2", "key": "DEMO", "name": "Demo Project"}
|
||||
{"id": "2", "key": "DEMO", "name": "Demo Project"},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
projects = self.connector.get_all_projects()
|
||||
|
||||
|
||||
self.assertEqual(len(projects), 2)
|
||||
self.assertEqual(projects[0]["key"], "TEST")
|
||||
self.assertEqual(projects[1]["key"], "DEMO")
|
||||
mock_api_request.assert_called_once_with("project")
|
||||
|
||||
@patch.object(JiraConnector, 'make_api_request')
|
||||
@patch.object(JiraConnector, "make_api_request")
|
||||
def test_get_all_issues(self, mock_api_request):
|
||||
"""Test getting all issues."""
|
||||
mock_api_request.return_value = {
|
||||
|
@ -114,15 +122,15 @@ class TestJiraConnector(unittest.TestCase):
|
|||
"issuetype": {"name": "Bug"},
|
||||
"project": {"key": "TEST"},
|
||||
"created": "2023-01-01T10:00:00.000+0000",
|
||||
"updated": "2023-01-01T12:00:00.000+0000"
|
||||
}
|
||||
"updated": "2023-01-01T12:00:00.000+0000",
|
||||
},
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
"total": 1,
|
||||
}
|
||||
|
||||
|
||||
issues = self.connector.get_all_issues()
|
||||
|
||||
|
||||
self.assertEqual(len(issues), 1)
|
||||
self.assertEqual(issues[0]["key"], "TEST-1")
|
||||
self.assertEqual(issues[0]["fields"]["summary"], "Test Issue")
|
||||
|
@ -144,18 +152,18 @@ class TestJiraConnector(unittest.TestCase):
|
|||
"reporter": {
|
||||
"accountId": "123",
|
||||
"displayName": "John Doe",
|
||||
"emailAddress": "john@example.com"
|
||||
"emailAddress": "john@example.com",
|
||||
},
|
||||
"assignee": {
|
||||
"accountId": "456",
|
||||
"displayName": "Jane Smith",
|
||||
"emailAddress": "jane@example.com"
|
||||
}
|
||||
}
|
||||
"emailAddress": "jane@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
formatted = self.connector.format_issue(raw_issue)
|
||||
|
||||
|
||||
self.assertEqual(formatted["id"], "1")
|
||||
self.assertEqual(formatted["key"], "TEST-1")
|
||||
self.assertEqual(formatted["title"], "Test Issue")
|
||||
|
@ -170,17 +178,17 @@ class TestJiraConnector(unittest.TestCase):
|
|||
"""Test date formatting."""
|
||||
iso_date = "2023-01-01T10:30:00.000+0000"
|
||||
formatted_date = JiraConnector.format_date(iso_date)
|
||||
|
||||
|
||||
self.assertEqual(formatted_date, "2023-01-01 10:30:00")
|
||||
|
||||
def test_format_date_invalid(self):
|
||||
"""Test date formatting with invalid input."""
|
||||
formatted_date = JiraConnector.format_date("invalid-date")
|
||||
self.assertEqual(formatted_date, "invalid-date")
|
||||
|
||||
|
||||
formatted_date = JiraConnector.format_date("")
|
||||
self.assertEqual(formatted_date, "Unknown date")
|
||||
|
||||
|
||||
formatted_date = JiraConnector.format_date(None)
|
||||
self.assertEqual(formatted_date, "Unknown date")
|
||||
|
||||
|
@ -198,11 +206,11 @@ class TestJiraConnector(unittest.TestCase):
|
|||
"created_at": "2023-01-01T10:00:00.000+0000",
|
||||
"updated_at": "2023-01-01T12:00:00.000+0000",
|
||||
"description": "Test Description",
|
||||
"comments": []
|
||||
"comments": [],
|
||||
}
|
||||
|
||||
|
||||
markdown = self.connector.format_issue_to_markdown(formatted_issue)
|
||||
|
||||
|
||||
self.assertIn("# TEST-1: Test Issue", markdown)
|
||||
self.assertIn("**Status:** Open", markdown)
|
||||
self.assertIn("**Priority:** High", markdown)
|
||||
|
@ -214,5 +222,5 @@ class TestJiraConnector(unittest.TestCase):
|
|||
self.assertIn("Test Description", markdown)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
from datetime import datetime
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional
|
||||
from pydantic import BaseModel, field_validator, ConfigDict
|
||||
from .base import IDModel, TimestampModel
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from app.db import SearchSourceConnectorType
|
||||
from pydantic import BaseModel, ConfigDict, field_validator
|
||||
|
||||
from .base import IDModel, TimestampModel
|
||||
|
||||
|
||||
class SearchSourceConnectorBase(BaseModel):
|
||||
name: str
|
||||
|
@ -11,105 +14,129 @@ class SearchSourceConnectorBase(BaseModel):
|
|||
is_indexable: bool
|
||||
last_indexed_at: Optional[datetime] = None
|
||||
config: Dict[str, Any]
|
||||
|
||||
@field_validator('config')
|
||||
|
||||
@field_validator("config")
|
||||
@classmethod
|
||||
def validate_config_for_connector_type(cls, config: Dict[str, Any], values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
connector_type = values.data.get('connector_type')
|
||||
|
||||
def validate_config_for_connector_type(
|
||||
cls, config: Dict[str, Any], values: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
connector_type = values.data.get("connector_type")
|
||||
|
||||
if connector_type == SearchSourceConnectorType.SERPER_API:
|
||||
# For SERPER_API, only allow SERPER_API_KEY
|
||||
allowed_keys = ["SERPER_API_KEY"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For SERPER_API connector type, config must only contain these keys: {allowed_keys}")
|
||||
|
||||
raise ValueError(
|
||||
f"For SERPER_API connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the API key is not empty
|
||||
if not config.get("SERPER_API_KEY"):
|
||||
raise ValueError("SERPER_API_KEY cannot be empty")
|
||||
|
||||
|
||||
elif connector_type == SearchSourceConnectorType.TAVILY_API:
|
||||
# For TAVILY_API, only allow TAVILY_API_KEY
|
||||
allowed_keys = ["TAVILY_API_KEY"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For TAVILY_API connector type, config must only contain these keys: {allowed_keys}")
|
||||
|
||||
raise ValueError(
|
||||
f"For TAVILY_API connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the API key is not empty
|
||||
if not config.get("TAVILY_API_KEY"):
|
||||
raise ValueError("TAVILY_API_KEY cannot be empty")
|
||||
|
||||
|
||||
elif connector_type == SearchSourceConnectorType.LINKUP_API:
|
||||
# For LINKUP_API, only allow LINKUP_API_KEY
|
||||
allowed_keys = ["LINKUP_API_KEY"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For LINKUP_API connector type, config must only contain these keys: {allowed_keys}")
|
||||
|
||||
raise ValueError(
|
||||
f"For LINKUP_API connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the API key is not empty
|
||||
if not config.get("LINKUP_API_KEY"):
|
||||
raise ValueError("LINKUP_API_KEY cannot be empty")
|
||||
|
||||
|
||||
elif connector_type == SearchSourceConnectorType.SLACK_CONNECTOR:
|
||||
# For SLACK_CONNECTOR, only allow SLACK_BOT_TOKEN
|
||||
allowed_keys = ["SLACK_BOT_TOKEN"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For SLACK_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||
raise ValueError(
|
||||
f"For SLACK_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the bot token is not empty
|
||||
if not config.get("SLACK_BOT_TOKEN"):
|
||||
raise ValueError("SLACK_BOT_TOKEN cannot be empty")
|
||||
|
||||
|
||||
elif connector_type == SearchSourceConnectorType.NOTION_CONNECTOR:
|
||||
# For NOTION_CONNECTOR, only allow NOTION_INTEGRATION_TOKEN
|
||||
allowed_keys = ["NOTION_INTEGRATION_TOKEN"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For NOTION_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||
|
||||
raise ValueError(
|
||||
f"For NOTION_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the integration token is not empty
|
||||
if not config.get("NOTION_INTEGRATION_TOKEN"):
|
||||
raise ValueError("NOTION_INTEGRATION_TOKEN cannot be empty")
|
||||
|
||||
|
||||
elif connector_type == SearchSourceConnectorType.GITHUB_CONNECTOR:
|
||||
# For GITHUB_CONNECTOR, only allow GITHUB_PAT and repo_full_names
|
||||
allowed_keys = ["GITHUB_PAT", "repo_full_names"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||
|
||||
raise ValueError(
|
||||
f"For GITHUB_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the token is not empty
|
||||
if not config.get("GITHUB_PAT"):
|
||||
raise ValueError("GITHUB_PAT cannot be empty")
|
||||
|
||||
|
||||
# Ensure the repo_full_names is present and is a non-empty list
|
||||
repo_full_names = config.get("repo_full_names")
|
||||
if not isinstance(repo_full_names, list) or not repo_full_names:
|
||||
raise ValueError("repo_full_names must be a non-empty list of strings")
|
||||
|
||||
|
||||
elif connector_type == SearchSourceConnectorType.LINEAR_CONNECTOR:
|
||||
# For LINEAR_CONNECTOR, only allow LINEAR_API_KEY
|
||||
allowed_keys = ["LINEAR_API_KEY"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For LINEAR_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||
|
||||
raise ValueError(
|
||||
f"For LINEAR_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the token is not empty
|
||||
if not config.get("LINEAR_API_KEY"):
|
||||
raise ValueError("LINEAR_API_KEY cannot be empty")
|
||||
|
||||
|
||||
elif connector_type == SearchSourceConnectorType.DISCORD_CONNECTOR:
|
||||
# For DISCORD_CONNECTOR, only allow DISCORD_BOT_TOKEN
|
||||
allowed_keys = ["DISCORD_BOT_TOKEN"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For DISCORD_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||
raise ValueError(
|
||||
f"For DISCORD_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the bot token is not empty
|
||||
if not config.get("DISCORD_BOT_TOKEN"):
|
||||
raise ValueError("DISCORD_BOT_TOKEN cannot be empty")
|
||||
elif connector_type == SearchSourceConnectorType.JIRA_CONNECTOR:
|
||||
# For JIRA_CONNECTOR, allow JIRA_PERSONAL_ACCESS_TOKEN and JIRA_BASE_URL
|
||||
allowed_keys = ["JIRA_PERSONAL_ACCESS_TOKEN", "JIRA_BASE_URL"]
|
||||
# For JIRA_CONNECTOR, require JIRA_EMAIL, JIRA_API_TOKEN and JIRA_BASE_URL
|
||||
allowed_keys = ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"]
|
||||
if set(config.keys()) != set(allowed_keys):
|
||||
raise ValueError(f"For JIRA_CONNECTOR connector type, config must only contain these keys: {allowed_keys}")
|
||||
raise ValueError(
|
||||
f"For JIRA_CONNECTOR connector type, config must only contain these keys: {allowed_keys}"
|
||||
)
|
||||
|
||||
# Ensure the token is not empty
|
||||
if not config.get("JIRA_PERSONAL_ACCESS_TOKEN"):
|
||||
raise ValueError("JIRA_PERSONAL_ACCESS_TOKEN cannot be empty")
|
||||
# Ensure the email is not empty
|
||||
if not config.get("JIRA_EMAIL"):
|
||||
raise ValueError("JIRA_EMAIL cannot be empty")
|
||||
|
||||
# Ensure the API token is not empty
|
||||
if not config.get("JIRA_API_TOKEN"):
|
||||
raise ValueError("JIRA_API_TOKEN cannot be empty")
|
||||
|
||||
# Ensure the base URL is not empty
|
||||
if not config.get("JIRA_BASE_URL"):
|
||||
|
@ -117,9 +144,11 @@ class SearchSourceConnectorBase(BaseModel):
|
|||
|
||||
return config
|
||||
|
||||
|
||||
class SearchSourceConnectorCreate(SearchSourceConnectorBase):
|
||||
pass
|
||||
|
||||
|
||||
class SearchSourceConnectorUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
connector_type: Optional[SearchSourceConnectorType] = None
|
||||
|
@ -127,7 +156,8 @@ class SearchSourceConnectorUpdate(BaseModel):
|
|||
last_indexed_at: Optional[datetime] = None
|
||||
config: Optional[Dict[str, Any]] = None
|
||||
|
||||
|
||||
class SearchSourceConnectorRead(SearchSourceConnectorBase, IDModel, TimestampModel):
|
||||
user_id: uuid.UUID
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional, Tuple
|
||||
|
@ -2041,10 +2042,11 @@ async def index_jira_issues(
|
|||
return 0, f"Connector with ID {connector_id} not found"
|
||||
|
||||
# Get the Jira credentials from the connector config
|
||||
jira_token = connector.config.get("JIRA_PERSONAL_ACCESS_TOKEN")
|
||||
jira_email = connector.config.get("JIRA_EMAIL")
|
||||
jira_api_token = connector.config.get("JIRA_API_TOKEN")
|
||||
jira_base_url = connector.config.get("JIRA_BASE_URL")
|
||||
|
||||
if not jira_token or not jira_base_url:
|
||||
if not jira_email or not jira_api_token or not jira_base_url:
|
||||
await task_logger.log_task_failure(
|
||||
log_entry,
|
||||
f"Jira credentials not found in connector config for connector {connector_id}",
|
||||
|
@ -2061,7 +2063,7 @@ async def index_jira_issues(
|
|||
)
|
||||
|
||||
jira_client = JiraConnector(
|
||||
base_url=jira_base_url, personal_access_token=jira_token
|
||||
base_url=jira_base_url, email=jira_email, api_token=jira_api_token
|
||||
)
|
||||
|
||||
# Calculate date range
|
||||
|
@ -2097,6 +2099,8 @@ async def index_jira_issues(
|
|||
start_date=start_date_str, end_date=end_date_str, include_comments=True
|
||||
)
|
||||
|
||||
print(json.dumps(issues, indent=2))
|
||||
|
||||
if error:
|
||||
logger.error(f"Failed to get Jira issues: {error}")
|
||||
|
||||
|
@ -2112,10 +2116,10 @@ async def index_jira_issues(
|
|||
f"Updated last_indexed_at to {connector.last_indexed_at} despite no issues found"
|
||||
)
|
||||
|
||||
await task_logger.log_task_completion(
|
||||
await task_logger.log_task_success(
|
||||
log_entry,
|
||||
f"No Jira issues found in date range {start_date_str} to {end_date_str}",
|
||||
{"indexed_count": 0},
|
||||
{"issues_found": 0},
|
||||
)
|
||||
return 0, None
|
||||
else:
|
||||
|
@ -2132,7 +2136,7 @@ async def index_jira_issues(
|
|||
await task_logger.log_task_progress(
|
||||
log_entry,
|
||||
f"Retrieved {len(issues)} issues from Jira API",
|
||||
{"stage": "processing_issues", "issue_count": len(issues)},
|
||||
{"stage": "processing_issues", "issues_found": len(issues)},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
|
@ -2254,10 +2258,10 @@ async def index_jira_issues(
|
|||
await session.commit()
|
||||
logger.info(f"Updated last_indexed_at to {connector.last_indexed_at}")
|
||||
|
||||
await task_logger.log_task_completion(
|
||||
await task_logger.log_task_success(
|
||||
log_entry,
|
||||
f"Successfully indexed {indexed_count} Jira issues",
|
||||
{"indexed_count": indexed_count},
|
||||
{"issues_indexed": indexed_count},
|
||||
)
|
||||
|
||||
logger.info(f"Successfully indexed {indexed_count} Jira issues")
|
||||
|
|
|
@ -193,10 +193,17 @@ export default function EditConnectorPage() {
|
|||
/>
|
||||
<EditSimpleTokenForm
|
||||
control={editForm.control}
|
||||
fieldName="JIRA_PERSONAL_ACCESS_TOKEN"
|
||||
fieldLabel="Jira Personal Access Token"
|
||||
fieldDescription="Update your Jira Personal Access Token if needed."
|
||||
placeholder="Your Jira Personal Access Token"
|
||||
fieldName="JIRA_EMAIL"
|
||||
fieldLabel="Jira Email"
|
||||
fieldDescription="Update your Atlassian account email if needed."
|
||||
placeholder="your.email@company.com"
|
||||
/>
|
||||
<EditSimpleTokenForm
|
||||
control={editForm.control}
|
||||
fieldName="JIRA_API_TOKEN"
|
||||
fieldLabel="Jira API Token"
|
||||
fieldDescription="Update your Jira API Token if needed."
|
||||
placeholder="Your Jira API Token"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -57,8 +57,11 @@ const jiraConnectorFormSchema = z.object({
|
|||
message: "Please enter a valid Jira instance URL",
|
||||
},
|
||||
),
|
||||
personal_access_token: z.string().min(10, {
|
||||
message: "Jira Personal Access Token is required and must be valid.",
|
||||
email: z.string().email({
|
||||
message: "Please enter a valid email address.",
|
||||
}),
|
||||
api_token: z.string().min(10, {
|
||||
message: "Jira API Token is required and must be valid.",
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -78,7 +81,8 @@ export default function JiraConnectorPage() {
|
|||
defaultValues: {
|
||||
name: "Jira Connector",
|
||||
base_url: "",
|
||||
personal_access_token: "",
|
||||
email: "",
|
||||
api_token: "",
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -91,7 +95,8 @@ export default function JiraConnectorPage() {
|
|||
connector_type: "JIRA_CONNECTOR",
|
||||
config: {
|
||||
JIRA_BASE_URL: values.base_url,
|
||||
JIRA_PERSONAL_ACCESS_TOKEN: values.personal_access_token,
|
||||
JIRA_EMAIL: values.email,
|
||||
JIRA_API_TOKEN: values.api_token,
|
||||
},
|
||||
is_indexable: true,
|
||||
last_indexed_at: null,
|
||||
|
@ -210,20 +215,40 @@ export default function JiraConnectorPage() {
|
|||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="personal_access_token"
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Personal Access Token</FormLabel>
|
||||
<FormLabel>Email Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your Jira Personal Access Token"
|
||||
type="email"
|
||||
placeholder="your.email@company.com"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Jira Personal Access Token will be encrypted
|
||||
and stored securely.
|
||||
Your Atlassian account email address.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="api_token"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>API Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="Your Jira API Token"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Your Jira API Token will be encrypted and stored securely.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
@ -296,8 +321,8 @@ export default function JiraConnectorPage() {
|
|||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">How it works</h3>
|
||||
<p className="text-muted-foreground">
|
||||
The Jira connector uses the Jira REST API to fetch all
|
||||
issues and comments that the Personal Access Token has
|
||||
The Jira connector uses the Jira REST API with Basic Authentication
|
||||
to fetch all issues and comments that your account has
|
||||
access to within your Jira instance.
|
||||
</p>
|
||||
<ul className="mt-2 list-disc pl-5 text-muted-foreground">
|
||||
|
@ -324,15 +349,14 @@ export default function JiraConnectorPage() {
|
|||
<AlertTitle>Read-Only Access is Sufficient</AlertTitle>
|
||||
<AlertDescription>
|
||||
You only need read access for this connector to work.
|
||||
The Personal Access Token will only be used to read
|
||||
your Jira data.
|
||||
The API Token will only be used to read your Jira data.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-medium mb-2">
|
||||
Step 1: Create a Personal Access Token
|
||||
Step 1: Create an API Token
|
||||
</h4>
|
||||
<ol className="list-decimal pl-5 space-y-3">
|
||||
<li>Log in to your Atlassian account</li>
|
||||
|
@ -369,10 +393,10 @@ export default function JiraConnectorPage() {
|
|||
Step 2: Grant necessary access
|
||||
</h4>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
The Personal Access Token will have access to all
|
||||
projects and issues that your user account can see.
|
||||
Make sure your account has appropriate permissions
|
||||
for the projects you want to index.
|
||||
The API Token will have access to all projects and
|
||||
issues that your user account can see. Make sure your
|
||||
account has appropriate permissions for the projects
|
||||
you want to index.
|
||||
</p>
|
||||
<Alert className="bg-muted">
|
||||
<Info className="h-4 w-4" />
|
||||
|
|
Loading…
Add table
Reference in a new issue