commit 983731b028c3ec5dc2c345019aadbc828cf9ec6d Author: Peter Williams Date: Sat Apr 20 23:17:56 2019 -0400 Initial import. Strongly deriving from current pywwt, but I've tried to go through and rip out all actual content. I want to start right off with docs, testing, code coverage, and so on. diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..b1831c5 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,12 @@ +[run] +omit = + wwt_api_client/*tests/* + +[report] +omit = + wwt_api_client/*tests/* + +exclude_lines = + pragma: no cover + if __name__ == .__main__.: + raise NotImplementedError diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46a96ef --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/*.egg +/*.egg-info +*.py[cod] +.coverage +__pycache__/ +build +dist +MANIFEST +sdist diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..550c770 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: c + +os: + - linux + +sudo: false + +notifications: + email: false + +env: + global: + - CONDA_DEPENDENCIES="pytest pytest-cov nomkl" + - PIP_DEPENDENCIES="sphinx-automodapi numpydoc sphinx_rtd_theme pytest-faulthandler codecov" + matrix: + - PYTHON_VERSION=2.7 + - PYTHON_VERSION=3.6 + - PYTHON_VERSION=3.7 + +install: + - git clone git://github.com/astropy/ci-helpers.git + - source ci-helpers/travis/setup_conda.sh + +script: + - python setup.py sdist + - cd dist; pip install *.tar.gz --verbose; cd .. + - pytest wwt_api_client --cov wwt_api_client + - cd docs ; make html linkcheck ; cd .. + +after_success: + - codecov diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..147da48 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,4 @@ +0.1.0 (unreleased) +------------------ + +- Nothing as yet. diff --git a/LICENSE.rst b/LICENSE.rst new file mode 100644 index 0000000..e429e7f --- /dev/null +++ b/LICENSE.rst @@ -0,0 +1,24 @@ +Copyright (c) 2019 the .Net Foundation + +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. +* Neither the name of the WorldWide Telescope project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +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..0a2c695 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,19 @@ +include LICENSE.rst +include README.rst +include CHANGES.rst + +include setupbase.py +include pytest.ini +include .coveragerc + +graft docs +prune docs/build +prune docs/gh-pages +prune docs/dist + +global-exclude *~ +global-exclude *.pyc +global-exclude *.pyo +global-exclude .git +global-exclude __pycache__ + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..8b2e68c --- /dev/null +++ b/README.rst @@ -0,0 +1,31 @@ +.. image:: https://travis-ci.org/WorldWideTelescope/wwt_api_client.svg?branch=tests + :target: https://travis-ci.org/WorldWideTelescope/wwt_api_client + +.. image:: https://readthedocs.org/projects/wwt_api_client/badge/?version=latest + :target: http://wwt_api_client.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + + +Accessing the AAS WorldWide Telescope Web Service from Python +============================================================= + +The wwt_api_client package provides a Python interface to the various web +services that power the `AAS `_ `WorldWide Telescope +`_ project. + +The documentation, including installation instructions, can be found at +http://wwt_api_client.readthedocs.io/ + + +Reporting issues +---------------- + +If you run into any issues, please open an issue `here +`_ + + +Acknowledgments +--------------- + +This work is funded through the American Astronomical Society’s support of the +WorldWide Telescope project. diff --git a/RELEASE.rst b/RELEASE.rst new file mode 100644 index 0000000..3c8816a --- /dev/null +++ b/RELEASE.rst @@ -0,0 +1,47 @@ +Releasing +========= + +(Copied from ``pywwt``.) + +To do a release, first edit the ``CHANGES.rst`` file to include the date of +the release after the version number, and make sure the changelog is up to +date. + +Then edit the following files to update the version numbers: + +* ``docs/conf.py`` +* ``wwt_api_client/_version.py`` (make sure the string in the tuple is set to ``final``) + +At this point, commit all changes and use the following for the commit +message:: + + Preparing release v + +Now make sure there are no temporary files in the repository:: + + git clean -fxd + +and build the release files:: + + python setup.py sdist bdist_wheel --universal + +Go inside the ``dist`` directory, and you should see a tar file and a wheel. +At this point, if you wish, you can untar the tar file and try installing and +testing the installation. Once you are satisfied that the release is good you +can upload the release using twine:: + + twine upload wwt_api_client-.tar.gz wwt_api_client--py2.py3-none-any.whl + +If you don't have twine installed, you can get it with ``pip install twine``. + +At this point, you can tag the release with:: + + git tag -m v v + +If you have PGP keys set up, you can sign the tag by also including ``-s``. + +Now change the versions in the files listed above to the next version — and +for the ``wwt_api_client/_version.py`` file, change ``final`` to ``dev``. +Commit the changes with:: + + Back to development: v diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..34d43d6 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -W +SPHINXBUILD = python -msphinx +SPHINXPROJ = wwt_api_client +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..24291e4 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,6 @@ +API Documentation +================= + +.. automodapi:: wwt_api_client + :no-inheritance-diagram: + :no-inherited-members: diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..c516008 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# wwt_api_client documentation build configuration file, copied from pywwt. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx_automodapi.automodapi', + 'sphinx_automodapi.smart_resolver', + 'numpydoc'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'wwt_api_client' +author = 'Peter K. G. Williams' +copyright = '2019 ' + author + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1.0dev0' +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + 'donate.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'wwt_api_clientdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'wwt_api_client.tex', 'wwt_api_client Documentation', + 'Thomas P. Robitaille', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'wwt_api_client', 'wwt_api_client Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'wwt_api_client', 'wwt_api_client Documentation', + author, 'wwt_api_client', 'One line description of project.', + 'Miscellaneous'), +] + + + +# Configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', + (None, 'http://data.astropy.org/intersphinx/python3.inv')), +} +numpydoc_show_class_members = False + +nitpicky = True +nitpick_ignore = [('py:class', 'ipywidgets.widgets.domwidget.DOMWidget')] + +default_role = 'obj' + +html_logo = 'images/logo.png' diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000..c6e198e Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..01f0874 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,34 @@ +wwt_api_client: Pythonic access to the WorldWide Telescope web services +======================================================================= + +The AAS `WorldWide Telescope `_ is a free +and powerful visualization engine developed by the `American Astronomical +Society `_ that can display astronomical and planetary data. +This engine is powered by a large (multi-terabyte) collection of astronomical +survey data stored in the cloud. The ``wwt_api_client`` package allows you to +invoke the web APIs that constitute the WWT backend API. + + +User guide +---------- + +.. toctree:: + :maxdepth: 1 + + installation + api + + +Getting help +------------ + +If you run into any issues when using ``wwt_api_client``, please open an issue +`on its GitHub repository +`_. + + +Acknowledgments +--------------- + +Work on ``wwt_api_client`` is funded through the American Astronomical +Society’s support of the WorldWide Telescope project. diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..4c175c5 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,30 @@ +Installation +============ + +Try without installing +---------------------- + +Not yet implemented. + +Installing wwt_api_client with conda (recommended) +----------------------------------------- + +Not yet implemented. + +Installing wwt_api_client with pip +------------------------- + +You can also install the latest release of wwt_api_client using `pip +`_:: + + pip install wwt_api_client + +Installing the developer version +-------------------------------- + +If you want to use the very latest developer version, you can clone this +repository and install the package manually:: + + git clone https://github.com/WorldWideTelescope/wwt_api_client.git + cd wwt_api_client + pip install -e . diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..93ef497 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts=-p no:logging diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 0000000..b387552 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,10 @@ +build: + image: latest + +python: + version: 3.7 + pip_install: true + extra_requirements: ['docs'] + +# Don't build any extra formats +formats: [] diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..d7f6981 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +universal=1 + +[metadata] +description-file = README.rst diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..e810deb --- /dev/null +++ b/setup.py @@ -0,0 +1,66 @@ +#! /usr/bin/env python +# -*- mode: python; coding: utf-8 -*- +# Copyright 2019 the .Net Foundation +# Distributed under the terms of the revised (3-clause) BSD license. + +from __future__ import absolute_import, division, print_function + +from os.path import join as pjoin +from setuptools import setup + +from setupbase import find_packages, get_version + +name = 'wwt_api_client' +version = get_version(pjoin(name, '_version.py')) + +with open('README.rst') as f: + LONG_DESCRIPTION = f.read() + +setup_args = dict( + name = name, + description = 'An API client for the AAS WorldWide Telescope web services', + long_description = LONG_DESCRIPTION, + version = version, + packages = find_packages(), + author = 'Peter K. G. Williams', + author_email = 'peter@newton.cx', + url = 'https://github.com/WorldWideTelescope/wwt_api_client', + license = 'BSD', + platforms = "Linux, Mac OS X, Windows", + keywords = ['Science'], + classifiers = [ + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Topic :: Multimedia :: Graphics', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + include_package_data = True, + install_requires = [ + 'six' + ], + extras_require = { + 'test': [ + 'pytest', + 'pytest-cov', + ], + 'docs': [ + 'sphinx>=1.6', + 'sphinx-automodapi', + 'numpydoc', + 'sphinx_rtd_theme', + ], + }, + entry_points = { + }, +) + +if __name__ == '__main__': + setup(**setup_args) diff --git a/setupbase.py b/setupbase.py new file mode 100644 index 0000000..42e6cfc --- /dev/null +++ b/setupbase.py @@ -0,0 +1,702 @@ +#! /usr/bin/env python +# -*- mode: python; coding: utf-8 -*- +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +"""This file originates from the 'jupyter-packaging' package. We borrowed it +from pywwt. We only use a couple of simple functions and could probably just +as well skip it. + +""" +from collections import defaultdict +from os.path import join as pjoin +import io +import os +import functools +import pipes +import re +import shlex +import subprocess +import sys + + +# BEFORE importing distutils, remove MANIFEST. distutils doesn't properly +# update it when the contents of directories change. +if os.path.exists('MANIFEST'): os.remove('MANIFEST') + + +from distutils.cmd import Command +from distutils.command.build_py import build_py +from distutils.command.sdist import sdist +from distutils import log + +from setuptools.command.develop import develop +from setuptools.command.bdist_egg import bdist_egg + +try: + from wheel.bdist_wheel import bdist_wheel +except ImportError: + bdist_wheel = None + +if sys.platform == 'win32': + from subprocess import list2cmdline +else: + def list2cmdline(cmd_list): + return ' '.join(map(pipes.quote, cmd_list)) + + +__version__ = '0.2.0' + +# --------------------------------------------------------------------------- +# Top Level Variables +# --------------------------------------------------------------------------- + +HERE = os.path.abspath(os.path.dirname(__file__)) +is_repo = os.path.exists(pjoin(HERE, '.git')) +node_modules = pjoin(HERE, 'node_modules') + +SEPARATORS = os.sep if os.altsep is None else os.sep + os.altsep + +npm_path = ':'.join([ + pjoin(HERE, 'node_modules', '.bin'), + os.environ.get('PATH', os.defpath), +]) + +if "--skip-npm" in sys.argv: + print("Skipping npm install as requested.") + skip_npm = True + sys.argv.remove("--skip-npm") +else: + skip_npm = False + + +# --------------------------------------------------------------------------- +# Public Functions +# --------------------------------------------------------------------------- + +def get_version(file, name='__version__'): + """Get the version of the package from the given file by + executing it and extracting the given `name`. + """ + path = os.path.realpath(file) + version_ns = {} + with io.open(path, encoding="utf8") as f: + exec(f.read(), {}, version_ns) + return version_ns[name] + + +def ensure_python(specs): + """Given a list of range specifiers for python, ensure compatibility. + """ + if not isinstance(specs, (list, tuple)): + specs = [specs] + v = sys.version_info + part = '%s.%s' % (v.major, v.minor) + for spec in specs: + if part == spec: + return + try: + if eval(part + spec): + return + except SyntaxError: + pass + raise ValueError('Python version %s unsupported' % part) + + +def find_packages(top=HERE): + """ + Find all of the packages. + """ + packages = [] + for d, dirs, _ in os.walk(top, followlinks=True): + if os.path.exists(pjoin(d, '__init__.py')): + packages.append(os.path.relpath(d, top).replace(os.path.sep, '.')) + elif d != top: + # Do not look for packages in subfolders if current is not a package + dirs[:] = [] + return packages + + +def update_package_data(distribution): + """update build_py options to get package_data changes""" + build_py = distribution.get_command_obj('build_py') + build_py.finalize_options() + + +class bdist_egg_disabled(bdist_egg): + """Disabled version of bdist_egg + + Prevents setup.py install performing setuptools' default easy_install, + which it should never ever do. + """ + def run(self): + sys.exit("Aborting implicit building of eggs. Use `pip install .` " + " to install from source.") + + +def create_cmdclass(prerelease_cmd=None, package_data_spec=None, + data_files_spec=None): + """Create a command class with the given optional prerelease class. + + Parameters + ---------- + prerelease_cmd: (name, Command) tuple, optional + The command to run before releasing. + package_data_spec: dict, optional + A dictionary whose keys are the dotted package names and + whose values are a list of glob patterns. + data_files_spec: list, optional + A list of (path, dname, pattern) tuples where the path is the + `data_files` install path, dname is the source directory, and the + pattern is a glob pattern. + + Notes + ----- + We use specs so that we can find the files *after* the build + command has run. + + The package data glob patterns should be relative paths from the package + folder containing the __init__.py file, which is given as the package + name. + e.g. `dict(foo=['./bar/*', './baz/**'])` + + The data files directories should be absolute paths or relative paths + from the root directory of the repository. Data files are specified + differently from `package_data` because we need a separate path entry + for each nested folder in `data_files`, and this makes it easier to + parse. + e.g. `('share/foo/bar', 'pkgname/bizz, '*')` + """ + wrapped = [prerelease_cmd] if prerelease_cmd else [] + if package_data_spec or data_files_spec: + wrapped.append('handle_files') + wrapper = functools.partial(_wrap_command, wrapped) + handle_files = _get_file_handler(package_data_spec, data_files_spec) + + if 'bdist_egg' in sys.argv: + egg = wrapper(bdist_egg, strict=True) + else: + egg = bdist_egg_disabled + + cmdclass = dict( + build_py=wrapper(build_py, strict=is_repo), + bdist_egg=egg, + sdist=wrapper(sdist, strict=True), + handle_files=handle_files, + ) + + if bdist_wheel: + cmdclass['bdist_wheel'] = wrapper(bdist_wheel, strict=True) + + cmdclass['develop'] = wrapper(develop, strict=True) + return cmdclass + + +def command_for_func(func): + """Create a command that calls the given function.""" + + class FuncCommand(BaseCommand): + + def run(self): + func() + update_package_data(self.distribution) + + return FuncCommand + + +def run(cmd, **kwargs): + """Echo a command before running it. Defaults to repo as cwd""" + log.info('> ' + list2cmdline(cmd)) + kwargs.setdefault('cwd', HERE) + kwargs.setdefault('shell', os.name == 'nt') + if not isinstance(cmd, (list, tuple)) and os.name != 'nt': + cmd = shlex.split(cmd) + cmd[0] = which(cmd[0]) + return subprocess.check_call(cmd, **kwargs) + + +def is_stale(target, source): + """Test whether the target file/directory is stale based on the source + file/directory. + """ + if not os.path.exists(target): + return True + target_mtime = recursive_mtime(target) or 0 + return compare_recursive_mtime(source, cutoff=target_mtime) + + +class BaseCommand(Command): + """Empty command because Command needs subclasses to override too much""" + user_options = [] + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def get_inputs(self): + return [] + + def get_outputs(self): + return [] + + +def combine_commands(*commands): + """Return a Command that combines several commands.""" + + class CombinedCommand(Command): + user_options = [] + + def initialize_options(self): + self.commands = [] + for C in commands: + self.commands.append(C(self.distribution)) + for c in self.commands: + c.initialize_options() + + def finalize_options(self): + for c in self.commands: + c.finalize_options() + + def run(self): + for c in self.commands: + c.run() + return CombinedCommand + + +def compare_recursive_mtime(path, cutoff, newest=True): + """Compare the newest/oldest mtime for all files in a directory. + + Cutoff should be another mtime to be compared against. If an mtime that is + newer/older than the cutoff is found it will return True. + E.g. if newest=True, and a file in path is newer than the cutoff, it will + return True. + """ + if os.path.isfile(path): + mt = mtime(path) + if newest: + if mt > cutoff: + return True + elif mt < cutoff: + return True + for dirname, _, filenames in os.walk(path, topdown=False): + for filename in filenames: + mt = mtime(pjoin(dirname, filename)) + if newest: # Put outside of loop? + if mt > cutoff: + return True + elif mt < cutoff: + return True + return False + + +def recursive_mtime(path, newest=True): + """Gets the newest/oldest mtime for all files in a directory.""" + if os.path.isfile(path): + return mtime(path) + current_extreme = None + for dirname, dirnames, filenames in os.walk(path, topdown=False): + for filename in filenames: + mt = mtime(pjoin(dirname, filename)) + if newest: # Put outside of loop? + if mt >= (current_extreme or mt): + current_extreme = mt + elif mt <= (current_extreme or mt): + current_extreme = mt + return current_extreme + + +def mtime(path): + """shorthand for mtime""" + return os.stat(path).st_mtime + + +def install_npm(path=None, build_dir=None, source_dir=None, build_cmd='build', force=False, npm=None): + """Return a Command for managing an npm installation. + + Note: The command is skipped if the `--skip-npm` flag is used. + + Parameters + ---------- + path: str, optional + The base path of the node package. Defaults to the repo root. + build_dir: str, optional + The target build directory. If this and source_dir are given, + the JavaScript will only be build if necessary. + source_dir: str, optional + The source code directory. + build_cmd: str, optional + The npm command to build assets to the build_dir. + npm: str or list, optional. + The npm executable name, or a tuple of ['node', executable]. + """ + + class NPM(BaseCommand): + description = 'install package.json dependencies using npm' + + def run(self): + if skip_npm: + log.info('Skipping npm-installation') + return + node_package = path or HERE + node_modules = pjoin(node_package, 'node_modules') + is_yarn = os.path.exists(pjoin(node_package, 'yarn.lock')) + + npm_cmd = npm + + if npm is None: + if is_yarn: + npm_cmd = ['yarn'] + else: + npm_cmd = ['npm'] + + if not which(npm_cmd[0]): + log.error("`{0}` unavailable. If you're running this command " + "using sudo, make sure `{0}` is availble to sudo" + .format(npm_cmd[0])) + return + + if force or is_stale(node_modules, pjoin(node_package, 'package.json')): + log.info('Installing build dependencies with npm. This may ' + 'take a while...') + run(npm_cmd + ['install'], cwd=node_package) + if build_dir and source_dir and not force: + should_build = is_stale(build_dir, source_dir) + else: + should_build = True + if should_build: + run(npm_cmd + ['run', build_cmd], cwd=node_package) + + return NPM + + +def ensure_targets(targets): + """Return a Command that checks that certain files exist. + + Raises a ValueError if any of the files are missing. + + Note: The check is skipped if the `--skip-npm` flag is used. + """ + + class TargetsCheck(BaseCommand): + def run(self): + if skip_npm: + log.info('Skipping target checks') + return + missing = [t for t in targets if not os.path.exists(t)] + if missing: + raise ValueError(('missing files: %s' % missing)) + + return TargetsCheck + + +# `shutils.which` function copied verbatim from the Python-3.3 source. +def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + """ + + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) and + not os.path.isdir(fn)) + + # Short circuit. If we're given a full path which matches the mode + # and it exists, we're done here. + if _access_check(cmd, mode): + return cmd + + path = (path or os.environ.get("PATH", os.defpath)).split(os.pathsep) + + if sys.platform == "win32": + # The current directory takes precedence on Windows. + if os.curdir not in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows. + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + # See if the given file matches any of the expected path extensions. + # This will allow us to short circuit when given "python.exe". + matches = [cmd for ext in pathext if cmd.lower().endswith(ext.lower())] + # If it does match, only test that one, otherwise we have to try + # others. + files = [cmd] if matches else [cmd + ext.lower() for ext in pathext] + else: + # On other platforms you don't have things like PATHEXT to tell you + # what file suffixes are executable, so just pass on cmd as-is. + files = [cmd] + + seen = set() + for dir in path: + dir = os.path.normcase(dir) + if dir not in seen: + seen.add(dir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None + + +# --------------------------------------------------------------------------- +# Private Functions +# --------------------------------------------------------------------------- + + +def _wrap_command(cmds, cls, strict=True): + """Wrap a setup command + + Parameters + ---------- + cmds: list(str) + The names of the other commands to run prior to the command. + strict: boolean, optional + Wether to raise errors when a pre-command fails. + """ + class WrappedCommand(cls): + + def run(self): + if not getattr(self, 'uninstall', None): + try: + [self.run_command(cmd) for cmd in cmds] + except Exception: + if strict: + raise + else: + pass + # update package data + update_package_data(self.distribution) + + result = cls.run(self) + return result + return WrappedCommand + + +def _get_file_handler(package_data_spec, data_files_spec): + """Get a package_data and data_files handler command. + """ + class FileHandler(BaseCommand): + + def run(self): + package_data = self.distribution.package_data + package_spec = package_data_spec or dict() + + for (key, patterns) in package_spec.items(): + package_data[key] = _get_package_data(key, patterns) + + self.distribution.data_files = _get_data_files( + data_files_spec, self.distribution.data_files + ) + + return FileHandler + + +def _get_data_files(data_specs, existing): + """Expand data file specs into valid data files metadata. + + Parameters + ---------- + data_specs: list of tuples + See [createcmdclass] for description. + existing: list of tuples + The existing distrubution data_files metadata. + + Returns + ------- + A valid list of data_files items. + """ + # Extract the existing data files into a staging object. + file_data = defaultdict(list) + for (path, files) in existing or []: + file_data[path] = files + + # Extract the files and assign them to the proper data + # files path. + for (path, dname, pattern) in data_specs or []: + dname = dname.replace(os.sep, '/') + offset = len(dname) + 1 + + files = _get_files(pjoin(dname, pattern)) + for fname in files: + # Normalize the path. + root = os.path.dirname(fname) + full_path = '/'.join([path, root[offset:]]) + if full_path.endswith('/'): + full_path = full_path[:-1] + file_data[full_path].append(fname) + + # Construct the data files spec. + data_files = [] + for (path, files) in file_data.items(): + data_files.append((path, files)) + return data_files + + +def _get_files(file_patterns, top=HERE): + """Expand file patterns to a list of paths. + + Parameters + ----------- + file_patterns: list or str + A list of glob patterns for the data file locations. + The globs can be recursive if they include a `**`. + They should be relative paths from the top directory or + absolute paths. + top: str + the directory to consider for data files + + Note: + Files in `node_modules` are ignored. + """ + if not isinstance(file_patterns, (list, tuple)): + file_patterns = [file_patterns] + + for i, p in enumerate(file_patterns): + if os.path.isabs(p): + file_patterns[i] = os.path.relpath(p, top) + + matchers = [_compile_pattern(p) for p in file_patterns] + + files = set() + + for root, dirnames, filenames in os.walk(top): + # Don't recurse into node_modules + if 'node_modules' in dirnames: + dirnames.remove('node_modules') + for m in matchers: + for filename in filenames: + fn = os.path.relpath(pjoin(root, filename), top) + if m(fn): + files.add(fn.replace(os.sep, '/')) + + return list(files) + + +def _get_package_data(root, file_patterns=None): + """Expand file patterns to a list of `package_data` paths. + + Parameters + ----------- + root: str + The relative path to the package root from `HERE`. + file_patterns: list or str, optional + A list of glob patterns for the data file locations. + The globs can be recursive if they include a `**`. + They should be relative paths from the root or + absolute paths. If not given, all files will be used. + + Note: + Files in `node_modules` are ignored. + """ + if file_patterns is None: + file_patterns = ['*'] + return _get_files(file_patterns, pjoin(HERE, root)) + + +def _compile_pattern(pat, ignore_case=True): + """Translate and compile a glob pattern to a regular expression matcher.""" + if isinstance(pat, bytes): + pat_str = pat.decode('ISO-8859-1') + res_str = _translate_glob(pat_str) + res = res_str.encode('ISO-8859-1') + else: + res = _translate_glob(pat) + flags = re.IGNORECASE if ignore_case else 0 + return re.compile(res, flags=flags).match + + +def _iexplode_path(path): + """Iterate over all the parts of a path. + + Splits path recursively with os.path.split(). + """ + (head, tail) = os.path.split(path) + if not head or (not tail and head == path): + if head: + yield head + if tail or not head: + yield tail + return + for p in _iexplode_path(head): + yield p + yield tail + + +def _translate_glob(pat): + """Translate a glob PATTERN to a regular expression.""" + translated_parts = [] + for part in _iexplode_path(pat): + translated_parts.append(_translate_glob_part(part)) + os_sep_class = '[%s]' % re.escape(SEPARATORS) + res = _join_translated(translated_parts, os_sep_class) + return '{res}\\Z(?ms)'.format(res=res) + + +def _join_translated(translated_parts, os_sep_class): + """Join translated glob pattern parts. + + This is different from a simple join, as care need to be taken + to allow ** to match ZERO or more directories. + """ + res = '' + for part in translated_parts[:-1]: + if part == '.*': + # drop separator, since it is optional + # (** matches ZERO or more dirs) + res += part + else: + res += part + os_sep_class + + if translated_parts[-1] == '.*': + # Final part is ** + res += '.+' + # Follow stdlib/git convention of matching all sub files/directories: + res += '({os_sep_class}?.*)?'.format(os_sep_class=os_sep_class) + else: + res += translated_parts[-1] + return res + + +def _translate_glob_part(pat): + """Translate a glob PATTERN PART to a regular expression.""" + # Code modified from Python 3 standard lib fnmatch: + if pat == '**': + return '.*' + i, n = 0, len(pat) + res = [] + while i < n: + c = pat[i] + i = i + 1 + if c == '*': + # Match anything but path separators: + res.append('[^%s]*' % SEPARATORS) + elif c == '?': + res.append('[^%s]?' % SEPARATORS) + elif c == '[': + j = i + if j < n and pat[j] == '!': + j = j + 1 + if j < n and pat[j] == ']': + j = j + 1 + while j < n and pat[j] != ']': + j = j + 1 + if j >= n: + res.append('\\[') + else: + stuff = pat[i:j].replace('\\', '\\\\') + i = j + 1 + if stuff[0] == '!': + stuff = '^' + stuff[1:] + elif stuff[0] == '^': + stuff = '\\' + stuff + res.append('[%s]' % stuff) + else: + res.append(re.escape(c)) + return ''.join(res) diff --git a/wwt_api_client/__init__.py b/wwt_api_client/__init__.py new file mode 100644 index 0000000..dd0bfef --- /dev/null +++ b/wwt_api_client/__init__.py @@ -0,0 +1,5 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright 2019 the .Net Foundation +# Distributed under the terms of the revised (3-clause) BSD license. + +from ._version import version_info, __version__ # noqa diff --git a/wwt_api_client/_version.py b/wwt_api_client/_version.py new file mode 100644 index 0000000..76f943c --- /dev/null +++ b/wwt_api_client/_version.py @@ -0,0 +1,9 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright 2019 the .Net Foundation +# Distributed under the terms of the revised (3-clause) BSD license. + +version_info = (0, 1, 0, 'dev', 0) + +_specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': '', 'dev': 'dev'} +_ext = '' if version_info[3] == 'final' else _specifier_[version_info[3]] + str(version_info[4]) +__version__ = '%s.%s.%s%s' % (version_info[0], version_info[1], version_info[2], ext) diff --git a/wwt_api_client/tests/__init__.py b/wwt_api_client/tests/__init__.py new file mode 100644 index 0000000..547aba0 --- /dev/null +++ b/wwt_api_client/tests/__init__.py @@ -0,0 +1,7 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright 2019 the .Net Foundation +# Distributed under the terms of the revised (3-clause) BSD license. + +"""This module contains tests of the wwt_api_client package. + +""" diff --git a/wwt_api_client/tests/coveragerc b/wwt_api_client/tests/coveragerc new file mode 100644 index 0000000..1c93d2c --- /dev/null +++ b/wwt_api_client/tests/coveragerc @@ -0,0 +1,21 @@ +[run] +source = {packagename} +omit = + {packagename}/tests/* + +[report] +exclude_lines = + pragma: no cover + + # Don't complain about packages we have installed + except ImportError + + # Don't complain if tests don't hit assertions + raise AssertionError + raise NotImplementedError + + # Don't complain about script hooks + def main\(.*\): + + # Ignore branches that don't pertain to this version of Python + pragma: py{ignore_python_version} diff --git a/wwt_api_client/tests/test_core.py b/wwt_api_client/tests/test_core.py new file mode 100644 index 0000000..23a335e --- /dev/null +++ b/wwt_api_client/tests/test_core.py @@ -0,0 +1,10 @@ +# -*- mode: python; coding: utf-8 -*- +# Copyright 2019 the .Net Foundation +# Distributed under the terms of the revised (3-clause) BSD license. + +import pytest + +#from ..core import BaseWWTWidget + +def test_something(): + pass