2024-06-08 13:12:11 -04:00
|
|
|
|
import json
|
2024-06-09 00:45:21 -04:00
|
|
|
|
from typing import Dict, List
|
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-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
|
|
|
|
|
from prompt_toolkit.styles import Style
|
2024-06-09 00:45:21 -04:00
|
|
|
|
from prompt_toolkit.validation import ValidationError, Validator
|
|
|
|
|
from prompt_toolkit.shortcuts import confirm
|
|
|
|
|
from pygments.lexer import RegexLexer
|
|
|
|
|
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-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,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
r"(?i)\b(?:Assignee|affectedVersion|Attachments|Category|Comment|Component|Created|createdDate|"
|
|
|
|
|
r"Creator|Description|Due|duedate|Filter|fixVersion|issuekey|issuetype|issueLinkType|Labels|"
|
|
|
|
|
r"lastViewed|Priority|Project|Reporter|Resolved|Sprint|Status|statusCategory|Summary|Text|"
|
2024-06-08 17:55:51 -04:00
|
|
|
|
r"timespent|Voter|Watcher)\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:
|
|
|
|
|
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",
|
|
|
|
|
"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:
|
|
|
|
|
def __init__(self, console: Console):
|
|
|
|
|
self.console = console
|
|
|
|
|
|
|
|
|
|
def print(self, text: str):
|
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):
|
|
|
|
|
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):
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
def __init__(self, jira, console):
|
|
|
|
|
self.jira = jira
|
|
|
|
|
self.console = console
|
|
|
|
|
self.session = self.create_jql_prompt_session()
|
|
|
|
|
self.jql = JQLPrinter(console)
|
|
|
|
|
self.query_count = 0
|
|
|
|
|
|
|
|
|
|
def get_query_count(self):
|
|
|
|
|
space = self.console.width // 4
|
|
|
|
|
query_count_str = f"Query count: {self.query_count}"
|
|
|
|
|
plain_text = f"{query_count_str:^{space}}{query_count_str:^{space}}{query_count_str:^{space}}{query_count_str:^{space}}"
|
|
|
|
|
return [("bg:#2E3440 #D8DEE9", plain_text)]
|
|
|
|
|
|
|
|
|
|
def create_jql_prompt_session(self):
|
|
|
|
|
completer = JQLCompleter(completions)
|
|
|
|
|
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
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def get_input(self):
|
2024-06-09 00:45:21 -04:00
|
|
|
|
user_input = self.session.prompt()
|
|
|
|
|
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(
|
|
|
|
|
(f"[+] Found {len(issues)} issues from JQL query: ", "green bold"),
|
|
|
|
|
self.jql.pygments_to_rich(user_input),
|
|
|
|
|
),
|
|
|
|
|
end="",
|
|
|
|
|
)
|
|
|
|
|
for issue in issues:
|
|
|
|
|
self.console.print(f"{issue.key}: {issue.fields.summary}")
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
while True:
|
|
|
|
|
try:
|
|
|
|
|
self.get_input()
|
|
|
|
|
except (EOFError, KeyboardInterrupt):
|
|
|
|
|
break
|
|
|
|
|
self.console.print("Goodbye!", style="#BF616A bold")
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
console = Console(color_system="truecolor")
|
|
|
|
|
jira = JIRA(server=config["server"], basic_auth=(config["username"], config["token"]))
|
2024-06-08 17:55:51 -04:00
|
|
|
|
prompt = JQLPrompt(jira, console)
|
|
|
|
|
prompt.run()
|
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()
|