diff --git a/.appveyor.yml b/.appveyor.yml
new file mode 100644
index 0000000..133fb36
--- /dev/null
+++ b/.appveyor.yml
@@ -0,0 +1,29 @@
+environment:
+ global:
+ RANDOM_SEED: 0
+ matrix:
+ - PYTHON_MAJOR: 2
+ PYTHON_MINOR: 7
+ - PYTHON_MAJOR: 3
+ PYTHON_MINOR: 3
+ - PYTHON_MAJOR: 3
+ PYTHON_MINOR: 4
+ - PYTHON_MAJOR: 3
+ PYTHON_MINOR: 5
+
+cache:
+ - env
+
+install:
+ # Export build paths
+ - copy C:\MinGW\bin\mingw32-make.exe C:\MinGW\bin\make.exe
+ - set PATH=%PATH%;C:\MinGW\bin
+ - make --version
+ # Install project dependencies
+ - make install
+
+build: off
+
+test_script:
+ - make check
+ - make test
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..b46d51a
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,5 @@
+[run]
+branch = true
+omit =
+ */env/*
+ */tests/*
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..201aadb
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto
+CHANGELOG.md merge=union
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4b08c89
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,51 @@
+# Temporary Python files
+*.pyc
+*.egg-info
+__pycache__
+.ipynb_checkpoints
+
+# Temporary OS files
+Icon*
+
+# Temporary virtual environment files
+.cache
+/env
+
+# Temporary server files
+.env
+*.pid
+
+# Generated documentation
+/docs/gen
+/docs/apidocs
+/site
+/*.html
+/*.rst
+/docs/*.png
+
+# Google Drive
+*.gdoc
+*.gsheet
+*.gslides
+*.gdraw
+
+# Testing and coverage results
+.cache
+.pytest
+.coverage
+.coverage.*
+/htmlcov
+/xmlreport
+/pyunit.xml
+*.tmp
+
+# Build and release directories
+*.spec
+/build
+/dist
+
+# Sublime Text
+*.sublime-workspace
+
+# Eclipse
+.settings
diff --git a/.pep257 b/.pep257
new file mode 100644
index 0000000..e226c3f
--- /dev/null
+++ b/.pep257
@@ -0,0 +1,13 @@
+[pep257]
+
+# D211: No blank lines allowed before class docstring
+add_select = D211
+
+# D100: Missing docstring in public module
+# D101: Missing docstring in public class
+# D102: Missing docstring in public method
+# D103: Missing docstring in public function
+# D104: Missing docstring in public package
+# D105: Missing docstring in magic method
+# D202: No blank lines allowed after function docstring
+add_ignore = D100,D101,D102,D103,D104,D105,D202
diff --git a/.pep8rc b/.pep8rc
new file mode 100644
index 0000000..7be92bf
--- /dev/null
+++ b/.pep8rc
@@ -0,0 +1,7 @@
+[pep8]
+
+# E402 module level import not at top of file (checked by PyLint)
+# E501: line too long (checked by PyLint)
+# E711: comparison to None (used to improve test style)
+# E712: comparison to True (used to improve test style)
+ignore = E402,E501,E711,E712
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..d3c3770
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,21 @@
+[MESSAGES CONTROL]
+
+disable=locally-disabled,fixme,too-few-public-methods,too-many-public-methods,invalid-name,global-statement,too-many-ancestors,missing-docstring
+
+[FORMAT]
+
+max-line-length=80
+
+ignore-long-lines=^.*((https?:)|(pragma:)|(TODO:)).*$
+
+[SIMILARITIES]
+
+min-similarity-lines=4
+
+ignore-imports=yes
+
+[REPORTS]
+
+reports=no
+
+msg-template={msg_id} ({symbol}):{line:3d},{column}: {msg}
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..84e61bf
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,9 @@
+checks:
+ python:
+ code_rating: true
+ duplicate_code: true
+tools:
+ external_code_coverage: true
+ pylint:
+ python_version: 2
+ config_file: '.pylintrc'
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..5f1c2e7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,32 @@
+language: python
+python:
+ - 2.7
+ - 3.3
+ - 3.4
+ - 3.5
+
+cache:
+ pip: true
+ directories:
+ - env
+
+env:
+ global:
+ - RANDOM_SEED=0
+
+install:
+ - make install
+
+script:
+ - make check
+ - make test
+
+after_success:
+ - pip install coveralls scrutinizer-ocular
+ - coveralls
+ - ocular
+
+notifications:
+ email:
+ on_success: never
+ on_failure: change
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3a64c20
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision History
+
+## 0.0.0 (YYYY/MM/DD)
+
+ - TBD
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..06ae232
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,76 @@
+# For Contributors
+
+## Setup
+
+### Requirements
+
+* Make:
+ * Windows: https://cygwin.com/install.html
+ * Mac: https://developer.apple.com/xcode
+ * Linux: https://www.gnu.org/software/make (likely already installed)
+* virtualenv: https://pypi.python.org/pypi/virtualenv#installation
+* Pandoc: http://johnmacfarlane.net/pandoc/installing.html
+* Graphviz: http://www.graphviz.org/Download.php
+
+### Installation
+
+Install project dependencies into a virtual environment:
+
+```sh
+$ make install
+```
+
+## Development Tasks
+
+### Testing
+
+Manually run the tests:
+
+```sh
+$ make test
+$ make tests # includes integration tests
+```
+
+or keep them running on change:
+
+```sh
+$ make watch
+```
+
+> In order to have OS X notifications, `brew install terminal-notifier`.
+
+### Documentation
+
+Build the documentation:
+
+```sh
+$ make doc
+```
+
+### Static Analysis
+
+Run linters and static analyzers:
+
+```sh
+$ make pep8
+$ make pep257
+$ make pylint
+$ make check # includes all checks
+```
+
+## Continuous Integration
+
+The CI server will report overall build status:
+
+```sh
+$ make ci
+```
+
+## Release Tasks
+
+Release to PyPI:
+
+```sh
+$ make upload-test # dry run upload to a test server
+$ make upload
+```
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..9d20c40
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,23 @@
+# License
+
+**The MIT License (MIT)**
+
+Copyright © 2016, Jace Browning
+
+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/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..855deca
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,4 @@
+include *.rst *.txt *.md
+recursive-include docs *.rst *.txt *.md
+graft */files
+graft */*/files
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..e500d8a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,325 @@
+# Project settings
+PROJECT := verchew
+PACKAGE := verchew
+REPOSITORY := jacebrowning/verchew
+PACKAGES := $(PACKAGE) tests
+CONFIG := $(shell ls *.py)
+MODULES := $(shell find $(PACKAGES) -name '*.py') $(CONFIG)
+
+# Python settings
+ifndef TRAVIS
+ PYTHON_MAJOR ?= 2
+ PYTHON_MINOR ?= 7
+endif
+
+# System paths
+PLATFORM := $(shell python -c 'import sys; print(sys.platform)')
+ifneq ($(findstring win32, $(PLATFORM)), )
+ WINDOWS := true
+ SYS_PYTHON_DIR := C:\\Python$(PYTHON_MAJOR)$(PYTHON_MINOR)
+ SYS_PYTHON := $(SYS_PYTHON_DIR)\\python.exe
+ SYS_VIRTUALENV := $(SYS_PYTHON_DIR)\\Scripts\\virtualenv.exe
+ # https://bugs.launchpad.net/virtualenv/+bug/449537
+ export TCL_LIBRARY=$(SYS_PYTHON_DIR)\\tcl\\tcl8.5
+else
+ ifneq ($(findstring darwin, $(PLATFORM)), )
+ MAC := true
+ else
+ LINUX := true
+ endif
+ SYS_PYTHON := python$(PYTHON_MAJOR)
+ ifdef PYTHON_MINOR
+ SYS_PYTHON := $(SYS_PYTHON).$(PYTHON_MINOR)
+ endif
+ SYS_VIRTUALENV := virtualenv
+endif
+
+# Virtual environment paths
+ENV := env
+ifneq ($(findstring win32, $(PLATFORM)), )
+ BIN := $(ENV)/Scripts
+ ACTIVATE := $(BIN)/activate.bat
+ OPEN := cmd /c start
+else
+ BIN := $(ENV)/bin
+ ACTIVATE := . $(BIN)/activate
+ ifneq ($(findstring cygwin, $(PLATFORM)), )
+ OPEN := cygstart
+ else
+ OPEN := open
+ endif
+endif
+
+# Virtual environment executables
+ifndef TRAVIS
+ BIN_ := $(BIN)/
+endif
+PYTHON := $(BIN_)python
+PIP := $(BIN_)pip
+EASY_INSTALL := $(BIN_)easy_install
+SNIFFER := $(BIN_)sniffer
+HONCHO := $(ACTIVATE) && $(BIN_)honcho
+
+# MAIN TASKS ###################################################################
+
+.PHONY: all
+all: doc
+
+.PHONY: ci
+ci: check test ## Run all tasks that determine CI status
+
+.PHONY: watch
+watch: install .clean-test ## Continuously run all CI tasks when files chanage
+ $(SNIFFER)
+
+.PHONY: run ## Start the program
+run: install
+ $(PYTHON) $(PACKAGE)/__main__.py
+
+# SYSTEM DEPENDENCIES ##########################################################
+
+.PHONY: doctor
+doctor: ## Confirm system dependencies are available
+ @ echo "Checking Python version:"
+ @ python --version | tee /dev/stderr | grep -q "2.7."
+
+# PROJECT DEPENDENCIES #########################################################
+
+DEPS_CI := $(ENV)/.install-ci
+DEPS_DEV := $(ENV)/.install-dev
+DEPS_BASE := $(ENV)/.install-base
+
+.PHONY: install
+install: $(DEPS_CI) $(DEPS_DEV) $(DEPS_BASE) ## Install all project dependencies
+
+$(DEPS_CI): requirements/ci.txt $(PIP)
+ $(PIP) install --upgrade -r $<
+ @ touch $@ # flag to indicate dependencies are installed
+
+$(DEPS_DEV): requirements/dev.txt $(PIP)
+ $(PIP) install --upgrade -r $<
+ifdef WINDOWS
+ @ echo "Manually install pywin32: https://sourceforge.net/projects/pywin32/files/pywin32"
+else ifdef MAC
+ $(PIP) install --upgrade pync MacFSEvents
+else ifdef LINUX
+ $(PIP) install --upgrade pyinotify
+endif
+ @ touch $@ # flag to indicate dependencies are installed
+
+$(DEPS_BASE): setup.py requirements.txt $(PYTHON)
+ $(PYTHON) setup.py develop
+ @ touch $@ # flag to indicate dependencies are installed
+
+$(PIP): $(PYTHON)
+ $(PYTHON) -m pip install --upgrade pip setuptools
+ @ touch $@
+
+$(PYTHON):
+ $(SYS_VIRTUALENV) --python $(SYS_PYTHON) $(ENV)
+
+# CHECKS #######################################################################
+
+PEP8 := $(BIN_)pep8
+PEP8RADIUS := $(BIN_)pep8radius
+PEP257 := $(BIN_)pep257
+PYLINT := $(BIN_)pylint
+
+.PHONY: check
+check: pep8 pep257 pylint ## Run linters and static analysis
+
+.PHONY: pep8
+pep8: install ## Check for convention issues
+ $(PEP8) $(PACKAGES) $(CONFIG) --config=.pep8rc
+
+.PHONY: pep257
+pep257: install ## Check for docstring issues
+ $(PEP257) $(PACKAGES) $(CONFIG)
+
+.PHONY: pylint
+pylint: install ## Check for code issues
+ $(PYLINT) $(PACKAGES) $(CONFIG) --rcfile=.pylintrc
+
+.PHONY: fix
+fix: install
+ $(PEP8RADIUS) --docformatter --in-place
+
+# TESTS ########################################################################
+
+PYTEST := $(BIN_)py.test
+COVERAGE := $(BIN_)coverage
+COVERAGE_SPACE := $(BIN_)coverage.space
+
+RANDOM_SEED ?= $(shell date +%s)
+
+PYTEST_CORE_OPTS := --doctest-modules -r xXw -vv
+PYTEST_COV_OPTS := --cov=$(PACKAGE) --no-cov-on-fail --cov-report=term-missing --cov-report=html
+PYTEST_RANDOM_OPTS := --random --random-seed=$(RANDOM_SEED)
+
+PYTEST_OPTS := $(PYTEST_CORE_OPTS) $(PYTEST_COV_OPTS) $(PYTEST_RANDOM_OPTS)
+PYTEST_OPTS_FAILFAST := $(PYTEST_OPTS) --last-failed --exitfirst
+
+FAILURES := .cache/v/cache/lastfailed
+REPORTS ?= xmlreport
+
+.PHONY: test
+test: test-all
+
+.PHONY: test-unit
+test-unit: install ## Run the unit tests
+ @- mv $(FAILURES) $(FAILURES).bak
+ $(PYTEST) $(PYTEST_OPTS) $(PACKAGE) --junitxml=$(REPORTS)/unit.xml
+ @- mv $(FAILURES).bak $(FAILURES)
+ $(COVERAGE_SPACE) $(REPOSITORY) unit
+
+.PHONY: test-int
+test-int: install ## Run the integration tests
+ @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) tests; fi
+ $(PYTEST) $(PYTEST_OPTS) tests --junitxml=$(REPORTS)/integration.xml
+ $(COVERAGE_SPACE) $(REPOSITORY) integration
+
+.PHONY: test-all
+test-all: install ## Run all the tests
+ @ if test -e $(FAILURES); then $(PYTEST) $(PYTEST_OPTS_FAILFAST) $(PACKAGES); fi
+ $(PYTEST) $(PYTEST_OPTS) $(PACKAGES) --junitxml=$(REPORTS)/overall.xml
+ $(COVERAGE_SPACE) $(REPOSITORY) overall
+
+.PHONY: read-coverage
+read-coverage:
+ $(OPEN) htmlcov/index.html
+
+# DOCUMENTATION ################################################################
+
+PYREVERSE := $(BIN_)pyreverse
+PDOC := $(PYTHON) $(BIN_)pdoc
+MKDOCS := $(BIN_)mkdocs
+
+PDOC_INDEX := docs/apidocs/$(PACKAGE)/index.html
+MKDOCS_INDEX := site/index.html
+
+.PHONY: doc
+doc: uml pdoc mkdocs ## Run documentation generators
+
+.PHONY: uml
+uml: install docs/*.png ## Generate UML diagrams for classes and packages
+docs/*.png: $(MODULES)
+ $(PYREVERSE) $(PACKAGE) -p $(PACKAGE) -a 1 -f ALL -o png --ignore tests
+ - mv -f classes_$(PACKAGE).png docs/classes.png
+ - mv -f packages_$(PACKAGE).png docs/packages.png
+
+.PHONY: pdoc
+pdoc: install $(PDOC_INDEX) ## Generate API documentaiton with pdoc
+$(PDOC_INDEX): $(MODULES)
+ $(PDOC) --html --overwrite $(PACKAGE) --html-dir docs/apidocs
+ @ touch $@
+
+.PHONY: mkdocs
+mkdocs: install $(MKDOCS_INDEX) ## Build the documentation site with mkdocs
+$(MKDOCS_INDEX): mkdocs.yml docs/*.md
+ ln -sf `realpath README.md --relative-to=docs` docs/index.md
+ ln -sf `realpath CHANGELOG.md --relative-to=docs/about` docs/about/changelog.md
+ ln -sf `realpath CONTRIBUTING.md --relative-to=docs/about` docs/about/contributing.md
+ ln -sf `realpath LICENSE.md --relative-to=docs/about` docs/about/license.md
+ $(MKDOCS) build --clean --strict
+
+.PHONY: mkdocs-live
+mkdocs-live: mkdocs ## Launch and continuously rebuild the mkdocs site
+ eval "sleep 3; open http://127.0.0.1:8000" &
+ $(MKDOCS) serve
+
+# BUILD ########################################################################
+
+PYINSTALLER := $(BIN_)pyinstaller
+PYINSTALLER_MAKESPEC := $(BIN_)pyi-makespec
+
+DIST_FILES := dist/*.tar.gz dist/*.whl
+EXE_FILES := dist/$(PROJECT).*
+
+.PHONY: dist
+dist: install $(DIST_FILES)
+$(DIST_FILES): $(MODULES) README.rst CHANGELOG.rst
+ rm -f $(DIST_FILES)
+ $(PYTHON) setup.py check --restructuredtext --strict --metadata
+ $(PYTHON) setup.py sdist
+ $(PYTHON) setup.py bdist_wheel
+
+%.rst: %.md
+ pandoc -f markdown_github -t rst -o $@ $<
+
+.PHONY: exe
+exe: install $(EXE_FILES)
+$(EXE_FILES): $(MODULES) $(PROJECT).spec
+ # For framework/shared support: https://github.com/yyuu/pyenv/wiki
+ $(PYINSTALLER) $(PROJECT).spec --noconfirm --clean
+
+$(PROJECT).spec:
+ $(PYINSTALLER_MAKESPEC) $(PACKAGE)/__main__.py --onefile --windowed --name=$(PROJECT)
+
+# RELEASE ######################################################################
+
+TWINE := $(BIN_)twine
+
+.PHONY: register
+register: dist ## Register the project on PyPI
+ @ echo NOTE: your project must be registered manually
+ @ echo https://github.com/pypa/python-packaging-user-guide/issues/263
+ # TODO: switch to twine when the above issue is resolved
+ # $(TWINE) register dist/*.whl
+
+.PHONY: upload
+upload: .git-no-changes register ## Upload the current version to PyPI
+ $(TWINE) upload dist/*
+ $(OPEN) https://pypi.python.org/pypi/$(PROJECT)
+
+.PHONY: .git-no-changes
+.git-no-changes:
+ @ if git diff --name-only --exit-code; \
+ then \
+ echo Git working copy is clean...; \
+ else \
+ echo ERROR: Git working copy is dirty!; \
+ echo Commit your changes and try again.; \
+ exit -1; \
+ fi;
+
+# CLEANUP ######################################################################
+
+.PHONY: clean
+clean: .clean-dist .clean-test .clean-doc .clean-build ## Delete all generated and temporary files
+
+.PHONY: clean-all
+clean-all: clean .clean-env .clean-workspace
+
+.PHONY: .clean-build
+.clean-build:
+ find $(PACKAGES) -name '*.pyc' -delete
+ find $(PACKAGES) -name '__pycache__' -delete
+ rm -rf *.egg-info
+
+.PHONY: .clean-doc
+.clean-doc:
+ rm -rf README.rst docs/apidocs *.html docs/*.png site
+
+.PHONY: .clean-test
+.clean-test:
+ rm -rf .cache .pytest .coverage htmlcov xmlreport
+
+.PHONY: .clean-dist
+.clean-dist:
+ rm -rf *.spec dist build
+
+.PHONY: .clean-env
+.clean-env: clean
+ rm -rf $(ENV)
+
+.PHONY: .clean-workspace
+.clean-workspace:
+ rm -rf *.sublime-workspace
+
+# HELP #########################################################################
+
+.PHONY: help
+help: all
+ @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+.DEFAULT_GOAL := help
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6b16e16
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+Unix: [![Unix Build Status](https://img.shields.io/travis/jacebrowning/verchew/develop.svg)](https://travis-ci.org/jacebrowning/verchew) Windows: [![Windows Build Status](https://img.shields.io/appveyor/ci/jacebrowning/verchew/develop.svg)](https://ci.appveyor.com/project/jacebrowning/verchew)
Metrics: [![Coverage Status](https://img.shields.io/coveralls/jacebrowning/verchew/develop.svg)](https://coveralls.io/r/jacebrowning/verchew) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/jacebrowning/verchew.svg)](https://scrutinizer-ci.com/g/jacebrowning/verchew/?branch=develop)
Usage: [![PyPI Version](https://img.shields.io/pypi/v/verchew.svg)](https://pypi.python.org/pypi/verchew) [![PyPI Downloads](https://img.shields.io/pypi/dm/verchew.svg)](https://pypi.python.org/pypi/verchew)
+
+# Overview
+
+This is an embeddable Python script to check the versions of your system dependencies.
+
+# Setup
+
+## Requirements
+
+* Python 2.7+ or Python 3.3+
+
+## Installation
+
+Install verchew with pip:
+
+```sh
+$ pip install verchew
+```
+
+or directly from the source code:
+
+```sh
+$ git clone https://github.com/jacebrowning/verchew.git
+$ cd verchew
+$ python setup.py install
+```
+
+# Usage
+
+After installation, the package can imported:
+
+```sh
+$ python
+>>> import verchew
+>>> verchew.__version__
+```
diff --git a/circle.yml b/circle.yml
new file mode 100644
index 0000000..0352491
--- /dev/null
+++ b/circle.yml
@@ -0,0 +1,18 @@
+machine:
+ python:
+ version: 3.5.0
+ environment:
+ RANDOM_SEED: 0
+
+dependencies:
+ override:
+ - make install
+ cache_directories:
+ - env
+ - components
+
+test:
+ override:
+ - make check
+ - make test-unit REPORTS=${CIRCLE_TEST_REPORTS}
+ - make test-int REPORTS=${CIRCLE_TEST_REPORTS}
diff --git a/docs/about/changelog.md b/docs/about/changelog.md
new file mode 120000
index 0000000..699cc9e
--- /dev/null
+++ b/docs/about/changelog.md
@@ -0,0 +1 @@
+../../CHANGELOG.md
\ No newline at end of file
diff --git a/docs/about/contributing.md b/docs/about/contributing.md
new file mode 120000
index 0000000..f939e75
--- /dev/null
+++ b/docs/about/contributing.md
@@ -0,0 +1 @@
+../../CONTRIBUTING.md
\ No newline at end of file
diff --git a/docs/about/license.md b/docs/about/license.md
new file mode 120000
index 0000000..f0608a6
--- /dev/null
+++ b/docs/about/license.md
@@ -0,0 +1 @@
+../../LICENSE.md
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 0000000..3e3318b
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,13 @@
+site_name: verchew
+site_description: Chews through your system dependencies, spitting out incompatible versions.
+site_author: Jace Browning
+repo_url: https://github.com/jacebrowning/verchew
+
+theme: readthedocs
+
+pages:
+- Home: index.md
+- About:
+ - Release Notes: about/changelog.md
+ - Contributing: about/contributing.md
+ - License: about/license.md
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..e69de29
diff --git a/requirements/ci.txt b/requirements/ci.txt
new file mode 100644
index 0000000..b91fe06
--- /dev/null
+++ b/requirements/ci.txt
@@ -0,0 +1,15 @@
+# Linters
+pep8
+pep257
+pylint
+
+# Testing
+pytest
+pytest-describe
+pytest-expecter
+pytest-cov
+pytest-random
+
+# Coverage
+coverage
+coverage.space >= 0.6.2
diff --git a/requirements/dev.txt b/requirements/dev.txt
new file mode 100644
index 0000000..857968f
--- /dev/null
+++ b/requirements/dev.txt
@@ -0,0 +1,21 @@
+# Documentation
+pylint
+docutils
+readme
+pdoc
+mkdocs
+pygments
+
+# Tooling
+pep8radius
+sniffer
+
+# Runner
+honcho
+
+# Build
+wheel
+pyinstaller
+
+# Release
+twine
diff --git a/scent.py b/scent.py
new file mode 100644
index 0000000..f881516
--- /dev/null
+++ b/scent.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+"""Configuration file for sniffer."""
+# pylint: disable=superfluous-parens,bad-continuation
+
+import os
+import time
+import subprocess
+
+from sniffer.api import select_runnable, file_validator, runnable
+try:
+ from pync import Notifier
+except ImportError:
+ notify = None
+else:
+ notify = Notifier.notify
+
+
+watch_paths = ["verchew", "tests"]
+
+
+@select_runnable('python')
+@file_validator
+def python_files(filename):
+ """Match Python source files."""
+
+ return all((
+ filename.endswith('.py'),
+ not os.path.basename(filename).startswith('.'),
+ ))
+
+
+@runnable
+def python(*_):
+ """Run targets for Python."""
+
+ for count, (command, title, retry) in enumerate((
+ (('make', 'test-unit', 'CI=true'), "Unit Tests", True),
+ (('make', 'test-int', 'CI=true'), "Integration Tests", False),
+ (('make', 'test-all'), "Combined Tests", False),
+ (('make', 'check'), "Static Analysis", True),
+ (('make', 'doc'), None, True),
+ ), start=1):
+
+ if not run(command, title, count, retry):
+ return False
+
+ return True
+
+
+GROUP = int(time.time()) # unique per run
+
+_show_coverage = False
+_rerun_args = None
+
+
+def run(command, title, count, retry):
+ """Run a command-line program and display the result."""
+ global _rerun_args
+
+ if _rerun_args:
+ args = _rerun_args
+ _rerun_args = None
+ if not run(*args):
+ return False
+
+ print("")
+ print("$ %s" % ' '.join(command))
+ failure = subprocess.call(command)
+
+ if failure:
+ mark = "❌" * count
+ message = mark + " [FAIL] " + mark
+ else:
+ mark = "✅" * count
+ message = mark + " [PASS] " + mark
+ show_notification(message, title)
+
+ show_coverage()
+
+ if failure and retry:
+ _rerun_args = command, title, count, retry
+
+ return not failure
+
+
+def show_notification(message, title):
+ """Show a user notification."""
+ if notify and title:
+ notify(message, title=title, group=GROUP)
+
+
+def show_coverage():
+ """Launch the coverage report."""
+ global _show_coverage
+
+ if _show_coverage:
+ subprocess.call(['make', 'read-coverage'])
+
+ _show_coverage = False
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..0903aeb
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+
+"""Setup script for the package."""
+
+import os
+import sys
+
+import setuptools
+
+
+PACKAGE_NAME = 'verchew'
+MINIMUM_PYTHON_VERSION = 2, 7
+
+
+def check_python_version():
+ """Exit when the Python version is too low."""
+ if sys.version_info < MINIMUM_PYTHON_VERSION:
+ sys.exit("Python {}.{}+ is required.".format(*MINIMUM_PYTHON_VERSION))
+
+
+def read_package_variable(key):
+ """Read the value of a variable from the package without importing."""
+ module_path = os.path.join(PACKAGE_NAME, '__init__.py')
+ with open(module_path) as module:
+ for line in module:
+ parts = line.strip().split(' ')
+ if parts and parts[0] == key:
+ return parts[-1].strip("'")
+ assert 0, "'{0}' not found in '{1}'".format(key, module_path)
+
+
+def read_descriptions():
+ """Build a description for the project from documentation files."""
+ try:
+ readme = open("README.rst").read()
+ changelog = open("CHANGELOG.rst").read()
+ except IOError:
+ return ""
+ else:
+ return readme + '\n' + changelog
+
+
+check_python_version()
+
+setuptools.setup(
+ name=read_package_variable('__project__'),
+ version=read_package_variable('__version__'),
+
+ description="System dependency version checker.",
+ url='https://github.com/jacebrowning/verchew',
+ author='Jace Browning',
+ author_email='jacebrowning@gmail.com',
+
+ packages=setuptools.find_packages(),
+
+ entry_points={'console_scripts': [
+ 'verchew-cli = verchew.cli:main',
+ 'verchew-gui = verchew.gui:main',
+ ]},
+
+ long_description=read_descriptions(),
+ license='MIT',
+ classifiers=[
+ # TODO: update this list to match your application: https://pypi.python.org/pypi?%3Aaction=list_classifiers
+ 'Development Status :: 1 - Planning',
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ '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',
+ ],
+
+ install_requires=open("requirements.txt").readlines(),
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..a916f18
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Integration tests for the package."""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..af9ed2e
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,3 @@
+"""Integration tests configuration file."""
+
+from verchew.tests.conftest import pytest_configure # pylint: disable=unused-import
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..803091a
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,19 @@
+"""Sample integration test module."""
+# pylint: disable=no-self-use,missing-docstring
+
+import unittest
+
+from click.testing import CliRunner
+
+from verchew.cli import main
+
+
+class TestVerchew(unittest.TestCase):
+ """Sample integration test class."""
+
+ def test_conversion(self):
+ runner = CliRunner()
+ result = runner.invoke(main, ['42'])
+
+ self.assertEqual(result.exit_code, 0)
+ self.assertEqual(result.output, "12.80165\n")
diff --git a/verchew/__init__.py b/verchew/__init__.py
new file mode 100644
index 0000000..4317b07
--- /dev/null
+++ b/verchew/__init__.py
@@ -0,0 +1,6 @@
+"""Package for verchew."""
+
+__project__ = 'verchew'
+__version__ = '0.0.0'
+
+VERSION = "{0} v{1}".format(__project__, __version__)
diff --git a/verchew/__main__.py b/verchew/__main__.py
new file mode 100644
index 0000000..9cdbecd
--- /dev/null
+++ b/verchew/__main__.py
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+
+"""Package entry point."""
+
+
+from verchew.gui import main
+
+
+if __name__ == '__main__':
+ main()
diff --git a/verchew/cli.py b/verchew/cli.py
new file mode 100644
index 0000000..27c0330
--- /dev/null
+++ b/verchew/cli.py
@@ -0,0 +1,23 @@
+import logging
+
+import click
+
+from . import utils
+
+
+log = logging.getLogger(__name__)
+
+
+@click.command()
+@click.argument('feet')
+def main(feet=None):
+ logging.basicConfig(level=logging.INFO)
+
+ meters = utils.feet_to_meters(feet)
+
+ if meters is not None:
+ click.echo(meters)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/verchew/gui.py b/verchew/gui.py
new file mode 100644
index 0000000..4128f1f
--- /dev/null
+++ b/verchew/gui.py
@@ -0,0 +1,104 @@
+"""A sample module."""
+from Tkinter import * # pylint: disable=wildcard-import,unused-wildcard-import
+from ttk import * # pylint: disable=wildcard-import,unused-wildcard-import
+import logging
+
+from . import utils
+
+
+log = logging.getLogger(__name__)
+
+
+class Application(object):
+ """A sample class."""
+
+ def __init__(self):
+ log.info("Starting the application...")
+
+ # Configure the root window
+ self.root = Tk()
+ self.root.title("Feet to Meters")
+ self.root.minsize(400, 150)
+
+ # Initialize elements
+ self.feet = StringVar()
+ self.meters = StringVar()
+ frame = self.init(self.root)
+ frame.pack(fill=BOTH, expand=1)
+
+ # Start the event loop
+ self.root.mainloop()
+
+ def init(self, root):
+ padded = {'padding': 5}
+ sticky = {'sticky': NSEW}
+
+ # Configure grid
+ frame = Frame(root, **padded)
+ frame.rowconfigure(0, weight=1)
+ frame.rowconfigure(1, weight=1)
+ frame.columnconfigure(0, weight=1)
+
+ def frame_inputs(root):
+ frame = Frame(root, **padded)
+
+ # Configure grid
+ frame.rowconfigure(0, weight=1)
+ frame.rowconfigure(1, weight=1)
+ frame.columnconfigure(0, weight=1)
+ frame.columnconfigure(1, weight=1)
+ frame.columnconfigure(2, weight=1)
+
+ # Place widgets
+ entry = Entry(frame, width=7, textvariable=self.feet)
+ entry.grid(column=1, row=0)
+ entry.focus()
+ label = Label(frame, text="feet")
+ label.grid(column=2, row=0)
+ label = Label(frame, text="is equivalent to")
+ label.grid(column=0, row=1)
+ label = Label(frame, text="meters")
+ label.grid(column=2, row=1)
+ label = Label(frame, textvariable=self.meters)
+ label.grid(column=1, row=1)
+
+ return frame
+
+ def frame_commands(root):
+ frame = Frame(root, **padded)
+
+ # Configure grid
+ frame.rowconfigure(0, weight=1)
+ frame.columnconfigure(0, weight=1)
+
+ # Place widgets
+ button = Button(frame, text="Calculate", command=self.calculate)
+ button.grid(column=0, row=0)
+ self.root.bind('', self.calculate)
+
+ return frame
+
+ def separator(root):
+ return Separator(root)
+
+ # Place widgets
+ frame_inputs(frame).grid(row=0, **sticky)
+ separator(frame).grid(row=1, padx=10, pady=5, **sticky)
+ frame_commands(frame).grid(row=2, **sticky)
+
+ return frame
+
+ def calculate(self, event=None):
+ log.debug("Event: %s", event)
+ meters = utils.feet_to_meters(self.feet.get())
+ if meters is not None:
+ self.meters.set(meters)
+
+
+def main():
+ logging.basicConfig(level=logging.INFO)
+ Application()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/verchew/tests/__init__.py b/verchew/tests/__init__.py
new file mode 100644
index 0000000..2a67748
--- /dev/null
+++ b/verchew/tests/__init__.py
@@ -0,0 +1 @@
+"""Unit tests for the package."""
diff --git a/verchew/tests/conftest.py b/verchew/tests/conftest.py
new file mode 100644
index 0000000..fdf2d76
--- /dev/null
+++ b/verchew/tests/conftest.py
@@ -0,0 +1,22 @@
+"""Unit tests configuration file."""
+
+import logging
+
+
+def pytest_configure(config):
+ """Disable verbose output when running tests."""
+ logging.basicConfig(level=logging.DEBUG)
+
+ terminal = config.pluginmanager.getplugin('terminal')
+ base = terminal.TerminalReporter
+
+ class QuietReporter(base):
+ """A py.test reporting that only shows dots when running tests."""
+
+ def __init__(self, *args, **kwargs):
+ base.__init__(self, *args, **kwargs)
+ self.verbosity = 0
+ self.showlongtestinfo = False
+ self.showfspath = False
+
+ terminal.TerminalReporter = QuietReporter
diff --git a/verchew/tests/test_utils.py b/verchew/tests/test_utils.py
new file mode 100644
index 0000000..27094e4
--- /dev/null
+++ b/verchew/tests/test_utils.py
@@ -0,0 +1,15 @@
+"""Sample unit test module."""
+
+import unittest
+
+from verchew import utils
+
+
+class TestVerchew(unittest.TestCase):
+ """Sample unit test class."""
+
+ def test_conversion_is_correct(self):
+ self.assertEqual(utils.feet_to_meters(42), 12.80165)
+
+ def test_invalid_inputs_are_handled(self):
+ self.assertEqual(utils.feet_to_meters("hello"), None)
diff --git a/verchew/utils.py b/verchew/utils.py
new file mode 100644
index 0000000..37d80b9
--- /dev/null
+++ b/verchew/utils.py
@@ -0,0 +1,16 @@
+"""A sample module."""
+
+import logging
+
+
+log = logging.getLogger(__name__)
+
+
+def feet_to_meters(feet):
+ """Convert feet to meters."""
+ try:
+ value = float(feet)
+ except ValueError:
+ log.error("Unable to convert to float: %s", feet)
+ else:
+ return (0.3048 * value * 10000.0 + 0.5) / 10000.0