python_examples/cli/jql_utils.py

418 lines
13 KiB
Python
Raw Normal View History

2024-06-08 13:12:11 -04:00
import json
2024-06-09 15:08:36 -04:00
from datetime import datetime
from typing import Dict, List, Optional
2024-06-08 13:12:11 -04:00
2024-06-09 00:45:21 -04:00
from jira import JIRA
from jira.exceptions import JIRAError
2024-06-09 15:08:36 -04:00
from jira.resources import Issue
2024-05-26 13:24:47 -04:00
from prompt_toolkit import PromptSession
2024-06-09 00:45:21 -04:00
from prompt_toolkit.completion import Completer, Completion
2024-05-26 13:24:47 -04:00
from prompt_toolkit.lexers import PygmentsLexer
2024-06-09 15:08:36 -04:00
from prompt_toolkit.shortcuts import confirm
2024-05-26 13:24:47 -04:00
from prompt_toolkit.styles import Style
2024-06-09 00:45:21 -04:00
from prompt_toolkit.validation import ValidationError, Validator
from pygments.lexer import RegexLexer
2024-06-09 15:08:36 -04:00
from pygments.token import (Error, Keyword, Name, Operator, Punctuation,
String, Text, Whitespace, _TokenType)
2024-06-08 13:12:11 -04:00
from rich.console import Console
from rich.text import Text as RichText
2024-05-26 13:24:47 -04:00
class JQLLexer(RegexLexer):
2024-06-09 15:52:30 -04:00
""" JQL Lexer for Pygments. """
2024-06-08 13:12:11 -04:00
name = "JQL"
aliases = ["jql"]
filenames = ["*.jql"]
2024-05-26 13:24:47 -04:00
tokens = {
2024-06-08 13:12:11 -04:00
"root": [
(r"\s+", Whitespace),
(r'"', String, "string"),
(r"'", String, "string"),
(
r"(?i)\b(?:issueHistory|openSprints|watchedIssues|myApproval|myPending|currentLogin|currentUser|"
r"membersOf|lastLogin|now|startOfDay|endOfDay|startOfWeek|endOfWeek|startOfMonth|endOfMonth|"
r"startOfYear|endOfYear)\b",
Name.Function,
),
(
r"(?i)\b(?:A|AND|ARE|AS|AT|BE|BUT|BY|FOR|IF|INTO|IT|NO|OF|ON|OR|S|SUCH|T|THAT|THE|THEIR|THEN|"
r"THERE|THESE|THEY|THIS|TO|WILL|WITH)\b",
Keyword,
),
(
2024-06-09 15:08:36 -04:00
r"(?i)\b(?:assignee|affectedVersion|attachments|category|comment|component|created|createdDate|"
r"creator|cescription|due|duedate|filter|fixVersion|issuekey|issuetype|issueLinkType|labels|"
r"lastViewed|priority|project|reporter|resolved|Sprint|status|statusCategory|summary|text|"
r"timespent|updated|updatedDate|voter|watcher|watchers)\b",
2024-06-08 13:12:11 -04:00
Name.Attribute,
),
(r"(?i)(=|!=|<|>|<=|>=|~|!~|IN|NOT IN|IS|IS NOT|WAS|WAS IN|WAS NOT IN|WAS NOT)", Operator),
(r"[\*\(/\^\.@;:+%#\[\|\?\),\$]", Punctuation),
2024-06-08 15:59:47 -04:00
(
r"(?i)\b(?:QUANTUM|NEBULA|GALACTIC|STELLAR|AETHER|NOVA|COSMIC|LUNAR|ASTRAL|PHOTON)\b",
Name.Other,
),
2024-06-08 13:12:11 -04:00
(r"[\w\.\-]+", Text),
2024-05-26 13:24:47 -04:00
],
2024-06-08 13:12:11 -04:00
"string": [
(r'"', String, "#pop"),
(r"'", String, "#pop"),
2024-05-26 13:24:47 -04:00
(r'[^"\']+', String),
],
}
2024-06-08 13:12:11 -04:00
2024-06-09 00:45:21 -04:00
class JQLStyles:
2024-06-09 15:52:30 -04:00
""" JQL Styles for Pygments.
Based on the Nord color palette: https://www.nordtheme.com/docs/colors-and-palettes
"""
2024-06-09 00:45:21 -04:00
nord: Style = Style.from_dict(
{
"pygments.whitespace": "#FFFFFF",
"pygments.keyword": "#81A1C1 bold",
"pygments.operator": "#EBCB8B bold",
"pygments.punctuation": "#BF616A",
"pygments.name.attribute": "#B48EAD",
"pygments.name.function": "#A3BE8C",
"pygments.literal.string": "#D08770",
"pygments.text": "#D8DEE9",
"pygments.name.other": "#D08770",
"pygments.error": "#BF616A bold",
}
)
token: Dict[_TokenType, str] = {
Whitespace: "#FFFFFF",
Keyword: "#81A1C1 bold",
Operator: "#EBCB8B bold",
Punctuation: "#BF616A",
Name.Attribute: "#B48EAD",
Name.Function: "#A3BE8C",
String: "#D08770",
Text: "#D8DEE9",
Error: "#BF616A bold",
Name.Other: "#D08770",
2024-06-08 13:12:11 -04:00
}
2024-05-26 13:24:47 -04:00
2024-06-09 00:45:21 -04:00
completion: Dict[str, str] = {
"Keywords": "#81A1C1 bold",
"Functions": "#A3BE8C",
"Attributes": "#B48EAD",
"Operators": "#EBCB8B bold",
"Projects": "#D08770",
}
2024-06-08 15:59:47 -04:00
2024-06-09 00:45:21 -04:00
completions: Dict[str, List[str]] = {
"Keywords": [
"A",
"AND",
"ARE",
"AS",
"AT",
"BE",
"BUT",
"BY",
"FOR",
"IF",
"INTO",
"IT",
"NO",
"NOT",
"OF",
"ON",
"OR",
"S",
"SUCH",
"T",
"THAT",
"THE",
"THEIR",
"THEN",
"THERE",
"THESE",
"THEY",
"THIS",
"TO",
"WILL",
"WITH",
],
"Functions": [
"issueHistory",
"openSprints",
"watchedIssues",
"myApproval",
"myPending",
"currentLogin",
"currentUser",
"membersOf",
"lastLogin",
"now",
"startOfDay",
"endOfDay",
"startOfWeek",
"endOfWeek",
"startOfMonth",
"endOfMonth",
"startOfYear",
"endOfYear",
],
"Attributes": [
"assignee",
"affectedVersion",
"attachments",
"comment",
"component",
"created",
"creator",
"description",
"due",
"duedate",
"filter",
"fixVersion",
"issuekey",
"labels",
"lastViewed",
"priority",
"project",
"reporter",
"resolved",
"sprint",
"status",
"statusCategory",
"summary",
"text",
"timespent",
2024-06-09 15:08:36 -04:00
"updated",
2024-06-09 00:45:21 -04:00
"voter",
"watcher",
],
"Operators": [
"=",
"!=",
"<",
">",
"<=",
">=",
"~",
"!~",
"IN",
"NOT IN",
"IS",
"IS NOT",
"WAS",
"WAS IN",
"WAS NOT IN",
"WAS NOT",
],
"Projects": [
"QUANTUM",
"NEBULA",
"GALACTIC",
"STELLAR",
"AETHER",
"NOVA",
"COSMIC",
"LUNAR",
"ASTRAL",
"PHOTON",
],
}
2024-05-26 13:24:47 -04:00
2024-06-08 13:12:11 -04:00
class JQLPrinter:
2024-06-09 15:52:30 -04:00
""" JQL Printer to print JQL queries with syntax highlighting. """
2024-06-08 13:12:11 -04:00
def __init__(self, console: Console):
self.console = console
def print(self, text: str):
2024-06-09 15:52:30 -04:00
""" Print JQL query with syntax highlighting. """
2024-06-08 15:59:47 -04:00
self.console.print(self.pygments_to_rich(text), end="")
2024-06-08 13:12:11 -04:00
def pygments_to_rich(self, text):
2024-06-09 15:52:30 -04:00
""" Convert Pygments tokens to RichText. """
2024-06-08 13:12:11 -04:00
tokens = list(JQLLexer().get_tokens(text))
rich_text = RichText()
for token_type, value in tokens:
2024-06-09 00:45:21 -04:00
style = JQLStyles.token.get(token_type, "white")
2024-06-08 13:12:11 -04:00
rich_text.append(value, style=style)
return rich_text
class JQLValidator(Validator):
2024-06-09 15:52:30 -04:00
""" JQL Validator to validate JQL queries. """
2024-06-08 13:12:11 -04:00
def __init__(self, jira_instance):
self.jira = jira_instance
def validate(self, document):
2024-06-09 15:52:30 -04:00
""" Validate JQL query. """
2024-06-08 13:12:11 -04:00
text = document.text
if text.lower() == "b" or text.lower() == "exit":
return
try:
self.jira.search_issues(text, maxResults=1)
except JIRAError as error:
error_text = error.response.json().get("errorMessages", ["Unknown error"])[0]
raise ValidationError(message=f"[!] {error_text}", cursor_position=len(text))
2024-06-08 15:59:47 -04:00
class JQLCompleter(Completer):
"""Custom JQL completer to categorize and color completions."""
def __init__(self, categorized_completions):
self.categorized_completions = categorized_completions
def get_completions(self, document, complete_event):
text = document.get_word_before_cursor().lower()
for category, words in self.categorized_completions.items():
for word in words:
if text in word.lower():
display_text = f"{word}"
yield Completion(
word,
start_position=-len(text),
display=display_text,
display_meta=category,
2024-06-09 00:45:21 -04:00
style=f"fg: #D8DEE9 bg: {JQLStyles.completion.get(category, 'white')}",
selected_style=f"fg: {JQLStyles.completion.get(category, 'white')} bg: #D8DEE9",
2024-06-08 15:59:47 -04:00
)
def load_config():
with open("config.json") as json_file:
try:
with open("config.json") as json_file:
return json.load(json_file)
except FileNotFoundError:
print("Configuration file not found.")
exit(1)
except json.JSONDecodeError:
print("Error decoding configuration file.")
exit(1)
2024-05-26 13:24:47 -04:00
2024-06-08 17:55:51 -04:00
class JQLPrompt:
2024-06-09 15:52:30 -04:00
""" JQL Prompt to interact with JIRA using JQL queries. """
2024-06-09 15:08:36 -04:00
def __init__(self, jira):
self.jira: JIRA = jira
self.console: Console = Console(color_system="truecolor", record=True)
self.session: PromptSession = self.create_jql_prompt_session()
self.jql: JQLPrinter = JQLPrinter(self.console)
self.query_count: int = 0
self.issue_count: int = 0
self.total_issue_count: int = 0
self.issues: List[Issue] = []
2024-06-08 17:55:51 -04:00
def get_query_count(self):
2024-06-09 15:08:36 -04:00
space = self.console.width // 3
query_count_str = f"Query Count: {self.query_count}" if self.query_count else ""
issue_count_str = f"Issues Added: {self.issue_count}" if self.issue_count else ""
total_issue_count_str = f"Total Issues: {self.total_issue_count}" if self.total_issue_count else ""
plain_text = f"{query_count_str:^{space}}{issue_count_str:^{space}}{total_issue_count_str:^{space}}"
2024-06-08 17:55:51 -04:00
return [("bg:#2E3440 #D8DEE9", plain_text)]
def create_jql_prompt_session(self):
2024-06-09 15:08:36 -04:00
completer: JQLCompleter = JQLCompleter(completions)
2024-06-08 17:55:51 -04:00
return PromptSession(
2024-06-09 00:45:21 -04:00
message=[("#B48EAD", "JQL \u276f ")],
2024-06-08 17:55:51 -04:00
lexer=PygmentsLexer(JQLLexer),
2024-06-09 00:45:21 -04:00
style=JQLStyles.nord,
2024-06-08 17:55:51 -04:00
completer=completer,
validator=JQLValidator(self.jira),
2024-06-09 00:45:21 -04:00
rprompt=[
("#5E81AC bold", "[b] Back "),
("#BF616A bold", "[exit] Exit"),
],
2024-06-08 17:55:51 -04:00
bottom_toolbar=self.get_query_count,
2024-06-09 00:45:21 -04:00
validate_while_typing=False,
2024-06-08 17:55:51 -04:00
)
2024-06-09 15:08:36 -04:00
def prompt(self) -> Optional[List[Issue]]:
2024-06-09 15:52:30 -04:00
""" Prompt the user for a JQL query.
Returns:
Optional[List[Issue]]: List of JIRA issues.
"""
2024-06-09 15:08:36 -04:00
user_input: str = self.session.prompt()
self.issue_count = 0
2024-06-09 00:45:21 -04:00
if not user_input:
do_empty_query = confirm(
[("#EBCB8B bold", "[?] "), ("#D8DEE9 bold", "Do you want to perform an empty query?")],
suffix=[("#81A1C1 bold", " (Y/n) ")],
)
if not do_empty_query:
return
2024-06-08 17:55:51 -04:00
if user_input.lower() == "b":
return
if user_input.lower() == "exit":
exit(0)
issues = self.jira.search_issues(user_input)
if issues:
self.query_count += 1
self.console.print(
RichText.assemble(
2024-06-09 15:08:36 -04:00
(f"[+] Found {len(issues)} issues from JQL query: ", "#A3BE8C bold"),
2024-06-08 17:55:51 -04:00
self.jql.pygments_to_rich(user_input),
),
end="",
)
2024-06-09 15:08:36 -04:00
return issues
self.console.print("[!] No issues found.", style="#BF616A bold")
2024-06-08 17:55:51 -04:00
2024-06-09 15:08:36 -04:00
def multi_prompt(self) -> Optional[List[Issue]]:
2024-06-09 15:52:30 -04:00
""" Prompt the user for multiple JQL queries.
Returns:
Optional[List[Issue]]: List of JIRA issues.
"""
2024-06-09 15:08:36 -04:00
self.issues = []
2024-06-08 17:55:51 -04:00
while True:
try:
2024-06-09 15:08:36 -04:00
issues = self.prompt()
if issues:
issues = [issue for issue in issues if issue not in self.issues]
self.issues.extend(issues)
self.issue_count += len(issues)
self.total_issue_count += len(issues)
self.console.print(f"[] Total issues: {len(self.issues)}", style="#D8DEE9")
get_more = confirm([("#A3BE8C", "[?] Get more issues?")], suffix=[("#81A1C1 bold", " (Y/n) ")])
if not get_more:
break
2024-06-08 17:55:51 -04:00
except (EOFError, KeyboardInterrupt):
break
2024-06-09 15:08:36 -04:00
if self.issues:
2024-06-09 15:17:09 -04:00
self.console.print(f"[+] Issues added: {self.total_issue_count}", style="#A3BE8C bold")
2024-06-09 15:08:36 -04:00
return self.issues
self.console.print("[!] No issues added.", style="#BF616A bold")
def save_log(self):
2024-06-09 15:52:30 -04:00
""" Save the console log to a file. """
2024-06-09 15:08:36 -04:00
log_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
with open(f"jql_{log_time}.txt", "w") as log_file:
log_file.write(self.console.export_text())
2024-06-08 17:55:51 -04:00
2024-06-09 00:45:21 -04:00
"""
[+] for additions
[-] for deletions
[~] for changes
[<] for incoming
[>] for outgoing
[] for success
[] for failure
[] for warnings
[?] for questions
[!] for errors
[] for information
"""
2024-05-26 13:24:47 -04:00
def main():
2024-06-08 15:59:47 -04:00
config = load_config()
2024-06-08 13:12:11 -04:00
jira = JIRA(server=config["server"], basic_auth=(config["username"], config["token"]))
2024-06-09 15:08:36 -04:00
prompt = JQLPrompt(jira)
prompt.multi_prompt()
2024-06-08 13:12:11 -04:00
2024-05-26 13:24:47 -04:00
2024-06-08 13:12:11 -04:00
if __name__ == "__main__":
2024-05-26 13:24:47 -04:00
main()