Merge pull request #10 from pkgw/deep-six

Fix up tests/build
This commit is contained in:
Peter Williams 2023-03-29 17:43:51 +00:00 коммит произвёл GitHub
Родитель ca7d8ca91a f8e3182e11
Коммит 397c9a8f3f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
19 изменённых файлов: 731 добавлений и 485 удалений

34
LICENSE
Просмотреть файл

@ -1,24 +1,18 @@
The MIT License The MIT License
Redistribution and use in source and binary forms, with or without Permission is hereby granted, free of charge, to any person obtaining a copy of
modification, are permitted provided that the following conditions are met: 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:
* Redistributions of source code must retain the above copyright notice, this The above copyright notice and this permission notice shall be included in all
list of conditions and the following disclaimer. copies or substantial portions of the Software.
* 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" THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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.

Просмотреть файл

@ -62,6 +62,7 @@ jobs:
source activate-conda.sh source activate-conda.sh
conda activate build conda activate build
set -x set -x
\conda install -y httpretty mock pytest pytest-cov pytest-mock
pytest wwt_api_client pytest wwt_api_client
displayName: Test displayName: Test
@ -87,7 +88,7 @@ jobs:
source activate-conda.sh source activate-conda.sh
conda activate build conda activate build
set -x set -x
\conda install -y pytest-cov \conda install -y httpretty mock pytest pytest-cov pytest-mock
pytest --cov-report=xml --cov=wwt_api_client wwt_api_client pytest --cov-report=xml --cov=wwt_api_client wwt_api_client
displayName: Test with coverage displayName: Test with coverage

Просмотреть файл

@ -66,7 +66,8 @@ jobs:
conda activate conda activate
conda config --add channels conda-forge conda config --add channels conda-forge
conda install -y \ conda install -y \
requests requests \
wwt_data_formats
pip install openidc_client pip install openidc_client
displayName: Set up dependencies displayName: Set up dependencies

Просмотреть файл

@ -1,14 +0,0 @@
API Documentation
=================
.. automodapi:: wwt_api_client
:no-inheritance-diagram:
:no-inherited-members:
.. automodapi:: wwt_api_client.communities
:no-inheritance-diagram:
:no-inherited-members:
.. automodapi:: wwt_api_client.enums
:no-inheritance-diagram:
:no-inherited-members:

Просмотреть файл

@ -0,0 +1,33 @@
LoginRequest
============
.. currentmodule:: wwt_api_client
.. autoclass:: LoginRequest
:show-inheritance:
.. rubric:: Attributes Summary
.. autosummary::
~LoginRequest.client_version
~LoginRequest.equinox_version_or_later
~LoginRequest.user_guid
.. rubric:: Methods Summary
.. autosummary::
~LoginRequest.invalidity_reason
~LoginRequest.make_request
.. rubric:: Attributes Documentation
.. autoattribute:: client_version
.. autoattribute:: equinox_version_or_later
.. autoattribute:: user_guid
.. rubric:: Methods Documentation
.. automethod:: invalidity_reason
.. automethod:: make_request

Просмотреть файл

@ -0,0 +1,3 @@
.. automodapi:: wwt_api_client.communities
:no-inheritance-diagram:
:inherited-members:

Просмотреть файл

@ -0,0 +1,3 @@
.. automodapi:: wwt_api_client.enums
:no-inheritance-diagram:
:inherited-members:

Просмотреть файл

@ -0,0 +1,3 @@
.. automodapi:: wwt_api_client
:no-inheritance-diagram:
:inherited-members:

Просмотреть файл

@ -1,10 +1,11 @@
# Configuration file for the Sphinx documentation builder. # -*- coding: utf-8 -*-
project = "wwt_api_client" project = "wwt_api_client"
author = "WorldWide Telescope project" author = "WorldWide Telescope project"
copyright = "2019-2023 " + author copyright = "2019-2023 " + author
release = "0.dev0" # cranko project-version release = "0.dev0" # cranko project-version
version = ".".join(release.split(".")[:2])
extensions = [ extensions = [
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
@ -17,12 +18,52 @@ extensions = [
"numpydoc", "numpydoc",
] ]
master_doc = "index"
templates_path = ["_templates"] templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
language = "en"
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
pygments_style = "sphinx"
todo_include_todos = False
numpydoc_class_members_toctree = False html_theme = "bootstrap-astropy"
html_theme_options = {
html_theme = "alabaster" "logotext1": "wwt_api_client",
"logotext2": "",
"logotext3": ":docs",
"astropy_project_menubar": False,
}
html_static_path = ["_static"] html_static_path = ["_static"]
htmlhelp_basename = "wwtapiclientdoc"
intersphinx_mapping = {
"python": (
"https://docs.python.org/3/",
(None, "http://data.astropy.org/intersphinx/python3.inv"),
),
"requests": ("https://requests.readthedocs.io/en/stable/", None),
}
numpydoc_show_class_members = False
nitpicky = True
nitpick_ignore = [
# Traitlets stuff that we have to ignore. This is all due to our need to
# turn on :inherited-members: in api.rst due to a sphinx-automodapi bug (see
# comment in api.rst).
("py:attr", "class_init"),
("py:attr", "name"),
("py:attr", "this_class"),
("py:class", "traitlets.traitlets.HasDescriptors"),
("py:class", "traitlets.traitlets.MetaHasDescriptors"),
("py:class", "traitlets.traitlets.MetaHasTraits"),
("py:obj", "handler"),
("py:obj", "remove"),
]
default_role = "obj"
html_logo = "images/logo.png" html_logo = "images/logo.png"
linkcheck_retries = 5
linkcheck_timeout = 10

Просмотреть файл

@ -1,6 +1,6 @@
.. _endpoint-Login: .. _endpoint-Login:
``wwtweb/login.aspx`` ``WWTWeb/Login.aspx``
===================== =====================
Windows clients invoke this API when they boot up. They send advisory version Windows clients invoke this API when they boot up. They send advisory version

Просмотреть файл

@ -1,27 +1,40 @@
wwt_api_client: Pythonic access to the WorldWide Telescope web services ===================================================================
======================================================================= wwt_api_client: Pythonic access to WorldWide Telescope web services
===================================================================
The AAS `WorldWide Telescope <http://worldwidetelescope.org/home>`_ is a free AAS `WorldWide Telescope <http://worldwidetelescope.org/home>`_ is a free
and powerful visualization engine developed by the `American Astronomical and powerful visualization engine developed by the `American Astronomical
Society <https://aas.org/>`_ that can display astronomical and planetary data. Society <https://aas.org/>`_ that can display astronomical and planetary data.
This engine is powered by a large (multi-terabyte) collection of astronomical 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 survey data stored in the cloud. The `wwt_api_client`_ package allows you to
invoke the web APIs that constitute the WWT backend API. invoke the web APIs that constitute the WWT backend API.
.. _wwt_api_client: https://wwt-api-client.readthedocs.io/
User guide
---------- Table of Contents
=================
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
installation installation
endpoints/index.rst endpoints/index.rst
api
API Reference
=============
.. toctree::
:maxdepth: 1
api/wwt_api_client
api/wwt_api_client.communities
api/wwt_api_client.enums
Getting help Getting help
------------ ============
If you run into any issues when using ``wwt_api_client``, please open an issue If you run into any issues when using ``wwt_api_client``, please open an issue
`on its GitHub repository `on its GitHub repository
@ -29,7 +42,21 @@ If you run into any issues when using ``wwt_api_client``, please open an issue
Acknowledgments Acknowledgments
--------------- ===============
`wwt_api_client`_ is part of the `AAS`_ `WorldWide Telescope`_ system, a
`.NET Foundation`_ project managed by the non-profit `American Astronomical
Society`_ (AAS). Work on WWT has been supported by the AAS, the US `National
Science Foundation`_ (grants 1550701_, 1642446_, and 2004840_), the `Gordon and Betty
Moore Foundation`_, and `Microsoft`_.
.. _.NET Foundation: https://dotnetfoundation.org/
.. _AAS: https://aas.org/
.. _American Astronomical Society: https://aas.org/
.. _National Science Foundation: https://www.nsf.gov/
.. _1550701: https://www.nsf.gov/awardsearch/showAward?AWD_ID=1550701
.. _1642446: https://www.nsf.gov/awardsearch/showAward?AWD_ID=1642446
.. _2004840: https://www.nsf.gov/awardsearch/showAward?AWD_ID=2004840
.. _Gordon and Betty Moore Foundation: https://www.moore.org/
.. _Microsoft: https://www.microsoft.com/
Work on ``wwt_api_client`` is funded through the American Astronomical
Societys support of the WorldWide Telescope project.

Просмотреть файл

@ -1,8 +1,8 @@
build: build:
image: latest image: latest
python: python:
version: 3.7 version: 3.10
pip_install: true pip_install: true
extra_requirements: ['docs'] extra_requirements: ['docs']

Просмотреть файл

@ -1,7 +1,6 @@
#! /usr/bin/env python #! /usr/bin/env python
# -*- mode: python; coding: utf-8 -*- # Copyright 2019-2023 the .NET Foundation
# Copyright 2019-2020 the .NET Foundation # Distributed under the MIT license
# Distributed under the terms of the revised (3-clause) BSD license.
from setuptools import setup from setuptools import setup
@ -52,7 +51,7 @@ setup_args = dict(
classifiers=[ classifiers=[
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Intended Audience :: Science/Research", "Intended Audience :: Science/Research",
"License :: OSI Approved :: BSD License", "License :: OSI Approved :: MIT License",
"Topic :: Multimedia :: Graphics", "Topic :: Multimedia :: Graphics",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.7",
@ -64,19 +63,21 @@ setup_args = dict(
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
"requests", "requests",
"wwt_data_formats",
], ],
extras_require={ extras_require={
"test": [ "test": [
"httpretty", "httpretty",
"mock",
"pytest", "pytest",
"pytest-cov", "pytest-cov",
"pytest-mock", "pytest-mock",
], ],
"docs": [ "docs": [
"sphinx>=1.6", "astropy-sphinx-theme",
"sphinx-automodapi",
"numpydoc", "numpydoc",
"sphinx_rtd_theme", "sphinx",
"sphinx-automodapi",
], ],
}, },
entry_points={}, entry_points={},

Просмотреть файл

@ -1,36 +1,29 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright 2019-2020 the .Net Foundation # Copyright 2019-2023 the .Net Foundation
# Distributed under the terms of the revised (3-clause) BSD license. # Distributed under the MIT license
from __future__ import absolute_import, division, print_function
import requests import requests
import six from urllib import parse as url_parse
from six.moves.urllib import parse as url_parse
from xml.sax.saxutils import escape as xml_escape from xml.sax.saxutils import escape as xml_escape
import warnings import warnings
from ._version import version_info, __version__ # noqa __all__ = """
__all__ = '''
__version__
APIRequest APIRequest
APIResponseError APIResponseError
Client Client
DEFAULT_API_BASE DEFAULT_API_BASE
LoginRequest
InvalidRequestError InvalidRequestError
ShowImageRequest ShowImageRequest
TileImageRequest TileImageRequest
version_info """.split()
'''.split()
DEFAULT_API_BASE = 'http://www.worldwidetelescope.org' DEFAULT_API_BASE = "http://www.worldwidetelescope.org"
class APIResponseError(Exception): class APIResponseError(Exception):
"""Raised when the API returns an HTTP error. """Raised when the API returns an HTTP error."""
"""
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
@ -39,9 +32,8 @@ class APIResponseError(Exception):
class InvalidRequestError(Exception): class InvalidRequestError(Exception):
"""Raised when an API request is not in a valid state """Raised when an API request is not in a valid state"""
"""
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
@ -58,12 +50,13 @@ class Client(object):
---------- ----------
api_base : URL string or None api_base : URL string or None
The base URL to use for accessing the WWT web APIs. Defaults to The base URL to use for accessing the WWT web APIs. Defaults to
:data:`DEFAULT_API_BASE`, which is probably equal to ``DEFAULT_API_BASE``, which is probably equal to
"http://www.worldwidetelescope.org". The API base is configurable to "http://www.worldwidetelescope.org". The API base is configurable to
make it possible to access testing servers, etc. This value should not make it possible to access testing servers, etc. This value should not
end in a slash. end in a slash.
""" """
_api_base = None _api_base = None
_session = None _session = None
@ -81,8 +74,12 @@ class Client(object):
return self._session return self._session
def login(self, user_guid='00000000-0000-0000-0000-000000000000', client_version='6.0.0.0', def login(
equinox_version_or_later=True): self,
user_guid="00000000-0000-0000-0000-000000000000",
client_version="6.0.0.0",
equinox_version_or_later=True,
):
"""Create a :ref:`Login <endpoint-Login>` request object. """Create a :ref:`Login <endpoint-Login>` request object.
Parameters are assigned to attributes of the return value; see Parameters are assigned to attributes of the return value; see
@ -109,9 +106,21 @@ class Client(object):
req.equinox_version_or_later = equinox_version_or_later req.equinox_version_or_later = equinox_version_or_later
return req return req
def show_image(self, image_url=None, name=None, credits=None, credits_url=None, def show_image(
dec_deg=0.0, ra_deg=0.0, reverse_parity=False, rotation_deg=0.0, self,
scale_arcsec=1.0, thumbnail_url=None, x_offset_pixels=0.0, y_offset_pixels=0.0): image_url=None,
name=None,
credits=None,
credits_url=None,
dec_deg=0.0,
ra_deg=0.0,
reverse_parity=False,
rotation_deg=0.0,
scale_arcsec=1.0,
thumbnail_url=None,
x_offset_pixels=0.0,
y_offset_pixels=0.0,
):
"""Create a :ref:`ShowImage <endpoint-ShowImage>` request object. """Create a :ref:`ShowImage <endpoint-ShowImage>` request object.
Parameters are assigned to attributes of the return value; see Parameters are assigned to attributes of the return value; see
@ -147,9 +156,19 @@ class Client(object):
req.y_offset_pixels = y_offset_pixels req.y_offset_pixels = y_offset_pixels
return req return req
def tile_image(self, image_url=None, credits=None, credits_url=None, def tile_image(
dec_deg=0.0, ra_deg=0.0, rotation_deg=0.0, scale_deg=1.0, self,
thumbnail_url=None, x_offset_deg=0.0, y_offset_deg=0.0): image_url=None,
credits=None,
credits_url=None,
dec_deg=0.0,
ra_deg=0.0,
rotation_deg=0.0,
scale_deg=1.0,
thumbnail_url=None,
x_offset_deg=0.0,
y_offset_deg=0.0,
):
"""Create a :ref:`TileImage <endpoint-TileImage>` request object. """Create a :ref:`TileImage <endpoint-TileImage>` request object.
Parameters are assigned to attributes of the return value; see Parameters are assigned to attributes of the return value; see
@ -195,12 +214,12 @@ def _get_our_encoding():
import sys import sys
enc = sys.getdefaultencoding() enc = sys.getdefaultencoding()
if enc == 'ascii': if enc == "ascii":
return 'utf-8' return "utf-8"
return enc return enc
def _maybe_as_bytes(obj, xml_esc=False, in_enc=None, out_enc='utf-8'): def _maybe_as_bytes(obj, xml_esc=False, in_enc=None, out_enc="utf-8"):
import codecs import codecs
if obj is None: if obj is None:
@ -209,7 +228,7 @@ def _maybe_as_bytes(obj, xml_esc=False, in_enc=None, out_enc='utf-8'):
if in_enc is None: if in_enc is None:
in_enc = _get_our_encoding() in_enc = _get_our_encoding()
if isinstance(obj, six.binary_type): if isinstance(obj, bytes):
# If we don't special-case, b'abc' becomes "b'abc'". # If we don't special-case, b'abc' becomes "b'abc'".
# #
# It would also be nice if we could validate that *obj* is # It would also be nice if we could validate that *obj* is
@ -218,10 +237,10 @@ def _maybe_as_bytes(obj, xml_esc=False, in_enc=None, out_enc='utf-8'):
# this API I don't expect the overhead to be significant. # this API I don't expect the overhead to be significant.
text = codecs.decode(obj, in_enc) text = codecs.decode(obj, in_enc)
else: else:
text = six.text_type(obj) text = str(obj)
if xml_esc: if xml_esc:
text = xml_escape(text, {'"': '&quot;'}) text = xml_escape(text, {'"': "&quot;"})
return codecs.encode(text, out_enc) return codecs.encode(text, out_enc)
@ -230,7 +249,7 @@ def _is_textable(obj, none_ok=False):
if obj is None: if obj is None:
return none_ok return none_ok
if isinstance(obj, six.binary_type): if isinstance(obj, bytes):
import codecs import codecs
try: try:
@ -240,7 +259,7 @@ def _is_textable(obj, none_ok=False):
return True return True
try: try:
six.text_type(obj) str(obj)
except Exception: except Exception:
return False return False
return True return True
@ -255,21 +274,21 @@ def _is_absurl(obj, none_ok=False):
# allow ASCII in and out. # allow ASCII in and out.
import codecs import codecs
if isinstance(obj, six.binary_type): if isinstance(obj, bytes):
# If we don't special-case, b'abc' becomes "b'abc'". # If we don't special-case, b'abc' becomes "b'abc'".
try: try:
text = codecs.decode(obj, 'ascii') text = codecs.decode(obj, "ascii")
except Exception: except Exception:
return False return False
else: else:
try: try:
text = six.text_type(obj) text = str(obj)
except Exception: except Exception:
return False return False
# We also need to be able to go the other way: # We also need to be able to go the other way:
try: try:
codecs.encode(text, 'ascii') codecs.encode(text, "ascii")
except Exception: except Exception:
return False return False
@ -292,7 +311,10 @@ def _is_scalar(obj, none_ok=False):
return False return False
import math import math
return not (math.isinf(val) or math.isnan(val)) # math.isfinite() only available in 3.x
return not (
math.isinf(val) or math.isnan(val)
) # math.isfinite() only available in 3.x
class APIRequest(object): class APIRequest(object):
@ -312,6 +334,7 @@ class APIRequest(object):
The client with which this request is associated. The client with which this request is associated.
""" """
_client = None _client = None
def __init__(self, client): def __init__(self, client):
@ -348,7 +371,7 @@ class APIRequest(object):
-------- --------
Get the URL that will be accessed for a request:: Get the URL that will be accessed for a request::
>>> from six.moves.urllib.parse import urlparse >>> from urllib.parse import urlparse
>>> from wwt_api_client import Client >>> from wwt_api_client import Client
>>> req = Client().show_image('http://example.com/space.jpg', 'My Image') >>> req = Client().show_image('http://example.com/space.jpg', 'My Image')
>>> parsed_url = urlparse(req.make_request().prepare().url) >>> parsed_url = urlparse(req.make_request().prepare().url)
@ -422,22 +445,22 @@ class APIRequest(object):
def to_xml(self): def to_xml(self):
"""Issue the request and return its results as parsed XML.""" """Issue the request and return its results as parsed XML."""
from xml.etree import ElementTree as etree from xml.etree import ElementTree as etree
text = self.send(raw_response=True).text text = self.send(raw_response=True).text
return etree.fromstring(text) return etree.fromstring(text)
class LoginRequest(APIRequest): class LoginRequest(APIRequest):
"""Indicate a client login to the server. """Indicate a client login to the server."""
""" user_guid = "00000000-0000-0000-0000-000000000000"
user_guid = '00000000-0000-0000-0000-000000000000'
"A GUID associated with the user logging in. The server doesn't track these." "A GUID associated with the user logging in. The server doesn't track these."
client_version = '6.0.0.0' client_version = "6.0.0.0"
"The version of the client logging in." "The version of the client logging in."
equinox_version_or_later = True equinox_version_or_later = True
"Whether this client is of the \"Equinox\" release (~2008) or later." 'Whether this client is of the "Equinox" release (~2008) or later.'
def invalidity_reason(self): def invalidity_reason(self):
if not _is_textable(self.user_guid): if not _is_textable(self.user_guid):
@ -453,22 +476,23 @@ class LoginRequest(APIRequest):
def make_request(self): def make_request(self):
params = [ params = [
('user', _maybe_as_bytes(self.user_guid)), ("user", _maybe_as_bytes(self.user_guid)),
('Version', _maybe_as_bytes(self.client_version)), ("Version", _maybe_as_bytes(self.client_version)),
] ]
if self.equinox_version_or_later: if self.equinox_version_or_later:
params.append(('Equinox', 'true')) params.append(("Equinox", "true"))
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = self._client._api_base + '/WWTWeb/login.aspx', url=self._client._api_base + "/WWTWeb/login.aspx",
params = params, params=params,
) )
# TODO: connect this to wwt_data_formats! # TODO: connect this to wwt_data_formats!
class ShowImageRequest(APIRequest): class ShowImageRequest(APIRequest):
"""Request a WTML XML document suitable for showing an image in a client. """Request a WTML XML document suitable for showing an image in a client.
@ -497,6 +521,7 @@ class ShowImageRequest(APIRequest):
<endpoint-ShowImage>` endpoint. <endpoint-ShowImage>` endpoint.
""" """
credits = None credits = None
"Free text describing where the image came from." "Free text describing where the image came from."
@ -555,9 +580,12 @@ class ShowImageRequest(APIRequest):
if not _is_textable(self.name): if not _is_textable(self.name):
return '"name" must be a string or an object that can be stringified' return '"name" must be a string or an object that can be stringified'
if ',' in str(self.name): if "," in str(self.name):
warnings.warn('ShowImage name {0} contains commas, which will be stripped ' warnings.warn(
'by the server'.format(self.name), UserWarning) "ShowImage name {0} contains commas, which will be stripped "
"by the server".format(self.name),
UserWarning,
)
if not _is_scalar(self.ra_deg): if not _is_scalar(self.ra_deg):
return '"ra_deg" must be a number' return '"ra_deg" must be a number'
@ -571,7 +599,7 @@ class ShowImageRequest(APIRequest):
if not _is_scalar(self.scale_arcsec): if not _is_scalar(self.scale_arcsec):
return '"scale_arcsec" must be a number' return '"scale_arcsec" must be a number'
if float(self.scale_arcsec) == 0.: if float(self.scale_arcsec) == 0.0:
return '"scale_arcsec" must not be zero' return '"scale_arcsec" must not be zero'
if not _is_absurl(self.thumbnail_url, none_ok=True): if not _is_absurl(self.thumbnail_url, none_ok=True):
@ -587,38 +615,64 @@ class ShowImageRequest(APIRequest):
def make_request(self): def make_request(self):
params = [ params = [
('dec', '%.18e' % float(self.dec_deg)), ("dec", "%.18e" % float(self.dec_deg)),
('imageurl', _maybe_as_bytes(self.image_url, xml_esc=True, in_enc='ascii', out_enc='ascii')), (
('name', _maybe_as_bytes(self.name, xml_esc=True)), "imageurl",
('ra', '%.18e' % (float(self.ra_deg) % 360)), # The API clips, but we wrap _maybe_as_bytes(
('rotation', '%.18e' % (float(self.rotation_deg) + 180)), # API is bizarre here self.image_url, xml_esc=True, in_enc="ascii", out_enc="ascii"
('scale', '%.18e' % float(self.scale_arcsec)), ),
('wtml', 't'), ),
('x', '%.18e' % float(self.x_offset_pixels)), ("name", _maybe_as_bytes(self.name, xml_esc=True)),
('y', '%.18e' % float(self.y_offset_pixels)), ("ra", "%.18e" % (float(self.ra_deg) % 360)), # The API clips, but we wrap
(
"rotation",
"%.18e" % (float(self.rotation_deg) + 180),
), # API is bizarre here
("scale", "%.18e" % float(self.scale_arcsec)),
("wtml", "t"),
("x", "%.18e" % float(self.x_offset_pixels)),
("y", "%.18e" % float(self.y_offset_pixels)),
] ]
if self.credits is not None: if self.credits is not None:
params.append(('credits', _maybe_as_bytes(self.credits, xml_esc=True))) params.append(("credits", _maybe_as_bytes(self.credits, xml_esc=True)))
if self.credits_url is not None: if self.credits_url is not None:
params.append(('creditsUrl', _maybe_as_bytes(self.credits_url, xml_esc=True, in_enc='ascii', out_enc='ascii'))) params.append(
(
"creditsUrl",
_maybe_as_bytes(
self.credits_url, xml_esc=True, in_enc="ascii", out_enc="ascii"
),
)
)
if self.reverse_parity: if self.reverse_parity:
params.append(('reverseparity', 't')) params.append(("reverseparity", "t"))
if self.thumbnail_url is not None: if self.thumbnail_url is not None:
params.append(('thumb', _maybe_as_bytes(self.thumbnail_url, xml_esc=True, in_enc='ascii', out_enc='ascii'))) params.append(
(
"thumb",
_maybe_as_bytes(
self.thumbnail_url,
xml_esc=True,
in_enc="ascii",
out_enc="ascii",
),
)
)
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = self._client._api_base + '/WWTWeb/ShowImage.aspx', url=self._client._api_base + "/WWTWeb/ShowImage.aspx",
params = params, params=params,
) )
# TODO: connect this to wwt_data_formats! # TODO: connect this to wwt_data_formats!
class TileImageRequest(APIRequest): class TileImageRequest(APIRequest):
"""Tile a large image on the server and obtain a WTML XML document suitable """Tile a large image on the server and obtain a WTML XML document suitable
for displaying it in a client. FITS images are not supported. for displaying it in a client. FITS images are not supported.
@ -647,6 +701,7 @@ class TileImageRequest(APIRequest):
<endpoint-TileImage>` endpoint. <endpoint-TileImage>` endpoint.
""" """
credits = None credits = None
"Free text describing where the image came from." "Free text describing where the image came from."
@ -691,6 +746,7 @@ class TileImageRequest(APIRequest):
Positive numbers move the image up relative to the viewport center. Positive numbers move the image up relative to the viewport center.
""" """
def invalidity_reason(self): def invalidity_reason(self):
if not _is_textable(self.credits, none_ok=True): if not _is_textable(self.credits, none_ok=True):
return '"credits" must be None or a string-like object' return '"credits" must be None or a string-like object'
@ -720,7 +776,7 @@ class TileImageRequest(APIRequest):
if self.scale_deg is not None: if self.scale_deg is not None:
scale = float(self.scale_deg) scale = float(self.scale_deg)
if scale == 0.: if scale == 0.0:
return '"scale_deg" must not be zero' return '"scale_deg" must not be zero'
if not _is_absurl(self.thumbnail_url, none_ok=True): if not _is_absurl(self.thumbnail_url, none_ok=True):
@ -736,38 +792,62 @@ class TileImageRequest(APIRequest):
def make_request(self): def make_request(self):
params = [ params = [
('imageurl', _maybe_as_bytes(self.image_url, xml_esc=True, in_enc='ascii', out_enc='ascii')), (
"imageurl",
_maybe_as_bytes(
self.image_url, xml_esc=True, in_enc="ascii", out_enc="ascii"
),
),
] ]
if self.credits is not None: if self.credits is not None:
params.append(('credits', _maybe_as_bytes(self.credits, xml_esc=True))) params.append(("credits", _maybe_as_bytes(self.credits, xml_esc=True)))
if self.credits_url is not None: if self.credits_url is not None:
params.append(('creditsUrl', _maybe_as_bytes(self.credits_url, xml_esc=True, in_enc='ascii', out_enc='ascii'))) params.append(
(
"creditsUrl",
_maybe_as_bytes(
self.credits_url, xml_esc=True, in_enc="ascii", out_enc="ascii"
),
)
)
if self.dec_deg is not None: if self.dec_deg is not None:
params.append(('dec', '%.18e' % float(self.dec_deg))) params.append(("dec", "%.18e" % float(self.dec_deg)))
if self.ra_deg is not None: if self.ra_deg is not None:
params.append(('ra', '%.18e' % float(self.ra_deg))) params.append(("ra", "%.18e" % float(self.ra_deg)))
if self.rotation_deg is not None: if self.rotation_deg is not None:
params.append(('rotation', '%.18e' % (float(self.rotation_deg) + 180))) # API is bizarre here params.append(
("rotation", "%.18e" % (float(self.rotation_deg) + 180))
) # API is bizarre here
if self.scale_deg is not None: if self.scale_deg is not None:
params.append(('scale', '%.18e' % float(self.scale_deg))) params.append(("scale", "%.18e" % float(self.scale_deg)))
if self.thumbnail_url is not None: if self.thumbnail_url is not None:
params.append(('thumb', _maybe_as_bytes(self.thumbnail_url, xml_esc=True, in_enc='ascii', out_enc='ascii'))) params.append(
(
"thumb",
_maybe_as_bytes(
self.thumbnail_url,
xml_esc=True,
in_enc="ascii",
out_enc="ascii",
),
)
)
if self.x_offset_deg is not None: if self.x_offset_deg is not None:
params.append(('x', '%.18e' % float(self.x_offset_deg))) params.append(("x", "%.18e" % float(self.x_offset_deg)))
if self.y_offset_deg is not None: if self.y_offset_deg is not None:
params.append(('y', '%.18e' % float(self.y_offset_deg))) params.append(("y", "%.18e" % float(self.y_offset_deg)))
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = self._client._api_base + '/WWTWeb/TileImage.aspx', url=self._client._api_base + "/WWTWeb/TileImage.aspx",
params = params, params=params,
) )

Просмотреть файл

@ -1,11 +0,0 @@
# Copyright 2019 the .Net Foundation
# Distributed under the terms of the revised (3-clause) BSD license.
#
# NOTE: this file explicitly does *not* have a mode/encoding line, because the
# way that we read it in setupbase.py breaks on Python 2.7 with a SyntaxError.
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)

Просмотреть файл

@ -1,5 +1,5 @@
# Copyright 2019-2020 the .NET Foundation # Copyright 2019-2020 the .NET Foundation
# Distributed under the terms of the revised (3-clause) BSD license. # Distributed under the MIT license
"""Interacting with the WWT Communities APIs.""" """Interacting with the WWT Communities APIs."""
@ -11,7 +11,7 @@ from urllib.parse import parse_qs, urlparse
from . import APIRequest, Client, enums from . import APIRequest, Client, enums
__all__ = ''' __all__ = """
CommunitiesAPIRequest CommunitiesAPIRequest
CommunitiesClient CommunitiesClient
CreateCommunityRequest CreateCommunityRequest
@ -22,15 +22,16 @@ GetMyProfileRequest
GetProfileEntitiesRequest GetProfileEntitiesRequest
IsUserRegisteredRequest IsUserRegisteredRequest
interactive_communities_login interactive_communities_login
'''.split() """.split()
LIVE_OAUTH_AUTH_SERVICE = "https://login.live.com/oauth20_authorize.srf" LIVE_OAUTH_AUTH_SERVICE = "https://login.live.com/oauth20_authorize.srf"
LIVE_OAUTH_TOKEN_SERVICE = "https://login.live.com/oauth20_token.srf" LIVE_OAUTH_TOKEN_SERVICE = "https://login.live.com/oauth20_token.srf"
LIVE_OAUTH_DESKTOP_ENDPOINT = "https://login.live.com/oauth20_desktop.srf" LIVE_OAUTH_DESKTOP_ENDPOINT = "https://login.live.com/oauth20_desktop.srf"
LIVE_AUTH_SCOPES = ['wl.emails', 'wl.signin'] LIVE_AUTH_SCOPES = ["wl.emails", "wl.signin"]
WWT_CLIENT_ID = '000000004015657B' WWT_CLIENT_ID = "000000004015657B"
OAUTH_STATE_BASENAME = 'communities-oauth.json' OAUTH_STATE_BASENAME = "communities-oauth.json"
CLIENT_SECRET_BASENAME = 'communities-client-secret.txt' CLIENT_SECRET_BASENAME = "communities-client-secret.txt"
class CommunitiesClient(object): class CommunitiesClient(object):
"""A client for WWT Communities API requests. """A client for WWT Communities API requests.
@ -46,18 +47,26 @@ class CommunitiesClient(object):
subsequent use. subsequent use.
""" """
_parent = None _parent = None
_state_dir = None _state_dir = None
_state = None _state = None
_access_token = None _access_token = None
_refresh_token = None _refresh_token = None
def __init__(self, parent_client, oauth_client_secret=None, interactive_login_if_needed=False, state_dir=None): def __init__(
self,
parent_client,
oauth_client_secret=None,
interactive_login_if_needed=False,
state_dir=None,
):
self._parent = parent_client self._parent = parent_client
if state_dir is None: if state_dir is None:
import appdirs import appdirs
state_dir = appdirs.user_state_dir('wwt_api_client', 'AAS_WWT')
state_dir = appdirs.user_state_dir("wwt_api_client", "AAS_WWT")
self._state_dir = state_dir self._state_dir = state_dir
@ -66,20 +75,24 @@ class CommunitiesClient(object):
if oauth_client_secret is None: if oauth_client_secret is None:
try: try:
with open(os.path.join(self._state_dir, CLIENT_SECRET_BASENAME), 'rt') as f: with open(
os.path.join(self._state_dir, CLIENT_SECRET_BASENAME), "rt"
) as f:
oauth_client_secret = f.readline().strip() oauth_client_secret = f.readline().strip()
except FileNotFoundError: except FileNotFoundError:
pass pass
if oauth_client_secret is None: if oauth_client_secret is None:
raise Exception('cannot create CommunitiesClient: the \"oauth client secret\" ' raise Exception(
'is not available to the program') 'cannot create CommunitiesClient: the "oauth client secret" '
"is not available to the program"
)
# Try to get state from a previous OAuth flow and decide what to do # Try to get state from a previous OAuth flow and decide what to do
# based on where we're at. # based on where we're at.
try: try:
with open(os.path.join(self._state_dir, OAUTH_STATE_BASENAME), 'rt') as f: with open(os.path.join(self._state_dir, OAUTH_STATE_BASENAME), "rt") as f:
self._state = json.load(f) self._state = json.load(f)
except FileNotFoundError: except FileNotFoundError:
pass pass
@ -89,9 +102,9 @@ class CommunitiesClient(object):
# redirect_uri's. # redirect_uri's.
token_service_params = { token_service_params = {
'client_id': WWT_CLIENT_ID, "client_id": WWT_CLIENT_ID,
'client_secret': oauth_client_secret, "client_secret": oauth_client_secret,
'redirect_uri': LIVE_OAUTH_DESKTOP_ENDPOINT, "redirect_uri": LIVE_OAUTH_DESKTOP_ENDPOINT,
} }
# Once set, the structure of oauth_data is : { # Once set, the structure of oauth_data is : {
@ -109,16 +122,16 @@ class CommunitiesClient(object):
# We have previous state -- hopefully, we only need a refresh, which # We have previous state -- hopefully, we only need a refresh, which
# can proceed non-interactively. # can proceed non-interactively.
token_service_params['grant_type'] = 'refresh_token' token_service_params["grant_type"] = "refresh_token"
token_service_params['refresh_token'] = self._state['refresh_token'] token_service_params["refresh_token"] = self._state["refresh_token"]
oauth_data = requests.post( oauth_data = requests.post(
LIVE_OAUTH_TOKEN_SERVICE, LIVE_OAUTH_TOKEN_SERVICE,
data = token_service_params, data=token_service_params,
).json() ).json()
if 'error' in oauth_data: if "error" in oauth_data:
if oauth_data['error'] == 'invalid_grant': if oauth_data["error"] == "invalid_grant":
# This indicates that our grant has expired. We need to # This indicates that our grant has expired. We need to
# rerun the auth flow. # rerun the auth flow.
self._state = None self._state = None
@ -132,67 +145,82 @@ class CommunitiesClient(object):
# programs pausing for user input on the terminal. # programs pausing for user input on the terminal.
if not interactive_login_if_needed: if not interactive_login_if_needed:
raise Exception('cannot create CommunitiesClient: an interactive login is ' raise Exception(
'required but unavailable right now') "cannot create CommunitiesClient: an interactive login is "
"required but unavailable right now"
)
params = { params = {
'client_id': WWT_CLIENT_ID, "client_id": WWT_CLIENT_ID,
'scope': ' '.join(LIVE_AUTH_SCOPES), "scope": " ".join(LIVE_AUTH_SCOPES),
'redirect_uri': LIVE_OAUTH_DESKTOP_ENDPOINT, "redirect_uri": LIVE_OAUTH_DESKTOP_ENDPOINT,
'response_type': 'code' "response_type": "code",
} }
preq = requests.Request(url=LIVE_OAUTH_AUTH_SERVICE, params=params).prepare() preq = requests.Request(
url=LIVE_OAUTH_AUTH_SERVICE, params=params
).prepare()
print() print()
print('To use the WWT Communities APIs, interactive authentication to Microsoft') print(
print('Live is required. Open this URL in a browser and log in:') "To use the WWT Communities APIs, interactive authentication to Microsoft"
)
print("Live is required. Open this URL in a browser and log in:")
print() print()
print(preq.url) print(preq.url)
print() print()
print('When done, copy the URL *that you are redirected to* and paste it here:') print(
print('>> ', end='') "When done, copy the URL *that you are redirected to* and paste it here:"
)
print(">> ", end="")
redir_url = input() redir_url = input()
# should look like: # should look like:
# 'https://login.live.com/oauth20_desktop.srf?code=MHEXHEXHE-XHEX-HEXH-EXHE-XHEXHEXHEXHE&lc=NNNN' # 'https://login.live.com/oauth20_desktop.srf?code=MHEXHEXHE-XHEX-HEXH-EXHE-XHEXHEXHEXHE&lc=NNNN'
parsed = urlparse(redir_url) parsed = urlparse(redir_url)
params = parse_qs(parsed.query) params = parse_qs(parsed.query)
code = params.get('code') code = params.get("code")
if not code: if not code:
raise Exception('didn\'t get "code" parameter from response URL') raise Exception('didn\'t get "code" parameter from response URL')
token_service_params['grant_type'] = 'authorization_code' token_service_params["grant_type"] = "authorization_code"
token_service_params['code'] = code token_service_params["code"] = code
oauth_data = requests.post( oauth_data = requests.post(
LIVE_OAUTH_TOKEN_SERVICE, LIVE_OAUTH_TOKEN_SERVICE,
data = token_service_params, data=token_service_params,
).json() ).json()
if 'error' in oauth_data: if "error" in oauth_data:
raise Exception(repr(oauth_data)) raise Exception(repr(oauth_data))
# Looks like it worked! Save the results for next time. # Looks like it worked! Save the results for next time.
os.makedirs(self._state_dir, exist_ok=True) os.makedirs(self._state_dir, exist_ok=True)
# Sigh, Python not making it easy to be secure ... # Sigh, Python not making it easy to be secure ...
fd = os.open(os.path.join(self._state_dir, OAUTH_STATE_BASENAME), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) fd = os.open(
f = open(fd, 'wt') os.path.join(self._state_dir, OAUTH_STATE_BASENAME),
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
0o600,
)
f = open(fd, "wt")
with f: with f:
json.dump(oauth_data, f) json.dump(oauth_data, f)
fd = os.open(os.path.join(self._state_dir, CLIENT_SECRET_BASENAME), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) fd = os.open(
f = open(fd, 'wt') os.path.join(self._state_dir, CLIENT_SECRET_BASENAME),
os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
0o600,
)
f = open(fd, "wt")
with f: with f:
print(oauth_client_secret, file=f) print(oauth_client_secret, file=f)
# And for this time: # And for this time:
self._access_token = oauth_data['access_token'] self._access_token = oauth_data["access_token"]
self._refresh_token = oauth_data['refresh_token'] self._refresh_token = oauth_data["refresh_token"]
def create_community(self, payload=None): def create_community(self, payload=None):
"""Create a new community owned by the current user. """Create a new community owned by the current user.
@ -211,7 +239,6 @@ class CommunitiesClient(object):
req.payload = payload req.payload = payload
return req return req
def delete_community(self, id=None): def delete_community(self, id=None):
"""Delete a community. """Delete a community.
@ -229,7 +256,6 @@ class CommunitiesClient(object):
req.id = id req.id = id
return req return req
def get_community_info(self, id=None): def get_community_info(self, id=None):
"""Get information about the specified community. """Get information about the specified community.
@ -247,7 +273,6 @@ class CommunitiesClient(object):
req.id = id req.id = id
return req return req
def get_latest_community(self): def get_latest_community(self):
"""Get information about the most recently created WWT Communities. """Get information about the most recently created WWT Communities.
@ -270,7 +295,6 @@ class CommunitiesClient(object):
""" """
return GetLatestCommunityRequest(self) return GetLatestCommunityRequest(self)
def get_my_profile(self): def get_my_profile(self):
"""Get the logged-in user's profile information. """Get the logged-in user's profile information.
@ -295,12 +319,11 @@ class CommunitiesClient(object):
""" """
return GetMyProfileRequest(self) return GetMyProfileRequest(self)
def get_profile_entities( def get_profile_entities(
self, self,
entity_type = enums.EntityType.CONTENT, entity_type=enums.EntityType.CONTENT,
current_page = 1, current_page=1,
page_size = 99999, page_size=99999,
): ):
"""Get "entities" associated with the logged-in user's profile. """Get "entities" associated with the logged-in user's profile.
@ -337,7 +360,6 @@ class CommunitiesClient(object):
req.page_size = page_size req.page_size = page_size
return req return req
def is_user_registered(self): def is_user_registered(self):
"""Query whether the logged-in Microsoft Live user is registered with """Query whether the logged-in Microsoft Live user is registered with
the WWT Communities system. the WWT Communities system.
@ -369,6 +391,7 @@ class CommunitiesAPIRequest(APIRequest):
These require that the user be logged in to a Microsoft Live account. These require that the user be logged in to a Microsoft Live account.
""" """
_comm_client = None _comm_client = None
def __init__(self, communities_client): def __init__(self, communities_client):
@ -382,6 +405,7 @@ class CreateCommunityRequest(CommunitiesAPIRequest):
The response gives the ID of the new community. The response gives the ID of the new community.
""" """
payload = None payload = None
"""The request payload is JSON resembling:: """The request payload is JSON resembling::
@ -403,6 +427,7 @@ class CreateCommunityRequest(CommunitiesAPIRequest):
data structure at the moment.) data structure at the moment.)
""" """
def invalidity_reason(self): def invalidity_reason(self):
if self.payload is None: if self.payload is None:
return '"payload" must be a JSON dictionary' return '"payload" must be a JSON dictionary'
@ -411,18 +436,18 @@ class CreateCommunityRequest(CommunitiesAPIRequest):
def make_request(self): def make_request(self):
return requests.Request( return requests.Request(
method = 'POST', method="POST",
url = self._client._api_base + '/Community/Create/New', url=self._client._api_base + "/Community/Create/New",
json = self.payload, json=self.payload,
cookies = { cookies={
'access_token': self._comm_client._access_token, "access_token": self._comm_client._access_token,
'refresh_token': self._comm_client._refresh_token, "refresh_token": self._comm_client._refresh_token,
}, },
) )
def _process_response(self, resp): def _process_response(self, resp):
s = json.loads(resp.text) s = json.loads(resp.text)
return s['ID'] return s["ID"]
class DeleteCommunityRequest(CommunitiesAPIRequest): class DeleteCommunityRequest(CommunitiesAPIRequest):
@ -431,6 +456,7 @@ class DeleteCommunityRequest(CommunitiesAPIRequest):
Returns True if the community was successfully deleted, False otherwise. Returns True if the community was successfully deleted, False otherwise.
""" """
id = None id = None
"The ID number of the community to delete" "The ID number of the community to delete"
@ -444,22 +470,22 @@ class DeleteCommunityRequest(CommunitiesAPIRequest):
# The API includes a {parentId} after the community ID, but it is # The API includes a {parentId} after the community ID, but it is
# unused. # unused.
return requests.Request( return requests.Request(
method = 'POST', method="POST",
url = f'{self._client._api_base}/Community/Delete/{self.id}/0', url=f"{self._client._api_base}/Community/Delete/{self.id}/0",
cookies = { cookies={
'access_token': self._comm_client._access_token, "access_token": self._comm_client._access_token,
'refresh_token': self._comm_client._refresh_token, "refresh_token": self._comm_client._refresh_token,
}, },
) )
def _process_response(self, resp): def _process_response(self, resp):
t = resp.text t = resp.text
if t == 'True': if t == "True":
return True return True
elif t == 'False': elif t == "False":
return False return False
raise Exception(f'unexpected response from IsUserRegistered API: {t!r}') raise Exception(f"unexpected response from IsUserRegistered API: {t!r}")
# TODO: we're not implementing the "isEdit" mode where you can update # TODO: we're not implementing the "isEdit" mode where you can update
@ -536,6 +562,7 @@ class GetCommunityInfoRequest(CommunitiesAPIRequest):
} }
} }
""" """
id = None id = None
"The ID number of the community to probe" "The ID number of the community to probe"
@ -547,13 +574,13 @@ class GetCommunityInfoRequest(CommunitiesAPIRequest):
def make_request(self): def make_request(self):
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = f'{self._client._api_base}/Community/Detail/{self.id}', url=f"{self._client._api_base}/Community/Detail/{self.id}",
cookies = { cookies={
'access_token': self._comm_client._access_token, "access_token": self._comm_client._access_token,
'refresh_token': self._comm_client._refresh_token, "refresh_token": self._comm_client._refresh_token,
}, },
headers = {'LiveUserToken': self._comm_client._access_token}, headers={"LiveUserToken": self._comm_client._access_token},
) )
def _process_response(self, resp): def _process_response(self, resp):
@ -566,19 +593,21 @@ class GetLatestCommunityRequest(CommunitiesAPIRequest):
sub-Folders corresponding to the communities. sub-Folders corresponding to the communities.
""" """
def invalidity_reason(self): def invalidity_reason(self):
return None return None
def make_request(self): def make_request(self):
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = self._client._api_base + '/Resource/Service/Browse/LatestCommunity', url=self._client._api_base + "/Resource/Service/Browse/LatestCommunity",
headers = {'LiveUserToken': self._comm_client._access_token}, headers={"LiveUserToken": self._comm_client._access_token},
) )
def _process_response(self, resp): def _process_response(self, resp):
from wwt_data_formats.folder import Folder from wwt_data_formats.folder import Folder
from xml.etree import ElementTree as etree from xml.etree import ElementTree as etree
xml = etree.fromstring(resp.text) xml = etree.fromstring(resp.text)
return Folder.from_xml(xml) return Folder.from_xml(xml)
@ -602,19 +631,20 @@ class GetMyProfileRequest(CommunitiesAPIRequest):
'IsSubscribed': False 'IsSubscribed': False
} }
""" """
def invalidity_reason(self): def invalidity_reason(self):
return None return None
def make_request(self): def make_request(self):
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = self._client._api_base + '/Profile/MyProfile/Get', url=self._client._api_base + "/Profile/MyProfile/Get",
headers = { headers={
'Accept': 'application/json, text/plain, */*', "Accept": "application/json, text/plain, */*",
}, },
cookies = { cookies={
'access_token': self._comm_client._access_token, "access_token": self._comm_client._access_token,
'refresh_token': self._comm_client._refresh_token, "refresh_token": self._comm_client._refresh_token,
}, },
) )
@ -628,6 +658,7 @@ class GetProfileEntitiesRequest(CommunitiesAPIRequest):
Entities include communities, folders, and content files. The response is JSON. Entities include communities, folders, and content files. The response is JSON.
""" """
entity_type = enums.EntityType.CONTENT entity_type = enums.EntityType.CONTENT
"What kind of entity to query. Only COMMUNITY and CONTENT are allowed." "What kind of entity to query. Only COMMUNITY and CONTENT are allowed."
@ -648,11 +679,11 @@ class GetProfileEntitiesRequest(CommunitiesAPIRequest):
def make_request(self): def make_request(self):
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = f'{self._client._api_base}/Profile/Entities/{self.entity_type.value}/{self.current_page}/{self.page_size}', url=f"{self._client._api_base}/Profile/Entities/{self.entity_type.value}/{self.current_page}/{self.page_size}",
cookies = { cookies={
'access_token': self._comm_client._access_token, "access_token": self._comm_client._access_token,
'refresh_token': self._comm_client._refresh_token, "refresh_token": self._comm_client._refresh_token,
}, },
) )
@ -665,41 +696,43 @@ class IsUserRegisteredRequest(CommunitiesAPIRequest):
Communities system. Communities system.
""" """
def invalidity_reason(self): def invalidity_reason(self):
return None return None
def make_request(self): def make_request(self):
return requests.Request( return requests.Request(
method = 'GET', method="GET",
url = self._client._api_base + '/Resource/Service/User', url=self._client._api_base + "/Resource/Service/User",
headers = {'LiveUserToken': self._comm_client._access_token}, headers={"LiveUserToken": self._comm_client._access_token},
) )
def _process_response(self, resp): def _process_response(self, resp):
t = resp.text t = resp.text
if t == 'True': if t == "True":
return True return True
elif t == 'False': elif t == "False":
return False return False
raise Exception(f'unexpected response from IsUserRegistered API: {t!r}') raise Exception(f"unexpected response from IsUserRegistered API: {t!r}")
# Command-line utility for initializing the OAuth state. # Command-line utility for initializing the OAuth state.
def interactive_communities_login(args): def interactive_communities_login(args):
import argparse import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument( parser.add_argument(
'--secret-file', "--secret-file",
metavar = 'PATH', metavar="PATH",
help = 'Path to a file from which to read the WWT client secret', help="Path to a file from which to read the WWT client secret",
) )
parser.add_argument( parser.add_argument(
'--secret-env', "--secret-env",
metavar = 'ENV-VAR-NAME', metavar="ENV-VAR-NAME",
help = 'Name of an environment variable containing the WWT client secret', help="Name of an environment variable containing the WWT client secret",
) )
settings = parser.parse_args(args) settings = parser.parse_args(args)
@ -712,24 +745,27 @@ def interactive_communities_login(args):
elif settings.secret_env is not None: elif settings.secret_env is not None:
client_secret = os.environ.get(settings.secret_env) client_secret = os.environ.get(settings.secret_env)
else: else:
print('error: the WWT \"client secret\" must be provided; ' print(
'use --secret-file or --secret-env', file=sys.stderr) 'error: the WWT "client secret" must be provided; '
"use --secret-file or --secret-env",
file=sys.stderr,
)
sys.exit(1) sys.exit(1)
if not client_secret: if not client_secret:
print('error: the WWT \"client secret\" is empty or unset', file=sys.stderr) print('error: the WWT "client secret" is empty or unset', file=sys.stderr)
sys.exit(1) sys.exit(1)
# Ready to go ... # Ready to go ...
CommunitiesClient( CommunitiesClient(
Client(), Client(),
oauth_client_secret = client_secret, oauth_client_secret=client_secret,
interactive_login_if_needed = True, interactive_login_if_needed=True,
) )
print('OAuth flow successfully completed.') print("OAuth flow successfully completed.")
if __name__ == '__main__': if __name__ == "__main__":
interactive_communities_login(sys.argv[1:]) interactive_communities_login(sys.argv[1:])

Просмотреть файл

@ -1,6 +1,6 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright 2019 the .Net Foundation # Copyright 2019 the .Net Foundation
# Distributed under the terms of the revised (3-clause) BSD license. # Distributed under the MIT license
"""This module contains tests of the wwt_api_client package. """This module contains tests of the wwt_api_client package.

Просмотреть файл

@ -1,6 +1,6 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright 2020 the .NET Foundation # Copyright 2020-2023 the .NET Foundation
# Distributed under the terms of the revised (3-clause) BSD license. # Distributed under the MIT license
import json import json
from mock import Mock from mock import Mock
@ -21,15 +21,16 @@ def fake_request_post(url, data=None, json=None, **kwargs):
if url == communities.LIVE_OAUTH_TOKEN_SERVICE: if url == communities.LIVE_OAUTH_TOKEN_SERVICE:
rv.json.return_value = { rv.json.return_value = {
'access_token': 'fake_access_token', "access_token": "fake_access_token",
'refresh_token': 'fake_refresh_token', "refresh_token": "fake_refresh_token",
} }
else: else:
raise Exception(f'unexpected URL to fake requests.post(): {url}') raise Exception(f"unexpected URL to fake requests.post(): {url}")
return rv return rv
GET_COMMUNITY_INFO_JSON_TEXT = '''
GET_COMMUNITY_INFO_JSON_TEXT = """
{ {
"community": { "community": {
"MemberCount": 0, "MemberCount": 0,
@ -96,25 +97,23 @@ GET_COMMUNITY_INFO_JSON_TEXT = '''
"IsFaulted": false "IsFaulted": false
} }
} }
''' """
GET_LATEST_COMMUNITY_XML_TEXT = '''\ GET_LATEST_COMMUNITY_XML_TEXT = """\
<?xml version='1.0' encoding='UTF-8'?> <?xml version='1.0' encoding='UTF-8'?>
<Folder Browseable="True" Group="Explorer" <Folder Browseable="True" Group="Explorer"
MSRCommunityId="0" MSRComponentId="0" Permission="0"
Searchable="True" Searchable="True"
Thumbnail="http://www.worldwidetelescope.org/Content/Images/defaultfolderwwtthumbnail.png" Thumbnail="http://www.worldwidetelescope.org/Content/Images/defaultfolderwwtthumbnail.png"
Type="Earth"> Type="Earth">
<Folder Browseable="True" Group="Explorer" Name="AAS Nova" <Folder Browseable="True" Group="Explorer" Name="AAS Nova"
MSRCommunityId="0" MSRComponentId="0" Permission="0"
Searchable="True" Searchable="True"
Thumbnail="http://www.worldwidetelescope.org/File/Thumbnail/80a8d8ef-8a76-414a-a398-349337baac8c" Thumbnail="http://www.worldwidetelescope.org/File/Thumbnail/80a8d8ef-8a76-414a-a398-349337baac8c"
Url="http://www.worldwidetelescope.org/Resource/Service/Folder/607649" Url="http://www.worldwidetelescope.org/Resource/Service/Folder/607649"
Type="Earth" /> Type="Earth" />
</Folder> </Folder>
''' """
GET_MY_PROFILE_JSON_TEXT = ''' GET_MY_PROFILE_JSON_TEXT = """
{ {
"ProfileId": 123456, "ProfileId": 123456,
"ProfileName": "Firstname Lastname", "ProfileName": "Firstname Lastname",
@ -128,9 +127,9 @@ GET_MY_PROFILE_JSON_TEXT = '''
"IsCurrentUser": true, "IsCurrentUser": true,
"IsSubscribed": false "IsSubscribed": false
} }
''' """
GET_PROFILE_ENTITIES_JSON_TEXT = ''' GET_PROFILE_ENTITIES_JSON_TEXT = """
{ {
"entities": [ "entities": [
{ {
@ -176,37 +175,40 @@ GET_PROFILE_ENTITIES_JSON_TEXT = '''
"TotalCount": 1 "TotalCount": 1
} }
} }
''' """
def fake_request_session_send(request, **kwargs): def fake_request_session_send(request, **kwargs):
rv = Mock() rv = Mock()
if request.url == DEFAULT_API_BASE + '/Community/Create/New': if request.url == DEFAULT_API_BASE + "/Community/Create/New":
rv.text = '{"ID": 800000}' rv.text = '{"ID": 800000}'
elif request.url == DEFAULT_API_BASE + '/Community/Delete/800000/0': elif request.url == DEFAULT_API_BASE + "/Community/Delete/800000/0":
rv.text = 'True' rv.text = "True"
elif request.url == DEFAULT_API_BASE + '/Community/Detail/800000': elif request.url == DEFAULT_API_BASE + "/Community/Detail/800000":
rv.text = GET_COMMUNITY_INFO_JSON_TEXT rv.text = GET_COMMUNITY_INFO_JSON_TEXT
elif request.url == DEFAULT_API_BASE + '/Profile/Entities/Content/1/99999': elif request.url == DEFAULT_API_BASE + "/Profile/Entities/Content/1/99999":
rv.text = GET_PROFILE_ENTITIES_JSON_TEXT rv.text = GET_PROFILE_ENTITIES_JSON_TEXT
elif request.url == DEFAULT_API_BASE + '/Profile/MyProfile/Get': elif request.url == DEFAULT_API_BASE + "/Profile/MyProfile/Get":
rv.text = GET_MY_PROFILE_JSON_TEXT rv.text = GET_MY_PROFILE_JSON_TEXT
elif request.url == DEFAULT_API_BASE + '/Resource/Service/Browse/LatestCommunity': elif request.url == DEFAULT_API_BASE + "/Resource/Service/Browse/LatestCommunity":
rv.text = GET_LATEST_COMMUNITY_XML_TEXT rv.text = GET_LATEST_COMMUNITY_XML_TEXT
elif request.url == DEFAULT_API_BASE + '/Resource/Service/User': elif request.url == DEFAULT_API_BASE + "/Resource/Service/User":
rv.text = 'True' rv.text = "True"
else: else:
raise Exception(f'unexpected URL to fake requests.Session.send(): {request.url}') raise Exception(
f"unexpected URL to fake requests.Session.send(): {request.url}"
)
return rv return rv
@pytest.fixture @pytest.fixture
def fake_requests(mocker): def fake_requests(mocker):
m = mocker.patch('requests.post') m = mocker.patch("requests.post")
m.side_effect = fake_request_post m.side_effect = fake_request_post
m = mocker.patch('requests.Session.send') m = mocker.patch("requests.Session.send")
m.side_effect = fake_request_session_send m.side_effect = fake_request_session_send
@ -214,19 +216,21 @@ def fake_requests(mocker):
def communities_client_cached(client, fake_requests): def communities_client_cached(client, fake_requests):
temp_state_dir = tempfile.mkdtemp() temp_state_dir = tempfile.mkdtemp()
with open(os.path.join(temp_state_dir, communities.CLIENT_SECRET_BASENAME), 'w') as f: with open(
print('fake_client_secret', file=f) os.path.join(temp_state_dir, communities.CLIENT_SECRET_BASENAME), "w"
) as f:
print("fake_client_secret", file=f)
with open(os.path.join(temp_state_dir, communities.OAUTH_STATE_BASENAME), 'w') as f: with open(os.path.join(temp_state_dir, communities.OAUTH_STATE_BASENAME), "w") as f:
oauth_data = { oauth_data = {
'access_token': 'fake_access_token', "access_token": "fake_access_token",
'refresh_token': 'fake_refresh_token', "refresh_token": "fake_refresh_token",
} }
json.dump(oauth_data, f) json.dump(oauth_data, f)
yield CommunitiesClient( yield CommunitiesClient(
client, client,
state_dir = temp_state_dir, state_dir=temp_state_dir,
) )
shutil.rmtree(temp_state_dir) shutil.rmtree(temp_state_dir)
@ -240,14 +244,14 @@ def test_create_client_cached(communities_client_cached):
def communities_client_interactive(client, fake_requests, mocker): def communities_client_interactive(client, fake_requests, mocker):
temp_state_dir = tempfile.mkdtemp() temp_state_dir = tempfile.mkdtemp()
m = mocker.patch('builtins.input') m = mocker.patch("builtins.input")
m.return_value = 'http://fakelogin.example.com?code=fake_code' m.return_value = "http://fakelogin.example.com?code=fake_code"
yield CommunitiesClient( yield CommunitiesClient(
client, client,
oauth_client_secret = 'fake_client_secret', oauth_client_secret="fake_client_secret",
interactive_login_if_needed=True, interactive_login_if_needed=True,
state_dir = temp_state_dir, state_dir=temp_state_dir,
) )
shutil.rmtree(temp_state_dir) shutil.rmtree(temp_state_dir)
@ -258,34 +262,34 @@ def test_create_client_interactive(communities_client_interactive):
def test_cli(communities_client_interactive, mocker): def test_cli(communities_client_interactive, mocker):
m = mocker.patch('wwt_api_client.communities.CommunitiesClient') m = mocker.patch("wwt_api_client.communities.CommunitiesClient")
m.return_value = communities_client_interactive m.return_value = communities_client_interactive
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
communities.interactive_communities_login([]) communities.interactive_communities_login([])
os.environ['FAKE_CLIENT_SECRET'] = 'fakey' os.environ["FAKE_CLIENT_SECRET"] = "fakey"
communities.interactive_communities_login(['--secret-env=FAKE_CLIENT_SECRET']) communities.interactive_communities_login(["--secret-env=FAKE_CLIENT_SECRET"])
f = tempfile.NamedTemporaryFile(mode='wt', delete=False) f = tempfile.NamedTemporaryFile(mode="wt", delete=False)
print('fake_client_secret', file=f) print("fake_client_secret", file=f)
f.close() f.close()
communities.interactive_communities_login([f'--secret-file={f.name}']) communities.interactive_communities_login([f"--secret-file={f.name}"])
os.unlink(f.name) os.unlink(f.name)
def test_create_community(communities_client_cached): def test_create_community(communities_client_cached):
payload = { payload = {
'communityJson': { "communityJson": {
'CategoryID': 20, "CategoryID": 20,
'ParentID': '610131', "ParentID": "610131",
'AccessTypeID': 2, "AccessTypeID": 2,
'IsOffensive': False, "IsOffensive": False,
'IsLink': False, "IsLink": False,
'CommunityType': 'Community', "CommunityType": "Community",
'Name': 'API Test Community', "Name": "API Test Community",
'Description': 'Community description', "Description": "Community description",
'Tags': 'tag1,tag2' "Tags": "tag1,tag2",
} }
} }
new_id = communities_client_cached.create_community(payload=payload).send() new_id = communities_client_cached.create_community(payload=payload).send()
@ -319,13 +323,16 @@ def test_get_my_profile(communities_client_cached):
def test_get_profile_entities(communities_client_cached): def test_get_profile_entities(communities_client_cached):
expected_json = json.loads(GET_PROFILE_ENTITIES_JSON_TEXT) expected_json = json.loads(GET_PROFILE_ENTITIES_JSON_TEXT)
observed_json = communities_client_cached.get_profile_entities( observed_json = communities_client_cached.get_profile_entities(
entity_type = enums.EntityType.CONTENT, entity_type=enums.EntityType.CONTENT,
current_page = 1, current_page=1,
page_size = 99999, page_size=99999,
).send() ).send()
assert observed_json == expected_json assert observed_json == expected_json
def test_is_user_registered(communities_client_cached): def test_is_user_registered(communities_client_cached):
assert communities_client_cached.is_user_registered().send(raw_response=True).text == 'True' assert (
communities_client_cached.is_user_registered().send(raw_response=True).text
== "True"
)
assert communities_client_cached.is_user_registered().send() assert communities_client_cached.is_user_registered().send()

Просмотреть файл

@ -1,6 +1,6 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright 2019 the .Net Foundation # Copyright 2019-2023 the .Net Foundation
# Distributed under the terms of the revised (3-clause) BSD license. # Distributed under the MIT license
"""Note! This test suite will hit the network! """Note! This test suite will hit the network!
@ -11,137 +11,157 @@ from xml.etree import ElementTree
from .. import Client from .. import Client
INF = float('inf') INF = float("inf")
NAN = float('nan') NAN = float("nan")
def _assert_xml_trees_equal(path, e1, e2, care_text_tags): def _assert_xml_trees_equal(path, e1, e2, care_text_tags):
"Derived from https://stackoverflow.com/a/24349916/3760486" "Derived from https://stackoverflow.com/a/24349916/3760486"
assert e1.tag == e2.tag, \ assert e1.tag == e2.tag, "at XML path {0}, tags {1} and {2} differed".format(
'at XML path {0}, tags {1} and {2} differed'.format(path, e1.tag, e2.tag) path, e1.tag, e2.tag
)
# We only sometimes care about this; often it's just whitespace # We only sometimes care about this; often it's just whitespace
if e1.tag in care_text_tags: if e1.tag in care_text_tags:
assert e1.text == e2.text, \ assert (
'at XML path {0}, texts {1!r} and {2!r} differed'.format(path, e1.text, e2.text) e1.text == e2.text
), "at XML path {0}, texts {1!r} and {2!r} differed".format(
path, e1.text, e2.text
)
# We never care about this, right? # We never care about this, right?
#assert e1.tail == e2.tail, \ # assert e1.tail == e2.tail, \
# 'at XML path {0}, tails {1!r} and {2!r} differed'.format(path, e1.tail, e2.tail) # 'at XML path {0}, tails {1!r} and {2!r} differed'.format(path, e1.tail, e2.tail)
assert e1.attrib == e2.attrib, \ assert (
'at XML path {0}, attributes {1!r} and {2!r} differed'.format(path, e1.attrib, e2.attrib) e1.attrib == e2.attrib
assert len(e1) == len(e2), \ ), "at XML path {0}, attributes {1!r} and {2!r} differed".format(
'at XML path {0}, number of children {1} and {2} differed'.format(path, len(e1), len(e2)) path, e1.attrib, e2.attrib
)
assert len(e1) == len(
e2
), "at XML path {0}, number of children {1} and {2} differed".format(
path, len(e1), len(e2)
)
subpath = '{0}>{1}'.format(path, e1.tag) subpath = "{0}>{1}".format(path, e1.tag)
for c1, c2 in zip (e1, e2): for c1, c2 in zip(e1, e2):
_assert_xml_trees_equal(subpath, c1, c2, care_text_tags) _assert_xml_trees_equal(subpath, c1, c2, care_text_tags)
def assert_xml_trees_equal(e1, e2, care_text_tags=()): def assert_xml_trees_equal(e1, e2, care_text_tags=()):
_assert_xml_trees_equal('(root)', e1, e2, care_text_tags) _assert_xml_trees_equal("(root)", e1, e2, care_text_tags)
@pytest.fixture @pytest.fixture
def client(): def client():
return Client() return Client()
@pytest.fixture @pytest.fixture
def login(client): def login(client):
"Return a valid login request object." "Return a valid login request object."
return client.login() return client.login()
def test_login_basic(login): def test_login_basic(login):
assert login.invalidity_reason() is None assert login.invalidity_reason() is None
login.send() login.send()
@pytest.fixture @pytest.fixture
def showimage(client): def showimage(client):
"Return a valid ShowImage request object." "Return a valid ShowImage request object."
return client.show_image('http://localhost/image.jpg', 'name') return client.show_image("http://localhost/image.jpg", "name")
SHOWIMAGE_BAD_SETTINGS = [ SHOWIMAGE_BAD_SETTINGS = [
('credits', b'\xff not unicodable'), ("credits", b"\xff not unicodable"),
('credits_url', u'http://olé/not_ascii_unicode_url'), ("credits_url", "http://olé/not_ascii_unicode_url"),
('credits_url', b'http://host/\x81/not_ascii_bytes_url'), ("credits_url", b"http://host/\x81/not_ascii_bytes_url"),
('credits_url', 'not_absolute_url'), ("credits_url", "not_absolute_url"),
('dec_deg', -90.00001), ("dec_deg", -90.00001),
('dec_deg', 90.00001), ("dec_deg", 90.00001),
('dec_deg', NAN), ("dec_deg", NAN),
('dec_deg', INF), ("dec_deg", INF),
('dec_deg', 'not numeric'), ("dec_deg", "not numeric"),
('image_url', None), ("image_url", None),
('image_url', u'http://olé/not_ascii_unicode_url'), ("image_url", "http://olé/not_ascii_unicode_url"),
('image_url', b'http://host/\x81/not_ascii_bytes_url'), ("image_url", b"http://host/\x81/not_ascii_bytes_url"),
('image_url', 'not_absolute_url'), ("image_url", "not_absolute_url"),
('name', None), ("name", None),
('ra_deg', NAN), ("ra_deg", NAN),
('ra_deg', INF), ("ra_deg", INF),
('ra_deg', 'not numeric'), ("ra_deg", "not numeric"),
('reverse_parity', 1), # only bools allowed ("reverse_parity", 1), # only bools allowed
('reverse_parity', 't'), # only bools allowed ("reverse_parity", "t"), # only bools allowed
('rotation_deg', NAN), ("rotation_deg", NAN),
('rotation_deg', INF), ("rotation_deg", INF),
('rotation_deg', 'not numeric'), ("rotation_deg", "not numeric"),
('scale_arcsec', 0.), ("scale_arcsec", 0.0),
('scale_arcsec', NAN), ("scale_arcsec", NAN),
('scale_arcsec', INF), ("scale_arcsec", INF),
('scale_arcsec', 'not numeric'), ("scale_arcsec", "not numeric"),
('thumbnail_url', u'http://olé/not_ascii_unicode_url'), ("thumbnail_url", "http://olé/not_ascii_unicode_url"),
('thumbnail_url', b'http://host/\x81/not_ascii_bytes_url'), ("thumbnail_url", b"http://host/\x81/not_ascii_bytes_url"),
('thumbnail_url', 'not_absolute_url'), ("thumbnail_url", "not_absolute_url"),
('x_offset_pixels', NAN), ("x_offset_pixels", NAN),
('x_offset_pixels', INF), ("x_offset_pixels", INF),
('x_offset_pixels', 'not numeric'), ("x_offset_pixels", "not numeric"),
('y_offset_pixels', NAN), ("y_offset_pixels", NAN),
('y_offset_pixels', INF), ("y_offset_pixels", INF),
('y_offset_pixels', 'not numeric'), ("y_offset_pixels", "not numeric"),
] ]
@pytest.mark.parametrize(('attr', 'val'), SHOWIMAGE_BAD_SETTINGS)
@pytest.mark.parametrize(("attr", "val"), SHOWIMAGE_BAD_SETTINGS)
def test_showimage_invalid_settings(showimage, attr, val): def test_showimage_invalid_settings(showimage, attr, val):
setattr(showimage, attr, val) setattr(showimage, attr, val)
assert showimage.invalidity_reason() is not None assert showimage.invalidity_reason() is not None
SHOWIMAGE_GOOD_SETTINGS = [ SHOWIMAGE_GOOD_SETTINGS = [
('credits', b'unicodable bytes'), ("credits", b"unicodable bytes"),
('credits', u'unicode é'), ("credits", "unicode é"),
('credits', None), ("credits", None),
('credits_url', b'http://localhost/absolute_bytes_url'), ("credits_url", b"http://localhost/absolute_bytes_url"),
('credits_url', u'//localhost/absolute_unicode_url'), ("credits_url", "//localhost/absolute_unicode_url"),
('credits_url', None), ("credits_url", None),
('dec_deg', -90), ("dec_deg", -90),
('dec_deg', 90), ("dec_deg", 90),
('image_url', b'http://localhost/absolute_bytes_url'), ("image_url", b"http://localhost/absolute_bytes_url"),
('image_url', u'//localhost/absolute_unicode_url'), ("image_url", "//localhost/absolute_unicode_url"),
('name', b'unicodable bytes'), ("name", b"unicodable bytes"),
('name', u'unicode é'), ("name", "unicode é"),
('ra_deg', -720.), ("ra_deg", -720.0),
('ra_deg', 980.), ("ra_deg", 980.0),
('reverse_parity', False), ("reverse_parity", False),
('reverse_parity', True), ("reverse_parity", True),
('rotation_deg', -1), ("rotation_deg", -1),
('scale_arcsec', -1.), ("scale_arcsec", -1.0),
('thumbnail_url', b'http://localhost/absolute_bytes_url'), ("thumbnail_url", b"http://localhost/absolute_bytes_url"),
('thumbnail_url', u'//localhost/absolute_unicode_url'), ("thumbnail_url", "//localhost/absolute_unicode_url"),
('thumbnail_url', None), ("thumbnail_url", None),
('x_offset_pixels', -1.), ("x_offset_pixels", -1.0),
('x_offset_pixels', 0), ("x_offset_pixels", 0),
('y_offset_pixels', -1.), ("y_offset_pixels", -1.0),
('y_offset_pixels', 0), ("y_offset_pixels", 0),
] ]
@pytest.mark.parametrize(('attr', 'val'), SHOWIMAGE_GOOD_SETTINGS)
@pytest.mark.parametrize(("attr", "val"), SHOWIMAGE_GOOD_SETTINGS)
def test_showimage_valid_settings(showimage, attr, val): def test_showimage_valid_settings(showimage, attr, val):
setattr(showimage, attr, val) setattr(showimage, attr, val)
assert showimage.invalidity_reason() is None assert showimage.invalidity_reason() is None
SHOWIMAGE_CARE_TEXT_TAGS = set(('Credits', 'CreditsUrl')) SHOWIMAGE_CARE_TEXT_TAGS = set(("Credits", "CreditsUrl"))
def _make_showimage_result(credurl='', name='name'):
return '''<?xml version="1.0" encoding="UTF-8"?> def _make_showimage_result(credurl="", name="name"):
return """<?xml version="1.0" encoding="UTF-8"?>
<Folder Name="{name}" Group="Goto"> <Folder Name="{name}" Group="Goto">
<Place Name="{name}" RA="0" Dec="0" ZoomLevel="0" DataSetType="Sky" Opacity="100" <Place Name="{name}" RA="0" Dec="0" ZoomLevel="0" DataSetType="Sky" Opacity="100"
Thumbnail="" Constellation=""> Thumbnail="" Constellation="">
@ -149,22 +169,29 @@ def _make_showimage_result(credurl='', name='name'):
<ImageSet DataSetType="Sky" BandPass="Visible" Url="http://localhost/image.jpg" <ImageSet DataSetType="Sky" BandPass="Visible" Url="http://localhost/image.jpg"
TileLevels="0" WidthFactor="2" Rotation="0" Projection="SkyImage" TileLevels="0" WidthFactor="2" Rotation="0" Projection="SkyImage"
FileType=".tif" CenterY="0" CenterX="0" BottomsUp="False" OffsetX="0" FileType=".tif" CenterY="0" CenterX="0" BottomsUp="False" OffsetX="0"
OffsetY="0" BaseTileLevel="0" BaseDegreesPerTile="0.000277777777777778"> OffsetY="0" BaseTileLevel="0" BaseDegreesPerTile="0.0002777777777777778">
<Credits></Credits> <Credits></Credits>
<CreditsUrl>{credurl}</CreditsUrl> <CreditsUrl>{credurl}</CreditsUrl>
</ImageSet> </ImageSet>
</ForegroundImageSet> </ForegroundImageSet>
</Place> </Place>
</Folder> </Folder>
'''.format(credurl=credurl, name=name) """.format(
credurl=credurl, name=name
)
SHOWIMAGE_RESULTS = [ SHOWIMAGE_RESULTS = [
(dict(), _make_showimage_result()), (dict(), _make_showimage_result()),
(dict(name='test&xml"esc'), _make_showimage_result(name='test&amp;xml&quot;esc')), (dict(name='test&xml"esc'), _make_showimage_result(name="test&amp;xml&quot;esc")),
(dict(credits_url='http://a/b&c'), _make_showimage_result(credurl='http://a/b&amp;c')), (
dict(credits_url="http://a/b&c"),
_make_showimage_result(credurl="http://a/b&amp;c"),
),
] ]
@pytest.mark.parametrize(('attrs', 'expected'), SHOWIMAGE_RESULTS)
@pytest.mark.parametrize(("attrs", "expected"), SHOWIMAGE_RESULTS)
def test_showimage_valid_settings(showimage, attrs, expected): def test_showimage_valid_settings(showimage, attrs, expected):
expected = ElementTree.fromstring(expected) expected = ElementTree.fromstring(expected)
@ -179,88 +206,95 @@ def test_showimage_valid_settings(showimage, attrs, expected):
@pytest.fixture @pytest.fixture
def tileimage(client): def tileimage(client):
"Return a valid TileImage request object." "Return a valid TileImage request object."
return client.tile_image('http://www.spitzer.caltech.edu/uploaded_files/images/0009/0848/sig12-011.jpg') return client.tile_image(
"http://www.spitzer.caltech.edu/uploaded_files/images/0009/0848/sig12-011.jpg"
)
TILEIMAGE_BAD_SETTINGS = [ TILEIMAGE_BAD_SETTINGS = [
('credits', b'\xff not unicodable'), ("credits", b"\xff not unicodable"),
('credits_url', u'http://olé/not_ascii_unicode_url'), ("credits_url", "http://olé/not_ascii_unicode_url"),
('credits_url', b'http://host/\x81/not_ascii_bytes_url'), ("credits_url", b"http://host/\x81/not_ascii_bytes_url"),
('credits_url', 'not_absolute_url'), ("credits_url", "not_absolute_url"),
('dec_deg', -90.00001), ("dec_deg", -90.00001),
('dec_deg', 90.00001), ("dec_deg", 90.00001),
('dec_deg', NAN), ("dec_deg", NAN),
('dec_deg', INF), ("dec_deg", INF),
('dec_deg', 'not numeric'), ("dec_deg", "not numeric"),
('image_url', None), ("image_url", None),
('image_url', u'http://olé/not_ascii_unicode_url'), ("image_url", "http://olé/not_ascii_unicode_url"),
('image_url', b'http://host/\x81/not_ascii_bytes_url'), ("image_url", b"http://host/\x81/not_ascii_bytes_url"),
('image_url', 'not_absolute_url'), ("image_url", "not_absolute_url"),
('ra_deg', NAN), ("ra_deg", NAN),
('ra_deg', INF), ("ra_deg", INF),
('ra_deg', 'not numeric'), ("ra_deg", "not numeric"),
('rotation_deg', NAN), ("rotation_deg", NAN),
('rotation_deg', INF), ("rotation_deg", INF),
('rotation_deg', 'not numeric'), ("rotation_deg", "not numeric"),
('scale_deg', 0.), ("scale_deg", 0.0),
('scale_deg', NAN), ("scale_deg", NAN),
('scale_deg', INF), ("scale_deg", INF),
('scale_deg', 'not numeric'), ("scale_deg", "not numeric"),
('thumbnail_url', u'http://olé/not_ascii_unicode_url'), ("thumbnail_url", "http://olé/not_ascii_unicode_url"),
('thumbnail_url', b'http://host/\x81/not_ascii_bytes_url'), ("thumbnail_url", b"http://host/\x81/not_ascii_bytes_url"),
('thumbnail_url', 'not_absolute_url'), ("thumbnail_url", "not_absolute_url"),
('x_offset_deg', NAN), ("x_offset_deg", NAN),
('x_offset_deg', INF), ("x_offset_deg", INF),
('x_offset_deg', 'not numeric'), ("x_offset_deg", "not numeric"),
('y_offset_deg', NAN), ("y_offset_deg", NAN),
('y_offset_deg', INF), ("y_offset_deg", INF),
('y_offset_deg', 'not numeric'), ("y_offset_deg", "not numeric"),
] ]
@pytest.mark.parametrize(('attr', 'val'), TILEIMAGE_BAD_SETTINGS)
@pytest.mark.parametrize(("attr", "val"), TILEIMAGE_BAD_SETTINGS)
def test_tileimage_invalid_settings(tileimage, attr, val): def test_tileimage_invalid_settings(tileimage, attr, val):
setattr(tileimage, attr, val) setattr(tileimage, attr, val)
assert tileimage.invalidity_reason() is not None assert tileimage.invalidity_reason() is not None
TILEIMAGE_GOOD_SETTINGS = [ TILEIMAGE_GOOD_SETTINGS = [
('credits', b'unicodable bytes'), ("credits", b"unicodable bytes"),
('credits', u'unicode é'), ("credits", "unicode é"),
('credits', None), ("credits", None),
('credits_url', b'http://localhost/absolute_bytes_url'), ("credits_url", b"http://localhost/absolute_bytes_url"),
('credits_url', u'//localhost/absolute_unicode_url'), ("credits_url", "//localhost/absolute_unicode_url"),
('credits_url', None), ("credits_url", None),
('dec_deg', 90), ("dec_deg", 90),
('dec_deg', 90), ("dec_deg", 90),
('dec_deg', None), ("dec_deg", None),
('image_url', b'http://localhost/absolute_bytes_url'), ("image_url", b"http://localhost/absolute_bytes_url"),
('image_url', u'//localhost/absolute_unicode_url'), ("image_url", "//localhost/absolute_unicode_url"),
('ra_deg', -720.), ("ra_deg", -720.0),
('ra_deg', 980.), ("ra_deg", 980.0),
('ra_deg', None), ("ra_deg", None),
('rotation_deg', -1), ("rotation_deg", -1),
('rotation_deg', None), ("rotation_deg", None),
('scale_deg', -1.), ("scale_deg", -1.0),
('scale_deg', None), ("scale_deg", None),
('thumbnail_url', b'http://localhost/absolute_bytes_url'), ("thumbnail_url", b"http://localhost/absolute_bytes_url"),
('thumbnail_url', u'//localhost/absolute_unicode_url'), ("thumbnail_url", "//localhost/absolute_unicode_url"),
('thumbnail_url', None), ("thumbnail_url", None),
('x_offset_deg', -1.), ("x_offset_deg", -1.0),
('x_offset_deg', 0), ("x_offset_deg", 0),
('x_offset_deg', None), ("x_offset_deg", None),
('y_offset_deg', -1.), ("y_offset_deg", -1.0),
('y_offset_deg', 0), ("y_offset_deg", 0),
('y_offset_deg', None), ("y_offset_deg", None),
] ]
@pytest.mark.parametrize(('attr', 'val'), TILEIMAGE_GOOD_SETTINGS)
@pytest.mark.parametrize(("attr", "val"), TILEIMAGE_GOOD_SETTINGS)
def test_tileimage_valid_settings(tileimage, attr, val): def test_tileimage_valid_settings(tileimage, attr, val):
setattr(tileimage, attr, val) setattr(tileimage, attr, val)
assert tileimage.invalidity_reason() is None assert tileimage.invalidity_reason() is None
TILEIMAGE_CARE_TEXT_TAGS = set(('Credits', 'CreditsUrl')) TILEIMAGE_CARE_TEXT_TAGS = set(("Credits", "CreditsUrl"))
def _make_tileimage_result(credits='', credurl='', ident=None, name='Image File'):
return '''<Folder Name="{name}" Group="Explorer"> def _make_tileimage_result(credits="", credurl="", ident=None, name="Image File"):
return """<Folder Name="{name}" Group="Explorer">
<Place Name="{name}" RA="0" Dec="0" ZoomLevel="32768" DataSetType="Sky" <Place Name="{name}" RA="0" Dec="0" ZoomLevel="32768" DataSetType="Sky"
Opacity="100" Thumbnail="http://www.worldwidetelescope.org/wwtweb/tilethumb.aspx?name={ident}" Opacity="100" Thumbnail="http://www.worldwidetelescope.org/wwtweb/tilethumb.aspx?name={ident}"
Constellation=""> Constellation="">
@ -277,18 +311,25 @@ def _make_tileimage_result(credits='', credurl='', ident=None, name='Image File'
</ForegroundImageSet> </ForegroundImageSet>
</Place> </Place>
</Folder> </Folder>
'''.format(credits=credits, credurl=credurl, ident=ident, name=name) """.format(
credits=credits, credurl=credurl, ident=ident, name=name
)
TILEIMAGE_RESULTS = [ TILEIMAGE_RESULTS = [
(dict(), _make_tileimage_result( (
credits = ' NASA/JPL-Caltech', dict(),
credurl = 'http://www.spitzer.caltech.edu/images/5259-sig12-011-The-Helix-Nebula-Unraveling-at-the-Seams', _make_tileimage_result(
ident = '1176481368', credits=" NASA/JPL-Caltech",
name = 'Helix Nebula', credurl="http://www.spitzer.caltech.edu/images/5259-sig12-011-The-Helix-Nebula-Unraveling-at-the-Seams",
)), ident="1176481368",
name="Helix Nebula",
),
),
] ]
@pytest.mark.parametrize(('attrs', 'expected'), TILEIMAGE_RESULTS)
@pytest.mark.parametrize(("attrs", "expected"), TILEIMAGE_RESULTS)
def test_tileimage_valid_settings(tileimage, attrs, expected): def test_tileimage_valid_settings(tileimage, attrs, expected):
expected = ElementTree.fromstring(expected) expected = ElementTree.fromstring(expected)