From 5b581a3589bf418fa31b933ee3108684ea380e69 Mon Sep 17 00:00:00 2001 From: Javier Torres Date: Tue, 5 Nov 2024 19:12:17 -0500 Subject: [PATCH] Fix Asignee and Reporter metadata for Epics in JiraReader (#16842) --- .../llama-index-readers-jira/CHANGELOG.md | 6 + .../llama_index/readers/jira/base.py | 37 +++- .../llama-index-readers-jira/pyproject.toml | 2 +- .../llama-index-readers-jira/tests/BUILD | 4 +- .../tests/test_readers_jira.py | 166 ++++++++++++++++++ 5 files changed, 204 insertions(+), 11 deletions(-) diff --git a/llama-index-integrations/readers/llama-index-readers-jira/CHANGELOG.md b/llama-index-integrations/readers/llama-index-readers-jira/CHANGELOG.md index 36bff877abcbe..d7ca778b88fa5 100644 --- a/llama-index-integrations/readers/llama-index-readers-jira/CHANGELOG.md +++ b/llama-index-integrations/readers/llama-index-readers-jira/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## [0.3.2] - 2024-11-05 + +- Add assignee and reporter emails as metadata +- Fix getting assignee and reporter for epics +- Add include_epics parameter + ## [0.1.2] - 2024-02-13 - Add maintainers and keywords from library.json (llamahub) diff --git a/llama-index-integrations/readers/llama-index-readers-jira/llama_index/readers/jira/base.py b/llama-index-integrations/readers/llama-index-readers-jira/llama_index/readers/jira/base.py index 4ff6741d88690..6ded6a6f5453f 100644 --- a/llama-index-integrations/readers/llama-index-readers-jira/llama_index/readers/jira/base.py +++ b/llama-index-integrations/readers/llama-index-readers-jira/llama_index/readers/jira/base.py @@ -39,6 +39,8 @@ class JiraReader(BaseReader): } """ + include_epics: bool = True + def __init__( self, email: Optional[str] = None, @@ -47,6 +49,7 @@ def __init__( BasicAuth: Optional[BasicAuth] = None, Oauth2: Optional[Oauth2] = None, PATauth: Optional[PATauth] = None, + include_epics: bool = True, ) -> None: from jira import JIRA @@ -75,6 +78,8 @@ def __init__( server=f"https://{BasicAuth['server_url']}", ) + self.include_epics = include_epics + def load_data( self, query: str, start_at: int = 0, max_results: int = 50 ) -> List[Document]: @@ -91,14 +96,26 @@ def load_data( epic_descripton = "" for issue in relevant_issues: - # Iterates through only issues and not epics - if "parent" in (issue.raw["fields"]): - if issue.fields.assignee: - assignee = issue.fields.assignee.displayName - - if issue.fields.reporter: - reporter = issue.fields.reporter.displayName - + issue_type = issue.fields.issuetype.name + if issue_type == "Epic" and not self.include_epics: + continue + + assignee = "" + assignee_email = "" + reporter = "" + reporter_email = "" + epic_key = "" + epic_summary = "" + epic_descripton = "" + + if issue.fields.assignee: + assignee = issue.fields.assignee.displayName + assignee_email = issue.fields.assignee.emailAddress + if issue.fields.reporter: + reporter = issue.fields.reporter.displayName + reporter_email = issue.fields.reporter.emailAddress + + if "parent" in issue.raw["fields"]: if issue.raw["fields"]["parent"]["key"]: epic_key = issue.raw["fields"]["parent"]["key"] @@ -123,9 +140,11 @@ def load_data( "labels": issue.fields.labels, "status": issue.fields.status.name, "assignee": assignee, + "assignee_email": assignee_email, "reporter": reporter, + "reporter_email": reporter_email, "project": issue.fields.project.name, - "issue_type": issue.fields.issuetype.name, + "issue_type": issue_type, "priority": issue.fields.priority.name, "epic_key": epic_key, "epic_summary": epic_summary, diff --git a/llama-index-integrations/readers/llama-index-readers-jira/pyproject.toml b/llama-index-integrations/readers/llama-index-readers-jira/pyproject.toml index 10c7bbe11d851..0771e51c48249 100644 --- a/llama-index-integrations/readers/llama-index-readers-jira/pyproject.toml +++ b/llama-index-integrations/readers/llama-index-readers-jira/pyproject.toml @@ -29,7 +29,7 @@ license = "MIT" maintainers = ["bearguy"] name = "llama-index-readers-jira" readme = "README.md" -version = "0.3.1" +version = "0.3.2" [tool.poetry.dependencies] python = ">=3.8.1,<4.0" diff --git a/llama-index-integrations/readers/llama-index-readers-jira/tests/BUILD b/llama-index-integrations/readers/llama-index-readers-jira/tests/BUILD index dabf212d7e716..30276a4ab6804 100644 --- a/llama-index-integrations/readers/llama-index-readers-jira/tests/BUILD +++ b/llama-index-integrations/readers/llama-index-readers-jira/tests/BUILD @@ -1 +1,3 @@ -python_tests() +python_tests( + dependencies=["llama-index-integrations/readers/llama-index-readers-jira:poetry"], +) diff --git a/llama-index-integrations/readers/llama-index-readers-jira/tests/test_readers_jira.py b/llama-index-integrations/readers/llama-index-readers-jira/tests/test_readers_jira.py index 9355aaf9e642f..9932ab620dce0 100644 --- a/llama-index-integrations/readers/llama-index-readers-jira/tests/test_readers_jira.py +++ b/llama-index-integrations/readers/llama-index-readers-jira/tests/test_readers_jira.py @@ -1,7 +1,173 @@ from llama_index.core.readers.base import BaseReader from llama_index.readers.jira import JiraReader +import pytest +from unittest.mock import patch, MagicMock def test_class(): names_of_base_classes = [b.__name__ for b in JiraReader.__mro__] assert BaseReader.__name__ in names_of_base_classes + + +@pytest.fixture(autouse=True) +def mock_jira(): + with patch("jira.JIRA") as mock_jira: + mock_jira.return_value = MagicMock() + yield mock_jira + + +@pytest.fixture() +def mock_issue(): + issue = MagicMock() + # Setup basic issue fields + issue.id = "TEST-123" + issue.fields.summary = "Test Summary" + issue.fields.description = "Test Description" + issue.fields.issuetype.name = "Story" + issue.fields.created = "2024-01-01" + issue.fields.updated = "2024-01-02" + issue.fields.labels = ["test-label"] + issue.fields.status.name = "In Progress" + issue.fields.project.name = "Test Project" + issue.fields.priority.name = "High" + + # Setup assignee and reporter + issue.fields.assignee = MagicMock() + issue.fields.assignee.displayName = "Test Assignee" + issue.fields.assignee.emailAddress = "assignee@test.com" + issue.fields.reporter = MagicMock() + issue.fields.reporter.displayName = "Test Reporter" + issue.fields.reporter.emailAddress = "reporter@test.com" + + # Setup raw fields for parent/epic info + issue.raw = { + "fields": { + "parent": { + "key": "EPIC-1", + "fields": { + "summary": "Epic Summary", + "status": {"description": "Epic Description"}, + }, + } + } + } + + issue.permalink.return_value = "https://test.atlassian.net/browse/TEST-123" + return issue + + +def test_basic_auth(mock_jira): + reader = JiraReader( + email="test@example.com", + api_token="test-token", + server_url="example.atlassian.net", + ) + + mock_jira.assert_called_once_with( + basic_auth=("test@example.com", "test-token"), + server="https://example.atlassian.net", + ) + + +def test_oauth2(mock_jira): + reader = JiraReader(Oauth2={"cloud_id": "test-cloud", "api_token": "test-token"}) + + mock_jira.assert_called_once_with( + options={ + "server": "https://api.atlassian.com/ex/jira/test-cloud", + "headers": {"Authorization": "Bearer test-token"}, + } + ) + + +def test_pat_auth(mock_jira): + reader = JiraReader( + PATauth={ + "server_url": "https://example.atlassian.net", + "api_token": "test-token", + } + ) + + mock_jira.assert_called_once_with( + options={ + "server": "https://example.atlassian.net", + "headers": {"Authorization": "Bearer test-token"}, + } + ) + + +def test_load_data_basic(mock_jira, mock_issue): + # Setup mock JIRA instance + jira_instance = mock_jira.return_value + jira_instance.search_issues.return_value = [mock_issue] + + reader = JiraReader( + email="test@example.com", + api_token="test-token", + server_url="example.atlassian.net", + ) + + documents = reader.load_data("project = TEST") + + # Verify search_issues was called correctly + jira_instance.search_issues.assert_called_once_with( + "project = TEST", startAt=0, maxResults=50 + ) + + # Verify returned document + assert len(documents) == 1 + doc = documents[0] + assert doc.doc_id == "TEST-123" + assert doc.text == "Test Summary \n Test Description" + + # Verify extra_info + assert doc.extra_info["assignee"] == "Test Assignee" + assert doc.extra_info["assignee_email"] == "assignee@test.com" + assert doc.extra_info["reporter"] == "Test Reporter" + assert doc.extra_info["reporter_email"] == "reporter@test.com" + assert doc.extra_info["epic_key"] == "EPIC-1" + assert doc.extra_info["epic_summary"] == "Epic Summary" + assert doc.extra_info["epic_description"] == "Epic Description" + + +def test_load_data_exclude_epics(mock_jira, mock_issue): + # Modify mock issue to be an epic + mock_issue.fields.issuetype.name = "Epic" + jira_instance = mock_jira.return_value + jira_instance.search_issues.return_value = [mock_issue] + + reader = JiraReader( + email="test@example.com", + api_token="test-token", + server_url="example.atlassian.net", + include_epics=False, + ) + + documents = reader.load_data("project = TEST") + + # Verify no documents returned since epics are excluded + assert len(documents) == 0 + + +def test_load_data_no_assignee_reporter(mock_jira, mock_issue): + # Remove assignee and reporter + mock_issue.fields.assignee = None + mock_issue.fields.reporter = None + jira_instance = mock_jira.return_value + jira_instance.search_issues.return_value = [mock_issue] + + reader = JiraReader( + email="test@example.com", + api_token="test-token", + server_url="example.atlassian.net", + ) + + documents = reader.load_data("project = TEST") + + # Verify document has empty assignee/reporter fields + assert len(documents) == 1 + doc = documents[0] + assert doc.extra_info["assignee"] == "" + assert doc.extra_info["assignee_email"] == "" + assert doc.extra_info["reporter"] == "" + assert doc.extra_info["reporter_email"] == ""