sbs/sbs/cli.py
2025-08-14 15:01:40 -04:00

164 lines
5 KiB
Python

#!/usr/bin/env python
# This file is part of sbs
#
# Copyright (C) 2025 Steve Milner
#
# SPDX-License-Identifier: GPL-3.0-or-later
#: Current version of the tool
__version__ = "0.1.0"
#: The license of the project
__license__ = "GPLv3+"
import argparse
import os
import sys
import typing
from sbs.ds import MAPPING
def load_map(fname: str) -> dict:
"""
Load the username=human name mapping
:param fname: File name to read from
"""
mapping = {}
with open(fname, "r") as f:
for line in f.readlines():
username, name = line.replace("\n", "").split("=")
mapping[username] = name
return mapping
class _TextOrFileAction(argparse.Action):
"""
Reads and sets content based on if the input is text (string)
or @path/to/file (read as string).
"""
def __call__(
self,
parser: argparse.ArgumentParser,
namespace: argparse.Namespace,
values: typing.Any,
option_string: str,
):
"""
Handles loading content from a file or passing the string value
through to the parser.
:param parser: Argument parser in use
:param namespace: The namespace associated with the parser
:param values: The value provided via the argument
:param option_string: The option used to set the value
"""
if isinstance(values, str):
if values.startswith("@"):
with open(values[1:]) as f:
content = f.read()
# We don't want newlines at the end
if content.endswith("\n"):
content = content[:-1]
setattr(namespace, self.dest, content)
else:
setattr(namespace, self.dest, values)
else:
raise argparse.ArgumentError(
f"{option_string} must be a path to a file starting with @ or text"
)
def _common_switches(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
"""
Append common switches to subparsers.
:param parser: Parser to attach common switches
"""
parser.add_argument("mapping", help="Mapping file for Jira Account=Name")
parser.add_argument("output", help="The file to write")
parser.add_argument(
"-p", "--prompt", help="Text or @file prompt to use", action=_TextOrFileAction
)
parser.add_argument(
"-i",
"--ignore-unknowns",
default=False,
action="store_true",
help="Ignores issues that don't have a known assignee",
)
return parser
def main():
"""
CLI entry point.
"""
parser = argparse.ArgumentParser(
description="Turns Jira output into a Gemini prompt to generate status",
)
# Parent parser for subcommands
subparsers = parser.add_subparsers(help="commands")
# jql parser handles is used for hitting the Jira API directly for the data
jql_parser = subparsers.add_parser("jql", help="API call to Jira to pull data")
jql_parser.add_argument(
"-q",
"--query",
help="Text or @file of the jql query to use",
action=_TextOrFileAction,
)
jql_parser.add_argument("-e", "--endpoint", help="Jira API endpoint")
jql_parser.add_argument("-t", "--token", help="Access token for the Jira API")
jql_parser = _common_switches(jql_parser)
# csv parser is used when the user wishes to manually grab a CSV file to inspect
csv_parser = subparsers.add_parser(
"csv", help="Read an exported CSV file from Jira"
)
csv_parser.add_argument("csvfile", help="Path to the CSV file to read")
csv_parser = _common_switches(csv_parser)
# Parse the args so we can do the work
args = parser.parse_args()
# But if we don't have any input show the help and exit
if args == argparse.Namespace():
parser.print_help()
raise SystemExit(0)
# Load the mapping of Jira account=Name. This is needed due to differences
# between Jira instances, CSV and JQL output, etc...
mapping = load_map(args.mapping)
# Don't be destructive. It's not nice! :-)
if os.path.exists(args.output):
print(f"{args.output} exists. Please choose another destination.")
raise SystemExit(1)
# Generate the prompt + content
with open(args.output, "w") as dest:
dest.write(f"{args.prompt}\n\n")
for x in MAPPING[sys.argv[1]](**args.__dict__):
assignee = x.assignee
if not assignee:
assignee = "Unknown"
if args.ignore_unknowns is True:
continue
else:
if assignee in mapping.keys():
assignee = mapping[assignee]
dest.write(f"# {x.key}\n")
dest.write(f"## Assignee\n{assignee}\n")
dest.write(f"## Story Points\n{x.story_points}\n")
dest.write(f"## Summary\n{x.summary}\n")
dest.write(f"## Context\n{x.description}")
dest.write("\n\n")
if __name__ == "__main__":
main()