diff options
author | Joseph Hunkeler <jhunkeler@gmail.com> | 2020-08-19 17:34:02 -0400 |
---|---|---|
committer | Joseph Hunkeler <jhunkeler@gmail.com> | 2020-08-19 17:34:02 -0400 |
commit | a8a04ad705a71880d2e0c887d64ee67311fa075e (patch) | |
tree | 6c7ecb6ebcf0f8620af089be7dc12e4a12a7d8ca | |
download | good_feeder-a8a04ad705a71880d2e0c887d64ee67311fa075e.tar.gz |
Initial commit
-rw-r--r-- | .gitignore | 141 | ||||
-rw-r--r-- | README.md | 39 | ||||
-rw-r--r-- | good_feeder/__init__.py | 7 | ||||
-rw-r--r-- | good_feeder/cli/__init__.py | 0 | ||||
-rw-r--r-- | good_feeder/cli/main.py | 135 | ||||
-rw-r--r-- | pyproject.toml | 2 | ||||
-rw-r--r-- | setup.py | 20 |
7 files changed, 344 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..150deb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Project specific +*.html + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..82a3386 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# good_feeder + +Dumps and colorizes Jenkins RSS feeds. + +## Usage + +``` +usage: good_feeder [-h] [-a AUTH] [--insecure] [-l] [-f] [-s [SEARCH]] [-n] [--html] [-V] [jenkins_url] + +positional arguments: + jenkins_url + +optional arguments: + -h, --help show this help message and exit + -a AUTH, --auth AUTH Format: "username:token" (or omit "-a" and use JENKINS_AUTH environment variable) + --insecure Disable SSL certificate verification + -l, --latest Show only latest builds + -f, --failed Show only failed builds + -s [SEARCH], --search [SEARCH] + Filter output on sub-string (case-sensitive) + -n, --negate Negate search filter + --html Output as html (i.e. email) + -V, --version Show version +``` + +## Example + +```sh +# Anonymous query +good_feeder https://my_jenkins_host + +# Authenticated query (via argument) +good_feeder -a "myUser:myPassword" https://my_jenkins_host + +# Authenticated query (via environment) +JENKINS_AUTH="myUser:myPassword" +export JENKINS_AUTH +good_feeder https://my_jenkins_host +``` diff --git a/good_feeder/__init__.py b/good_feeder/__init__.py new file mode 100644 index 0000000..4b6c40f --- /dev/null +++ b/good_feeder/__init__.py @@ -0,0 +1,7 @@ +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version(__name__) +except PackageNotFoundError: + # package is not installed + pass diff --git a/good_feeder/cli/__init__.py b/good_feeder/cli/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/good_feeder/cli/__init__.py diff --git a/good_feeder/cli/main.py b/good_feeder/cli/main.py new file mode 100644 index 0000000..6c1f3fc --- /dev/null +++ b/good_feeder/cli/main.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +import os +import colorama +import feedparser +import urllib3 +import requests +import sys +from pprint import pprint +from .. import __version__ + +def auth_valid(s): + if ":" in s: + return True + return False + +def auth_split(s): + return s.split(":", 1) + +def main(): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('-a', '--auth', help='Format: "username:token" (or omit "-a" and use JENKINS_AUTH environment variable)', type=str) + parser.add_argument('--insecure', help='Disable SSL certificate verification', action='store_true') + parser.add_argument('-l', '--latest', help="Show only latest builds", action='store_true') + parser.add_argument('-f', '--failed', help="Show only failed builds", action='store_true') + parser.add_argument('-s', '--search', help='Filter output on sub-string (case-sensitive)', action='append', nargs="?", type=str) + parser.add_argument('-n', '--negate', help='Negate search filter', action='store_true') + parser.add_argument('--html', help='Output as html (i.e. email)', action='store_true') + + parser.add_argument('-V', '--version', help='Show version', action='store_true'); + parser.add_argument('jenkins_url', type=str, nargs='?') + args = parser.parse_args() + + username = "" + token = "" + + if args.version: + print(__version__) + exit(0) + + if not args.jenkins_url: + print("Jenkins URL required (i.e. https://jenkins OR https://jenkins/job/folder") + exit(1) + + # Disable SSL drivel when in insecure mode + if args.insecure: + urllib3.disable_warnings() + + # Consume authentication string argument, or via environment (not both) + # i.e. username:token + if args.auth: + if not auth_valid(args.auth): + print("Invalid auth string") + exit(1) + username, token = auth_split(args.auth) + elif os.environ.get("JENKINS_AUTH"): + if not auth_valid(os.environ["JENKINS_AUTH"]): + print("Invalid JENKINS_AUTH string") + exit(1) + username, token = auth_split(os.environ["JENKINS_AUTH"]) + + # Set default server entrypoint + if args.failed: + jenkins_url = f"{args.jenkins_url}/rssFailed" + elif args.latest: + jenkins_url = f"{args.jenkins_url}/rssLatest" + else: + jenkins_url = f"{args.jenkins_url}/rssAll" + + # Is this an anonymous or authenticated server query? + if not username and not token: + auth_data=None + else: + auth_data = (username, token) + + # Get RSS data + data = requests.get(jenkins_url, auth=auth_data, verify=not args.insecure) + + # On failure dump whatever Jenkins needs you to know (RAW HTML) + if not data: + print(data.text, file=sys.stderr) + exit(1) + + + # Consume RSS feed data + feed = feedparser.parse(data.text) + + if args.html: + color_date = "black" + else: + color_date = colorama.Fore.BLUE + + # Verify we have data to iterate over + if not feed or not feed.entries: + print("No records") + exit(0) + + # Begin dumping RSS records + for rec in feed.entries: + if args.html: + color_title = "red" + else: + color_title = colorama.Fore.RED + + if '(stable)' in rec.title or '(back to normal)' in rec.title: + if args.html: + color_title = "green" + else: + color_title = colorama.Fore.GREEN + + if args.html: + output_fmt = f"<span style=\"color:{color_date}\">{rec.published}</span>: " \ + f"<span style=\"color:{color_title}\">{rec.title}</span> " \ + f"(<a href=\"{rec.link}\">link</a>)<br>" + else: + output_fmt = f"{color_date}{rec.published}{colorama.Style.RESET_ALL}: " \ + f"{color_title}{rec.title}{colorama.Style.RESET_ALL} " \ + f"({rec.link})" + + # User-defined search is additive (-s str1 -s str2 -s str...) + if args.search: + for pattern in args.search: + if args.negate: + # Negated search + if pattern not in rec.title: + print(output_fmt) + else: + # Normal search + if pattern in rec.title: + print(output_fmt) + else: + print(output_fmt) + +if __name__ == '__main__': + sys.exit(main) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..51000e4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4"] diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..fc8ff62 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +from setuptools import setup, find_packages + +setup( + name='good_feeder', + use_scm_version=True, + setup_requires=[ + 'setuptools_scm' + ], + install_requires=[ + 'colorama', + 'feedreader', + 'requests', + ], + packages=find_packages(), + entry_points={ + 'console_scripts': [ + 'good_feeder = good_feeder.cli.main:main', + ], + }, +) |