pycodetags-issue-tracker 0.1.0__py2.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.
@@ -0,0 +1,7 @@
1
+ """Metadata for pycodetags_issue_tracker."""
2
+
3
+ __all__ = ["__title__", "__version__", "__description__"]
4
+
5
+ __title__ = "pycodetags-issue-tracker"
6
+ __version__ = "0.1.0"
7
+ __description__ = "Plugin for pycodetags to track issues, e.g. bugs, TODO, etc in your code"
@@ -0,0 +1,29 @@
1
+ __all__ = [
2
+ "TODO",
3
+ "FIXME",
4
+ "REQUIREMENT",
5
+ "STORY",
6
+ "IDEA",
7
+ "BUG",
8
+ "HACK",
9
+ "CLEVER",
10
+ "MAGIC",
11
+ "ALERT",
12
+ "PORT",
13
+ "DOCUMENT",
14
+ ]
15
+
16
+ from pycodetags_issue_tracker.todo_tag_types import TODO
17
+ from pycodetags_issue_tracker.todo_tag_types_aliases import (
18
+ ALERT,
19
+ BUG,
20
+ CLEVER,
21
+ DOCUMENT,
22
+ FIXME,
23
+ HACK,
24
+ IDEA,
25
+ MAGIC,
26
+ PORT,
27
+ REQUIREMENT,
28
+ STORY,
29
+ )
@@ -0,0 +1,21 @@
1
+ # Templates
2
+
3
+ Copy paste these on creation, update and close
4
+
5
+ ## TODO
6
+ ```python
7
+ # TODO: ...
8
+ # <matth 2025-07-04 status:development category:parser priority:medium>
9
+ ```
10
+
11
+ ## PLANNING
12
+ ```python
13
+ # TODO: ...
14
+ # <matth 2025-07-04 status:planned release:... iteration:1 due:...>
15
+ ```
16
+
17
+ ## DONE
18
+ ```python
19
+ # TODO: ...
20
+ # <matth 2025-07-04 status:done release:... >
21
+ ```
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from collections.abc import Sequence
5
+ from typing import cast
6
+
7
+ from pycodetags_issue_tracker import TODO, views, views_templated
8
+
9
+ from pycodetags import DATA
10
+ from pycodetags.config import CodeTagsConfig
11
+
12
+
13
+ def handle_cli(subparsers: argparse._SubParsersAction):
14
+ report_parser = subparsers.add_parser(
15
+ "issues",
16
+ # parents=[base_parser],
17
+ help="Reports for TODOs and BUGs",
18
+ )
19
+ # report runs collectors, collected things can be validated
20
+ report_parser.add_argument("--module", action="append", help="Python module to inspect (e.g., 'my_project.main')")
21
+ report_parser.add_argument("--src", action="append", help="file or folder of source code")
22
+
23
+ report_parser.add_argument("--output", help="destination file or folder")
24
+
25
+ supported_formats = ["changelog", "validate", "html", "todomd", "donefile", "text"]
26
+
27
+ report_parser.add_argument(
28
+ "--format",
29
+ choices=supported_formats,
30
+ default="text",
31
+ help="Output format for the report.",
32
+ )
33
+
34
+ common_switches(report_parser)
35
+
36
+
37
+ def common_switches(parser) -> None:
38
+ parser.add_argument("--config", help="Path to config file, defaults to current folder pyproject.toml")
39
+ parser.add_argument("--verbose", default=False, action="store_true", help="verbose level logging output")
40
+ parser.add_argument("--info", default=False, action="store_true", help="info level logging output")
41
+ parser.add_argument("--bug-trail", default=False, action="store_true", help="enable bug trail, local logging")
42
+
43
+
44
+ def run_cli_command(
45
+ command_name: str,
46
+ args: argparse.Namespace,
47
+ found_data: Sequence[DATA | TODO],
48
+ # pylint: disable=unused-argument)
49
+ config: CodeTagsConfig,
50
+ ) -> bool:
51
+ format_name = args.format
52
+ # args.output
53
+ if command_name == "issues":
54
+ if format_name == "validate":
55
+ views.print_validate(cast(list[TODO], found_data))
56
+ return True
57
+ if format_name == "html":
58
+ views_templated.print_html(cast(list[TODO], found_data))
59
+ return True
60
+ if format_name == "todomd":
61
+ views.print_todo_md(cast(list[TODO], found_data))
62
+ return True
63
+ if format_name == "text":
64
+ views.print_text(cast(list[TODO], found_data))
65
+ return True
66
+ if format_name == "changelog":
67
+ views.print_changelog(cast(list[TODO], found_data))
68
+ return True
69
+ if format_name == "donefile":
70
+ views.print_done_file(cast(list[TODO], found_data))
71
+ return True
72
+ return False
73
+ return False
@@ -0,0 +1,161 @@
1
+ """
2
+ Converters for FolkTag and PEP350Tag to TODO
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from collections.abc import Iterable
9
+ from typing import Any
10
+
11
+ from pycodetags_issue_tracker.todo_object_schema import TODO_KEYWORDS
12
+ from pycodetags_issue_tracker.todo_tag_types import TODO
13
+
14
+ from pycodetags import DATA
15
+ from pycodetags.data_tags_schema import DataTag
16
+ from pycodetags.folk_code_tags import FolkTag
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def blank_to_null(value: str | None) -> str | None:
22
+ """
23
+ Convert a blank string to None.
24
+
25
+ Args:
26
+ value (str | None): The value to convert.
27
+
28
+ Returns:
29
+ str | None: The converted value.
30
+ """
31
+ if isinstance(value, list):
32
+ return [_.strip() for _ in value]
33
+ if value is None or value.strip() == "":
34
+ return None
35
+ return value.strip()
36
+
37
+
38
+ def convert_datas_to_TODOs(tags: Iterable[DATA]) -> Iterable[TODO]:
39
+ """Syntactic sugar to convert many tags"""
40
+ return [convert_data_to_TODO(_) for _ in tags]
41
+
42
+
43
+ def get_from_custom_or_data(name: str, tag: DATA) -> Any:
44
+ value = (tag.data_fields or {}).get(name)
45
+ if value:
46
+ return value
47
+ value = (tag.data_fields or {}).get(name)
48
+ if value:
49
+ return value
50
+ return None
51
+
52
+
53
+ def convert_data_to_TODO(tag: DATA) -> TODO:
54
+ return TODO(
55
+ code_tag=tag.code_tag,
56
+ comment=tag.comment,
57
+ default_fields=tag.default_fields or {},
58
+ data_fields=tag.data_fields or {},
59
+ custom_fields=tag.custom_fields or {},
60
+ unprocessed_defaults=tag.unprocessed_defaults or [],
61
+ assignee=get_from_custom_or_data("assignee", tag),
62
+ originator=get_from_custom_or_data("originator", tag),
63
+ origination_date=get_from_custom_or_data("origination_date", tag),
64
+ due=get_from_custom_or_data("due", tag),
65
+ release_due=get_from_custom_or_data("release_due", tag),
66
+ release=get_from_custom_or_data("release", tag),
67
+ iteration=get_from_custom_or_data("iteration", tag),
68
+ change_type=get_from_custom_or_data("change_type", tag),
69
+ closed_date=get_from_custom_or_data("closed_date", tag),
70
+ closed_comment=get_from_custom_or_data("closed_comment", tag),
71
+ tracker=get_from_custom_or_data("tracker", tag),
72
+ file_path=tag.file_path,
73
+ line_number=tag.line_number,
74
+ original_text=tag.original_text,
75
+ original_schema=tag.original_schema,
76
+ offsets=tag.offsets,
77
+ priority=get_from_custom_or_data("priority", tag),
78
+ status=get_from_custom_or_data("status", tag),
79
+ category=get_from_custom_or_data("category", tag),
80
+ )
81
+
82
+
83
+ def convert_folk_tag_to_TODO(folk_tag: FolkTag) -> TODO:
84
+ """
85
+ Convert a FolkTag to a TODO object.
86
+
87
+ Args:
88
+ folk_tag (FolkTag): The FolkTag to convert.
89
+ """
90
+ kwargs = {
91
+ "code_tag": folk_tag.get("code_tag"),
92
+ "file_path": folk_tag.get("file_path"),
93
+ "line_number": folk_tag.get("line_number"),
94
+ # folk_tag.get("default_field"),
95
+ "custom_fields": folk_tag.get("custom_fields"),
96
+ "comment": folk_tag["comment"], # required
97
+ "tracker": folk_tag.get("tracker"),
98
+ "assignee": blank_to_null(folk_tag.get("assignee")),
99
+ "originator": blank_to_null(folk_tag.get("originator")),
100
+ # person=folk_tag.get("person")
101
+ "original_text": folk_tag.get("original_text"),
102
+ "original_schema": "folk",
103
+ }
104
+ custom_fields = folk_tag.get("custom_fields", {})
105
+ for keyword in TODO_KEYWORDS:
106
+ for field_key, field_value in custom_fields.items():
107
+ # Promote custom fields to kwargs if they match the keyword
108
+ # and the keyword is not already in kwargs
109
+ if keyword == field_key and keyword not in kwargs:
110
+ kwargs[keyword] = field_value
111
+ if keyword == field_key and keyword not in kwargs:
112
+ logger.warning("Duplicate keyword found in custom fields: %s", keyword)
113
+ return TODO(**kwargs) # type: ignore[arg-type]
114
+
115
+
116
+ def convert_pep350_tag_to_TODO(pep350_tag: DataTag) -> TODO:
117
+ """
118
+ Convert a PEP350Tag to a TODO object.
119
+
120
+ Args:
121
+ pep350_tag (PEP350Tag): The PEP350Tag to convert.
122
+ """
123
+ # default fields should have already been promoted to data_fields by now.
124
+ data_fields = pep350_tag["fields"]["data_fields"]
125
+ custom_fields = pep350_tag["fields"]["custom_fields"]
126
+ kwargs = {
127
+ "code_tag": pep350_tag["code_tag"],
128
+ "comment": pep350_tag["comment"],
129
+ "custom_fields": custom_fields,
130
+ # specific fields
131
+ "assignee": blank_to_null(data_fields.get("assignee")),
132
+ "originator": blank_to_null(data_fields.get("originator")),
133
+ # due dates
134
+ "due": data_fields.get("due"),
135
+ "iteration": data_fields.get("iteration"),
136
+ "release": data_fields.get("release"),
137
+ # integrations
138
+ "tracker": data_fields.get("tracker"),
139
+ # idiosyncratic
140
+ "priority": data_fields.get("priority"),
141
+ "status": data_fields.get("status"),
142
+ "category": data_fields.get("category"),
143
+ # Source Mapping
144
+ "file_path": data_fields.get("file_path"),
145
+ "line_number": data_fields.get("line_number"),
146
+ "original_text": pep350_tag.get("original_text"),
147
+ "original_schema": "pep350",
148
+ "offsets": pep350_tag.get("offsets"),
149
+ }
150
+
151
+ custom_fields = pep350_tag["fields"].get("custom_fields", {})
152
+ for keyword in TODO_KEYWORDS:
153
+ for field_key, field_value in custom_fields.items():
154
+ # Promote custom fields to kwargs if they match the keyword
155
+ # and the keyword is not already in kwargs
156
+ if keyword == field_key and keyword not in kwargs:
157
+ kwargs[keyword] = field_value
158
+ if keyword == field_key and keyword not in kwargs:
159
+ logger.warning("Duplicate keyword found in custom fields: %s", keyword)
160
+
161
+ return TODO(**kwargs) # type: ignore[arg-type]
@@ -0,0 +1,233 @@
1
+ """
2
+ A module for calculating health metrics based on TODO items.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from collections import defaultdict
8
+ from typing import Any
9
+
10
+ from pycodetags_issue_tracker.todo_tag_types import TODO
11
+
12
+
13
+ class HealthOMeter:
14
+ """
15
+ Calculates various health metrics from a list of TODO items.
16
+
17
+ This class operates solely on the provided TODO objects and has no
18
+ dependencies on file systems or reporting mechanisms.
19
+ """
20
+
21
+ # Define sentiment scores for specific code tags
22
+ SENTIMENT_SCORES_PER_TAG = {
23
+ "CLEVER": 2, # Very positive
24
+ "MAGIC": 1, # Positive
25
+ "HACK": -1, # Negative
26
+ "FIXME": -2, # More negative
27
+ "BUG": -3, # Most negative
28
+ }
29
+
30
+ def __init__(self, todos: list[TODO]) -> None:
31
+ """
32
+ Initializes the HealthOMeter with a list of TODO items.
33
+
34
+ Args:
35
+ todos: A list of TODO objects to analyze.
36
+ """
37
+ self.todos = todos
38
+ # Cache the total number of TODOs to avoid repeated recalculations
39
+ self.total_todos_count = len(todos)
40
+
41
+ def calculate_todos_per_file(self) -> dict[str, int]:
42
+ """
43
+ Calculates the count of TODOs for each file.
44
+
45
+ TODO: Consider a metric for average TODOs per file.
46
+ TODO: Add a metric for files with no TODOs, but high complexity.
47
+
48
+ Returns:
49
+ A dictionary mapping file paths to the count of TODOs in them.
50
+ """
51
+ todos_per_file: dict[str, int] = defaultdict(int)
52
+ for todo in self.todos:
53
+ if todo.file_path:
54
+ todos_per_file[todo.file_path] += 1
55
+ return dict(todos_per_file)
56
+
57
+ def calculate_total_dones(self) -> int:
58
+ """
59
+ Calculates the total count of TODOs marked as "done".
60
+
61
+ TODO: Add a metric for the rate of DONE items over time.
62
+
63
+ Returns:
64
+ The total count of TODOs marked as "done".
65
+ """
66
+ return sum(1 for todo in self.todos if todo.is_probably_done())
67
+
68
+ def calculate_total_todos(self) -> int:
69
+ """
70
+ Calculates the total count of all TODO items.
71
+
72
+ TODO: Add a metric for the trend of total TODOs over time (increasing/decreasing).
73
+
74
+ Returns:
75
+ The total count of all TODOs.
76
+ """
77
+ return self.total_todos_count
78
+
79
+ def calculate_sentiment_score(self) -> float:
80
+ """
81
+ Calculates a sentiment score based on the presence of specific tags
82
+ (CLEVER, MAGIC, HACK, FIXME, BUG). A higher score indicates better sentiment.
83
+
84
+ TODO: Allow configuration of which tags influence sentiment and their scores.
85
+
86
+ Returns:
87
+ A score normalized between 0.0 and 1.0. Returns 1.0 if no TODOs exist.
88
+ """
89
+ if self.total_todos_count == 0:
90
+ return 1.0
91
+
92
+ raw_sentiment_sum = 0
93
+ for todo in self.todos:
94
+ # Check if the code_tag exists and is in our sentiment mapping
95
+ if todo.code_tag and todo.code_tag.upper() in self.SENTIMENT_SCORES_PER_TAG:
96
+ raw_sentiment_sum += self.SENTIMENT_SCORES_PER_TAG[todo.code_tag.upper()]
97
+
98
+ # Determine the min and max possible sentiment sums for normalization
99
+ max_score_per_todo = max(self.SENTIMENT_SCORES_PER_TAG.values())
100
+ min_score_per_todo = min(self.SENTIMENT_SCORES_PER_TAG.values())
101
+
102
+ min_possible_total = self.total_todos_count * min_score_per_todo
103
+ max_possible_total = self.total_todos_count * max_score_per_todo
104
+
105
+ # Normalize the raw sum to a 0-1 scale: (actual - min_possible) / (max_possible - min_possible)
106
+ if max_possible_total == min_possible_total:
107
+ # Avoid division by zero if all scores are identical or there are no relevant tags
108
+ return 1.0 # Represents a neutral or ideal state in such cases
109
+
110
+ normalized_score = (raw_sentiment_sum - min_possible_total) / (max_possible_total - min_possible_total)
111
+
112
+ # Ensure the score stays within the [0, 1] range due to potential edge cases or non-linear distribution
113
+ return max(0.0, min(1.0, normalized_score))
114
+
115
+ def calculate_quality_score(self) -> float:
116
+ """
117
+ Calculates a quality score based on the presence of "BUG" tags.
118
+ A higher score indicates better quality (fewer BUGs).
119
+
120
+ Returns:
121
+ A score normalized between 0.0 and 1.0. Returns 1.0 if no TODOs exist.
122
+ """
123
+ if self.total_todos_count == 0:
124
+ return 1.0
125
+
126
+ total_bugs = sum(1 for todo in self.todos if todo.code_tag and todo.code_tag.upper() == "BUG")
127
+ # Quality decreases with more bugs. Scale from 1.0 (no bugs) to 0.0 (all todos are bugs, worst case).
128
+ return max(0.0, (self.total_todos_count - total_bugs) / self.total_todos_count)
129
+
130
+ def calculate_bug_density(self) -> float:
131
+ """
132
+ Calculates the density of BUG tags as the ratio of BUGs to total TODOs.
133
+ This serves as a proxy for 'bugs per line of code/function' given
134
+ the class's constraint of not accessing the file system directly.
135
+
136
+ TODO: Enhance this metric by allowing external provision of actual
137
+ Lines of Code (LoC) or function counts per file to get a true density.
138
+
139
+ Returns:
140
+ The ratio of BUGs to total TODOs (0.0 if no TODOs).
141
+ """
142
+ if self.total_todos_count == 0:
143
+ return 0.0 # No bugs if no todos
144
+
145
+ total_bugs = sum(1 for todo in self.todos if todo.code_tag and todo.code_tag.upper() == "BUG")
146
+ return total_bugs / self.total_todos_count
147
+
148
+ def get_total_todos_scale(self, total_todos_value: int) -> str:
149
+ """Provides a descriptive scale for the total number of TODOs."""
150
+ if total_todos_value == 0:
151
+ return "Immaculate Codebase"
152
+ if 1 <= total_todos_value <= 10:
153
+ return "Well-Maintained"
154
+ if 11 <= total_todos_value <= 50:
155
+ return "Active Development"
156
+ if 51 <= total_todos_value <= 100:
157
+ return "Busy Beaver"
158
+ # > 100
159
+ return "Technical Debt Accumulation"
160
+
161
+ def get_sentiment_scale(self, sentiment_score_value: float) -> str:
162
+ """Provides a descriptive scale for the sentiment score."""
163
+ if 0.8 <= sentiment_score_value <= 1.0:
164
+ return "Positive Outlook"
165
+ if 0.5 <= sentiment_score_value < 0.8:
166
+ return "Mixed Feelings"
167
+ if 0.2 <= sentiment_score_value < 0.5:
168
+ return "Caution Advised"
169
+ # 0.0 <= score < 0.2
170
+ return "Coding Horror"
171
+
172
+ def get_quality_scale(self, quality_score_value: float) -> str:
173
+ """Provides a descriptive scale for the code quality score."""
174
+ if 0.95 <= quality_score_value <= 1.0:
175
+ return "High Quality"
176
+ if 0.8 <= quality_score_value < 0.95:
177
+ return "Good Quality"
178
+ if 0.5 <= quality_score_value < 0.8:
179
+ return "Average Quality"
180
+ # 0.0 <= score < 0.5
181
+ return "Buggy Waters"
182
+
183
+ def get_bug_density_scale(self, bug_density_value: float) -> str:
184
+ """Provides a descriptive scale for the bug density."""
185
+ if 0.0 == bug_density_value:
186
+ return "Bug-Free Zone"
187
+ if 0.0 < bug_density_value <= 0.05:
188
+ return "Occasional Pests"
189
+ if 0.05 < bug_density_value <= 0.15:
190
+ return "Frequent Critters"
191
+ # > 0.15
192
+ return "Infestation"
193
+
194
+ def get_total_dones_scale(self, value: int) -> str:
195
+ if value == 0:
196
+ return "No Progress"
197
+ if value <= 5:
198
+ return "Initial Cleanup"
199
+ if value <= 20:
200
+ return "Steady Progress"
201
+ return "High Velocity"
202
+
203
+ def calculate_metrics(self) -> dict[str, Any]:
204
+ """
205
+ Calculates and returns a dictionary of all health metrics, including their
206
+ descriptive scales.
207
+
208
+ Returns:
209
+ A dictionary containing the calculated metrics and their associated scales.
210
+ """
211
+ total_todos = self.calculate_total_todos()
212
+ sentiment_score = self.calculate_sentiment_score()
213
+ quality_score = self.calculate_quality_score()
214
+ bug_density = self.calculate_bug_density()
215
+ total_dones = self.calculate_total_dones()
216
+
217
+ metrics = {
218
+ "todos_per_file": self.calculate_todos_per_file(),
219
+ "total_dones": total_dones,
220
+ "total_todos": total_todos,
221
+ "sentiment": sentiment_score,
222
+ "quality": quality_score,
223
+ "bug_density": bug_density,
224
+ "total_todos_scale": self.get_total_todos_scale(total_todos),
225
+ "sentiment_scale": self.get_sentiment_scale(sentiment_score),
226
+ "quality_scale": self.get_quality_scale(quality_score),
227
+ "bug_density_scale": self.get_bug_density_scale(bug_density),
228
+ "total_dones_scale": self.get_total_dones_scale(total_dones),
229
+ }
230
+
231
+ # Add scale descriptions to the metrics
232
+
233
+ return metrics
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from pycodetags_issue_tracker.user import get_current_user
4
+ from pycodetags_issue_tracker.users_from_authors import parse_authors_file_simple
5
+
6
+ from pycodetags.config import CodeTagsConfig, careful_to_bool, get_code_tags_config
7
+
8
+
9
+ class IssueTrackerConfig:
10
+ def __init__(self, parent_config: CodeTagsConfig, set_user: str | None = None):
11
+
12
+ self.parent_config = parent_config
13
+ self.user_override = set_user
14
+
15
+ def user_env_var(self) -> str:
16
+ """Environment variable with active user."""
17
+ return str(self.parent_config.config.get("user_env_var", ""))
18
+
19
+ def current_user(self) -> str:
20
+ if self.user_override:
21
+ return self.user_override
22
+ return get_current_user(self.user_identification_technique(), self.user_env_var())
23
+
24
+ def user_identification_technique(self) -> str:
25
+ """Technique for identifying current user. If not set, related features are disabled."""
26
+ field = "user_identification_technique"
27
+ result = self.parent_config.config.get(field, "")
28
+ accepted = ("os", "env", "git", "")
29
+ if result not in accepted:
30
+ raise TypeError(f"Invalid configuration: {field} must be in {accepted}")
31
+ return str(result)
32
+
33
+ def valid_authors_file(self) -> str:
34
+ """Author list, overrides valid authors if specified. File must exist."""
35
+ field = "valid_authors_file"
36
+ return str(self.parent_config.config.get(field, ""))
37
+
38
+ def valid_authors_schema(self) -> str:
39
+ """Author schema, must be specified if authors from file is set."""
40
+ field = "valid_authors_schema"
41
+ result = self.parent_config.config.get(field, "")
42
+ accepted = ("gnu_gnits", "single_column", "")
43
+ if result not in accepted:
44
+ raise TypeError(f"Invalid configuration: {field} must be in {accepted}")
45
+ if self.valid_authors_file() and result == "":
46
+ raise TypeError(
47
+ "Invalid configuration: if valid_authors_from_file is set, "
48
+ f"then must be valid_authors_schema must be set to one of {accepted}"
49
+ )
50
+ return str(self.parent_config.config.get("valid_authors_schema", ""))
51
+
52
+ # Property accessors
53
+ def valid_authors(self) -> list[str]:
54
+ """Author list, if empty or None, all are valid, unless file specified"""
55
+ author_file = self.valid_authors_file()
56
+ schema = self.valid_authors_schema()
57
+ if author_file and schema:
58
+ if schema == "single_column":
59
+ with open(author_file, encoding="utf-8") as file_handle:
60
+ authors = [_ for _ in file_handle.readlines() if _]
61
+ return authors
62
+ if schema == "gnu_gnits":
63
+ authors = parse_authors_file_simple(author_file)
64
+ return authors
65
+
66
+ return [_.lower() for _ in self.parent_config.config.get("valid_authors", [])]
67
+
68
+ def valid_releases(self) -> list[str]:
69
+ """Releases (Version numbers), if empty or None, all are valid.
70
+ Past releases that do not match current schema are valid.
71
+ """
72
+ return [str(_).lower() for _ in self.parent_config.config.get("valid_releases", [])]
73
+
74
+ def valid_releases_file(self) -> str:
75
+ """File name of file with valid releases"""
76
+ valid_releases_from_file = self.parent_config.config.get("valid_releases_file", "")
77
+ return str(valid_releases_from_file).lower() if valid_releases_from_file else str(valid_releases_from_file)
78
+
79
+ def valid_releases_file_schema(self) -> str:
80
+ """Schema used to read from a known file type the valid versions."""
81
+ field = "valid_releases_file_schema"
82
+ result = self.parent_config.config.get(field, "")
83
+ accepted = ("keepachangelog",)
84
+ if result not in accepted:
85
+ raise TypeError(f"Invalid configuration: {field} must be in {accepted}")
86
+ if self.valid_releases_file() and not result:
87
+ raise TypeError(f"When valid_releases_from_file is set, {field} must be in {accepted}")
88
+ return str(result)
89
+
90
+ def releases_schema(self) -> str:
91
+ """Schema used to parse, sort release (version) numbers.
92
+ Not used to validate anything
93
+ """
94
+ field = "releases_schema"
95
+ result = self.parent_config.config.get(field, "")
96
+ accepted = ("semantic", "pep440", "")
97
+ if result not in accepted:
98
+ raise TypeError(f"Invalid configuration: {field} must be in {accepted}")
99
+ return str(result)
100
+
101
+ def valid_priorities(self) -> list[str]:
102
+ """Priority list, if empty or None, all are valid"""
103
+ return [_.lower() for _ in self.parent_config.config.get("valid_priorities", [])]
104
+
105
+ def valid_iterations(self) -> list[str]:
106
+ """Iteration list, if empty or None, all are valid."""
107
+ return [_.lower() for _ in self.parent_config.config.get("valid_iterations", [])]
108
+
109
+ def valid_custom_field_names(self) -> list[str]:
110
+ """Custom field names, if empty or None, all are valid."""
111
+ return [_.lower() for _ in self.parent_config.config.get("valid_custom_field_names", [])]
112
+
113
+ def mandatory_fields(self) -> list[str]:
114
+ """Mandatory fields, if empty or None, no mandatory fields."""
115
+ return [_.lower() for _ in self.parent_config.config.get("mandatory_fields", [])]
116
+
117
+ def tracker_domain(self) -> str:
118
+ """Domain of the tracker, used to make ticket links clickable."""
119
+ return str(self.parent_config.config.get("tracker_domain", ""))
120
+
121
+ def tracker_style(self) -> str:
122
+ """Style of the tracker, used to make ticket links clickable."""
123
+ field = "tracker_style"
124
+ result = self.parent_config.config.get(field, "")
125
+ accepted = ("url", "ticket", "")
126
+ if result not in accepted:
127
+ raise TypeError(f"Invalid configuration: {field} must be in {accepted}")
128
+ return str(result)
129
+
130
+ def valid_status(self) -> list[str]:
131
+ """Status list, if empty or None, all are valid"""
132
+ return [str(_).lower() for _ in self.parent_config.config.get("valid_status", [])]
133
+
134
+ def valid_categories(self) -> list[str]:
135
+ """Category list, if empty or None, all are valid"""
136
+ return [str(_).lower() for _ in self.parent_config.config.get("valid_categories", [])]
137
+
138
+ def closed_status(self) -> list[str]:
139
+ """If status equals this,then it is closed, needed for business rules"""
140
+ closed_status = self.parent_config.config.get("closed_status", [])
141
+ return [str(_).lower() for _ in closed_status]
142
+
143
+ def action_on_past_due(self) -> bool:
144
+ """Do actions do the default action"""
145
+ return careful_to_bool(self.parent_config.config.get("action_on_past_due", False), False)
146
+
147
+ def action_only_on_responsible_user(self) -> bool:
148
+ """Do actions do the default action when active user matches"""
149
+ return careful_to_bool(self.parent_config.config.get("action_only_on_responsible_user", False), False)
150
+
151
+
152
+ def get_issue_tracker_config() -> IssueTrackerConfig:
153
+ return IssueTrackerConfig(get_code_tags_config())