python_examples/cli/jql_utils.py

399 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import json
from datetime import datetime
from typing import Dict, List, Optional
from jira import JIRA
from jira.exceptions import JIRAError
from jira.resources import Issue
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.lexers import PygmentsLexer
from prompt_toolkit.shortcuts import confirm
from prompt_toolkit.styles import Style
from prompt_toolkit.validation import ValidationError, Validator
from pygments.lexer import RegexLexer
from pygments.token import (Error, Keyword, Name, Operator, Punctuation,
String, Text, Whitespace, _TokenType)
from rich.console import Console
from rich.text import Text as RichText
class JQLLexer(RegexLexer):
name = "JQL"
aliases = ["jql"]
filenames = ["*.jql"]
tokens = {
"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,
),
(
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",
Name.Attribute,
),
(r"(?i)(=|!=|<|>|<=|>=|~|!~|IN|NOT IN|IS|IS NOT|WAS|WAS IN|WAS NOT IN|WAS NOT)", Operator),
(r"[\*\(/\^\.@;:+%#\[\|\?\),\$]", Punctuation),
(
r"(?i)\b(?:QUANTUM|NEBULA|GALACTIC|STELLAR|AETHER|NOVA|COSMIC|LUNAR|ASTRAL|PHOTON)\b",
Name.Other,
),
(r"[\w\.\-]+", Text),
],
"string": [
(r'"', String, "#pop"),
(r"'", String, "#pop"),
(r'[^"\']+', String),
],
}
class JQLStyles:
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",
}
completion: Dict[str, str] = {
"Keywords": "#81A1C1 bold",
"Functions": "#A3BE8C",
"Attributes": "#B48EAD",
"Operators": "#EBCB8B bold",
"Projects": "#D08770",
}
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",
"updated",
"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",
],
}
class JQLPrinter:
def __init__(self, console: Console):
self.console = console
def print(self, text: str):
self.console.print(self.pygments_to_rich(text), end="")
def pygments_to_rich(self, text):
tokens = list(JQLLexer().get_tokens(text))
rich_text = RichText()
for token_type, value in tokens:
style = JQLStyles.token.get(token_type, "white")
rich_text.append(value, style=style)
return rich_text
class JQLValidator(Validator):
def __init__(self, jira_instance):
self.jira = jira_instance
def validate(self, document):
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))
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,
style=f"fg: #D8DEE9 bg: {JQLStyles.completion.get(category, 'white')}",
selected_style=f"fg: {JQLStyles.completion.get(category, 'white')} bg: #D8DEE9",
)
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)
class JQLPrompt:
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] = []
def get_query_count(self):
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}}"
return [("bg:#2E3440 #D8DEE9", plain_text)]
def create_jql_prompt_session(self):
completer: JQLCompleter = JQLCompleter(completions)
return PromptSession(
message=[("#B48EAD", "JQL \u276f ")],
lexer=PygmentsLexer(JQLLexer),
style=JQLStyles.nord,
completer=completer,
validator=JQLValidator(self.jira),
rprompt=[
("#5E81AC bold", "[b] Back "),
("#BF616A bold", "[exit] Exit"),
],
bottom_toolbar=self.get_query_count,
validate_while_typing=False,
)
def prompt(self) -> Optional[List[Issue]]:
user_input: str = self.session.prompt()
self.issue_count = 0
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
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(
(f"[+] Found {len(issues)} issues from JQL query: ", "#A3BE8C bold"),
self.jql.pygments_to_rich(user_input),
),
end="",
)
return issues
self.console.print("[!] No issues found.", style="#BF616A bold")
def multi_prompt(self) -> Optional[List[Issue]]:
self.issues = []
while True:
try:
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
except (EOFError, KeyboardInterrupt):
break
if self.issues:
self.console.print(f"[+] Issues added: {self.total_issue_count}", style="#A3BE8C bold")
return self.issues
self.console.print("[!] No issues added.", style="#BF616A bold")
def save_log(self):
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())
"""
[+] for additions
[-] for deletions
[~] for changes
[<] for incoming
[>] for outgoing
[✔] for success
[✖] for failure
[⚠] for warnings
[?] for questions
[!] for errors
[] for information
"""
def main():
config = load_config()
jira = JIRA(server=config["server"], basic_auth=(config["username"], config["token"]))
prompt = JQLPrompt(jira)
prompt.multi_prompt()
if __name__ == "__main__":
main()