t-bug-catcher 0.6.12__tar.gz → 0.6.14__tar.gz
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.
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/PKG-INFO +1 -1
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/setup.cfg +1 -1
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/setup.py +1 -1
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/__init__.py +1 -1
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/jira.py +42 -12
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/PKG-INFO +1 -1
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/SOURCES.txt +1 -0
- t_bug_catcher-0.6.14/tests/test_jira_grouping.py +112 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/MANIFEST.in +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/README.rst +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/pyproject.toml +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/requirements.txt +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/bug_catcher.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/bug_snag.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/config.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/exceptions.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/resources/whispers_config.yml +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/stack_saver.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/__init__.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/common.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/utils/logger.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher/workitems.py +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/dependency_links.txt +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/not-zip-safe +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/requires.txt +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/t_bug_catcher.egg-info/top_level.txt +0 -0
- {t_bug_catcher-0.6.12 → t_bug_catcher-0.6.14}/tests/test_t_bug_catcher.py +0 -0
|
@@ -26,7 +26,7 @@ setup(
|
|
|
26
26
|
packages=find_packages(include=["t_bug_catcher", "t_bug_catcher.*"]),
|
|
27
27
|
test_suite="tests",
|
|
28
28
|
url="https://www.thoughtful.ai/",
|
|
29
|
-
version="0.6.
|
|
29
|
+
version="0.6.14",
|
|
30
30
|
zip_safe=False,
|
|
31
31
|
install_requires=install_requirements,
|
|
32
32
|
include_package_data=True,
|
|
@@ -164,17 +164,42 @@ class Jira:
|
|
|
164
164
|
"Content-Type": "application/json",
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _adf_to_text(adf) -> str:
|
|
169
|
+
"""Extract plaintext from Atlassian Document Format (ADF) nodes.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
adf: ADF structure (dict/list) or a plain string.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
str: Concatenated text content.
|
|
176
|
+
"""
|
|
177
|
+
if not adf:
|
|
178
|
+
return ""
|
|
179
|
+
if isinstance(adf, str):
|
|
180
|
+
return adf
|
|
181
|
+
if isinstance(adf, list):
|
|
182
|
+
return "".join(Jira._adf_to_text(item) for item in adf)
|
|
183
|
+
if isinstance(adf, dict):
|
|
184
|
+
# Prefer explicit text if present
|
|
185
|
+
text_value = adf.get("text")
|
|
186
|
+
if isinstance(text_value, str):
|
|
187
|
+
return text_value
|
|
188
|
+
# Otherwise, walk nested content
|
|
189
|
+
return Jira._adf_to_text(adf.get("content", []))
|
|
190
|
+
return ""
|
|
191
|
+
|
|
167
192
|
@retry_if_bad_request
|
|
168
193
|
def get_issues(self, project_key: Optional[str] = None) -> dict:
|
|
169
194
|
"""A function to get the issues using a Jira API.
|
|
170
195
|
|
|
171
196
|
It updates the headers, sets up a JQL query, specifies additional query parameters,
|
|
172
|
-
makes a
|
|
197
|
+
makes a POST request to the Jira API, and returns the JSON response.
|
|
173
198
|
"""
|
|
174
199
|
project_key = project_key or self._project_key
|
|
175
200
|
jql_query = f'project = "{project_key}"'
|
|
176
201
|
|
|
177
|
-
# Use POST method with JSON body
|
|
202
|
+
# Use POST method with JSON body for the /rest/api/3/search/jql endpoint
|
|
178
203
|
request_body = {
|
|
179
204
|
"jql": jql_query,
|
|
180
205
|
"maxResults": 100,
|
|
@@ -183,7 +208,7 @@ class Jira:
|
|
|
183
208
|
|
|
184
209
|
response = requests.request(
|
|
185
210
|
"POST",
|
|
186
|
-
self._base_url + "/rest/3/search/jql",
|
|
211
|
+
self._base_url + "/rest/api/3/search/jql",
|
|
187
212
|
headers=self.__get_headers(),
|
|
188
213
|
auth=self._auth,
|
|
189
214
|
json=request_body,
|
|
@@ -1406,11 +1431,15 @@ class Jira:
|
|
|
1406
1431
|
all_issues = self.get_issues()["issues"]
|
|
1407
1432
|
|
|
1408
1433
|
if group_id:
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1434
|
+
search_prefix = f"Error string ID: {error_id}"
|
|
1435
|
+
existing_tickets = []
|
|
1436
|
+
for ticket in all_issues:
|
|
1437
|
+
description = ticket.get("fields", {}).get("description")
|
|
1438
|
+
if not description:
|
|
1439
|
+
continue
|
|
1440
|
+
description_text = self._adf_to_text(description)
|
|
1441
|
+
if search_prefix in description_text:
|
|
1442
|
+
existing_tickets.append(ticket)
|
|
1414
1443
|
summary = self.__create_summary(
|
|
1415
1444
|
type(exception),
|
|
1416
1445
|
exception,
|
|
@@ -1724,13 +1753,14 @@ class Jira:
|
|
|
1724
1753
|
Returns:
|
|
1725
1754
|
dict or None: The matching ticket if found, otherwise None.
|
|
1726
1755
|
"""
|
|
1756
|
+
search_token = f"Error string ID: {error_id}"
|
|
1727
1757
|
for ticket in all_tickets:
|
|
1728
|
-
description = ticket.get("fields", {}).get("description"
|
|
1758
|
+
description = ticket.get("fields", {}).get("description")
|
|
1729
1759
|
if not description:
|
|
1730
1760
|
continue
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1761
|
+
description_text = self._adf_to_text(description)
|
|
1762
|
+
if search_token in description_text:
|
|
1763
|
+
return ticket
|
|
1734
1764
|
|
|
1735
1765
|
else:
|
|
1736
1766
|
return None
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import MagicMock, patch
|
|
3
|
+
|
|
4
|
+
from t_bug_catcher.jira import Jira
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestJiraGrouping(unittest.TestCase):
|
|
8
|
+
"""Tests for Jira ADF parsing and ticket grouping logic."""
|
|
9
|
+
|
|
10
|
+
def setUp(self):
|
|
11
|
+
"""Set up the test environment."""
|
|
12
|
+
self.jira = Jira()
|
|
13
|
+
|
|
14
|
+
def test_adf_to_text_extracts_error_id(self):
|
|
15
|
+
"""Ensure ADF parser extracts plain text including the error id token."""
|
|
16
|
+
error_id = "abc123"
|
|
17
|
+
adf = {
|
|
18
|
+
"type": "doc",
|
|
19
|
+
"version": 1,
|
|
20
|
+
"content": [
|
|
21
|
+
{
|
|
22
|
+
"type": "paragraph",
|
|
23
|
+
"content": [
|
|
24
|
+
{"type": "text", "text": "Error string ID: "},
|
|
25
|
+
{"type": "text", "text": error_id, "marks": [{"type": "em"}]},
|
|
26
|
+
],
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
text = Jira._adf_to_text(adf)
|
|
32
|
+
self.assertIn(f"Error string ID: {error_id}", text)
|
|
33
|
+
|
|
34
|
+
def test_filter_tickets_matches_adf_description(self):
|
|
35
|
+
"""filter_tickets should find a ticket when the error id is embedded in ADF description."""
|
|
36
|
+
error_id = "abc123"
|
|
37
|
+
ticket = {
|
|
38
|
+
"id": "10001",
|
|
39
|
+
"key": "TEST-1",
|
|
40
|
+
"fields": {
|
|
41
|
+
"description": {
|
|
42
|
+
"type": "doc",
|
|
43
|
+
"version": 1,
|
|
44
|
+
"content": [
|
|
45
|
+
{
|
|
46
|
+
"type": "paragraph",
|
|
47
|
+
"content": [
|
|
48
|
+
{"type": "text", "text": f"Error string ID: {error_id}"},
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
matched = self.jira.filter_tickets(all_tickets=[ticket], error_id=error_id)
|
|
57
|
+
self.assertIsNotNone(matched)
|
|
58
|
+
self.assertEqual(matched["key"], "TEST-1")
|
|
59
|
+
|
|
60
|
+
@patch.object(Jira, "_Jira__update_existing_ticket")
|
|
61
|
+
@patch.object(Jira, "_Jira__create_new_ticket")
|
|
62
|
+
@patch.object(Jira, "get_issues")
|
|
63
|
+
@patch.object(Jira, "_Jira__generate_error_id")
|
|
64
|
+
def test_report_error_groups_updates_existing(self, mock_gen_id, mock_get_issues, mock_create, mock_update):
|
|
65
|
+
"""report_error should create once, then update existing when the same error is reported again."""
|
|
66
|
+
fixed_id = "deadbeef"
|
|
67
|
+
mock_gen_id.return_value = fixed_id
|
|
68
|
+
|
|
69
|
+
# First call: no issues present -> should create
|
|
70
|
+
# Second call: an issue present with matching error id in ADF -> should update
|
|
71
|
+
adf_issue = {
|
|
72
|
+
"id": "10002",
|
|
73
|
+
"key": "TEST-2",
|
|
74
|
+
"fields": {
|
|
75
|
+
"description": {
|
|
76
|
+
"type": "doc",
|
|
77
|
+
"version": 1,
|
|
78
|
+
"content": [
|
|
79
|
+
{
|
|
80
|
+
"type": "paragraph",
|
|
81
|
+
"content": [
|
|
82
|
+
{"type": "text", "text": f"Error string ID: {fixed_id}"},
|
|
83
|
+
],
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
mock_get_issues.side_effect = [
|
|
90
|
+
{"issues": []},
|
|
91
|
+
{"issues": [adf_issue]},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Prevent any network side effects
|
|
95
|
+
mock_create.return_value = MagicMock(status_code=201, json=lambda: {"key": "TEST-NEW", "id": "10010"})
|
|
96
|
+
mock_update.return_value = None
|
|
97
|
+
|
|
98
|
+
# Raise and catch the same exception twice to keep traceback and ids deterministic
|
|
99
|
+
for call_idx in range(2):
|
|
100
|
+
try:
|
|
101
|
+
d = {"a": 1}
|
|
102
|
+
_ = d["missing"]
|
|
103
|
+
except Exception as ex:
|
|
104
|
+
self.jira.report_error(exception=ex)
|
|
105
|
+
|
|
106
|
+
# Should create once and then update once
|
|
107
|
+
self.assertEqual(mock_create.call_count, 1)
|
|
108
|
+
self.assertEqual(mock_update.call_count, 1)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|