diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..aa84bc1 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,20 @@ +[bumpversion] +current_version = 0.1.0 +commit = True +tag = True + +[bumpversion:file:setup.py] +search = version='{current_version}' +replace = version='{new_version}' + +[bumpversion:file:README.rst] +search = v{current_version}. +replace = v{new_version}. + +[bumpversion:file:docs/conf.py] +search = version = release = '{current_version}' +replace = version = release = '{new_version}' + +[bumpversion:file:src/catalpa_tools/__init__.py] +search = __version__ = '{current_version}' +replace = __version__ = '{new_version}' diff --git a/.cookiecutterrc b/.cookiecutterrc new file mode 100644 index 0000000..3d74e87 --- /dev/null +++ b/.cookiecutterrc @@ -0,0 +1,53 @@ +# This file exists so you can easily regenerate your project. +# +# `cookiepatcher` is a convenient shim around `cookiecutter` +# for regenerating projects (it will generate a .cookiecutterrc +# automatically for any template). To use it: +# +# pip install cookiepatcher +# cookiepatcher gh:ionelmc/cookiecutter-pylibrary project-path +# +# See: +# https://pypi.python.org/pypi/cookiepatcher +# +# Alternatively, you can run: +# +# cookiecutter --overwrite-if-exists --config-file=project-path/.cookiecutterrc gh:ionelmc/cookiecutter-pylibrary + +default_context: + + _template: 'gh:ionelmc/cookiecutter-pylibrary' + appveyor: 'yes' + c_extension_function: 'longest' + c_extension_module: '_catalpa_tools' + c_extension_optional: 'no' + c_extension_support: 'no' + codacy: 'yes' + codeclimate: 'yes' + codecov: 'no' + command_line_interface: 'argparse' + command_line_interface_bin_name: 'catalpa-tools' + coveralls: 'yes' + distribution_name: 'catalpa-tools' + email: 'peter@catalpa.io' + full_name: 'Peter Coward' + github_username: 'PeteCoward' + landscape: 'yes' + license: 'BSD 2-Clause License' + linter: 'flake8' + package_name: 'catalpa_tools' + project_name: 'Catalpa Tools' + project_short_description: 'A collection of useful tools developed for and by Catalpa International' + release_date: 'today' + repo_name: 'python-catalpa-tools' + requiresio: 'yes' + scrutinizer: 'yes' + sphinx_doctest: 'yes' + sphinx_theme: 'sphinx-py3doc-enhanced-theme' + test_matrix_configurator: 'no' + test_matrix_separate_coverage: 'no' + test_runner: 'pytest' + travis: 'yes' + version: '0.1.0' + website: 'https://catalpa.io' + year: 'now' diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..8490784 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,16 @@ +[paths] +source = + src/catalpa_tools + */site-packages/catalpa_tools + +[run] +branch = true +source = + catalpa_tools + tests +parallel = true + +[report] +show_missing = true +precision = 2 +omit = *migrations* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4000618 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# see http://editorconfig.org +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 +charset = utf-8 + +[*.{bat,cmd,ps1}] +end_of_line = crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a74475a --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +*.py[cod] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +eggs +.eggs +parts +bin +var +sdist +wheelhouse +develop-eggs +.installed.cfg +lib +lib64 +venv*/ +pyvenv*/ + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +.coverage.* +nosetests.xml +coverage.xml +htmlcov + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject +.idea +*.iml +*.komodoproject + +# Complexity +output/*.html +output/*/index.html + +# Sphinx +docs/_build + +.DS_Store +*~ +.*.sw[po] +.build +.ve +.env +.cache +.pytest +.bootstrap +.appveyor.token +*.bak diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9b0b947 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,49 @@ +language: python +sudo: false +cache: pip +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all + matrix: + - TOXENV=check + - TOXENV=docs +matrix: + include: + - python: '2.7' + env: + - TOXENV=py27,report,coveralls + - python: '3.3' + env: + - TOXENV=py33,report,coveralls + - python: '3.4' + env: + - TOXENV=py34,report,coveralls + - python: '3.5' + env: + - TOXENV=py35,report,coveralls + - python: '3.6' + env: + - TOXENV=py36,report,coveralls + - python: 'pypy-5.4' + env: + - TOXENV=pypy,report,coveralls +before_install: + - python --version + - uname -a + - lsb_release -a +install: + - pip install tox + - virtualenv --version + - easy_install --version + - pip --version + - tox --version +script: + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..7b64852 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,5 @@ + +Authors +======= + +* Peter Coward - https://catalpa.io diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..1997ba9 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,8 @@ + +Changelog +========= + +0.1.0 (2018-05-18) +------------------ + +* First release on PyPI. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..1019ed8 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,90 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +Bug reports +=========== + +When `reporting a bug `_ please include: + + * Your operating system name and version. + * Any details about your local setup that might be helpful in troubleshooting. + * Detailed steps to reproduce the bug. + +Documentation improvements +========================== + +Catalpa Tools could always use more documentation, whether as part of the +official Catalpa Tools docs, in docstrings, or even on the web in blog posts, +articles, and such. + +Feature requests and feedback +============================= + +The best way to send feedback is to file an issue at https://github.com/PeteCoward/python-catalpa-tools/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that code contributions are welcome :) + +Development +=========== + +To set up `python-catalpa-tools` for local development: + +1. Fork `python-catalpa-tools `_ + (look for the "Fork" button). +2. Clone your fork locally:: + + git clone git@github.com:your_name_here/python-catalpa-tools.git + +3. Create a branch for local development:: + + git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +4. When you're done making changes, run all the checks, doc builder and spell checker with `tox `_ one command:: + + tox + +5. Commit your changes and push your branch to GitHub:: + + git add . + git commit -m "Your detailed description of your changes." + git push origin name-of-your-bugfix-or-feature + +6. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +If you need some code review or feedback while you're developing the code just make the pull request. + +For merging, you should: + +1. Include passing tests (run ``tox``) [1]_. +2. Update documentation when there's new API, functionality etc. +3. Add a note to ``CHANGELOG.rst`` about the changes. +4. Add yourself to ``AUTHORS.rst``. + +.. [1] If you don't have all the necessary python versions available locally you can rely on Travis - it will + `run the tests `_ for each change you add in the pull request. + + It will be slower though ... + +Tips +---- + +To run a subset of tests:: + + tox -e envname -- pytest -k test_myfeature + +To run all the test environments in *parallel* (you need to ``pip install detox``):: + + detox diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..93fc672 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +BSD 2-Clause License + +Copyright (c) 2018, Peter Coward +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following +conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following + disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF +USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..3ae9b54 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,19 @@ +graft docs +graft src +graft ci +graft tests + +include .bumpversion.cfg +include .coveragerc +include .cookiecutterrc +include .editorconfig + +include AUTHORS.rst +include CHANGELOG.rst +include CONTRIBUTING.rst +include LICENSE +include README.rst + +include tox.ini .travis.yml appveyor.yml + +global-exclude *.py[cod] __pycache__ *.so *.dylib diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..1a6166e --- /dev/null +++ b/README.rst @@ -0,0 +1,117 @@ +======== +Overview +======== + +.. start-badges + +.. list-table:: + :stub-columns: 1 + + * - docs + - |docs| + * - tests + - | |travis| |appveyor| |requires| + | |coveralls| + | |landscape| |scrutinizer| |codacy| |codeclimate| + * - package + - | |version| |wheel| |supported-versions| |supported-implementations| + | |commits-since| + +.. |docs| image:: https://readthedocs.org/projects/python-catalpa-tools/badge/?style=flat + :target: https://readthedocs.org/projects/python-catalpa-tools + :alt: Documentation Status + +.. |travis| image:: https://travis-ci.org/PeteCoward/python-catalpa-tools.svg?branch=master + :alt: Travis-CI Build Status + :target: https://travis-ci.org/PeteCoward/python-catalpa-tools + +.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/github/PeteCoward/python-catalpa-tools?branch=master&svg=true + :alt: AppVeyor Build Status + :target: https://ci.appveyor.com/project/PeteCoward/python-catalpa-tools + +.. |requires| image:: https://requires.io/github/PeteCoward/python-catalpa-tools/requirements.svg?branch=master + :alt: Requirements Status + :target: https://requires.io/github/PeteCoward/python-catalpa-tools/requirements/?branch=master + +.. |coveralls| image:: https://coveralls.io/repos/PeteCoward/python-catalpa-tools/badge.svg?branch=master&service=github + :alt: Coverage Status + :target: https://coveralls.io/r/PeteCoward/python-catalpa-tools + +.. |landscape| image:: https://landscape.io/github/PeteCoward/python-catalpa-tools/master/landscape.svg?style=flat + :target: https://landscape.io/github/PeteCoward/python-catalpa-tools/master + :alt: Code Quality Status + +.. |codacy| image:: https://img.shields.io/codacy/REPLACE_WITH_PROJECT_ID.svg + :target: https://www.codacy.com/app/PeteCoward/python-catalpa-tools + :alt: Codacy Code Quality Status + +.. |codeclimate| image:: https://codeclimate.com/github/PeteCoward/python-catalpa-tools/badges/gpa.svg + :target: https://codeclimate.com/github/PeteCoward/python-catalpa-tools + :alt: CodeClimate Quality Status + +.. |version| image:: https://img.shields.io/pypi/v/catalpa-tools.svg + :alt: PyPI Package latest release + :target: https://pypi.python.org/pypi/catalpa-tools + +.. |commits-since| image:: https://img.shields.io/github/commits-since/PeteCoward/python-catalpa-tools/v0.1.0.svg + :alt: Commits since latest release + :target: https://github.com/PeteCoward/python-catalpa-tools/compare/v0.1.0...master + +.. |wheel| image:: https://img.shields.io/pypi/wheel/catalpa-tools.svg + :alt: PyPI Wheel + :target: https://pypi.python.org/pypi/catalpa-tools + +.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/catalpa-tools.svg + :alt: Supported versions + :target: https://pypi.python.org/pypi/catalpa-tools + +.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/catalpa-tools.svg + :alt: Supported implementations + :target: https://pypi.python.org/pypi/catalpa-tools + +.. |scrutinizer| image:: https://img.shields.io/scrutinizer/g/PeteCoward/python-catalpa-tools/master.svg + :alt: Scrutinizer Status + :target: https://scrutinizer-ci.com/g/PeteCoward/python-catalpa-tools/ + + +.. end-badges + +A collection of useful tools developed for and by Catalpa International + +* Free software: BSD 2-Clause License + +Installation +============ + +:: + + pip install catalpa-tools + +Documentation +============= + +https://python-catalpa-tools.readthedocs.io/ + +Development +=========== + +To run the all tests run:: + + tox + +Note, to combine the coverage data from all the tox environments run: + +.. list-table:: + :widths: 10 90 + :stub-columns: 1 + + - - Windows + - :: + + set PYTEST_ADDOPTS=--cov-append + tox + + - - Other + - :: + + PYTEST_ADDOPTS=--cov-append tox diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..4589f20 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,87 @@ +version: '{branch}-{build}' +build: off +cache: + - '%LOCALAPPDATA%\pip\Cache' +environment: + global: + WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' + matrix: + - TOXENV: check + TOXPYTHON: C:\Python27\python.exe + PYTHON_HOME: C:\Python27 + PYTHON_VERSION: '2.7' + PYTHON_ARCH: '32' + - TOXENV: 'py27,report' + TOXPYTHON: C:\Python27\python.exe + PYTHON_HOME: C:\Python27 + PYTHON_VERSION: '2.7' + PYTHON_ARCH: '32' + - TOXENV: 'py27,report' + TOXPYTHON: C:\Python27-x64\python.exe + WINDOWS_SDK_VERSION: v7.0 + PYTHON_HOME: C:\Python27-x64 + PYTHON_VERSION: '2.7' + PYTHON_ARCH: '64' + - TOXENV: 'py33,report' + TOXPYTHON: C:\Python33\python.exe + PYTHON_HOME: C:\Python33 + PYTHON_VERSION: '3.3' + PYTHON_ARCH: '32' + - TOXENV: 'py33,report' + TOXPYTHON: C:\Python33-x64\python.exe + WINDOWS_SDK_VERSION: v7.1 + PYTHON_HOME: C:\Python33-x64 + PYTHON_VERSION: '3.3' + PYTHON_ARCH: '64' + - TOXENV: 'py34,report' + TOXPYTHON: C:\Python34\python.exe + PYTHON_HOME: C:\Python34 + PYTHON_VERSION: '3.4' + PYTHON_ARCH: '32' + - TOXENV: 'py34,report' + TOXPYTHON: C:\Python34-x64\python.exe + WINDOWS_SDK_VERSION: v7.1 + PYTHON_HOME: C:\Python34-x64 + PYTHON_VERSION: '3.4' + PYTHON_ARCH: '64' + - TOXENV: 'py35,report' + TOXPYTHON: C:\Python35\python.exe + PYTHON_HOME: C:\Python35 + PYTHON_VERSION: '3.5' + PYTHON_ARCH: '32' + - TOXENV: 'py35,report' + TOXPYTHON: C:\Python35-x64\python.exe + PYTHON_HOME: C:\Python35-x64 + PYTHON_VERSION: '3.5' + PYTHON_ARCH: '64' + - TOXENV: 'py36,report' + TOXPYTHON: C:\Python36\python.exe + PYTHON_HOME: C:\Python36 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '32' + - TOXENV: 'py36,report' + TOXPYTHON: C:\Python36-x64\python.exe + PYTHON_HOME: C:\Python36-x64 + PYTHON_VERSION: '3.6' + PYTHON_ARCH: '64' +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - python -u ci\appveyor-bootstrap.py + - '%PYTHON_HOME%\Scripts\virtualenv --version' + - '%PYTHON_HOME%\Scripts\easy_install --version' + - '%PYTHON_HOME%\Scripts\pip --version' + - '%PYTHON_HOME%\Scripts\tox --version' +test_script: + - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' + +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* +artifacts: + - path: dist\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/ci/appveyor-bootstrap.py b/ci/appveyor-bootstrap.py new file mode 100644 index 0000000..01f2c7b --- /dev/null +++ b/ci/appveyor-bootstrap.py @@ -0,0 +1,116 @@ +""" +AppVeyor will at least have few Pythons around so there's no point of implementing a bootstrapper in PowerShell. + +This is a port of https://github.com/pypa/python-packaging-user-guide/blob/master/source/code/install.ps1 +with various fixes and improvements that just weren't feasible to implement in PowerShell. +""" +from __future__ import print_function +from os import environ +from os.path import exists +from subprocess import check_call + +try: + from urllib.request import urlretrieve +except ImportError: + from urllib import urlretrieve + +BASE_URL = "https://www.python.org/ftp/python/" +GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py" +GET_PIP_PATH = "C:\get-pip.py" +URLS = { + ("2.7", "64"): BASE_URL + "2.7.13/python-2.7.13.amd64.msi", + ("2.7", "32"): BASE_URL + "2.7.13/python-2.7.13.msi", + # NOTE: no .msi installer for 3.3.6 + ("3.3", "64"): BASE_URL + "3.3.5/python-3.3.5.amd64.msi", + ("3.3", "32"): BASE_URL + "3.3.5/python-3.3.5.msi", + ("3.4", "64"): BASE_URL + "3.4.4/python-3.4.4.amd64.msi", + ("3.4", "32"): BASE_URL + "3.4.4/python-3.4.4.msi", + ("3.5", "64"): BASE_URL + "3.5.4/python-3.5.4-amd64.exe", + ("3.5", "32"): BASE_URL + "3.5.4/python-3.5.4.exe", + ("3.6", "64"): BASE_URL + "3.6.2/python-3.6.2-amd64.exe", + ("3.6", "32"): BASE_URL + "3.6.2/python-3.6.2.exe", +} +INSTALL_CMD = { + # Commands are allowed to fail only if they are not the last command. Eg: uninstall (/x) allowed to fail. + "2.7": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], + ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], + "3.3": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], + ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], + "3.4": [["msiexec.exe", "/L*+!", "install.log", "/qn", "/x", "{path}"], + ["msiexec.exe", "/L*+!", "install.log", "/qn", "/i", "{path}", "TARGETDIR={home}"]], + "3.5": [["{path}", "/quiet", "TargetDir={home}"]], + "3.6": [["{path}", "/quiet", "TargetDir={home}"]], +} + + +def download_file(url, path): + print("Downloading: {} (into {})".format(url, path)) + progress = [0, 0] + + def report(count, size, total): + progress[0] = count * size + if progress[0] - progress[1] > 1000000: + progress[1] = progress[0] + print("Downloaded {:,}/{:,} ...".format(progress[1], total)) + + dest, _ = urlretrieve(url, path, reporthook=report) + return dest + + +def install_python(version, arch, home): + print("Installing Python", version, "for", arch, "bit architecture to", home) + if exists(home): + return + + path = download_python(version, arch) + print("Installing", path, "to", home) + success = False + for cmd in INSTALL_CMD[version]: + cmd = [part.format(home=home, path=path) for part in cmd] + print("Running:", " ".join(cmd)) + try: + check_call(cmd) + except Exception as exc: + print("Failed command", cmd, "with:", exc) + if exists("install.log"): + with open("install.log") as fh: + print(fh.read()) + else: + success = True + if success: + print("Installation complete!") + else: + print("Installation failed") + + +def download_python(version, arch): + for _ in range(3): + try: + return download_file(URLS[version, arch], "installer.exe") + except Exception as exc: + print("Failed to download:", exc) + print("Retrying ...") + + +def install_pip(home): + pip_path = home + "/Scripts/pip.exe" + python_path = home + "/python.exe" + if exists(pip_path): + print("pip already installed.") + else: + print("Installing pip...") + download_file(GET_PIP_URL, GET_PIP_PATH) + print("Executing:", python_path, GET_PIP_PATH) + check_call([python_path, GET_PIP_PATH]) + + +def install_packages(home, *packages): + cmd = [home + "/Scripts/pip.exe", "install"] + cmd.extend(packages) + check_call(cmd) + + +if __name__ == "__main__": + install_python(environ['PYTHON_VERSION'], environ['PYTHON_ARCH'], environ['PYTHON_HOME']) + install_pip(environ['PYTHON_HOME']) + install_packages(environ['PYTHON_HOME'], "setuptools>=18.0.1", "wheel", "tox", "virtualenv>=13.1.0") diff --git a/ci/appveyor-download.py b/ci/appveyor-download.py new file mode 100755 index 0000000..f3621d8 --- /dev/null +++ b/ci/appveyor-download.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +""" +Use the AppVeyor API to download Windows artifacts. + +Taken from: https://bitbucket.org/ned/coveragepy/src/tip/ci/download_appveyor.py +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +""" +from __future__ import unicode_literals + +import argparse +import os +import zipfile + +import requests + + +def make_auth_headers(): + """Make the authentication headers needed to use the Appveyor API.""" + path = os.path.expanduser("~/.appveyor.token") + if not os.path.exists(path): + raise RuntimeError( + "Please create a file named `.appveyor.token` in your home directory. " + "You can get the token from https://ci.appveyor.com/api-token" + ) + with open(path) as f: + token = f.read().strip() + + headers = { + 'Authorization': 'Bearer {}'.format(token), + } + return headers + + +def download_latest_artifacts(account_project, build_id): + """Download all the artifacts from the latest build.""" + if build_id is None: + url = "https://ci.appveyor.com/api/projects/{}".format(account_project) + else: + url = "https://ci.appveyor.com/api/projects/{}/build/{}".format(account_project, build_id) + build = requests.get(url, headers=make_auth_headers()).json() + jobs = build['build']['jobs'] + print(u"Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs))) + + for job in jobs: + name = job['name'] + print(u" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job)) + + url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts".format(job['jobId']) + response = requests.get(url, headers=make_auth_headers()) + artifacts = response.json() + + for artifact in artifacts: + is_zip = artifact['type'] == "Zip" + filename = artifact['fileName'] + print(u" {0}, {1} bytes".format(filename, artifact['size'])) + + url = "https://ci.appveyor.com/api/buildjobs/{}/artifacts/{}".format(job['jobId'], filename) + download_url(url, filename, make_auth_headers()) + + if is_zip: + unpack_zipfile(filename) + os.remove(filename) + + +def ensure_dirs(filename): + """Make sure the directories exist for `filename`.""" + dirname = os.path.dirname(filename) + if dirname and not os.path.exists(dirname): + os.makedirs(dirname) + + +def download_url(url, filename, headers): + """Download a file from `url` to `filename`.""" + ensure_dirs(filename) + response = requests.get(url, headers=headers, stream=True) + if response.status_code == 200: + with open(filename, 'wb') as f: + for chunk in response.iter_content(16 * 1024): + f.write(chunk) + else: + print(u" Error downloading {}: {}".format(url, response)) + + +def unpack_zipfile(filename): + """Unpack a zipfile, using the names in the zip.""" + with open(filename, 'rb') as fzip: + z = zipfile.ZipFile(fzip) + for name in z.namelist(): + print(u" extracting {}".format(name)) + ensure_dirs(name) + z.extract(name) + + +parser = argparse.ArgumentParser(description='Download artifacts from AppVeyor.') +parser.add_argument('--id', + metavar='PROJECT_ID', + default='PeteCoward/python-catalpa-tools', + help='Project ID in AppVeyor.') +parser.add_argument('build', + nargs='?', + metavar='BUILD_ID', + help='Build ID in AppVeyor. Eg: master-123') + +if __name__ == "__main__": + # import logging + # logging.basicConfig(level="DEBUG") + args = parser.parse_args() + download_latest_artifacts(args.id, args.build) diff --git a/ci/appveyor-with-compiler.cmd b/ci/appveyor-with-compiler.cmd new file mode 100644 index 0000000..289585f --- /dev/null +++ b/ci/appveyor-with-compiler.cmd @@ -0,0 +1,23 @@ +:: Very simple setup: +:: - if WINDOWS_SDK_VERSION is set then activate the SDK. +:: - disable the WDK if it's around. + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET WIN_WDK="c:\Program Files (x86)\Windows Kits\10\Include\wdf" +ECHO SDK: %WINDOWS_SDK_VERSION% ARCH: %PYTHON_ARCH% + +IF EXIST %WIN_WDK% ( + REM See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ + REN %WIN_WDK% 0wdf +) +IF "%WINDOWS_SDK_VERSION%"=="" GOTO main + +SET DISTUTILS_USE_SDK=1 +SET MSSdk=1 +"%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% +CALL "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + +:main +ECHO Executing: %COMMAND_TO_RUN% +CALL %COMMAND_TO_RUN% || EXIT 1 diff --git a/ci/bootstrap.py b/ci/bootstrap.py new file mode 100755 index 0000000..e5292aa --- /dev/null +++ b/ci/bootstrap.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from __future__ import absolute_import, print_function, unicode_literals + +import os +import sys +from os.path import abspath +from os.path import dirname +from os.path import exists +from os.path import join + + +if __name__ == "__main__": + base_path = dirname(dirname(abspath(__file__))) + print("Project path: {0}".format(base_path)) + env_path = join(base_path, ".tox", "bootstrap") + if sys.platform == "win32": + bin_path = join(env_path, "Scripts") + else: + bin_path = join(env_path, "bin") + if not exists(env_path): + import subprocess + + print("Making bootstrap env in: {0} ...".format(env_path)) + try: + subprocess.check_call(["virtualenv", env_path]) + except subprocess.CalledProcessError: + subprocess.check_call([sys.executable, "-m", "virtualenv", env_path]) + print("Installing `jinja2` into bootstrap environment...") + subprocess.check_call([join(bin_path, "pip"), "install", "jinja2"]) + activate = join(bin_path, "activate_this.py") + # noinspection PyCompatibility + exec(compile(open(activate, "rb").read(), activate, "exec"), dict(__file__=activate)) + + import jinja2 + + import subprocess + + jinja = jinja2.Environment( + loader=jinja2.FileSystemLoader(join(base_path, "ci", "templates")), + trim_blocks=True, + lstrip_blocks=True, + keep_trailing_newline=True + ) + + tox_environments = [ + line.strip() + # WARNING: 'tox' must be installed globally or in the project's virtualenv + for line in subprocess.check_output(['tox', '--listenvs'], universal_newlines=True).splitlines() + ] + tox_environments = [line for line in tox_environments if line not in ['clean', 'report', 'docs', 'check']] + + for name in os.listdir(join("ci", "templates")): + with open(join(base_path, name), "w") as fh: + fh.write(jinja.get_template(name).render(tox_environments=tox_environments)) + print("Wrote {}".format(name)) + print("DONE.") diff --git a/ci/templates/.travis.yml b/ci/templates/.travis.yml new file mode 100644 index 0000000..4459e87 --- /dev/null +++ b/ci/templates/.travis.yml @@ -0,0 +1,36 @@ +language: python +sudo: false +cache: pip +env: + global: + - LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so + - SEGFAULT_SIGNALS=all + matrix: + - TOXENV=check + - TOXENV=docs +matrix: + include: +{%- for env in tox_environments %}{{ '' }} + - python: '{{ '{0[0]}-5.4'.format(env.split('-')) if env.startswith('pypy') else '{0[2]}.{0[3]}'.format(env) }}' + env: + - TOXENV={{ env }},report,coveralls +{%- endfor %}{{ '' }} +before_install: + - python --version + - uname -a + - lsb_release -a +install: + - pip install tox + - virtualenv --version + - easy_install --version + - pip --version + - tox --version +script: + - tox -v +after_failure: + - more .tox/log/* | cat + - more .tox/*/log/* | cat +notifications: + email: + on_success: never + on_failure: always diff --git a/ci/templates/appveyor.yml b/ci/templates/appveyor.yml new file mode 100644 index 0000000..4d440ff --- /dev/null +++ b/ci/templates/appveyor.yml @@ -0,0 +1,51 @@ +version: '{branch}-{build}' +build: off +cache: + - '%LOCALAPPDATA%\pip\Cache' +environment: + global: + WITH_COMPILER: 'cmd /E:ON /V:ON /C .\ci\appveyor-with-compiler.cmd' + matrix: + - TOXENV: check + TOXPYTHON: C:\Python27\python.exe + PYTHON_HOME: C:\Python27 + PYTHON_VERSION: '2.7' + PYTHON_ARCH: '32' +{% for env in tox_environments %}{{ '' }}{% if env.startswith(('py2', 'py3')) %} + - TOXENV: '{{ env }},report' + TOXPYTHON: C:\Python{{ env[2:4] }}\python.exe + PYTHON_HOME: C:\Python{{ env[2:4] }} + PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' + PYTHON_ARCH: '32' + - TOXENV: '{{ env }},report' + TOXPYTHON: C:\Python{{ env[2:4] }}-x64\python.exe + {%- if env.startswith(('py2', 'py33', 'py34')) %} + + WINDOWS_SDK_VERSION: v7.{{ '1' if env.startswith('py3') else '0' }} + {%- endif %} + + PYTHON_HOME: C:\Python{{ env[2:4] }}-x64 + PYTHON_VERSION: '{{ env[2] }}.{{ env[3] }}' + PYTHON_ARCH: '64' +{% endif %}{% endfor %} +init: + - ps: echo $env:TOXENV + - ps: ls C:\Python* +install: + - python -u ci\appveyor-bootstrap.py + - '%PYTHON_HOME%\Scripts\virtualenv --version' + - '%PYTHON_HOME%\Scripts\easy_install --version' + - '%PYTHON_HOME%\Scripts\pip --version' + - '%PYTHON_HOME%\Scripts\tox --version' +test_script: + - '%WITH_COMPILER% %PYTHON_HOME%\Scripts\tox' + +on_failure: + - ps: dir "env:" + - ps: get-content .tox\*\log\* +artifacts: + - path: dist\* + +### To enable remote debugging uncomment this (also, see: http://www.appveyor.com/docs/how-to/rdp-to-build-worker): +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..e122f91 --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1 @@ +.. include:: ../AUTHORS.rst diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..565b052 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGELOG.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..d7e6f99 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import os + + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.coverage', + 'sphinx.ext.doctest', + 'sphinx.ext.extlinks', + 'sphinx.ext.ifconfig', + 'sphinx.ext.napoleon', + 'sphinx.ext.todo', + 'sphinx.ext.viewcode', +] +if os.getenv('SPELLCHECK'): + extensions += 'sphinxcontrib.spelling', + spelling_show_suggestions = True + spelling_lang = 'en_US' + +source_suffix = '.rst' +master_doc = 'index' +project = 'Catalpa Tools' +year = '2018' +author = 'Peter Coward' +copyright = '{0}, {1}'.format(year, author) +version = release = '0.1.0' + +pygments_style = 'trac' +templates_path = ['.'] +extlinks = { + 'issue': ('https://github.com/PeteCoward/python-catalpa-tools/issues/%s', '#'), + 'pr': ('https://github.com/PeteCoward/python-catalpa-tools/pull/%s', 'PR #'), +} +import sphinx_py3doc_enhanced_theme +html_theme = "sphinx_py3doc_enhanced_theme" +html_theme_path = [sphinx_py3doc_enhanced_theme.get_html_theme_path()] +html_theme_options = { + 'githuburl': 'https://github.com/PeteCoward/python-catalpa-tools/' +} + +html_use_smartypants = True +html_last_updated_fmt = '%b %d, %Y' +html_split_index = False +html_sidebars = { + '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'], +} +html_short_title = '%s-%s' % (project, version) + +napoleon_use_ivar = True +napoleon_use_rtype = False +napoleon_use_param = False diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000..e582053 --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..40f35b5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +======== +Contents +======== + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + reference/index + contributing + authors + changelog + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..2ba296b --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,7 @@ +============ +Installation +============ + +At the command line:: + + pip install catalpa-tools diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 0000000..72a3355 --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1 @@ +.. include:: ../README.rst diff --git a/docs/reference/catalpa_tools.rst b/docs/reference/catalpa_tools.rst new file mode 100644 index 0000000..da397ac --- /dev/null +++ b/docs/reference/catalpa_tools.rst @@ -0,0 +1,9 @@ +catalpa_tools +============= + +.. testsetup:: + + from catalpa_tools import * + +.. automodule:: catalpa_tools + :members: diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 0000000..75cf295 --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,7 @@ +Reference +========= + +.. toctree:: + :glob: + + catalpa_tools* diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..ef4a013 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=1.3 +sphinx-py3doc-enhanced-theme +-e . diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 0000000..f95eb78 --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,11 @@ +builtin +builtins +classmethod +staticmethod +classmethods +staticmethods +args +kwargs +callstack +Changelog +Indices diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..370e4d6 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,7 @@ +===== +Usage +===== + +To use Catalpa Tools in a project:: + + import catalpa_tools diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..bd7a364 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,32 @@ +[bdist_wheel] +universal = 1 + + +[flake8] +max-line-length = 140 +exclude = */migrations/* + +[tool:pytest] +testpaths = tests +norecursedirs = + migrations + +python_files = + test_*.py + *_test.py + tests.py +addopts = + -ra + --strict + --doctest-modules + --doctest-glob=\*.rst + --tb=short + +[isort] +force_single_line = True +line_length = 120 +known_first_party = catalpa_tools +default_section = THIRDPARTY +forced_separate = test_catalpa_tools +not_skip = __init__.py +skip = migrations diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..39dffa7 --- /dev/null +++ b/setup.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +from __future__ import absolute_import +from __future__ import print_function + +import io +import re +from glob import glob +from os.path import basename +from os.path import dirname +from os.path import join +from os.path import splitext + +from setuptools import find_packages +from setuptools import setup + + +def read(*names, **kwargs): + return io.open( + join(dirname(__file__), *names), + encoding=kwargs.get('encoding', 'utf8') + ).read() + + +setup( + name='catalpa-tools', + version='0.1.0', + license='BSD 2-Clause License', + description='A collection of useful tools developed for and by Catalpa International', + long_description='%s\n%s' % ( + re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), + re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')) + ), + author='Peter Coward', + author_email='peter@catalpa.io', + url='https://github.com/PeteCoward/python-catalpa-tools', + packages=find_packages('src'), + package_dir={'': 'src'}, + py_modules=[splitext(basename(path))[0] for path in glob('src/*.py')], + include_package_data=True, + zip_safe=False, + classifiers=[ + # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: Unix', + 'Operating System :: POSIX', + 'Operating System :: Microsoft :: Windows', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + '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 :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + # uncomment if you test on these interpreters: + # 'Programming Language :: Python :: Implementation :: IronPython', + # 'Programming Language :: Python :: Implementation :: Jython', + # 'Programming Language :: Python :: Implementation :: Stackless', + 'Topic :: Utilities', + ], + keywords=[ + # eg: 'keyword1', 'keyword2', 'keyword3', + ], + install_requires=[ + # eg: 'aspectlib==1.1.1', 'six>=1.7', + ], + extras_require={ + # eg: + # 'rst': ['docutils>=0.11'], + # ':python_version=="2.6"': ['argparse'], + }, + entry_points={ + 'console_scripts': [ + 'catalpa-tools = catalpa_tools.cli:main', + ] + }, +) diff --git a/src/catalpa_tools/__init__.py b/src/catalpa_tools/__init__.py new file mode 100644 index 0000000..b794fd4 --- /dev/null +++ b/src/catalpa_tools/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.0' diff --git a/src/catalpa_tools/__main__.py b/src/catalpa_tools/__main__.py new file mode 100644 index 0000000..37a72e9 --- /dev/null +++ b/src/catalpa_tools/__main__.py @@ -0,0 +1,14 @@ +""" +Entrypoint module, in case you use `python -mcatalpa_tools`. + + +Why does this file exist, and why __main__? For more info, read: + +- https://www.python.org/dev/peps/pep-0338/ +- https://docs.python.org/2/using/cmdline.html#cmdoption-m +- https://docs.python.org/3/using/cmdline.html#cmdoption-m +""" +from catalpa_tools.cli import main + +if __name__ == "__main__": + main() diff --git a/src/catalpa_tools/cli.py b/src/catalpa_tools/cli.py new file mode 100644 index 0000000..e893fa4 --- /dev/null +++ b/src/catalpa_tools/cli.py @@ -0,0 +1,26 @@ +""" +Module that contains the command line app. + +Why does this file exist, and why not put this in __main__? + + You might be tempted to import things from __main__ later, but that will cause + problems: the code will get executed twice: + + - When you run `python -mcatalpa_tools` python will execute + ``__main__.py`` as a script. That means there won't be any + ``catalpa_tools.__main__`` in ``sys.modules``. + - When you import __main__ it will get executed again (as a module) because + there's no ``catalpa_tools.__main__`` in ``sys.modules``. + + Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration +""" +import argparse + +parser = argparse.ArgumentParser(description='Command description.') +parser.add_argument('names', metavar='NAME', nargs=argparse.ZERO_OR_MORE, + help="A name of something.") + + +def main(args=None): + args = parser.parse_args(args=args) + print(args.names) diff --git a/tests/test_catalpa_tools.py b/tests/test_catalpa_tools.py new file mode 100644 index 0000000..3b0a58a --- /dev/null +++ b/tests/test_catalpa_tools.py @@ -0,0 +1,6 @@ + +from catalpa_tools.cli import main + + +def test_main(): + main([]) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b8cb302 --- /dev/null +++ b/tox.ini @@ -0,0 +1,102 @@ +; a generative tox configuration, see: https://testrun.org/tox/latest/config.html#generative-envlist + +[tox] +envlist = + clean, + check, + {py27,py33,py34,py35,py36,pypy}, + report, + docs + +[testenv] +basepython = + pypy: {env:TOXPYTHON:pypy} + {py27,docs,spell}: {env:TOXPYTHON:python2.7} + py33: {env:TOXPYTHON:python3.3} + py34: {env:TOXPYTHON:python3.4} + py35: {env:TOXPYTHON:python3.5} + py36: {env:TOXPYTHON:python3.6} + {bootstrap,clean,check,report,coveralls,codecov}: {env:TOXPYTHON:python3} +setenv = + PYTHONPATH={toxinidir}/tests + PYTHONUNBUFFERED=yes +passenv = + * +usedevelop = false +deps = + pytest + pytest-travis-fold + pytest-cov +commands = + {posargs:pytest --cov --cov-report=term-missing -vv tests} + +[testenv:bootstrap] +deps = + jinja2 + matrix +skip_install = true +commands = + python ci/bootstrap.py +[testenv:spell] +setenv = + SPELLCHECK=1 +commands = + sphinx-build -b spelling docs dist/docs +skip_install = true +deps = + -r{toxinidir}/docs/requirements.txt + sphinxcontrib-spelling + pyenchant + +[testenv:docs] +deps = + -r{toxinidir}/docs/requirements.txt +commands = + sphinx-build {posargs:-E} -b doctest docs dist/docs + sphinx-build {posargs:-E} -b html docs dist/docs + sphinx-build -b linkcheck docs dist/docs + +[testenv:check] +deps = + docutils + check-manifest + flake8 + readme-renderer + pygments + isort +skip_install = true +commands = + python setup.py check --strict --metadata --restructuredtext + check-manifest {toxinidir} + flake8 src tests setup.py + isort --verbose --check-only --diff --recursive src tests setup.py + +[testenv:coveralls] +deps = + coveralls +skip_install = true +commands = + coveralls [] + +[testenv:codecov] +deps = + codecov +skip_install = true +commands = + coverage xml --ignore-errors + codecov [] + + +[testenv:report] +deps = coverage +skip_install = true +commands = + coverage combine --append + coverage report + coverage html + +[testenv:clean] +commands = coverage erase +skip_install = true +deps = coverage +