commit f6faf473fbe314cc9065d316c9e8b6425ee506d4 Author: Joseph Manley Date: Thu Feb 4 18:27:19 2021 -0500 Intial commit diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..16f349b --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.155.1/containers/python-3/.devcontainer/base.Dockerfile + +# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 +ARG VARIANT="3" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +# [Option] Install Node.js +ARG INSTALL_NODE="true" +ARG NODE_VERSION="lts/*" +RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. +# COPY requirements.txt /tmp/pip-tmp/ +# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ +# && rm -rf /tmp/pip-tmp + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..185fcf2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,47 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.155.1/containers/python-3 +{ + "name": "Python 3", + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 + "VARIANT": "3.9", + // Options + "INSTALL_NODE": "false", + "NODE_VERSION": "lts/*" + } + }, + + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/local/bin/python", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "pip3 install --user -r requirements.txt", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode" +} diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..c4f5d40 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,24 @@ +name: Build +on: push + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build + run: | + python setup.py sdist + - name: Upload Artifact + uses: actions/upload-artifact@v2 + with: + name: jira-tracker.tar.gz + path: dist/jira-tracker-*.tar.gz \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..bc4d3be --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,54 @@ +name: Publish Release +on: + release: + types: + - created + +jobs: + pypi: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Update Version + run: | + sed -i "s/[0-9.]*\.[0-9.]*\.[0-9.]*/${GITHUB_REF}/g" jira_tracker/__init__.py + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: build + run: | + python setup.py sdist bdist_wheel + - name: PyPi Publish + uses: pypa/gh-action-pypi-publish@v1.0.0a0 + with: + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Update Version + run: | + sed -i "s/[0-9.]*\.[0-9.]*\.[0-9.]*/${GITHUB_REF}/g" jira_tracker/__init__.py + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build + run: | + python setup.py sdist + - name: Upload to release + uses: JasonEtco/upload-to-release@v0.1.1 + with: + args: dist/jira-tracker-*.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5391d87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# 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/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f4181c --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2021 Joseph Manley and others + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b934fa --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# jira-tracker \ No newline at end of file diff --git a/jira_tracker/__init__.py b/jira_tracker/__init__.py new file mode 100644 index 0000000..4eb0592 --- /dev/null +++ b/jira_tracker/__init__.py @@ -0,0 +1,61 @@ +import argparse, traceback, sys +from jira_tracker.logger import logger, enable_debug_logger +from jira_tracker.jira_auth import JiraAuth +from jira_tracker.project_tracker import ProjectTracker + +version = "0.0.0" + +def main(): + + parser = argparse.ArgumentParser(description="test") + parser.add_argument("-d", "--debug", help="Enables debug logging and traceback", action="store_true", dest="debug", default=False) + parser.add_argument("-v", "--version", help="Return current version number", dest="return_version", action="store_true", default=False) + parser.add_argument("--update-config", help ="Prompts input for updating config file", dest="update_config", action="store_true", default=False) + + args = parser.parse_args() + + try: + if args.debug: + enable_debug_logger() + + if args.return_version: + print(version) + return + + + auth = JiraAuth() + + if args.update_config: + auth.stdin_configure() + auth.update_config(auth.config_location) + + + row_format ="{:<12}{:<64}{:>4}{:>16}" + print(row_format.format("Task","Summary","Points","Status")) + + points = 0 + jira_client = auth.get_client() + all_issues = [] + for jira_project in jira_client.projects(): + logger.debug(f"Searching Project: {jira_project.key}") + proj = ProjectTracker(jira_client, jira_project.key) + issues = sorted(proj.this_weeks_issues(), key=lambda issue: issue.status) + all_issues = all_issues + issues + for issue in issues: + if issue.status == "Done": + points = points + float(issue.points) + + for issue in all_issues: + print(row_format.format(issue.name, issue.summary, issue.points, issue.status)) + print(f"\nTotal points this week: {points}") + + + except (Exception) as e: + logger.critical(e) + if args.debug: + tb = sys.exc_info()[2] + traceback.print_tb(tb) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/jira_tracker/__init__.pye b/jira_tracker/__init__.pye new file mode 100644 index 0000000..4eb0592 --- /dev/null +++ b/jira_tracker/__init__.pye @@ -0,0 +1,61 @@ +import argparse, traceback, sys +from jira_tracker.logger import logger, enable_debug_logger +from jira_tracker.jira_auth import JiraAuth +from jira_tracker.project_tracker import ProjectTracker + +version = "0.0.0" + +def main(): + + parser = argparse.ArgumentParser(description="test") + parser.add_argument("-d", "--debug", help="Enables debug logging and traceback", action="store_true", dest="debug", default=False) + parser.add_argument("-v", "--version", help="Return current version number", dest="return_version", action="store_true", default=False) + parser.add_argument("--update-config", help ="Prompts input for updating config file", dest="update_config", action="store_true", default=False) + + args = parser.parse_args() + + try: + if args.debug: + enable_debug_logger() + + if args.return_version: + print(version) + return + + + auth = JiraAuth() + + if args.update_config: + auth.stdin_configure() + auth.update_config(auth.config_location) + + + row_format ="{:<12}{:<64}{:>4}{:>16}" + print(row_format.format("Task","Summary","Points","Status")) + + points = 0 + jira_client = auth.get_client() + all_issues = [] + for jira_project in jira_client.projects(): + logger.debug(f"Searching Project: {jira_project.key}") + proj = ProjectTracker(jira_client, jira_project.key) + issues = sorted(proj.this_weeks_issues(), key=lambda issue: issue.status) + all_issues = all_issues + issues + for issue in issues: + if issue.status == "Done": + points = points + float(issue.points) + + for issue in all_issues: + print(row_format.format(issue.name, issue.summary, issue.points, issue.status)) + print(f"\nTotal points this week: {points}") + + + except (Exception) as e: + logger.critical(e) + if args.debug: + tb = sys.exc_info()[2] + traceback.print_tb(tb) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/jira_tracker/jira_auth.py b/jira_tracker/jira_auth.py new file mode 100644 index 0000000..d41057f --- /dev/null +++ b/jira_tracker/jira_auth.py @@ -0,0 +1,77 @@ +from jira_tracker.logger import logger +import jira, yaml +import pathlib, os + +class JiraAuth: + def __init__(self): + self.config = None + + # Intialize config + self.load_config(self.config_location) + + if not "token" in self.config: + self.stdin_configure() + self.update_config(self.config_location) + + logger.debug("Successfully intialized JiraAuth!") + + + + def load_config(self, config_file): + self.config = {} + logger.debug(f"Loading config from: {config_file}") + + + if self.config_location.exists(): + data = None + + # Load config file + with open(config_file) as f: + data = yaml.load(f, Loader=yaml.SafeLoader) + + # Update config with config data + for key in data: + self.config[key] = data[key] + else: + logger.warning(f"Config does not exist at: {config_file}") + + def update_config(self, config_file): + if not config_file.exists(): + config_file.parent.mkdir(parents=True) + + with open(config_file, 'w') as f: + data = yaml.dump(self.config, f) + + def stdin_configure(self): + + # Clear keys that need new inputs + for key in ["server", "user", "token"]: + self.config[key] = "" + + # Prompt sever + print("Please enter the jira server to interface with") + print("eg. jira.atlassian.com") + while not "server" in self.config or self.config["server"] == "": + self.config["server"] = input("Server: ") + + # Prompt Username + print("Please enter your username") + print("Example: user@domain.tld") + while not "user" in self.config or self.config["user"] == "": + self.config["user"] = input("Username: ") + + # Prompt Token + print("Please generate a token and enter it here") + print("https://id.atlassian.com/manage-profile/security/api-tokens") + while not "token" in self.config or self.config["token"] == "": + self.config["token"] = input("Token: ") + + if not "points_field" in self.config: + self.config["points_field"] = "customfield_10016" + + def get_client(self): + return jira.JIRA(self.config["server"], basic_auth=(self.config["user"], self.config["token"])) + + @property + def config_location(self): + return pathlib.Path(str(pathlib.Path.home()), ".jira-tracker", "config.yaml") \ No newline at end of file diff --git a/jira_tracker/logger.py b/jira_tracker/logger.py new file mode 100644 index 0000000..49100d1 --- /dev/null +++ b/jira_tracker/logger.py @@ -0,0 +1,16 @@ +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') +ch.setFormatter(formatter) + +logger.addHandler(ch) + +def enable_debug_logger(): + logger.setLevel(logging.DEBUG) + ch.setLevel(logging.DEBUG) \ No newline at end of file diff --git a/jira_tracker/project_tracker.py b/jira_tracker/project_tracker.py new file mode 100644 index 0000000..76587ea --- /dev/null +++ b/jira_tracker/project_tracker.py @@ -0,0 +1,20 @@ +from jira_tracker.logger import logger +from jira_tracker.tracker_issue import TrackerIssue +import datetime + +class ProjectTracker: + def __init__(self, jira_client, project_key): + self.jira = jira_client + self.key = project_key + + def this_weeks_issues(self): + result = self.jira.search_issues( + f"project = {self.key} AND assignee = currentuser() AND ((resolutiondate >= -{datetime.datetime.today().weekday()}d AND status = Done) OR status != Done)" + ) + logger.debug(f"Found {len(result)} valid records this week in {self.key}") + + issues = [] + for issue in result: + issues.append(TrackerIssue(issue)) + + return issues \ No newline at end of file diff --git a/jira_tracker/tracker_issue.py b/jira_tracker/tracker_issue.py new file mode 100644 index 0000000..1ed5c68 --- /dev/null +++ b/jira_tracker/tracker_issue.py @@ -0,0 +1,21 @@ +from jira_tracker.logger import logger + +class TrackerIssue: + def __init__(self, jira_issue): + self.data = jira_issue + + @property + def name(self): + return str(self.data) + + @property + def summary(self): + return str(self.data.fields.summary) + + @property + def status(self): + return str(self.data.fields.status) + + @property + def points(self): + return str(self.data.fields.customfield_10016) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3e2c67c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# requirements.txt +# +# installs dependencies from ./setup.py, and the package itself, +# in editable mode +-e . \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..98f7a8e --- /dev/null +++ b/setup.py @@ -0,0 +1,67 @@ + +import re +from os import path + +from setuptools import setup +from codecs import open + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with open(path.join(here, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + +def read(*parts): + return open(path.join(here, *parts), 'r').read() + +def find_version(*file_paths): + version_file = read(*file_paths) + version_match = re.search(r"^version = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + +setup( + name='jira-tracker', + version=find_version('jira_tracker','__init__.py'), + description='Simple Jira story point tracking tool', + long_description=long_description, + long_description_content_type="text/markdown", + + # The project's main homepage. + url='https://github.com/josephbmanley/jira-tracker', + + # Author details + author='Joseph Manley', + author_email='j@cloudsumu.com', + + # Choose your license + license='MIT', + + # See https://pypi.org/classifiers/ + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9' + ], + keywords='Jira', + packages=['jira_tracker'], + install_requires=['argparse','jira','pyyaml','wheel'], + package_data={}, + entry_points={ + 'console_scripts' : [ + 'jira-tracker=jira_tracker:main' + ] + } +)