scripts: Initial commit.
This commit is contained in:
parent
aea9fc0536
commit
50fd55482d
3 changed files with 333 additions and 0 deletions
156
scripts/rt-auto-remind
Executable file
156
scripts/rt-auto-remind
Executable file
|
@ -0,0 +1,156 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import os
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
DATETIME_FMT = '%Y-%m-%d %H:%M:%S'
|
||||
try:
|
||||
_data_home = os.environ['XDG_DATA_HOME']
|
||||
except KeyError:
|
||||
_data_home = pathlib.Path(os.path.expanduser('~'), '.local', 'share')
|
||||
SHARE_DIR = pathlib.Path(_data_home, 'rt-auto-remind')
|
||||
SHARE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class LastRunStorage:
|
||||
DB_FILENAME = 'RunData.db'
|
||||
V1_CREATE_SQL = '''
|
||||
CREATE TABLE IF NOT EXISTS LastRunV1(
|
||||
key text PRIMARY KEY,
|
||||
max_date text
|
||||
);
|
||||
'''
|
||||
V1_SELECT = 'SELECT max_date FROM LastRunV1 WHERE key = ?'
|
||||
V1_INSERT = 'INSERT OR REPLACE INTO LastRunV1 VALUES(?, ?);'
|
||||
|
||||
def __init__(self, args):
|
||||
self.db = sqlite3.connect(str(SHARE_DIR / self.DB_FILENAME))
|
||||
self.db.executescript(self.V1_CREATE_SQL)
|
||||
if args.key is None:
|
||||
self.key = ';'.join(
|
||||
'{}={!r}'.format(key, str(getattr(args, key)))
|
||||
for key in ['body_file', 'date_field', 'max_days_diff', 'search']
|
||||
)
|
||||
else:
|
||||
self.key = args.key
|
||||
|
||||
def load(self):
|
||||
cursor = self.db.execute(self.V1_SELECT, (self.key,))
|
||||
result = cursor.fetchone()
|
||||
if result is None:
|
||||
return None
|
||||
else:
|
||||
return datetime.datetime.strptime(result[0], DATETIME_FMT)
|
||||
|
||||
def save(self, end_date):
|
||||
end_date_s = end_date.strftime(DATETIME_FMT)
|
||||
with self.db:
|
||||
self.db.execute(self.V1_INSERT, (self.key, end_date_s))
|
||||
|
||||
def close(self):
|
||||
self.db.close()
|
||||
|
||||
|
||||
def parse_arguments(arglist):
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
action = parser.add_mutually_exclusive_group()
|
||||
action.add_argument(
|
||||
'--correspond',
|
||||
dest='action', action='store_const', const='correspond',
|
||||
default='correspond',
|
||||
help="Send correspondence on found tickets (default)",
|
||||
)
|
||||
action.add_argument(
|
||||
'--comment',
|
||||
dest='action', action='store_const', const='comment',
|
||||
help="Comment on found tickets",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--key', '-k',
|
||||
help="Use this string to load and save run times in the database"
|
||||
" (default is auto-generated)",
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run', '-n',
|
||||
action='store_true',
|
||||
help="Show the rt-bulk-send command that would be run; don't run it",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'date_field',
|
||||
help="RT date field to constrain in the search",
|
||||
)
|
||||
parser.add_argument(
|
||||
'min_days_diff',
|
||||
type=int,
|
||||
help="The earliest end of the date constraint, in days from today",
|
||||
)
|
||||
parser.add_argument(
|
||||
'max_days_diff',
|
||||
type=int,
|
||||
help="The latest end of the date constraint, in days from today",
|
||||
)
|
||||
parser.add_argument(
|
||||
'search',
|
||||
help="TicketSQL search, like you would pass to `rt search`",
|
||||
)
|
||||
parser.add_argument(
|
||||
'body_file',
|
||||
type=pathlib.Path,
|
||||
help="Path to file that has the content of your correspondence/comment",
|
||||
)
|
||||
parser.add_argument(
|
||||
'rt_args', metavar='rt arguments',
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Additional arguments to pass to `rt correspond/comment`",
|
||||
)
|
||||
args = parser.parse_args(arglist)
|
||||
args.min_delta = datetime.timedelta(days=args.min_days_diff)
|
||||
args.max_delta = datetime.timedelta(days=args.max_days_diff)
|
||||
return args
|
||||
|
||||
def main(arglist=None, stdout=sys.stdout, stderr=sys.stderr):
|
||||
args = parse_arguments(arglist)
|
||||
last_run_db = LastRunStorage(args)
|
||||
last_run_range_end = last_run_db.load()
|
||||
start_datetime = datetime.datetime.utcnow()
|
||||
date_range_start = start_datetime + args.min_delta
|
||||
if (last_run_range_end is not None) and (last_run_range_end > date_range_start):
|
||||
date_range_start = last_run_range_end
|
||||
date_range_end = start_datetime + args.max_delta
|
||||
search = '({search}) AND {field} > "{start}" AND {field} <= "{end}"'.format(
|
||||
search=args.search,
|
||||
field=args.date_field,
|
||||
start=date_range_start.strftime(DATETIME_FMT),
|
||||
end=date_range_end.strftime(DATETIME_FMT),
|
||||
)
|
||||
send_cmd = [
|
||||
'rt-bulk-send', '--{}'.format(args.action), '--loglevel=debug',
|
||||
search, str(args.body_file), *args.rt_args,
|
||||
]
|
||||
if args.dry_run:
|
||||
import pprint
|
||||
pprint.pprint(send_cmd)
|
||||
returncode = 0
|
||||
else:
|
||||
try:
|
||||
subprocess.run(send_cmd, check=True)
|
||||
except subprocess.CalledProcessError as error:
|
||||
returncode = error.returncode
|
||||
else:
|
||||
last_run_db.save(date_range_end)
|
||||
returncode = 0
|
||||
last_run_db.close()
|
||||
return returncode
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
105
scripts/rt-bulk-send
Executable file
105
scripts/rt-bulk-send
Executable file
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger('rt-bulk-send')
|
||||
|
||||
def parse_arguments(arglist):
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
action = parser.add_mutually_exclusive_group()
|
||||
action.add_argument(
|
||||
'--correspond',
|
||||
dest='action', action='store_const', const='correspond',
|
||||
default='correspond',
|
||||
help="Send correspondence on found tickets (default)",
|
||||
)
|
||||
action.add_argument(
|
||||
'--comment',
|
||||
dest='action', action='store_const', const='comment',
|
||||
help="Comment on found tickets",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--loglevel',
|
||||
choices=['debug', 'info', 'warning', 'error', 'critical'],
|
||||
default='warning',
|
||||
help="Show log messages at this level (default %(default)s)",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'search',
|
||||
help="TicketSQL search, like you would pass to `rt search`",
|
||||
)
|
||||
parser.add_argument(
|
||||
'body_file',
|
||||
type=pathlib.Path,
|
||||
help="Path to file that has the content of your correspondence/comment",
|
||||
)
|
||||
parser.add_argument(
|
||||
'rt_args', metavar='rt arguments',
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Additional arguments to pass to `rt correspond/comment`",
|
||||
)
|
||||
return parser.parse_args(arglist)
|
||||
|
||||
def setup_logger(logger, loglevel, stream):
|
||||
formatter = logging.Formatter('%(name)s: %(levelname)s: %(message)s')
|
||||
handler = logging.StreamHandler(stream)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(loglevel)
|
||||
|
||||
def act_on_ticket(ticket_id, args, body_file):
|
||||
body_file.seek(0)
|
||||
return subprocess.run(
|
||||
['rt', args.action, *args.rt_args, '-m', '-', ticket_id],
|
||||
stdin=body_file,
|
||||
check=True,
|
||||
)
|
||||
|
||||
def main(arglist=None, stdout=sys.stdout, stderr=sys.stderr):
|
||||
args = parse_arguments(arglist)
|
||||
setup_logger(logger, getattr(logging, args.loglevel.upper()), stderr)
|
||||
try:
|
||||
body_file = args.body_file.open()
|
||||
except OSError as error:
|
||||
logger.critical("error opening {}: {}".format(args.body_file, error))
|
||||
return 3
|
||||
if not body_file.seekable():
|
||||
logger.critical("file {} must be seekable".format(args.body_file))
|
||||
with body_file:
|
||||
return 3
|
||||
|
||||
failures = 0
|
||||
with body_file, subprocess.Popen(
|
||||
['rt', 'search', '-i', args.search],
|
||||
stdout=subprocess.PIPE,
|
||||
universal_newlines=True,
|
||||
) as search_pipe:
|
||||
for line in search_pipe.stdout:
|
||||
ticket_id = line.rstrip('\n')
|
||||
if not ticket_id:
|
||||
continue
|
||||
logger.info("Acting on %s", ticket_id)
|
||||
try:
|
||||
act_on_ticket(ticket_id, args, body_file)
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.error("Failed to %s on %s: rt returned exit code %s",
|
||||
args.action, ticket_id, error.returncode)
|
||||
failures += 1
|
||||
|
||||
if search_pipe.returncode != 0:
|
||||
logger.critical("`rt search` returned exit code %s", search_pipe.returncode)
|
||||
return 4
|
||||
elif failures == 0:
|
||||
return 0
|
||||
else:
|
||||
return min(10 + failures, 99)
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
72
scripts/rt-send-all-reminders
Executable file
72
scripts/rt-send-all-reminders
Executable file
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
try:
|
||||
_data_home = os.environ['XDG_DATA_HOME']
|
||||
except KeyError:
|
||||
_data_home = pathlib.Path(os.path.expanduser('~'), '.local', 'share')
|
||||
SHARE_DIR = pathlib.Path(_data_home, 'rt-auto-remind')
|
||||
SHARE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
class Reminder:
|
||||
def __init__(self, key, min_days, max_days, search,
|
||||
date_field='Due', body_file=None, action='correspond'):
|
||||
if body_file is None:
|
||||
body_file = pathlib.Path(SHARE_DIR, 'templates', key + '.txt')
|
||||
self.key = key
|
||||
self.min_days_diff = int(min_days)
|
||||
self.max_days_diff = int(max_days)
|
||||
self.search = search
|
||||
self.date_field = date_field
|
||||
self.body_file = body_file
|
||||
self.action = action
|
||||
|
||||
def remind_cmd(self):
|
||||
return [
|
||||
'rt-auto-remind', '--{}'.format(self.action),
|
||||
'--key', self.key,
|
||||
self.date_field,
|
||||
str(self.min_days_diff),
|
||||
str(self.max_days_diff),
|
||||
self.search,
|
||||
str(self.body_file),
|
||||
]
|
||||
|
||||
|
||||
def parse_arguments(arglist):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'yaml_files', metavar='PATH',
|
||||
type=pathlib.Path,
|
||||
nargs='+',
|
||||
help="YAML file(s) with configuration of reminders to send out",
|
||||
)
|
||||
return parser.parse_args(arglist)
|
||||
|
||||
def main(arglist=None, stdout=sys.stdout, stderr=sys.stderr):
|
||||
args = parse_arguments(arglist)
|
||||
failures = 0
|
||||
for yaml_path in args.yaml_files:
|
||||
with yaml_path.open() as yaml_file:
|
||||
yaml_data = yaml.safe_load(yaml_file)
|
||||
for key in yaml_data:
|
||||
reminder = Reminder(key, **yaml_data[key])
|
||||
try:
|
||||
subprocess.run(reminder.remind_cmd(), check=True)
|
||||
except subprocess.CalledProcessError as error:
|
||||
print("warning: reminder {} exited {}".format(key, error.returncode))
|
||||
failures += 1
|
||||
if failures == 0:
|
||||
return 0
|
||||
else:
|
||||
return min(10 + failures, 99)
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
Loading…
Reference in a new issue