diff --git a/.gitignore b/.gitignore index da5ec99..5a9a0b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,13 @@ mywebhook.txt .*.swp .DS_Store *.pyc +*.sublime-project test.py -python/dist -python/build -python/jarjar.egg-info -python/update-pip.sh +update-pip.sh +dist/ +build/ +jarjar.egg-info/ +venv*/ +install.log +jarjar-shell +build/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0d9c2c8 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = jarjar +SOURCEDIR = docs +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/README.md b/README.md index 7a8613b..44f82fd 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,89 @@ # Jarjar Slack Notifier -Jarjar is a collection of scripts that lets you programmatically send notifications to your Slack team. +Jarjar is a python utility that makes it easy to send slack notifications to your teams. You can import it as a python module or use our command line tool. -## What can jarjar do for me? +[![Documentation Status](https://readthedocs.org/projects/jarjar/badge/?version=latest)](https://jarjar.readthedocs.io/en/latest/?badge=latest) [![PyPI version](https://badge.fury.io/py/jarjar.svg)](https://badge.fury.io/py/jarjar) -Here are some things _we_ use it for: -- Send reminders to yourself/group. -- Notify users when their jobs (simulations, backups, etc) are completed. -- Combine jarjar with cron to send out [daily positive vibes](http://i.imgur.com/YkqMwCx.png). +## What can jarjar do for me? +Jarjar was developed at the [Austerweil Lab at UW-Madison](http://alab.psych.wisc.edu/) as a tool for scientists. We use it for all sorts of things, such as: -## But How? +1. Sending a message so that we know when long-running processes have finished. -We have designed two interfaces into jarjar: a shell command and a Python Module. +![](docs/img/simulations-complete.png) -# The Shell Command +2. Sending notices when scheduled tasks have failed. -The `sh/` directory contains a shell command `jarjar` and a configuration file `.jarjar`. +![](docs/img/backups-failed.png) -Fill the configuration file out with useful defaults. Critically, you'll want to paste in your Slack webhook so that jarjar knows where to send the message. Then put the configuration file in your home directory (`~/`), that's where jarjar will look for it. Don't worry, you can override those defaults later. Here's a sample `.jarjar`: +3. Sending out daily positive vibes. -```sh -channel="@my_username" # or "#general" -message="Hi! Meesa Jar Jar Binks." -webhook="your-webhook-here" -``` - -Then, add the `jarjar` _script_ [to your path](https://stackoverflow.com/questions/20054538/add-a-bash-script-to-path). After that, you can use it like so: - -```sh -# echo the default message to the default channel -jarjar -e +![](docs/img/positive-vibes.png) -# echo a message to the #general channel -jarjar -e -u "#general" -m "Hi, everyone!!" +## Quickstart -# Send yourself a notification when a script is completed -jarjar -u @username -m "Your job is finished!" python my-script.py -jarjar ./my-script.sh +[Installation](docs/install.md) is simple! -# send a message to the non-default slack team -jarjar -e -u @username -m "Hi!" -w "their-webhook-url" +```shell +pip install jarjar ``` -## Modifiers +My guess is that you'll want to create jarjar's config file, `~/.jarjar`. This tells jarjar what you'd like to use as a default for your slack team's webhook, the channel to post to, and the message it sends. Don't worry, you can over-ride these anytime. -| Modifier | Description | -| --- | --- | -| `-e` | Echo the message. If this flag is not included, jarjar waits until a provided process is completed to send the message. By default (without the `-e` flag), jarjar launches a screen with your script (which terminates when your script ends). You can always resume a screen launched by jarjar by finding the appropriate PID: `screen -ls` and `screen -r PID`. | -| `-r` | Attaches screen created by jarjar (when `-e` is not used) | -| `-m` | Message to be sent | -| `-u` | Username (or channel). Usernames must begin with `@`, channels with `#`. | -| `-w` | Webhook for the Slack team. | +Edit this snippet and add it to `~/.jarjar`: -# The Python Module - -This module implements jarjar's functionality more fluidly within Python scripts. Importing the jarjar module provides a simple class, which can be used to send messages like the shell command. - -Installation is simple: - -1. `pip install jarjar` -2. _Optional_: create a `~/.jarjar` file with some defaults (this is shared with the shell command). - -```sh -channel="@my_username" # or "#general" -message="Hi! Meesa Jar Jar Binks." -webhook="your-webhook-here" +```shell +channel='@username' +message='Custom message' +webhook='https://hooks.slack.com/services/your/teams/webhook' ``` +If you don't know your team's webhook, you might have to [make one](https://api.slack.com/incoming-webhooks) + +### Python API -Then, you're good to go! You can use it as follows: +Use the jarjar python api like: ```python from jarjar import jarjar -# initialize with defaults from .jarjar -jj = jarjar() - -# initialize with custom defaults -jj = jarjar(channel='#channel', webhook='slack-webhook-url') - -# initialization is not picky -- provide one or both arguments -jj = jarjar(webhook = 'slack-webhook-url') +# initialize a jarjar object +jj = jarjar() # defaults from .jarjar +jj = jarjar(channel='#channel', webhook='slack-webhook-url') +jj = jarjar(webhook='slack-webhook-url') # send a text message -jj.text('Hi!') - -# send a message to multiple channels or users -jj.text('Hi!', channel=["@jeffzemla","#channel"]) +jj.text('Hi!') +jj.text('Hi!', channel=["@jeffzemla", "#channel"]) # send an attachment -jj.attach(dict(status='it\'s all good')) - -# send both -jj.post(text='Hi', attach=dict(status='it\'s all good')) - -# override defaults after initializing -jj.attach(dict(status='it\'s all good'), channel = '@jeffzemla') -jj.text('Hi!', channel = '@nolan', webhook = 'another-webhook') +jj.attach({'meesa': 'jarjar binks'}), text='Hello!') ``` -## Methods - -### `text` - -> `jj.text(text, **kwargs)` +### Command Line Tool -Send a text message, specified by a string, `text`. User may optionally supply the channel and webhook in the `kwargs`. +We also made a [command line tool](docs/clt.md) for use outside of python scripts. The command line tool adds functionality to execute processes and send messages when they are complete. -### `attach` - -> `jj.attach(attach, **kwargs)` - -Send attachments, specified by values in a dict, `attach`. User may optionally supply the channel and webhook in the `kwargs`. - -### `post` +```shell +jarjar sleep 1 -m 'Meesa took a nap!' +``` -> `jj.post(text=None, attach=None, channel=None, webhook=None)` +And then in your slack team: -The generic post method. `jj.text(...)` and `jj.attach(...)` are simply convenience functions wrapped around this method. User may supply text and/or attachments, and may override the default channel and webhook url. +![](docs/img/nap.png) +Custom attachments are not supported in the CLT at this time, but everything else is: -# How to configure a Slack Webhook +```sh +jarjar -m 'Meesa jarjar binks!' +jarjar -m 'Hi, everyone!!' --webhook '' -c '#general' +``` -You'll need to configure [Incoming Webhooks](https://api.slack.com/incoming-webhooks) for your Slack team. You need to specify a default channel (which jarjar overrides), and Slack will give you a webhook url. That's it! +## Documentation -When you're setting things up, you can also specify a custom name and custom icon. We named our webhook robot `jar-jar`, and we used [this icon](http://i.imgur.com/hTHrg6i.png), so messages look like this: +We're on [Read The Docs](http://jarjar.readthedocs.io/en/latest/)! -![](http://i.imgur.com/g9RG16j.png) +## Having Trouble? Or a feature request? +We are terrible developers and you'll probably run into all sorts of problems. Don't be shy, [file an issue on github](https://github.com/AusterweilLab/jarjar/issues/new)! diff --git a/bin/jarjar b/bin/jarjar new file mode 100755 index 0000000..aa5a9ac --- /dev/null +++ b/bin/jarjar @@ -0,0 +1,313 @@ +#! /usr/bin/env python + +from jarjar import jarjar +from jarjar import __version__ +from jarjar import Screen +import os +import sys +import argparse +import datetime +import warnings + +PARSER = argparse.ArgumentParser(usage='%(prog)s [program] [options]') + +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( + "-v", + "--version", + help="List jarjar version.", + default=False, + action='store_true' +) + +PARSER.add_argument( + "-w", + "--webhook", + help="Set webhook target.", + default=None +) + +PARSER.add_argument( + "-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' +) + +PARSER.add_argument( + "-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( + "--no-exit", + dest='noexit', + default=False, + help="Do not exit the task screen even if successful.", + action='store_true' +) + +PARSER.add_argument( + "--no-jarjar", + dest='nojarjar', + help="Do not send a Slack message when the task completes", + default=False, + action='store_true' +) + +PARSER.add_argument( + "-e", + dest='echo', + help=argparse.SUPPRESS, + default=False, + action='store_true' +) + +# get arguments from parser +ARGS, UNKNOWN = PARSER.parse_known_args() + +# warns / checks +if UNKNOWN: + warnings.warn( + 'Ignoring unknown arguments `{}`.'.format(' '.join(UNKNOWN)) + ) + +if ARGS.echo: + print('Psst-- `-e` is no longer needed!') + +if not ARGS.program: + if ARGS.attach: + warnings.warn('You asked to attach but there is no task to run.') + + if ARGS.screen_name is not None: + warnings.warn('You named a screen but there is no task to run.') + + if ARGS.noexit: + warnings.warn('You called `--no-exit` but there is no task to run.') + + if ARGS.nojarjar: + warnings.warn('You called `--no-jarjar` but there is no task to run.') + +# check screen is availible +if ARGS.program and os.system('which screen') != 0: + raise RuntimeError( + 'Jarjar cannot find `screen`! ' + + 'Make sure it is on your path and try again.' + ) + +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 _fmt_time(seconds): + """Convert a seconds integer into a formatted time string.""" + return ( + str(datetime.timedelta(seconds=seconds)) + ) + + +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 = [ + '{executable} {jarjarfile}' + .format( + executable=sys.executable, + jarjarfile=os.path.realpath(__file__) + ) + ] + + # add message if provided + if m: + cmd += make_flag('--message', m) + + cmd += make_flag('--channel', c) + cmd += make_flag('--webhook', w) + return ' '.join(cmd) + + +def _make_screen_name(command): + return ( + '_'.join(command) + .replace(' ', '_') + .replace(';', '_') + )[:10] + + +def main(): + + # output version if requested + if ARGS.version: + print("jarjar v %s" % __version__) + sys.exit() + + # parse args + MESSAGE = ' '.join(ARGS.message) + WEBHOOK = ARGS.webhook + CHANNEL = ARGS.channel + PROGRAM = ' '.join(ARGS.program) + ATTACH = ARGS.attach + NOEXIT = ARGS.noexit + 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 + + # confirm channel and webhook exist + jj = jarjar() # dummy jj to check defaults + if not (WEBHOOK or jj.default_webhook): + raise NameError( + 'No webhook to post to. Set up your `.jarjar` or add a webhook!' + ) + + if not (CHANNEL or jj.default_channel): + raise NameError( + 'No channel to post to. Set up your `.jarjar` or add a channel!' + ) + + # 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): + SECONDS = int(UNIX_END) - int(UNIX_START) + ATTACHMENT = { + 'Exit Status': EXIT, + 'Time Elapsed': _fmt_time(SECONDS) + } + else: + ATTACHMENT = None + + # send the notification + jj = jarjar(channel=CHANNEL, webhook=WEBHOOK) + if EXIT is not None and int(EXIT) > 0: + jj.attachment_args['color'] = "danger" + + RESPONSE = jj.text(MESSAGE, attach=ATTACHMENT) + + if RESPONSE.status_code != 200: + warnings.warn( + 'Response code {0}: {1}' + .format(RESPONSE.status_code, RESPONSE.text) + ) + + 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 a screen is launched + 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 + if not ARGS.nojarjar: + screen.send_commands(NOTIFY) + + # send exit command if not no-exit or attach + if not (NOEXIT or ATTACH): + # some versions of screen require the variable to be escaped; this conditional will test + # whether variables need to be escaped and then exit if $JARJAR_EXIT equals 0 + screen.send_commands( + '''if [ -z $JARJAR_EXIT ]; + then if [ \$JARJAR_EXIT -eq 0 ] ; + then exit; + fi; + else if [ $JARJAR_EXIT -eq 0 ] ; + then exit; + fi; + fi;''' + ) + + # attach if needed + if ATTACH: + os.system('screen -r {}'.format(SCREEN_NAME)) + + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/docs/_test.py b/docs/_test.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..b5f2627 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,9 @@ +API Documentation +================= + +.. toctree:: + +.. automodule:: jarjar + +.. autoclass:: jarjar + :members: text, attach, set_webhook, set_channel, set_message \ No newline at end of file diff --git a/docs/clt.md b/docs/clt.md new file mode 100644 index 0000000..8d9b0df --- /dev/null +++ b/docs/clt.md @@ -0,0 +1,76 @@ +# Using the jarjar command line tool + +The CLT provides basic posting functionality like in the python API but it also provides a useful task execution facility. + +## Posting to your team + +The jarjar CLT offers all the functionality of the python API, except for posting attachments (sorry). Posting messages is super easy though! + +You can use your defaults from .jarjar +```sh +jarjar --message 'Meesa jarjar binks!' +``` + +Or not. + +```sh +jarjar -m 'Hi, everyone!!' --webhook '' --channel '#general' +``` + +## Running processes with jarjar + +We use jarjar to run a lot of longer processes when we don't want to keep our terminal sessions around. You can use jarjar for this sort of thing. + +```sh +jarjar sleep 3600 +``` + +Generally speaking it is safer to wrap your program in quotes so that its clear which arguments are meant for jarjar and which are meant for your task. + +```sh +jarjar 'python3 simulations.py --niters 100 --out results.csv' +``` + +Now you can head out for some lunch. Here's what's going on under the hood: + +1. **Start up a [screen](https://www.gnu.org/software/screen/)**. The screen can have a custom name (using the `-S` or `--screen_name` flags) but if you don't provide one it'll be named using the program you provide. Above, the screen was named `sleep_3600`. +2. **Run your process in that screen**. If you want you can attach to the screen (using the `-a`, `-r`, or `--attach` flags) and see the magic happen. +3. **Send a message when the process is complete**. If you specified a message (using the `-m` or `--message` flags) jarjar will send it. Jarjar will then kill your screen if: + * You don't tell it to keep the screen (using the `--no-exit` flag). + * You didn't attach to it (using the `-a`, `-r`, or `--attach` flags). + * The program you ran exited with status 0. + +### Examples + +```sh +# send a custom message +jarjar python run-simulations.py --message 'Simulations Complete!' + +# name your screen +jarjar sleep 1 --screen-name 'snooze' + +# watch the magic happen +jarjar --attach + +# keep the screen around for debugging +jarjar --no-exit + +# show jarjar version +jarjar --version + +# get help +jarjar --help +``` + +## Argument Reference + +- `-h`, `--help`. Show help message. +- `-v`, `--version`. Show jarjar version. +- `-m`, `--message`. Specify message to send. This best done in single-quotes (`jarjar -m 'hi'`) but jarjar rolls with the punches (like `jarjar -m hi`). +- `-w`, `--webhook`. Specify webhook to post to. +- `-c`, `--channel`. Specify channel to post to. Unlike in the python module, only one channel can be supplied at a time. Since `#` is interpreted as a shell comment, you'll want to put this in single quotes (`jarjar -c '#general'`). +- `-a`, `-r`, `--attach`. Attach to the screen once the program has started running. If you didn't provide a program jarjar will think you are weird. +- `-S`, `--screen_name`. Specify the name of the screen created for the program. If you didn't provide a program jarjar will think you are weird. +- `--no-exit`. Don't exit the screen even if the program exited successfully. If you didn't provide a program jarjar will think you are weird. +- `--no-jarjar`. Run a program but don't send a slack message about it. In this case jarjar is just acting as a screen generator. If you didn't provide a program jarjar will think you are weird. + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..96df57e --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert( + 0, + os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] +) + + +# -- Project information ----------------------------------------------------- + +project = 'jarjar' +copyright = '2018, The Austerweil Lab at UW-Madison' +author = 'The Austerweil Lab at UW-Madison' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '3.0' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'numpydoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] +source_parsers = { + '.md': 'recommonmark.parser.CommonMarkParser', +} + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'jarjardoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'jarjar.tex', 'jarjar Documentation', + 'The Austerweil Lab at UW-Madison', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'jarjar', 'jarjar Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'jarjar', 'jarjar Documentation', + author, 'jarjar', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/img/backups-failed.png b/docs/img/backups-failed.png new file mode 100644 index 0000000..bb064a3 Binary files /dev/null and b/docs/img/backups-failed.png differ diff --git a/docs/img/nap.png b/docs/img/nap.png new file mode 100644 index 0000000..a10e83e Binary files /dev/null and b/docs/img/nap.png differ diff --git a/docs/img/positive-vibes.png b/docs/img/positive-vibes.png new file mode 100644 index 0000000..1778414 Binary files /dev/null and b/docs/img/positive-vibes.png differ diff --git a/docs/img/simulations-complete.png b/docs/img/simulations-complete.png new file mode 100644 index 0000000..3bfa628 Binary files /dev/null and b/docs/img/simulations-complete.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..3b1e563 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,114 @@ +.. jarjar documentation master file, created by + sphinx-quickstart on Sat Jun 2 13:53:50 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +################################## +Welcome to jarjar's documentation! +################################## + +Jarjar is a python utility that makes it easy to send slack notifications to your teams. +You can import it as a python module or use our command line tool. + +************************** +What Can Jarjar Do For Me? +************************** + +Jarjar was developed at the `Austerweil Lab at UW-Madison `_ as a tool +for scientists. We use it for all sorts of things, such as: + +1. Sending a message so that we know when long-running processes have finished. + + .. image:: img/simulations-complete.png + +2. Sending notices when scheduled tasks have failed. + + .. image:: img/backups-failed.png + +3. Sending out daily positive vibes. + + .. image:: img/positive-vibes.png + + +********** +Quickstart +********** + + +Install +------- + +:doc:`Installation ` is simple! + +.. code-block:: shell + + pip install jarjar + +My guess is that you'll want to create jarjar's config file, ``~/.jarjar``. This tells jarjar +what you'd like to use as a default for your slack team's webhook, the channel to post to, +and the message it sends. Don't worry, you can over-ride these anytime. + +Edit this snippet and add it to ``~/.jarjar``: + +.. code-block:: shell + + channel='@username' + message='Custom message' + webhook='https://hooks.slack.com/services/your/teams/webhook' + +If you don't know your team's webhook, you might have to +`make one `_. + +Python API +---------- + +Use the :doc:`jarjar python api ` like: + +.. code-block:: python + + from jarjar import jarjar + + # initialize a jarjar object + jj = jarjar() # defaults from .jarjar + jj = jarjar(channel='#channel', webhook='slack-webhook-url') + jj = jarjar(webhook='slack-webhook-url') + + # send a text message + jj.text('Hi!') + jj.text('Hi!', channel=["@jeffzemla", "#channel"]) + + # send an attachment + jj.attach({'meesa': 'jarjar binks'}), text='Hello!') + +Command Line Tool +----------------- + +We also made a :doc:`command line tool ` for use outside of python scripts. +The command line tool adds functionality to execute processes and send messages when they +are complete. + + .. code-block:: shell + + jarjar sleep 1 -m 'Meesa took a nap!' + +And then in your slack team: + + .. image:: img/nap.png + +Custom attachments are not supported in the CLT at this time, but everything else is: + + .. code-block:: shell + + jarjar -m 'Meesa jarjar binks!' + jarjar -m 'Hi, everyone!!' --webhook '' -c '#general' + +******************** +Detailed Documention +******************** + +.. toctree:: + :maxdepth: 1 + + install + clt + api diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..257683f --- /dev/null +++ b/docs/install.md @@ -0,0 +1,31 @@ +# Installing jarjar + +## Just use pip. + +We're on [pypi](https://pypi.org/project/jarjar/). + +```shell +pip install jarjar +``` + +## Config File + +You can use jarjar without a config file, but you'll need to tell it your slack webhook and channel each time. + +You don't want to live that way. + +Jarjar looks for a special file in your user home `~/.jarjar` for default webhook, channel, and/or message. You can over-ride anything in there pretty much any time you want. + +```sh +channel='@username' +message='Custom message' +webhook='https://hooks.slack.com/services/your/teams/webhook' +``` + +## Configuring Slack + +For this to work in the first place, you need to [set up a slack webhook for your team](https://api.slack.com/incoming-webhooks). + +While you're doing that, you can also specify a custom name and custom icon. We named our webhook robot `jar-jar`, and we used [this icon](http://i.imgur.com/hTHrg6i.png), so messages look like this: + +![](http://i.imgur.com/g9RG16j.png) diff --git a/jarjar/__init__.py b/jarjar/__init__.py new file mode 100644 index 0000000..686bd5a --- /dev/null +++ b/jarjar/__init__.py @@ -0,0 +1,22 @@ +from sys import version_info as _version_info + +if isinstance(_version_info, tuple): + version = _version_info[0] +else: + version = _version_info.major + +# different importing for python 2 and 3 +if version == 2: + from jarjar import jarjar + from _version import __version__ + from screen import Screen +else: + from jarjar.jarjar import jarjar + from jarjar._version import __version__ + from jarjar.screen import Screen + +__all__ = [ + '__version__', + 'Screen', + 'jarjar', +] diff --git a/jarjar/_version.py b/jarjar/_version.py new file mode 100644 index 0000000..fd24f38 --- /dev/null +++ b/jarjar/_version.py @@ -0,0 +1 @@ +__version__ = "3.0" diff --git a/jarjar/jarjar.py b/jarjar/jarjar.py new file mode 100644 index 0000000..d615fc6 --- /dev/null +++ b/jarjar/jarjar.py @@ -0,0 +1,396 @@ +"""Class file for jarjar +""" + +import requests +import json +import time +import os +import imp +import warnings +import copy + + +class jarjar(object): + """A jarjar slack messenger. + + This is largely a wrapper around functionality in ``requests.post()`` with + facilities to store and set default values for the desired message to + send, channel to post within, and slack team webhook. + + Inference for these values proceeds as follows. + + 1. Any argument provided to :func:`~jarjar.jarjar.text` or + :func:`~jarjar.jarjar.attach` supersedes all defaults. + 2. Defaults can be provided at initialization or via a config file + (``~/.jarjar``), which looks like: + + .. code:: + + channel="@username" + message="Custom message" + webhook="https://hooks.slack.com/services/your/teams/webhook" + + 3. Arguments provided at initialization supersede those in ``~/.jarjar``. + If the channel or webhook arguments are never provided, an error is + raised. If the channel and webhook are provided but not a message or + attachment, jarjar will make something up. + + Methods + ------- + attach(attach, channel=None, webhook=None, message=None) + Send an attachment. User may also include a text message. + text(message, channel=None, webhook=None, attach=None) + Send a text message. User may also include an attachment. + set_webhook(webhook) + Set jarjar's default webhook. + set_channel(channel) + Set jarjar's default channel. + set_message(message) + Set jarjar's default message. + + Parameters + ---------- + message : str + Optional. Default message to send. + channel : str, list + Optional. Name of the default channel to post within. + webhook : str + Optional. Webhook URL for the default slack team. + + """ + + _expected_kwargs = ['message', 'attach', 'channel', 'webhook'] + _final_default_message = 'Meesa Jarjar Binks!' + _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(' ', ' ') + ) + + # defaults; exposed for the user + headers = {'Content-Type': 'application/json'} + + def __init__(self, message=None, channel=None, webhook=None): + + # read config file, set defaults + self._read_config() + self._set_defaults(channel=channel, webhook=webhook, message=message) + + # default attach and payload args + self.attachment_args = dict( + fallback="New attachments are ready!", + color="#36a64f", + fields=[] + ) + self.payload_args = dict() + + def _set_defaults(self, channel=None, webhook=None, message=None): + """Set the default channel and webhook and message.""" + # set default channel + if channel in (None, ''): + self.default_channel = self.cfg_channel + else: + self.default_channel = channel + + if webhook in (None, ''): + self.default_webhook = self.cfg_webhook + else: + self.default_webhook = webhook + + if message in (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 and kwargs[arg] not in ('', None): + 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 NameError('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']: + return None + + # otherwise use a super-default and warn the user. + warnings.warn(self._no_message_warn) + return self._final_default_message + + # check unexpected args + for k, _ in kwargs.items(): + if k in self._expected_kwargs: + continue + warnings.warn('Recieved unexpected kwarg: `%s`.' % k) + + result = dict() + for arg in ['message', 'attach', 'channel', 'webhook']: + result[arg] = _get(arg) + return result + + def _attachment_formatter(self, attach): + """Format a dict to become a slack attachment.""" + attachments = copy.deepcopy(self.attachment_args) + attachments['ts'] = time.time() + + for key in attach: + + if isinstance(attach[key], str): + outval = attach[key] + else: + try: + outval = str(attach[key]) + except UnicodeEncodeError: + outval = unicode(attach[key]) + except Exception: + raise + + attachments['fields'].append(dict( + title=key, + value=outval, + short=len(outval) < 20 + )) + return [attachments] + + def attach(self, attach=None, **kwargs): + """Send an attachment. + + This method is largely identical to :func:`~jarjar.jarjar.text`, + only differing in the first argument (``attach``), which is expected + to be a dictionary. + + Parameters + ---------- + attach : dict + Attachment data. Optional *but weird if you don't provide one*. + All values are converted to string for the slack payload so don't + sweat it. + message : str + Text to send. Optional. If attach is None and there is no + default *and* you don't provide one here, jarjar just wings it. + channel : str, 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 + ------- + response : requests.models.Response + Requests response object for the POST request to slack. + + """ + 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, message=None, **kwargs): + """Send a text message. + + This method is largely identical to :func:`~jarjar.jarjar.attach`, only + differing in the first argument (``message``), which is expected to be + a string. + + Parameters + ---------- + message : str + Text to send. Optional *but weird if you don't provide one*. + If attach is None and there is no default *and* you don't provide + one here, jarjar just wings it. + attach : dict + Attachment data. Optional. All values are converted to string for + the slack payload so don't sweat it. + channel : str, 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 + ------- + response : requests.models.Response + Requests response object for the POST request to slack. + + """ + kwargs = self._infer_kwargs(message=message, **kwargs) + return self._post(**kwargs) + + def post(self, *args, **kwargs): + """**DEPRECATED**. Pass args to .text and raises a warning.""" + warnings.warn( + 'jarjar.post() is deprecated! I\'ll let this slide ' + + 'but you should switch to text or attach' + ) + return self.text(*args, **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. + + Parameters + ---------- + message : str, NoneType + Text to send. + + attach : dict, NoneType + Attachment data. + + channel : str, 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 + ------- + response : requests.models.Response + Requests response object for the POST request to slack. + + """ + def _check_arg(arg, name, types, noneable=False): + """Ensure arguments are valid.""" + # NoneType handler + if arg is None: + if not noneable: + raise NameError( + 'User did not provide kwarg `{}`.'.format(name) + ) + else: + return + + if not isinstance(arg, types): + 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 NameError('user must provide a message or attachment.') + + # define tupe of string types + # try/except is control for python 3. + try: + str_types = (str, unicode) + except Exception: + str_types = (str,) + + # check kwargs + _check_arg(message, 'message', str_types, noneable=True) + _check_arg(attach, 'attach', (dict,), noneable=True) + _check_arg(channel, 'channel', str_types + (list, )) + _check_arg(webhook, 'webhook', str_types) + + # 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, + webhook=webhook + ) + ) + return status + + # construct a payload + payload = copy.deepcopy(self.payload_args) + payload['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. + + Parameters + ---------- + webhook : str + Webhook URL for the slack team. + + """ + self.default_webhook = webhook + + def set_channel(self, channel): + """Set default channel. + + Parameters + ---------- + channel : str + Name of the channel to post within. + + """ + self.default_channel = channel + + def set_message(self, message): + """Set default message. + + Parameters + ---------- + message : str + Default message to send. + + """ + self.default_message = message diff --git a/jarjar/screen.py b/jarjar/screen.py new file mode 100644 index 0000000..167a57b --- /dev/null +++ b/jarjar/screen.py @@ -0,0 +1,173 @@ +# This file is modified from the screenutils Python module +# https://pypi.org/project/screenutils/ +# https://github.com/Christophe31/screenutils + +# -*- 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. + +try: + from commands import getoutput +except Exception: + from subprocess import getoutput +from os import system +from time import sleep + + +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) + + +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 + 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 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 -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: + + # use single quote unless that is a part of the command + if "'" in command: + q = "\"" + else: + q = "\'" + + self._screen_commands( + 'stuff {q}{c}{q}'.format(q=q, c=command), + 'eval "stuff \\015"' + ) + + 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.""" + self._check_exists() + for command in commands: + cmd = 'screen -x {0}.{1} -p 0 -X {2}'.format(self.id, self.name, command) + 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/license.txt b/license.txt similarity index 100% rename from python/license.txt rename to license.txt diff --git a/python/jarjar.egg-info/PKG-INFO b/python/jarjar.egg-info/PKG-INFO deleted file mode 100644 index d87c202..0000000 --- a/python/jarjar.egg-info/PKG-INFO +++ /dev/null @@ -1,17 +0,0 @@ -Metadata-Version: 1.1 -Name: jarjar -Version: 2.0.2 -Summary: Programatically send messages to your slack team -Home-page: https://github.com/AusterweilLab/jarjar -Author: The Austerweil Lab at UW-Madison -Author-email: austerweil.lab@gmail.com -License: MIT -Description: UNKNOWN -Keywords: slack,messaging -Platform: UNKNOWN -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 2.7 -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.3 -Classifier: Programming Language :: Python :: 3.4 -Classifier: Programming Language :: Python :: 3.5 diff --git a/python/jarjar.egg-info/SOURCES.txt b/python/jarjar.egg-info/SOURCES.txt deleted file mode 100644 index d56c343..0000000 --- a/python/jarjar.egg-info/SOURCES.txt +++ /dev/null @@ -1,9 +0,0 @@ -setup.cfg -setup.py -jarjar/__init__.py -jarjar/jarjar.py -jarjar.egg-info/PKG-INFO -jarjar.egg-info/SOURCES.txt -jarjar.egg-info/dependency_links.txt -jarjar.egg-info/requires.txt -jarjar.egg-info/top_level.txt \ No newline at end of file diff --git a/python/jarjar.egg-info/dependency_links.txt b/python/jarjar.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/python/jarjar.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python/jarjar.egg-info/requires.txt b/python/jarjar.egg-info/requires.txt deleted file mode 100644 index cf56d5b..0000000 --- a/python/jarjar.egg-info/requires.txt +++ /dev/null @@ -1 +0,0 @@ -requests>=2 diff --git a/python/jarjar.egg-info/top_level.txt b/python/jarjar.egg-info/top_level.txt deleted file mode 100644 index 8ed2eec..0000000 --- a/python/jarjar.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -jarjar diff --git a/python/jarjar/__init__.py b/python/jarjar/__init__.py deleted file mode 100644 index 6e3f57d..0000000 --- a/python/jarjar/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from sys import version_info as _version_info - -if isinstance(_version_info, tuple): - version = _version_info[0] -else: - version = _version_info.major - -# different importing for python 2 and 3 -if version == 2: - from jarjar import jarjar -else: - from jarjar.jarjar import jarjar diff --git a/python/jarjar/jarjar.py b/python/jarjar/jarjar.py deleted file mode 100644 index ea8ea76..0000000 --- a/python/jarjar/jarjar.py +++ /dev/null @@ -1,160 +0,0 @@ -import requests -import json -import time -import os -import imp - -class jarjar(): - - def __init__(self, channel=None, webhook=None): - - # read config file, set defaults - self._read_config() - self._set_defaults(channel=channel, webhook=webhook) - - # headers for post request - self.headers = {'Content-Type': 'application/json'} - - - def _set_defaults(self, channel=None, webhook=None): - """ - Set the default channel and webhook - 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 - - - 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']: - - # 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): - """ - 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 - - return channel, webhook - - @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, text, **kwargs): - """ - Send a message, without attachments. This is a wrapper around - self.post - """ - return self.post(text = text, **kwargs) - - def post(self, text=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 - channel, webhook = self._args_handler(channel, webhook) - - # 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)) - return status - - # construct a payload - payload = dict(channel = channel) - - # add text and attachments if provided - if text is not None: - payload['text'] = text - - 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 diff --git a/python/setup.py b/python/setup.py deleted file mode 100644 index 5efd6fa..0000000 --- a/python/setup.py +++ /dev/null @@ -1,21 +0,0 @@ -from setuptools import setup - -setup(name='jarjar', - version='2.0.2', - description='Programatically send messages to your slack team', - url='https://github.com/AusterweilLab/jarjar', - author='The Austerweil Lab at UW-Madison', - author_email='austerweil.lab@gmail.com', - license='MIT', - keywords=['slack', 'messaging'], - packages=['jarjar'], - install_requires = ['requests>=2'], - classifiers=[ - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - ] - ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d632d5c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +numpydoc \ No newline at end of file diff --git a/python/setup.cfg b/setup.cfg similarity index 100% rename from python/setup.cfg rename to setup.cfg diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..23fb32c --- /dev/null +++ b/setup.py @@ -0,0 +1,44 @@ +from setuptools import setup +from setuptools import find_packages +import re + + +# get jarjar version +# https://stackoverflow.com/a/7071358/353278 +VERSIONFILE = "jarjar/_version.py" +verstrline = open(VERSIONFILE, "rt").read() +VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" +mo = re.search(VSRE, verstrline, re.M) +if mo: + verstr = mo.group(1) +else: + raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) + +long_description = '\ +Jarjar is a python utility that makes it easy to send slack notifications \ +to your teams. You can import it as a python module or use our command \ +line tool.' + +# install jarjar +setup( + name='jarjar', + version=verstr, + description='Use python to send messages to your slack team', + long_description=long_description, + url='https://github.com/AusterweilLab/jarjar', + author='The Austerweil Lab at UW-Madison', + author_email='austerweil.lab@gmail.com', + license='MIT', + keywords=['slack', 'messaging'], + packages=find_packages('.'), + install_requires=['requests'], + python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + zip_safe=False, + classifiers=[ + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], + scripts=['bin/jarjar'], +) 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 100755 index cad6667..0000000 --- a/sh/jarjar +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash - -# example usage: ./jarjar [options] python