From e10131c4699f3e3e3dc5b09958378c3796accb16 Mon Sep 17 00:00:00 2001 From: nolan Date: Sun, 25 Mar 2018 16:08:15 -0400 Subject: [PATCH 01/71] draft jj python-only shell script --- python/jj | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ sh/jarjar | 0 2 files changed, 48 insertions(+) create mode 100755 python/jj mode change 100755 => 100644 sh/jarjar diff --git a/python/jj b/python/jj new file mode 100755 index 0000000..164ab86 --- /dev/null +++ b/python/jj @@ -0,0 +1,48 @@ +#! /usr/bin/env python + +from jarjar import jarjar +import argparse + +PARSER = argparse.ArgumentParser() + +''' +X Send a message, no muss no fuss: jarjar {message} +Spin up a screen and do a task, then send message: jarjar {message} --arg {task} +Spin up a screen and do a task, then send exit status: jarjar --arg {task} +''' + +PARSER.add_argument( + 'message', + help="message to send", + nargs='+', + default=[] +) + +PARSER.add_argument( + "-w", + "--webhook", + help="Set webhook target.", + default=None +) + +PARSER.add_argument( + "-u", '-c', + "--channel", + help="Set user/channel target.", + default=None +) + + +ARGS = PARSER.parse_args() +MESSAGE = ' '.join(ARGS.message) +WEBHOOK = ARGS.webhook +CHANNEL = ARGS.channel + + +def main(): + jj = jarjar(webhook=WEBHOOK, channel=CHANNEL) + jj.text(MESSAGE) + + +if __name__ == '__main__': + main() diff --git a/sh/jarjar b/sh/jarjar old mode 100755 new mode 100644 From 1d4a3e951159ad058ee404e10c19ef91f33f917e Mon Sep 17 00:00:00 2001 From: nolan Date: Sun, 8 Apr 2018 19:51:28 -0400 Subject: [PATCH 02/71] oof the shell script is working EXCEPT for the screen api --- python/jarjar/_screenutils/__init__.py | 8 ++ python/jarjar/_screenutils/errors.py | 8 ++ python/jarjar/_screenutils/screen.py | 191 +++++++++++++++++++++++++ python/jarjar/jarjar | 187 ++++++++++++++++++++++++ python/jarjar/txt | 0 python/jj | 48 ------- 6 files changed, 394 insertions(+), 48 deletions(-) create mode 100755 python/jarjar/_screenutils/__init__.py create mode 100755 python/jarjar/_screenutils/errors.py create mode 100755 python/jarjar/_screenutils/screen.py create mode 100755 python/jarjar/jarjar create mode 100644 python/jarjar/txt delete mode 100755 python/jj diff --git a/python/jarjar/_screenutils/__init__.py b/python/jarjar/_screenutils/__init__.py new file mode 100755 index 0000000..939bcfe --- /dev/null +++ b/python/jarjar/_screenutils/__init__.py @@ -0,0 +1,8 @@ +from .errors import ScreenNotFoundError +from .screen import list_screens, Screen + +__all__ = ( + "list_screens", + "Screen", + "ScreenNotFoundError" +) diff --git a/python/jarjar/_screenutils/errors.py b/python/jarjar/_screenutils/errors.py new file mode 100755 index 0000000..7d12420 --- /dev/null +++ b/python/jarjar/_screenutils/errors.py @@ -0,0 +1,8 @@ +"""Errors for the screenutils module""" + +class ScreenNotFoundError(Exception): + """raised when the screen does not exists""" + def __init__(self, message, screen_name): + message += " Screen \"{0}\" not found".format(screen_name) + self.screen_name = screen_name + super(ScreenNotFoundError, self).__init__(message) diff --git a/python/jarjar/_screenutils/screen.py b/python/jarjar/_screenutils/screen.py new file mode 100755 index 0000000..505b98d --- /dev/null +++ b/python/jarjar/_screenutils/screen.py @@ -0,0 +1,191 @@ +# -*- coding:utf-8 -*- +# +# This program is free software. It comes without any warranty, to +# the extent permitted by applicable law. You can redistribute it +# and/or modify it under the terms of the GNU Public License 2 or upper. +# Please ask if you wish a more permissive license. + +from _screenutils.errors import ScreenNotFoundError + +try: + from commands import getoutput +except: + from subprocess import getoutput +from threading import Thread +from os import system +from os.path import getsize +from time import sleep + + +def tailf(file_): + """Each value is content added to the log file since last value return""" + last_size = getsize(file_) + while True: + cur_size = getsize(file_) + if (cur_size != last_size): + f = open(file_, 'r') + f.seek(last_size if cur_size > last_size else 0) + text = f.read() + f.close() + last_size = cur_size + yield text + else: + yield "" + + +def list_screens(): + """List all the existing screens and build a Screen instance for each + """ + list_cmd = "screen -ls" + return [ + Screen(".".join(l.split(".")[1:]).split("\t")[0]) + for l in getoutput(list_cmd).split('\n') + if "\t" in l and ".".join(l.split(".")[1:]).split("\t")[0] + ] + + +class Screen(object): + """Represents a gnu-screen object:: + + >>> s=Screen("screenName", initialize=True) + >>> s.name + 'screenName' + >>> s.exists + True + >>> s.state + >>> s.send_commands("man -k keyboard") + >>> s.kill() + >>> s.exists + False + """ + + def __init__(self, name, initialize=False): + self.name = name + self._id = None + self._status = None + self.logs = None + self._logfilename = None + if initialize: + self.initialize() + + @property + def id(self): + """return the identifier of the screen as string""" + if not self._id: + self._set_screen_infos() + return self._id + + @property + def status(self): + """return the status of the screen as string""" + self._set_screen_infos() + return self._status + + @property + def exists(self): + """Tell if the screen session exists or not.""" + # Parse the screen -ls call, to find if the screen exists or not. + # " 28062.G.Terminal (Detached)" + lines = getoutput("screen -ls").split('\n') + return self.name in [".".join(l.split(".")[1:]).split("\t")[0] + for l in lines if self.name in l] + + def enable_logs(self, filename=None): + if filename is None: + filename = self.name + self._screen_commands("logfile " + filename, "log on") + self._logfilename = filename + open(filename, 'w+') + self.logs = tailf(filename) + + def disable_logs(self, remove_logfile=False): + self._screen_commands("log off") + if remove_logfile: + system('rm ' + self._logfilename) + self.logs = None + + def initialize(self): + """initialize a screen, if does not exists yet""" + if not self.exists: + self._id = None + # Detach the screen once attached, on a new tread. + # support Unicode (-U), + # attach to a new/existing named screen (-R). + + # ORIGINAL + # Thread(target=self._delayed_detach).start() + # system('screen -s sh -UR -S ' + self.name) + + # CUSTOM + system('screen -d -m -U -s sh -S ' + self.name) + + def interrupt(self): + """Insert CTRL+C in the screen session""" + self._screen_commands("eval \"stuff \\003\"") + + def kill(self): + """Kill the screen applications then close the screen""" + self._screen_commands('quit') + + def detach(self): + """detach the screen""" + self._check_exists() + system("screen -d " + self.id) + + def send_commands(self, *commands): + """send commands to the active gnu-screen""" + self._check_exists() + for command in commands: + # self._screen_commands( + # 'stuff "' + command + '" ', + # 'eval "stuff \\015"' + # ) + self._screen_commands(command) + + def add_user_access(self, unix_user_name): + """allow to share your session with an other unix user""" + self._screen_commands('multiuser on', 'acladd ' + unix_user_name) + + def _screen_commands(self, *commands): + """allow to insert generic screen specific commands + a glossary of the existing screen command in `man screen`""" + self._check_exists() + for command in commands: + cmd = 'screen -x ' + self.id + ' -X ' + command + # 'screen -x ' + self.id + ' -X ' + command + print cmd + system(cmd) + sleep(0.02) + + def _check_exists(self, message="Error code: 404."): + """check whereas the screen exist. if not, raise an exception""" + if not self.exists: + raise ScreenNotFoundError(message, self.name) + + def _set_screen_infos(self): + """set the screen information related parameters""" + if self.exists: + line = "" + for l in getoutput("screen -ls").split("\n"): + if ( + l.startswith('\t') and + self.name in l and + self.name == ".".join( + l.split('\t')[1].split('.')[1:]) in l): + line = l + if not line: + raise ScreenNotFoundError("While getting info.", self.name) + infos = line.split('\t')[1:] + self._id = infos[0].split('.')[0] + if len(infos) == 3: + self._date = infos[1][1:-1] + self._status = infos[2][1:-1] + else: + self._status = infos[1][1:-1] + + def _delayed_detach(self): + sleep(0.5) + self.detach() + + def __repr__(self): + return "<%s '%s'>" % (self.__class__.__name__, self.name) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar new file mode 100755 index 0000000..7e5436e --- /dev/null +++ b/python/jarjar/jarjar @@ -0,0 +1,187 @@ +#! /usr/bin/env python + +from jarjar import jarjar +from _screenutils import Screen, ScreenNotFoundError + +import time +import os +import sys +import argparse +import re + +PARSER = argparse.ArgumentParser() + +''' +set up screens +Evniornment wizard +Suggested commands on errors + +''' + +PARSER.add_argument( + 'program', + help="Set task to run in a screen.", + nargs='*', + default=[] +) + +PARSER.add_argument( + "-m", + "--message", + help="Message to send.", + nargs='*', + default=[] +) + +PARSER.add_argument( + "-w", + "--webhook", + help="Set webhook target.", + default=None +) + +PARSER.add_argument( + "-u", '-c', + "--channel", + help="Set user/channel target.", + default=None +) + +PARSER.add_argument( + "-a", '-r', + "--attach", + help="Attach to screen.", + default=False, + action='store_true' +) + +PARSER.add_argument( + "-S", + dest='screen_name', + help="Set the name of the screen. Default to first 10 characters of the task", + default=None, + nargs=1 +) + +PARSER.add_argument( + "-e", + dest='echo', + action='store_true', + help=argparse.SUPPRESS, + default=False +) + +# get arguments from parser +ARGS = PARSER.parse_args() + +# warns / checks +if not ARGS.message and not ARGS.program: + PARSER.print_help(sys.stderr) + print('\nProvide a message or some task too complete!') + sys.exit(1) + +if ARGS.echo: + print('Psst-- `-e` is no longer needed!') + +if not ARGS.program: + if ARGS.attach: + print('Note: You asked to attach but there is no task to run.') + + if ARGS.screen_name is not None: + print('Note: You named a screen but there is no task to run.') + + +def _append_to_name(name): + """Append an `__{integer}` to a name or add to it.""" + + suffix = '__' + appended = suffix in name and name.split(suffix)[-1].isdigit() + if not appended: + return name + suffix + '1' + + parts = name.split(suffix) + stem = suffix.join(parts[:-1]) + return stem + suffix + str(int(parts[-1]) + 1) + + +def _make_jarjar_shell(m=None, c=None, w=None): + """Construct a jarjar shell command.""" + + def make_flag(k, v): + """Ignore flag if None, otherwise wrap in single quotes.""" + if v is None: + return [] + else: + return [k, '\'{}\''.format(v)] + + # start out the command + cmd = ['./jarjar'] + + # exit status default message if needed + if not m: + m = '\"Final task exited with status: $?.\"' + cmd += ['--message', m] + else: + cmd += make_flag('--message', m) + + cmd += make_flag('--channel', c) + cmd += make_flag('--webhook', w) + return ' '.join(cmd) + + +def main(): + + # pare args + MESSAGE = ' '.join(ARGS.message) + WEBHOOK = ARGS.webhook + CHANNEL = ARGS.channel + PROGRAM = ' '.join(ARGS.program) + ATTACH = ARGS.attach + if ARGS.screen_name is None: + if PROGRAM: + SCREEN_NAME = '_'.join(ARGS.program).replace(' ', '_') + SCREEN_NAME = re.sub(r'\W+', '', SCREEN_NAME) + SCREEN_NAME = SCREEN_NAME[:min(len(SCREEN_NAME), 10)] + else: + SCREEN_NAME = None + else: + SCREEN_NAME = ARGS.screen_name + + # if there is no program, then we're done here... + if not PROGRAM: + jarjar(channel=CHANNEL, webhook=WEBHOOK).text(MESSAGE) + return + + # make jarjar shell command + NOTIFY = _make_jarjar_shell(m=MESSAGE, w=WEBHOOK, c=CHANNEL) + + # make sure screen is unique + # --- raise error for manual names. + # --- append suffix for auto names. + screen = Screen(SCREEN_NAME, initialize=False) + if screen.exists: + if ARGS.screen_name is not None: + raise Exception('There is already a screen by that name!') + + while screen.exists: + SCREEN_NAME = _append_to_name(SCREEN_NAME) + screen = Screen(SCREEN_NAME) + + # notify user of the screen name if not provided. + if ARGS.screen_name is None: + print('Creating screen: `{}`.'.format(SCREEN_NAME)) + + # spin up the screen and run through the task + screen.initialize() + + screen.send_commands('echo;' + PROGRAM + ';' + NOTIFY) + + # attach if needed + if ATTACH: + os.system('screen -r {}'.format(SCREEN_NAME)) + + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/python/jarjar/txt b/python/jarjar/txt new file mode 100644 index 0000000..e69de29 diff --git a/python/jj b/python/jj deleted file mode 100755 index 164ab86..0000000 --- a/python/jj +++ /dev/null @@ -1,48 +0,0 @@ -#! /usr/bin/env python - -from jarjar import jarjar -import argparse - -PARSER = argparse.ArgumentParser() - -''' -X Send a message, no muss no fuss: jarjar {message} -Spin up a screen and do a task, then send message: jarjar {message} --arg {task} -Spin up a screen and do a task, then send exit status: jarjar --arg {task} -''' - -PARSER.add_argument( - 'message', - help="message to send", - nargs='+', - default=[] -) - -PARSER.add_argument( - "-w", - "--webhook", - help="Set webhook target.", - default=None -) - -PARSER.add_argument( - "-u", '-c', - "--channel", - help="Set user/channel target.", - default=None -) - - -ARGS = PARSER.parse_args() -MESSAGE = ' '.join(ARGS.message) -WEBHOOK = ARGS.webhook -CHANNEL = ARGS.channel - - -def main(): - jj = jarjar(webhook=WEBHOOK, channel=CHANNEL) - jj.text(MESSAGE) - - -if __name__ == '__main__': - main() From 49d4f56959b427cf4a126450ea194a79d87382b3 Mon Sep 17 00:00:00 2001 From: nolan Date: Wed, 11 Apr 2018 21:18:56 -0400 Subject: [PATCH 03/71] shell app drafted --- python/jarjar/TODO | 9 +++ python/jarjar/_screenutils/screen.py | 110 +++++++++++---------------- python/jarjar/jarjar | 17 ++--- python/jarjar/txt | 0 4 files changed, 60 insertions(+), 76 deletions(-) create mode 100644 python/jarjar/TODO delete mode 100644 python/jarjar/txt diff --git a/python/jarjar/TODO b/python/jarjar/TODO new file mode 100644 index 0000000..7d27b40 --- /dev/null +++ b/python/jarjar/TODO @@ -0,0 +1,9 @@ + +Environment wizard +Allow custom images/names/etc in .jarjar +Figure out how to place this into a bin +Test python 2/3 support +Think of list of edge cases and test them +Documentation needs a major revamp. +Remove the original .sh command +Auto detect where jarjar executable is \ No newline at end of file diff --git a/python/jarjar/_screenutils/screen.py b/python/jarjar/_screenutils/screen.py index 505b98d..782cd31 100755 --- a/python/jarjar/_screenutils/screen.py +++ b/python/jarjar/_screenutils/screen.py @@ -9,16 +9,15 @@ try: from commands import getoutput -except: +except Exception: from subprocess import getoutput -from threading import Thread from os import system from os.path import getsize from time import sleep def tailf(file_): - """Each value is content added to the log file since last value return""" + """Each value is content added to the log file since last value return.""" last_size = getsize(file_) while True: cur_size = getsize(file_) @@ -34,50 +33,47 @@ def tailf(file_): def list_screens(): - """List all the existing screens and build a Screen instance for each - """ + """List all the existing screens and build a Screen instance for each.""" list_cmd = "screen -ls" return [ - Screen(".".join(l.split(".")[1:]).split("\t")[0]) - for l in getoutput(list_cmd).split('\n') - if "\t" in l and ".".join(l.split(".")[1:]).split("\t")[0] - ] + Screen(".".join(l.split(".")[1:]).split("\t")[0]) + for l in getoutput(list_cmd).split('\n') + if "\t" in l and ".".join(l.split(".")[1:]).split("\t")[0] + ] class Screen(object): - """Represents a gnu-screen object:: - - >>> s=Screen("screenName", initialize=True) - >>> s.name - 'screenName' - >>> s.exists - True - >>> s.state - >>> s.send_commands("man -k keyboard") - >>> s.kill() - >>> s.exists - False + """Represents a gnu-screen object. + + >>> s=Screen("screenName", initialize=True) + >>> s.name + 'screenName' + >>> s.exists + True + >>> s.state + >>> s.send_commands("man -k keyboard") + >>> s.kill() + >>> s.exists + False """ def __init__(self, name, initialize=False): self.name = name self._id = None self._status = None - self.logs = None - self._logfilename = None if initialize: self.initialize() @property def id(self): - """return the identifier of the screen as string""" + """Return the identifier of the screen as string.""" if not self._id: self._set_screen_infos() return self._id @property def status(self): - """return the status of the screen as string""" + """Return the status of the screen as string.""" self._set_screen_infos() return self._status @@ -87,25 +83,14 @@ def exists(self): # Parse the screen -ls call, to find if the screen exists or not. # " 28062.G.Terminal (Detached)" lines = getoutput("screen -ls").split('\n') - return self.name in [".".join(l.split(".")[1:]).split("\t")[0] - for l in lines if self.name in l] - - def enable_logs(self, filename=None): - if filename is None: - filename = self.name - self._screen_commands("logfile " + filename, "log on") - self._logfilename = filename - open(filename, 'w+') - self.logs = tailf(filename) - - def disable_logs(self, remove_logfile=False): - self._screen_commands("log off") - if remove_logfile: - system('rm ' + self._logfilename) - self.logs = None + return self.name in [ + ".".join(l.split(".")[1:]).split("\t")[0] + for l in lines + if self.name in l + ] def initialize(self): - """initialize a screen, if does not exists yet""" + """Initialize a screen, if does not exists yet.""" if not self.exists: self._id = None # Detach the screen once attached, on a new tread. @@ -117,61 +102,58 @@ def initialize(self): # system('screen -s sh -UR -S ' + self.name) # CUSTOM - system('screen -d -m -U -s sh -S ' + self.name) + system('screen -d -m -S ' + self.name) def interrupt(self): - """Insert CTRL+C in the screen session""" + """Insert CTRL+C in the screen session.""" self._screen_commands("eval \"stuff \\003\"") def kill(self): - """Kill the screen applications then close the screen""" + """Kill the screen applications then close the screen.""" self._screen_commands('quit') def detach(self): - """detach the screen""" + """Detach the screen.""" self._check_exists() system("screen -d " + self.id) def send_commands(self, *commands): - """send commands to the active gnu-screen""" + """Send commands to the active gnu-screen.""" self._check_exists() for command in commands: - # self._screen_commands( - # 'stuff "' + command + '" ', - # 'eval "stuff \\015"' - # ) - self._screen_commands(command) + self._screen_commands( + 'stuff \"{0}\"'.format(command), + 'eval "stuff \\015"' + ) def add_user_access(self, unix_user_name): - """allow to share your session with an other unix user""" + """Allow to share your session with an other unix user.""" self._screen_commands('multiuser on', 'acladd ' + unix_user_name) def _screen_commands(self, *commands): - """allow to insert generic screen specific commands - a glossary of the existing screen command in `man screen`""" + """Allow to insert generic screen specific commands.""" self._check_exists() for command in commands: - cmd = 'screen -x ' + self.id + ' -X ' + command - # 'screen -x ' + self.id + ' -X ' + command - print cmd + cmd = 'screen -x {0} -p 0 -X {1}'.format(self.name, command) + # print cmd system(cmd) sleep(0.02) def _check_exists(self, message="Error code: 404."): - """check whereas the screen exist. if not, raise an exception""" + """Check whereas the screen exist. if not, raise an exception.""" if not self.exists: raise ScreenNotFoundError(message, self.name) def _set_screen_infos(self): - """set the screen information related parameters""" + """Set the screen information related parameters.""" if self.exists: line = "" for l in getoutput("screen -ls").split("\n"): if ( - l.startswith('\t') and - self.name in l and - self.name == ".".join( - l.split('\t')[1].split('.')[1:]) in l): + l.startswith('\t') and + self.name in l and + self.name == ".".join(l.split('\t')[1].split('.')[1:]) in l + ): line = l if not line: raise ScreenNotFoundError("While getting info.", self.name) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index 7e5436e..a69e769 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -1,9 +1,8 @@ #! /usr/bin/env python from jarjar import jarjar -from _screenutils import Screen, ScreenNotFoundError +from _screenutils import Screen -import time import os import sys import argparse @@ -11,12 +10,6 @@ import re PARSER = argparse.ArgumentParser() -''' -set up screens -Evniornment wizard -Suggested commands on errors - -''' PARSER.add_argument( 'program', @@ -115,11 +108,11 @@ def _make_jarjar_shell(m=None, c=None, w=None): return [k, '\'{}\''.format(v)] # start out the command - cmd = ['./jarjar'] + cmd = ['jarjar'] # exit status default message if needed if not m: - m = '\"Final task exited with status: $?.\"' + m = '\'Your task has completed with exit status $?.\'' cmd += ['--message', m] else: cmd += make_flag('--message', m) @@ -169,12 +162,12 @@ def main(): # notify user of the screen name if not provided. if ARGS.screen_name is None: - print('Creating screen: `{}`.'.format(SCREEN_NAME)) + print('Creating screen: `{0}`.'.format(SCREEN_NAME)) # spin up the screen and run through the task screen.initialize() - screen.send_commands('echo;' + PROGRAM + ';' + NOTIFY) + screen.send_commands(PROGRAM + '; ' + NOTIFY) # attach if needed if ATTACH: diff --git a/python/jarjar/txt b/python/jarjar/txt deleted file mode 100644 index e69de29..0000000 From 7ec750175603b75e68e7304258cf6597695cb9b0 Mon Sep 17 00:00:00 2001 From: nolan Date: Wed, 11 Apr 2018 21:21:38 -0400 Subject: [PATCH 04/71] bug fix --- python/jarjar/TODO | 3 ++- python/jarjar/jarjar | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/python/jarjar/TODO b/python/jarjar/TODO index 7d27b40..ab15338 100644 --- a/python/jarjar/TODO +++ b/python/jarjar/TODO @@ -6,4 +6,5 @@ Test python 2/3 support Think of list of edge cases and test them Documentation needs a major revamp. Remove the original .sh command -Auto detect where jarjar executable is \ No newline at end of file +Auto detect where jarjar executable is +Figure out how to add an exit status to the default message \ No newline at end of file diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index a69e769..fe20950 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -112,7 +112,7 @@ def _make_jarjar_shell(m=None, c=None, w=None): # exit status default message if needed if not m: - m = '\'Your task has completed with exit status $?.\'' + m = '\'Your task has completed.\'' cmd += ['--message', m] else: cmd += make_flag('--message', m) From 901446b4694ef4b3c928c21a0533096c066f79c3 Mon Sep 17 00:00:00 2001 From: Jeff Zemla Date: Thu, 12 Apr 2018 15:51:16 -0500 Subject: [PATCH 05/71] minor changes incl.: added --user, --screen_name --- python/jarjar/jarjar | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index fe20950..d740350 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -1,5 +1,11 @@ #! /usr/bin/env python +# TODO: +# silently suppress unused args +# return exit status from program +# specify screen name doesn't work +# use default jarjar message when program but not message is specified + from jarjar import jarjar from _screenutils import Screen @@ -34,14 +40,15 @@ PARSER.add_argument( ) PARSER.add_argument( - "-u", '-c', + "-c", "-u", "--channel", + "--user", help="Set user/channel target.", default=None ) PARSER.add_argument( - "-a", '-r', + "-a", "-r", "--attach", help="Attach to screen.", default=False, @@ -50,6 +57,7 @@ PARSER.add_argument( PARSER.add_argument( "-S", + "--screen_name", dest='screen_name', help="Set the name of the screen. Default to first 10 characters of the task", default=None, @@ -59,9 +67,9 @@ PARSER.add_argument( PARSER.add_argument( "-e", dest='echo', - action='store_true', help=argparse.SUPPRESS, - default=False + default=False, + action='store_true' ) # get arguments from parser @@ -124,7 +132,7 @@ def _make_jarjar_shell(m=None, c=None, w=None): def main(): - # pare args + # parse args MESSAGE = ' '.join(ARGS.message) WEBHOOK = ARGS.webhook CHANNEL = ARGS.channel @@ -133,8 +141,7 @@ def main(): if ARGS.screen_name is None: if PROGRAM: SCREEN_NAME = '_'.join(ARGS.program).replace(' ', '_') - SCREEN_NAME = re.sub(r'\W+', '', SCREEN_NAME) - SCREEN_NAME = SCREEN_NAME[:min(len(SCREEN_NAME), 10)] + SCREEN_NAME = SCREEN_NAME[:10] else: SCREEN_NAME = None else: From ca0c1388755d3fa2c5a04223042d998f6b77e6c7 Mon Sep 17 00:00:00 2001 From: nolan Date: Thu, 12 Apr 2018 19:38:49 -0400 Subject: [PATCH 06/71] objectivize jarjar shell command --- python/jarjar/_screenutils/screen.py | 19 +------------------ python/jarjar/jarjar | 7 +++---- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/python/jarjar/_screenutils/screen.py b/python/jarjar/_screenutils/screen.py index 782cd31..3e59ec6 100755 --- a/python/jarjar/_screenutils/screen.py +++ b/python/jarjar/_screenutils/screen.py @@ -12,26 +12,9 @@ except Exception: from subprocess import getoutput from os import system -from os.path import getsize from time import sleep -def tailf(file_): - """Each value is content added to the log file since last value return.""" - last_size = getsize(file_) - while True: - cur_size = getsize(file_) - if (cur_size != last_size): - f = open(file_, 'r') - f.seek(last_size if cur_size > last_size else 0) - text = f.read() - f.close() - last_size = cur_size - yield text - else: - yield "" - - def list_screens(): """List all the existing screens and build a Screen instance for each.""" list_cmd = "screen -ls" @@ -135,7 +118,7 @@ def _screen_commands(self, *commands): self._check_exists() for command in commands: cmd = 'screen -x {0} -p 0 -X {1}'.format(self.name, command) - # print cmd + print cmd system(cmd) sleep(0.02) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index d740350..45bde99 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -12,7 +12,6 @@ from _screenutils import Screen import os import sys import argparse -import re PARSER = argparse.ArgumentParser() @@ -42,7 +41,7 @@ PARSER.add_argument( PARSER.add_argument( "-c", "-u", "--channel", - "--user", + "--user", help="Set user/channel target.", default=None ) @@ -57,7 +56,7 @@ PARSER.add_argument( PARSER.add_argument( "-S", - "--screen_name", + "--screen_name", dest='screen_name', help="Set the name of the screen. Default to first 10 characters of the task", default=None, @@ -116,7 +115,7 @@ def _make_jarjar_shell(m=None, c=None, w=None): return [k, '\'{}\''.format(v)] # start out the command - cmd = ['jarjar'] + cmd = ['/usr/bin/env python {}'.format(os.path.realpath(__file__))] # exit status default message if needed if not m: From 51d8a542477d43052e0b655a1af20cea014e1410 Mon Sep 17 00:00:00 2001 From: nolan Date: Thu, 12 Apr 2018 19:44:38 -0400 Subject: [PATCH 07/71] add unknown arg warning --- python/jarjar/TODO | 1 - python/jarjar/_screenutils/screen.py | 2 +- python/jarjar/jarjar | 5 ++++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/python/jarjar/TODO b/python/jarjar/TODO index ab15338..6666a3a 100644 --- a/python/jarjar/TODO +++ b/python/jarjar/TODO @@ -6,5 +6,4 @@ Test python 2/3 support Think of list of edge cases and test them Documentation needs a major revamp. Remove the original .sh command -Auto detect where jarjar executable is Figure out how to add an exit status to the default message \ No newline at end of file diff --git a/python/jarjar/_screenutils/screen.py b/python/jarjar/_screenutils/screen.py index 3e59ec6..f3582f4 100755 --- a/python/jarjar/_screenutils/screen.py +++ b/python/jarjar/_screenutils/screen.py @@ -118,7 +118,7 @@ def _screen_commands(self, *commands): self._check_exists() for command in commands: cmd = 'screen -x {0} -p 0 -X {1}'.format(self.name, command) - print cmd + # print(cmd) system(cmd) sleep(0.02) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index 45bde99..3bfe9d3 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -72,9 +72,12 @@ PARSER.add_argument( ) # get arguments from parser -ARGS = PARSER.parse_args() +ARGS, UNKNOWN = PARSER.parse_known_args() # warns / checks +if UNKNOWN: + print('Warn: ignoring unknown arguments `{}`.'.format(' '.join(UNKNOWN))) + if not ARGS.message and not ARGS.program: PARSER.print_help(sys.stderr) print('\nProvide a message or some task too complete!') From 38dc4444764b666b00b95d21070cb001a8ea736f Mon Sep 17 00:00:00 2001 From: nolan Date: Thu, 12 Apr 2018 19:45:58 -0400 Subject: [PATCH 08/71] fix screen naming error --- python/jarjar/jarjar | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index 3bfe9d3..e03184f 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -59,8 +59,7 @@ PARSER.add_argument( "--screen_name", dest='screen_name', help="Set the name of the screen. Default to first 10 characters of the task", - default=None, - nargs=1 + default=None ) PARSER.add_argument( From e199402fcbc5a5d3f0e1c7f024a1fe4526ad7e1d Mon Sep 17 00:00:00 2001 From: nolan Date: Thu, 12 Apr 2018 19:53:00 -0400 Subject: [PATCH 09/71] add todo --- python/jarjar/TODO | 9 --------- python/jarjar/TODO.md | 17 +++++++++++++++++ python/jarjar/jarjar | 3 +-- 3 files changed, 18 insertions(+), 11 deletions(-) delete mode 100644 python/jarjar/TODO create mode 100644 python/jarjar/TODO.md diff --git a/python/jarjar/TODO b/python/jarjar/TODO deleted file mode 100644 index 6666a3a..0000000 --- a/python/jarjar/TODO +++ /dev/null @@ -1,9 +0,0 @@ - -Environment wizard -Allow custom images/names/etc in .jarjar -Figure out how to place this into a bin -Test python 2/3 support -Think of list of edge cases and test them -Documentation needs a major revamp. -Remove the original .sh command -Figure out how to add an exit status to the default message \ No newline at end of file diff --git a/python/jarjar/TODO.md b/python/jarjar/TODO.md new file mode 100644 index 0000000..fe2b15c --- /dev/null +++ b/python/jarjar/TODO.md @@ -0,0 +1,17 @@ +# TOP PRIORITY + +- Make a test suite and pass it with > Python 2.7, 3. +- Figure out how to place the shell script into a bin +- Default message support because jeff uses it. +- Documentation needs a major revamp. + +# WOULD NE NICE + +- Figure out how to add an exit status to the default message + +# MOONSHOT + +- Allow custom images/names/etc in .jarjar +- Move to JSON- or config-based .jarjar +- Environment wizard to let users edit their .jarjar without a terminal. + diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index e03184f..97ece6f 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -169,8 +169,7 @@ def main(): screen = Screen(SCREEN_NAME) # notify user of the screen name if not provided. - if ARGS.screen_name is None: - print('Creating screen: `{0}`.'.format(SCREEN_NAME)) + print('Creating screen: `{0}`.'.format(SCREEN_NAME)) # spin up the screen and run through the task screen.initialize() From 8cb3ad25aa03d236b62b06991496ef612738e353 Mon Sep 17 00:00:00 2001 From: nolan Date: Thu, 12 Apr 2018 19:54:21 -0400 Subject: [PATCH 10/71] major changes that hugely impact perfomance. jk its a spell check --- python/jarjar/TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/jarjar/TODO.md b/python/jarjar/TODO.md index fe2b15c..2060b2b 100644 --- a/python/jarjar/TODO.md +++ b/python/jarjar/TODO.md @@ -5,7 +5,7 @@ - Default message support because jeff uses it. - Documentation needs a major revamp. -# WOULD NE NICE +# WOULD BE NICE - Figure out how to add an exit status to the default message From db95233927e74ea58444cae56636b4c29e12958b Mon Sep 17 00:00:00 2001 From: Jeff Zemla Date: Mon, 16 Apr 2018 14:31:24 -0500 Subject: [PATCH 11/71] reads in default message from .jarjar now --- python/jarjar/jarjar | 3 ++- python/jarjar/jarjar.py | 48 ++++++++++++++++++++++++----------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index 97ece6f..b5fa529 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -15,7 +15,6 @@ import argparse PARSER = argparse.ArgumentParser() - PARSER.add_argument( 'program', help="Set task to run in a screen.", @@ -125,6 +124,8 @@ def _make_jarjar_shell(m=None, c=None, w=None): cmd += ['--message', m] else: cmd += make_flag('--message', m) + #if m: + # cmd += make_flag('--message', m) cmd += make_flag('--channel', c) cmd += make_flag('--webhook', w) diff --git a/python/jarjar/jarjar.py b/python/jarjar/jarjar.py index ea8ea76..c667b77 100644 --- a/python/jarjar/jarjar.py +++ b/python/jarjar/jarjar.py @@ -6,19 +6,18 @@ class jarjar(): - def __init__(self, channel=None, webhook=None): + def __init__(self, channel=None, webhook=None, message=None): # read config file, set defaults self._read_config() - self._set_defaults(channel=channel, webhook=webhook) + self._set_defaults(channel=channel, webhook=webhook, message=message) # headers for post request self.headers = {'Content-Type': 'application/json'} - - def _set_defaults(self, channel=None, webhook=None): + def _set_defaults(self, channel=None, webhook=None, message=None): """ - Set the default channel and webhook + Set the default channel and webhook and message This could be a little drier.... """ @@ -34,6 +33,11 @@ def _set_defaults(self, channel=None, webhook=None): else: self.default_webhook = webhook + # same thing for message + if message is None: + self.default_message = self.cfg_message + else: + self.default_message = message def _read_config(self): """ @@ -51,7 +55,7 @@ def _read_config(self): cfg = imp.load_source('_jarjar', filename) # assign variables - for field in ['channel','webhook']: + for field in ['channel','webhook','message']: # read from config, or set to none if hasattr(cfg, field): @@ -63,7 +67,7 @@ def _read_config(self): setattr(self, 'cfg_%s' % field, data) - def _args_handler(self, channel, webhook): + def _args_handler(self, channel, webhook, message): """ Decide to use the default or provided arguments """ @@ -74,12 +78,13 @@ def _args_handler(self, channel, webhook): if [self.default_webhook, webhook] == [None, None]: raise Exception('No webhook url provided!') - + # use defaults if not overridden if channel is None: channel = self.default_channel if webhook is None: webhook = self.default_webhook + if message is None: message = self.default_message - return channel, webhook + return channel, webhook, message @staticmethod def _attachment_formatter(attach): @@ -113,38 +118,38 @@ def attach(self, attach, **kwargs): """ return self.post(attach = attach, **kwargs) - def text(self, text, **kwargs): + def text(self, message, **kwargs): """ Send a message, without attachments. This is a wrapper around self.post """ - return self.post(text = text, **kwargs) + return self.post(message = message, **kwargs) - def post(self, text=None, attach=None, channel=None, webhook=None): + def post(self, message=None, attach=None, channel=None, webhook=None): """ Generic method to send a message to slack. Defaults may be overridden. The user may specify text or attachments. """ - # return if there is nothing to send - if [text, attach] == [None, None]: return None + # get channel and webhook and message + channel, webhook, message = self._args_handler(channel, webhook, message) - # get channel and webhook - channel, webhook = self._args_handler(channel, webhook) + # return if there is nothing to send + if [message, attach] == [None, None]: return None # recursively post to all channels in array of channels if isinstance(channel, list): status=[] for c in channel: - status.append(self.post(text=text, attach=attach, channel=c, url=webhook)) + status.append(self.post(message=message, attach=attach, channel=c, url=webhook)) return status # construct a payload payload = dict(channel = channel) - + # add text and attachments if provided - if text is not None: - payload['text'] = text + if message is not None: + payload['text'] = message if attach is not None: payload['attachments']= self._attachment_formatter(attach) @@ -158,3 +163,6 @@ def set_webhook(self, webhook): def set_channel(self, channel): self.default_channel = channel + + def set_message(self, message): + self.default_message = message From 52dfd0376985edc42f53775c75f55663a7112d14 Mon Sep 17 00:00:00 2001 From: nolan Date: Thu, 26 Apr 2018 21:59:09 -0400 Subject: [PATCH 12/71] make .post() strict about args; centralize argument inference --- python/jarjar/TODO.md | 1 - python/jarjar/jarjar.py | 397 ++++++++++++++++++++++++---------------- python/test-script.py | 4 + 3 files changed, 241 insertions(+), 161 deletions(-) create mode 100644 python/test-script.py diff --git a/python/jarjar/TODO.md b/python/jarjar/TODO.md index 2060b2b..4fc02ed 100644 --- a/python/jarjar/TODO.md +++ b/python/jarjar/TODO.md @@ -2,7 +2,6 @@ - Make a test suite and pass it with > Python 2.7, 3. - Figure out how to place the shell script into a bin -- Default message support because jeff uses it. - Documentation needs a major revamp. # WOULD BE NICE diff --git a/python/jarjar/jarjar.py b/python/jarjar/jarjar.py index c667b77..5449349 100644 --- a/python/jarjar/jarjar.py +++ b/python/jarjar/jarjar.py @@ -3,166 +3,243 @@ import time import os import imp +import warnings + +_no_message_warn = '''Slow down cowboy! You didn't provide a message and there is no default +in your .jarjar, so I'll just wing it.''' + class jarjar(): - def __init__(self, channel=None, webhook=None, message=None): - - # read config file, set defaults - self._read_config() - self._set_defaults(channel=channel, webhook=webhook, message=message) - - # headers for post request - self.headers = {'Content-Type': 'application/json'} - - def _set_defaults(self, channel=None, webhook=None, message=None): - """ - Set the default channel and webhook and message - This could be a little drier.... - """ - - # set default channel - if channel is None: - self.default_channel = self.cfg_channel - else: - self.default_channel = channel - - # same thing for webhook - if webhook is None: - self.default_webhook = self.cfg_webhook - else: - self.default_webhook = webhook - - # same thing for message - if message is None: - self.default_message = self.cfg_message - else: - self.default_message = message - - def _read_config(self): - """ - Read the .jarjar file for defaults. - """ - - # get .jarjar path - filename = os.path.join(os.path.expanduser('~'), '.jarjar') - - # make empty .jarjar if needed - if not os.path.exists(filename): - open(filename, 'a').close() - - # load config - cfg = imp.load_source('_jarjar', filename) - - # assign variables - for field in ['channel','webhook','message']: - - # read from config, or set to none - if hasattr(cfg, field): - data = getattr(cfg, field) - else: - data = None - - # set value - setattr(self, 'cfg_%s' % field, data) - - - def _args_handler(self, channel, webhook, message): - """ - Decide to use the default or provided arguments - """ - - # make sure channel and URL are _somewhere_ - if [self.default_channel, channel] == [None, None]: - raise Exception('No channel provided!') - - if [self.default_webhook, webhook] == [None, None]: - raise Exception('No webhook url provided!') - - # use defaults if not overridden - if channel is None: channel = self.default_channel - if webhook is None: webhook = self.default_webhook - if message is None: message = self.default_message - - return channel, webhook, message - - @staticmethod - def _attachment_formatter(attach): - """ - Convert a dict, fields, into a a correctly-formatted - attachment object for Slack. - """ - attachments = dict( - fallback = "New attachments are ready!", - color = "#36a64f", - ts = time.time(), - fields = [] - ) - - field_array = [] - for key in attach: - if isinstance(attach[key], str): outval = attach[key] - else: outval = str(attach[key]) - attachments['fields'].append(dict( - title = key, - value = outval, - short = len(outval) < 20 - )) - - return [attachments] - - def attach(self, attach, **kwargs): - """ - Send an attachment, without text. This is a wrapper around - self.post - """ - return self.post(attach = attach, **kwargs) - - def text(self, message, **kwargs): - """ - Send a message, without attachments. This is a wrapper around - self.post - """ - return self.post(message = message, **kwargs) - - def post(self, message=None, attach=None, channel=None, webhook=None): - """ - Generic method to send a message to slack. Defaults may be overridden. - The user may specify text or attachments. - """ - - # get channel and webhook and message - channel, webhook, message = self._args_handler(channel, webhook, message) - - # return if there is nothing to send - if [message, attach] == [None, None]: return None - - # recursively post to all channels in array of channels - if isinstance(channel, list): - status=[] - for c in channel: - status.append(self.post(message=message, attach=attach, channel=c, url=webhook)) - return status - - # construct a payload - payload = dict(channel = channel) - - # add text and attachments if provided - if message is not None: - payload['text'] = message - - if attach is not None: - payload['attachments']= self._attachment_formatter(attach) - - # convert payload to json and return - payload = json.dumps(payload) - return requests.post(webhook, data=payload, headers=self.headers) - - def set_webhook(self, webhook): - self.default_webhook = webhook - - def set_channel(self, channel): - self.default_channel = channel - - def set_message(self, message): - self.default_message = message + def __init__(self, channel=None, webhook=None, message=None): + + # read config file, set defaults + self._read_config() + self._set_defaults(channel=channel, webhook=webhook, message=message) + + # headers for post request + self.headers = {'Content-Type': 'application/json'} + + def _set_defaults(self, channel=None, webhook=None, message=None): + """Set the default channel and webhook and message.""" + # set default channel + if channel is None: + self.default_channel = self.cfg_channel + else: + self.default_channel = channel + + if webhook is None: + self.default_webhook = self.cfg_webhook + else: + self.default_webhook = webhook + + if message is None: + self.default_message = self.cfg_message + else: + self.default_message = message + + def _read_config(self): + """Read the .jarjar file for defaults.""" + # get .jarjar path + filename = os.path.join(os.path.expanduser('~'), '.jarjar') + + # make empty .jarjar if needed + if not os.path.exists(filename): + open(filename, 'a').close() + + # load config + cfg = imp.load_source('_jarjar', filename) + + # assign variables + for field in ['channel', 'webhook', 'message']: + + # read from config, or set to none + if hasattr(cfg, field): + data = getattr(cfg, field) + else: + data = None + + # set value + setattr(self, 'cfg_%s' % field, data) + + def _infer_kwargs(self, **kwargs): + """Infer kwargs for later method calls.""" + def _get(arg): + """Return provided arg if it exists. Otherwise, infer.""" + if arg in kwargs: + return kwargs[arg] + + # No support for default attach ATM. + if arg == 'attach': + return None + + # get a default + default = getattr(self, 'default_{}'.format(arg)) + + # return defaults for channel and webhook + if arg in ['channel', 'webhook']: + if not default: + raise Exception('No {} provided!'.format(arg)) + else: + return default + + # return default message if provided + if self.default_message is not None: + return self.default_message + + # no message is allowed if there is an attach + if 'attach' in kwargs and kwargs['attach'] is not None: + return None + + # otherwise use a super-default and warn the user. + warnings.warn(_no_message_warn) + return 'Meesa Jarjar Binks!' + + result = dict() + for arg in ['message', 'attach', 'channel', 'webhook']: + result[arg] = _get(arg) + return result + + @staticmethod + def _attachment_formatter(attach): + """Format a dict to become a slack attachment.""" + attachments = dict( + fallback="New attachments are ready!", + color="#36a64f", + ts=time.time(), + fields=[] + ) + + for key in attach: + + if isinstance(attach[key], str): + outval = attach[key] + else: + outval = str(attach[key]) + + attachments['fields'].append(dict( + title=key, + value=outval, + short=len(outval) < 20 + )) + + return [attachments] + + def attach(self, attach=None, **kwargs): + """Send an attachment. + + This is a convenience function which wraps around .post. Arguments + not explicitly provided are inferred. + + Arguments + + * message (str; optional): Text to send. If attach is None and there is no + default, jarjar just makes it up. + * attach (dict; optional): Attachment data. All values are converted to str. + * channel (str or list; optional): Name of the channel to post within. + Can also be a list of channel names; jarjar will post to each. + * webhook (str; optional): Webhook URL for the slack team. + + Returns a requests object for the POST request. + """ + if attach is None: + warnings.warn('You called `attach` but there is no attachment? Weird.') + kwargs = self._infer_kwargs(attach=attach, **kwargs) + return self.text(**kwargs) + + def text(self, text=None, **kwargs): + """Send a text message. + + This is a convenience function which wraps around .post. Arguments + not explicitly provided are inferred. + + Arguments + + * message (str; optional): Text to send. If attach is None and there is no + default, jarjar just makes it up. + * attach (dict; optional): Attachment data. All values are converted to str. + * channel (str or list; optional): Name of the channel to post within. + Can also be a list of channel names; jarjar will post to each. + * webhook (str; optional): Webhook URL for the slack team. + + Returns a requests object for the POST request. + """ + kwargs = self._infer_kwargs(text=text, **kwargs) + return self.post(**kwargs) + + def post(self, message=None, attach=None, channel=None, webhook=None): + """Send a message to slack. + + Arguments are not inferred and all must be provided. Use the `text` or + `attach` methods for argument inference. + + Arguments + + * message (str; optional): Text to send. Must be provided if attach is None. + * attach (dict; optional): Attachment data. All values are converted to str. + Must be provided if message is None. + * channel (str or list): Name of the channel to post within. Can also be a + list of channel names; jarjar will post to each. + * webhook (str; optional): Webhook URL for the slack team. + + Returns a requests object for the POST request. + """ + def _check_arg(arg, name, types, noneable=False): + """Ensure arguments are valid.""" + # NoneType handler + if arg is None: + if not noneable: + raise Exception('User did not provide kwarg `{}`.'.format(name)) + else: + return + + if not isinstance(arg, types): + raise Exception( + 'Kwarg `{0}` has invalid type. Options: ({1})' + .format(name, ','.join(map(str, types))) + ) + + # ensure message or attach is provided + if message is None and attach is None: + raise Exception('user must provide a message or attachment.') + + # check kwargs + _check_arg(message, 'message', (str,), noneable=True) + _check_arg(attach, 'attach', (dict,), noneable=True) + _check_arg(channel, 'channel', (str, list)) + _check_arg(webhook, 'webhook', (str,)) + + # recursively post to all channels in array of channels + if isinstance(channel, list): + status = [] + for c in channel: + status.append( + self.post(message=message, attach=attach, channel=c, url=webhook) + ) + return status + + # construct a payload + payload = dict(channel=channel) + + # add text and attachments if provided + if message is not None: + payload['text'] = message + + if attach is not None: + payload['attachments'] = self._attachment_formatter(attach) + + # convert payload to json and return + payload = json.dumps(payload) + return requests.post(webhook, data=payload, headers=self.headers) + + def set_webhook(self, webhook): + """Set default webhook.""" + self.default_webhook = webhook + + def set_channel(self, channel): + """Set default channel.""" + self.default_channel = channel + + def set_message(self, message): + """Set default message.""" + self.default_message = message diff --git a/python/test-script.py b/python/test-script.py new file mode 100644 index 0000000..a52da7f --- /dev/null +++ b/python/test-script.py @@ -0,0 +1,4 @@ +import jarjar + +jj = jarjar.jarjar() +jj.attach(dict(a=1)) From 441327a9e4c68aca44c900d820ce2dda918b0976 Mon Sep 17 00:00:00 2001 From: nolan Date: Sat, 28 Apr 2018 17:03:01 -0400 Subject: [PATCH 13/71] enable default message, exit status, elapsed time in CLT --- .gitignore | 1 + python/jarjar/__init__.py | 4 + python/jarjar/_screenutils/screen.py | 10 +- python/jarjar/jarjar | 288 ++++++++++++++++----------- python/jarjar/jarjar.py | 21 +- 5 files changed, 197 insertions(+), 127 deletions(-) diff --git a/.gitignore b/.gitignore index da5ec99..0a229dc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ mywebhook.txt .*.swp .DS_Store *.pyc +*.sublime-project test.py python/dist python/build diff --git a/python/jarjar/__init__.py b/python/jarjar/__init__.py index 6e3f57d..0e03f99 100644 --- a/python/jarjar/__init__.py +++ b/python/jarjar/__init__.py @@ -10,3 +10,7 @@ from jarjar import jarjar else: from jarjar.jarjar import jarjar + +__all__ = [ + 'jarjar' +] diff --git a/python/jarjar/_screenutils/screen.py b/python/jarjar/_screenutils/screen.py index f3582f4..149fd90 100755 --- a/python/jarjar/_screenutils/screen.py +++ b/python/jarjar/_screenutils/screen.py @@ -104,8 +104,15 @@ def send_commands(self, *commands): """Send commands to the active gnu-screen.""" self._check_exists() for command in commands: + + # use single quote unless that is a part of the command + if "'" in command: + q = "\"" + else: + q = "\'" + self._screen_commands( - 'stuff \"{0}\"'.format(command), + 'stuff {q}{c}{q}'.format(q=q, c=command), 'eval "stuff \\015"' ) @@ -118,7 +125,6 @@ def _screen_commands(self, *commands): self._check_exists() for command in commands: cmd = 'screen -x {0} -p 0 -X {1}'.format(self.name, command) - # print(cmd) system(cmd) sleep(0.02) diff --git a/python/jarjar/jarjar b/python/jarjar/jarjar index b5fa529..e8c2ee3 100755 --- a/python/jarjar/jarjar +++ b/python/jarjar/jarjar @@ -12,61 +12,62 @@ from _screenutils import Screen import os import sys import argparse +import warnings PARSER = argparse.ArgumentParser() PARSER.add_argument( - 'program', - help="Set task to run in a screen.", - nargs='*', - default=[] + 'program', + help="Set task to run in a screen.", + nargs='*', + default=[] ) PARSER.add_argument( - "-m", - "--message", - help="Message to send.", - nargs='*', - default=[] + "-m", + "--message", + help="Message to send.", + nargs='*', + default=[] ) PARSER.add_argument( - "-w", - "--webhook", - help="Set webhook target.", - default=None + "-w", + "--webhook", + help="Set webhook target.", + default=None ) PARSER.add_argument( - "-c", "-u", - "--channel", - "--user", - help="Set user/channel target.", - default=None + "-c", "-u", + "--channel", + "--user", + help="Set user/channel target.", + default=None ) PARSER.add_argument( - "-a", "-r", - "--attach", - help="Attach to screen.", - default=False, - action='store_true' + "-a", "-r", + "--attach", + help="Attach to screen.", + default=False, + action='store_true' ) PARSER.add_argument( - "-S", - "--screen_name", - dest='screen_name', - help="Set the name of the screen. Default to first 10 characters of the task", - default=None + "-S", + "--screen_name", + dest='screen_name', + help="Set the name of the screen. Default to first 10 chars of the task", + default=None ) PARSER.add_argument( - "-e", - dest='echo', - help=argparse.SUPPRESS, - default=False, - action='store_true' + "-e", + dest='echo', + help=argparse.SUPPRESS, + default=False, + action='store_true' ) # get arguments from parser @@ -74,115 +75,164 @@ ARGS, UNKNOWN = PARSER.parse_known_args() # warns / checks if UNKNOWN: - print('Warn: ignoring unknown arguments `{}`.'.format(' '.join(UNKNOWN))) - -if not ARGS.message and not ARGS.program: - PARSER.print_help(sys.stderr) - print('\nProvide a message or some task too complete!') - sys.exit(1) + warnings.warn( + 'Ignoring unknown arguments `{}`.'.format(' '.join(UNKNOWN)) + ) if ARGS.echo: - print('Psst-- `-e` is no longer needed!') + print('Psst-- `-e` is no longer needed!') if not ARGS.program: - if ARGS.attach: - print('Note: You asked to attach but there is no task to run.') + if ARGS.attach: + warnings.warn('Note: You asked to attach but there is no task to run.') - if ARGS.screen_name is not None: - print('Note: You named a screen but there is no task to run.') + if ARGS.screen_name is not None: + warnings.warn('Note: You named a screen but there is no task to run.') def _append_to_name(name): - """Append an `__{integer}` to a name or add to it.""" - - suffix = '__' - appended = suffix in name and name.split(suffix)[-1].isdigit() - if not appended: - return name + suffix + '1' - - parts = name.split(suffix) - stem = suffix.join(parts[:-1]) - return stem + suffix + str(int(parts[-1]) + 1) - - -def _make_jarjar_shell(m=None, c=None, w=None): - """Construct a jarjar shell command.""" - - def make_flag(k, v): - """Ignore flag if None, otherwise wrap in single quotes.""" - if v is None: - return [] - else: - return [k, '\'{}\''.format(v)] + """Append an `__{integer}` to a name or add to it.""" - # start out the command - cmd = ['/usr/bin/env python {}'.format(os.path.realpath(__file__))] + suffix = '__' + appended = suffix in name and name.split(suffix)[-1].isdigit() + if not appended: + return name + suffix + '1' - # exit status default message if needed - if not m: - m = '\'Your task has completed.\'' - cmd += ['--message', m] - else: - cmd += make_flag('--message', m) - #if m: - # cmd += make_flag('--message', m) + parts = name.split(suffix) + stem = suffix.join(parts[:-1]) + return stem + suffix + str(int(parts[-1]) + 1) - cmd += make_flag('--channel', c) - cmd += make_flag('--webhook', w) - return ' '.join(cmd) +def _fmt_time(seconds): + """Convert a seconds integer into a formatted time string.""" + if seconds < 30: + return('{0:d} seconds'.format(seconds)) + elif seconds < 1800: # 30 mins + return('{0:0.1f} minutes'.format(seconds / 60.0)) + else: # hours + return('{0:0.1f} hours'.format(seconds / 60.0**2)) -def main(): - # parse args - MESSAGE = ' '.join(ARGS.message) - WEBHOOK = ARGS.webhook - CHANNEL = ARGS.channel - PROGRAM = ' '.join(ARGS.program) - ATTACH = ARGS.attach - if ARGS.screen_name is None: - if PROGRAM: - SCREEN_NAME = '_'.join(ARGS.program).replace(' ', '_') - SCREEN_NAME = SCREEN_NAME[:10] - else: - SCREEN_NAME = None - else: - SCREEN_NAME = ARGS.screen_name +def _make_jarjar_shell(m=None, c=None, w=None): + """Construct a jarjar shell command.""" - # if there is no program, then we're done here... - if not PROGRAM: - jarjar(channel=CHANNEL, webhook=WEBHOOK).text(MESSAGE) - return + def make_flag(k, v): + """Ignore flag if None, otherwise wrap in single quotes.""" + if v is None: + return [] + else: + return [k, '\'{}\''.format(v)] - # make jarjar shell command - NOTIFY = _make_jarjar_shell(m=MESSAGE, w=WEBHOOK, c=CHANNEL) + # start out the command + cmd = ['/usr/bin/env python {}'.format(os.path.realpath(__file__))] - # make sure screen is unique - # --- raise error for manual names. - # --- append suffix for auto names. - screen = Screen(SCREEN_NAME, initialize=False) - if screen.exists: - if ARGS.screen_name is not None: - raise Exception('There is already a screen by that name!') + # add message if provided + if m: + cmd += make_flag('--message', m) - while screen.exists: - SCREEN_NAME = _append_to_name(SCREEN_NAME) - screen = Screen(SCREEN_NAME) + cmd += make_flag('--channel', c) + cmd += make_flag('--webhook', w) + return ' '.join(cmd) - # notify user of the screen name if not provided. - print('Creating screen: `{0}`.'.format(SCREEN_NAME)) - # spin up the screen and run through the task - screen.initialize() +def _make_screen_name(command): + return ( + '_'.join(command) + .replace(' ', '_') + .replace(';', '_') + )[:10] - screen.send_commands(PROGRAM + '; ' + NOTIFY) - # attach if needed - if ATTACH: - os.system('screen -r {}'.format(SCREEN_NAME)) +def main(): - sys.exit(0) + # parse args + MESSAGE = ' '.join(ARGS.message) + WEBHOOK = ARGS.webhook + CHANNEL = ARGS.channel + PROGRAM = ' '.join(ARGS.program) + ATTACH = ARGS.attach + if ARGS.screen_name is None: + if PROGRAM: + SCREEN_NAME = _make_screen_name(ARGS.program) + else: + SCREEN_NAME = None + else: + SCREEN_NAME = ARGS.screen_name + + # if there is no program, then we're done here... + if not PROGRAM: + + # get env variables + UNIX_START = os.environ.get('JJ_UNIX_START') + UNIX_END = os.environ.get('JJ_UNIX_END') + EXIT = os.environ.get('JARJAR_EXIT') + if not MESSAGE: + MESSAGE = None + + # if vars are defined and there is no message, use them as the default + if None not in (UNIX_START, UNIX_END, EXIT) and not MESSAGE: + SECONDS = int(UNIX_END) - int(UNIX_START) + MESSAGE = ( + 'Process exited in {0} with status {1}.' + .format(_fmt_time(SECONDS), EXIT) + ) + + # send the notification + jarjar(channel=CHANNEL, webhook=WEBHOOK).text(MESSAGE) + return + + # make jarjar shell command + NOTIFY = _make_jarjar_shell(m=MESSAGE, w=WEBHOOK, c=CHANNEL) + + # make sure screen is unique + # --- raise error for manual names. + # --- append suffix for auto names. + screen = Screen(SCREEN_NAME, initialize=False) + if screen.exists: + if ARGS.screen_name is not None: + raise Exception('There is already a screen by that name!') + + while screen.exists: + SCREEN_NAME = _append_to_name(SCREEN_NAME) + screen = Screen(SCREEN_NAME, initialize=False) + + # notify user of the screen name if not provided. + if ARGS.screen_name is None: + print('Creating screen: `{0}`.'.format(SCREEN_NAME)) + + # spin up the screen and run through the task + screen.initialize() + + # ------------ + # Run commands + + # hide from history + screen.send_commands('unset HISTFILE') + + # capture start time + screen.send_commands('''export JJ_UNIX_START=$(date -u +%s);''') + + # run command + screen.send_commands(PROGRAM) + + # capture exit time and status + screen.send_commands('''export JARJAR_EXIT=$?;''') + screen.send_commands('''export JJ_UNIX_END=$(date -u +%s);''') + + # send notification + screen.send_commands(NOTIFY) + + # unset vars + screen.send_commands('unset JJ_UNIX_START') + screen.send_commands('unset JJ_UNIX_END') + screen.send_commands('unset JARJAR_EXIT') + + # attach if needed + if ATTACH: + os.system('screen -r {}'.format(SCREEN_NAME)) + + sys.exit(0) if __name__ == '__main__': - main() + main() diff --git a/python/jarjar/jarjar.py b/python/jarjar/jarjar.py index 5449349..8f09ca2 100644 --- a/python/jarjar/jarjar.py +++ b/python/jarjar/jarjar.py @@ -5,8 +5,17 @@ import imp import warnings -_no_message_warn = '''Slow down cowboy! You didn't provide a message and there is no default -in your .jarjar, so I'll just wing it.''' +# a warning for if the defaultiest message is used +_no_message_warn = ( + ''' + Slow down cowboy! You didn't provide a message and there is + no default in your .jarjar, so I'll just wing it. + ''' + .strip() + .replace('\n', ' ') + .replace('\t', ' ') + .replace(' ', ' ') +) class jarjar(): @@ -66,7 +75,7 @@ def _infer_kwargs(self, **kwargs): """Infer kwargs for later method calls.""" def _get(arg): """Return provided arg if it exists. Otherwise, infer.""" - if arg in kwargs: + if arg in kwargs and kwargs[arg]: return kwargs[arg] # No support for default attach ATM. @@ -88,7 +97,7 @@ def _get(arg): return self.default_message # no message is allowed if there is an attach - if 'attach' in kwargs and kwargs['attach'] is not None: + if 'attach' in kwargs and kwargs['attach']: return None # otherwise use a super-default and warn the user. @@ -147,7 +156,7 @@ def attach(self, attach=None, **kwargs): kwargs = self._infer_kwargs(attach=attach, **kwargs) return self.text(**kwargs) - def text(self, text=None, **kwargs): + def text(self, message=None, **kwargs): """Send a text message. This is a convenience function which wraps around .post. Arguments @@ -164,7 +173,7 @@ def text(self, text=None, **kwargs): Returns a requests object for the POST request. """ - kwargs = self._infer_kwargs(text=text, **kwargs) + kwargs = self._infer_kwargs(message=message, **kwargs) return self.post(**kwargs) def post(self, message=None, attach=None, channel=None, webhook=None): From 660397b95ab8a55e620b8b76e7f3a79f044fee67 Mon Sep 17 00:00:00 2001 From: nolan Date: Mon, 30 Apr 2018 19:57:40 -0400 Subject: [PATCH 14/71] draft testing script --- .../PKG-INFO | 0 .../SOURCES.txt | 0 .../dependency_links.txt | 0 .../requires.txt | 0 .../top_level.txt | 0 {python/jarjar => jarjar}/TODO.md | 0 {python/jarjar => jarjar}/__init__.py | 0 .../_screenutils/__init__.py | 0 .../jarjar => jarjar}/_screenutils/errors.py | 0 .../jarjar => jarjar}/_screenutils/screen.py | 0 {python/jarjar => jarjar}/jarjar | 0 {python/jarjar => jarjar}/jarjar.py | 27 ++-- python/license.txt => license.txt | 0 python/test-script.py | 4 - python/setup.cfg => setup.cfg | 0 python/setup.py => setup.py | 0 sh/.jarjar | 4 - sh/jarjar | 61 --------- test-script.py | 117 ++++++++++++++++++ 19 files changed, 135 insertions(+), 78 deletions(-) rename {python/jarjar.egg-info => jarjar.egg-info}/PKG-INFO (100%) rename {python/jarjar.egg-info => jarjar.egg-info}/SOURCES.txt (100%) rename {python/jarjar.egg-info => jarjar.egg-info}/dependency_links.txt (100%) rename {python/jarjar.egg-info => jarjar.egg-info}/requires.txt (100%) rename {python/jarjar.egg-info => jarjar.egg-info}/top_level.txt (100%) rename {python/jarjar => jarjar}/TODO.md (100%) rename {python/jarjar => jarjar}/__init__.py (100%) rename {python/jarjar => jarjar}/_screenutils/__init__.py (100%) rename {python/jarjar => jarjar}/_screenutils/errors.py (100%) rename {python/jarjar => jarjar}/_screenutils/screen.py (100%) rename {python/jarjar => jarjar}/jarjar (100%) rename {python/jarjar => jarjar}/jarjar.py (91%) rename python/license.txt => license.txt (100%) delete mode 100644 python/test-script.py rename python/setup.cfg => setup.cfg (100%) rename python/setup.py => setup.py (100%) delete mode 100644 sh/.jarjar delete mode 100644 sh/jarjar create mode 100644 test-script.py diff --git a/python/jarjar.egg-info/PKG-INFO b/jarjar.egg-info/PKG-INFO similarity index 100% rename from python/jarjar.egg-info/PKG-INFO rename to jarjar.egg-info/PKG-INFO diff --git a/python/jarjar.egg-info/SOURCES.txt b/jarjar.egg-info/SOURCES.txt similarity index 100% rename from python/jarjar.egg-info/SOURCES.txt rename to jarjar.egg-info/SOURCES.txt diff --git a/python/jarjar.egg-info/dependency_links.txt b/jarjar.egg-info/dependency_links.txt similarity index 100% rename from python/jarjar.egg-info/dependency_links.txt rename to jarjar.egg-info/dependency_links.txt diff --git a/python/jarjar.egg-info/requires.txt b/jarjar.egg-info/requires.txt similarity index 100% rename from python/jarjar.egg-info/requires.txt rename to jarjar.egg-info/requires.txt diff --git a/python/jarjar.egg-info/top_level.txt b/jarjar.egg-info/top_level.txt similarity index 100% rename from python/jarjar.egg-info/top_level.txt rename to jarjar.egg-info/top_level.txt diff --git a/python/jarjar/TODO.md b/jarjar/TODO.md similarity index 100% rename from python/jarjar/TODO.md rename to jarjar/TODO.md diff --git a/python/jarjar/__init__.py b/jarjar/__init__.py similarity index 100% rename from python/jarjar/__init__.py rename to jarjar/__init__.py diff --git a/python/jarjar/_screenutils/__init__.py b/jarjar/_screenutils/__init__.py similarity index 100% rename from python/jarjar/_screenutils/__init__.py rename to jarjar/_screenutils/__init__.py diff --git a/python/jarjar/_screenutils/errors.py b/jarjar/_screenutils/errors.py similarity index 100% rename from python/jarjar/_screenutils/errors.py rename to jarjar/_screenutils/errors.py diff --git a/python/jarjar/_screenutils/screen.py b/jarjar/_screenutils/screen.py similarity index 100% rename from python/jarjar/_screenutils/screen.py rename to jarjar/_screenutils/screen.py diff --git a/python/jarjar/jarjar b/jarjar/jarjar similarity index 100% rename from python/jarjar/jarjar rename to jarjar/jarjar diff --git a/python/jarjar/jarjar.py b/jarjar/jarjar.py similarity index 91% rename from python/jarjar/jarjar.py rename to jarjar/jarjar.py index 8f09ca2..617a1e8 100644 --- a/python/jarjar/jarjar.py +++ b/jarjar/jarjar.py @@ -20,6 +20,9 @@ class jarjar(): + _expected_kwargs = ['message', 'attach', 'channel', 'webhook'] + _final_default_message = 'Meesa Jarjar Binks!' + def __init__(self, channel=None, webhook=None, message=None): # read config file, set defaults @@ -32,17 +35,17 @@ def __init__(self, channel=None, webhook=None, message=None): def _set_defaults(self, channel=None, webhook=None, message=None): """Set the default channel and webhook and message.""" # set default channel - if channel is None: + if channel in (None, ''): self.default_channel = self.cfg_channel else: self.default_channel = channel - if webhook is None: + if webhook in (None, ''): self.default_webhook = self.cfg_webhook else: self.default_webhook = webhook - if message is None: + if message in (None, ''): self.default_message = self.cfg_message else: self.default_message = message @@ -75,7 +78,7 @@ def _infer_kwargs(self, **kwargs): """Infer kwargs for later method calls.""" def _get(arg): """Return provided arg if it exists. Otherwise, infer.""" - if arg in kwargs and kwargs[arg]: + if arg in kwargs and kwargs[arg] not in ('', None): return kwargs[arg] # No support for default attach ATM. @@ -88,7 +91,7 @@ def _get(arg): # return defaults for channel and webhook if arg in ['channel', 'webhook']: if not default: - raise Exception('No {} provided!'.format(arg)) + raise NameError('No {} provided!'.format(arg)) else: return default @@ -102,7 +105,13 @@ def _get(arg): # otherwise use a super-default and warn the user. warnings.warn(_no_message_warn) - return 'Meesa Jarjar Binks!' + return self._final_default_message + + # check unexpected args + for k in kwargs.keys(): + if k in self._expected_kwargs: + continue + warnings.warn('Recieved unexpected kwarg: `%s`.' % k) result = dict() for arg in ['message', 'attach', 'channel', 'webhook']: @@ -198,19 +207,19 @@ def _check_arg(arg, name, types, noneable=False): # NoneType handler if arg is None: if not noneable: - raise Exception('User did not provide kwarg `{}`.'.format(name)) + raise NameError('User did not provide kwarg `{}`.'.format(name)) else: return if not isinstance(arg, types): - raise Exception( + raise TypeError( 'Kwarg `{0}` has invalid type. Options: ({1})' .format(name, ','.join(map(str, types))) ) # ensure message or attach is provided if message is None and attach is None: - raise Exception('user must provide a message or attachment.') + raise NameError('user must provide a message or attachment.') # check kwargs _check_arg(message, 'message', (str,), noneable=True) diff --git a/python/license.txt b/license.txt similarity index 100% rename from python/license.txt rename to license.txt diff --git a/python/test-script.py b/python/test-script.py deleted file mode 100644 index a52da7f..0000000 --- a/python/test-script.py +++ /dev/null @@ -1,4 +0,0 @@ -import jarjar - -jj = jarjar.jarjar() -jj.attach(dict(a=1)) diff --git a/python/setup.cfg b/setup.cfg similarity index 100% rename from python/setup.cfg rename to setup.cfg diff --git a/python/setup.py b/setup.py similarity index 100% rename from python/setup.py rename to setup.py diff --git a/sh/.jarjar b/sh/.jarjar deleted file mode 100644 index ef822db..0000000 --- a/sh/.jarjar +++ /dev/null @@ -1,4 +0,0 @@ -# uncomment the lines that you would like to configure -#channel="@my_username" # or "#general" -#message="Hi! I am jarjar." -#webhook="your-webhook-here" diff --git a/sh/jarjar b/sh/jarjar deleted file mode 100644 index cad6667..0000000 --- a/sh/jarjar +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# example usage: ./jarjar [options] python