Source code for umansysprop.client

# vim: set et sw=4 sts=4 fileencoding=utf-8:
#
# Copyright 2014 Dave Jones <dave@waveform.org.uk>.
#
# This file is part of umansysprop.
#
# umansysprop is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 2 of the License, or (at your option) any later
# version.
#
# umansysprop is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# umansysprop.  If not, see <http://www.gnu.org/licenses/>.

"""
=============================
``umansysprop.client`` Module
=============================

This module contains the client library for interacting with the UManSysProp
server. Only one user-accessible class is defined in the module:

UManSysProp
===========

.. autoclass:: UManSysProp

"""

from __future__ import (
    unicode_literals,
    absolute_import,
    print_function,
    division,
    )
str = type('')


import types
import json
try:
    from urllib.parse import urljoin
except ImportError:
    from urlparse import urljoin

import requests

from . import results


[docs]class UManSysProp(object): """ Provides a simple Python interface to the methods provided via the `JSON API`_ on the `UManSysProp`_ website. Constructing an instance of this class will cause the new instance to query the server for all available methods. Each method will be exposed as a method of the instance, with docstrings obtained from the server. For example:: >>> import umansysprop.client >>> client = umansysprop.client.UManSysProp() >>> client.vapour_pressure <bound method ?.vapour_pressure of <umansysprop.client.UManSysProp object at 0x7f80fbae7b50>> >>> client.vapour_pressure.__doc__ u"\\nCalculates vapour pressures for all specified *compounds* (given..." The class can be constructed with an alternative *base_url* if you wish to point it a different server. The *base_url* parameter defaults to the `UManSysProp`_ website. Methods can be called like a normal Python method, but will result in a request being sent to the web-server, processed, and the JSON-formatted results being re-constructed as a :class:`~umansysprop.results.Result` list on the client. For example:: >>> result = client.vapour_pressure([ ... 'CCCC', 'C(CC(=O)O)C(=O)O', 'C(=O)(C(=O)O)O', ... 'CCCCC/C=C/C/C=C/CC/C=C/CCCC(=O)O'], ... [298.15, 299.15, 300.15, 310.15], ... 'nannoolal', 'nannoolal') >>> result [<Table name="pressures">] >>> result.pressures <Table name="pressures"> >>> result.pressures.data {(298.15, u'C(=O)(C(=O)O)O'): -5.196360545314141, (298.15, u'C(CC(=O)O)C(=O)O'): -6.33293991047814, (298.15, u'CCCC'): 0.22091492301164387, (298.15, u'CCCCC/C=C/C/C=C/CC/C=C/CCCC(=O)O'): -9.660331395164858, (299.15, u'C(=O)(C(=O)O)O'): -5.151703772558254, (299.15, u'C(CC(=O)O)C(=O)O'): -6.281177618554678, (299.15, u'CCCC'): 0.2354793193478599, (299.15, u'CCCCC/C=C/C/C=C/CC/C=C/CCCC(=O)O'): -9.58901500825095, (300.15, u'C(=O)(C(=O)O)O'): -5.107428775107059, (300.15, u'C(CC(=O)O)C(=O)O'): -6.229864995169396, (300.15, u'CCCC'): 0.24993365754879804, (300.15, u'CCCCC/C=C/C/C=C/CC/C=C/CCCC(=O)O'): -9.518352766693454, (310.15, u'C(=O)(C(=O)O)O'): -4.684643528880829, (310.15, u'C(CC(=O)O)C(=O)O'): -5.74023509658808, (310.15, u'CCCC'): 0.38868830156274703, (310.15, u'CCCCC/C=C/C/C=C/CC/C=C/CCCC(=O)O'): -8.845815136269506} Please refer to the reference for :class:`~umansysprop.results.Result` and :class:`~umansysprop.results.Table` for more information on accessing the result data. .. _UManSysProp: http://umansysprop.seaes.manchester.ac.uk/ .. _JSON API: http://umansysprop.seaes.manchester.ac.uk/api """ _method_template = """\ def {name}(self, {params}): return self._json_rpc("{url}", {call}) """ def __init__(self, base_url='http://umansysprop.seaes.manchester.ac.uk/'): self._base_url = base_url response = requests.get(urljoin(self._base_url, 'api'), headers={ 'Accept': 'application/json'}) response.raise_for_status() for name, props in response.json().items(): # Construct a dynamic method for each function that the API # defines. The method will take the parameters specified by the # API, and will have a doc-string also specified by the API method_definition = self._method_template.format( name=name, url=props['url'], params=', '.join(props['params']), call=', '.join( '%s=%s' % (param, param) for param in props['params']) ) l = {} exec(method_definition, globals(), l) f = l[name] f.__doc__ = props['doc'] setattr(self, name, types.MethodType(f, self)) def _json_rpc(self, url, **params): response = requests.post( urljoin(self._base_url, url), data=json.dumps(params), headers={ 'Accept': 'application/json', 'Content-Type': 'application/json', }) if 400 <= response.status_code < 500: # Some kind of client error; try and decode the body as JSON to # determine the details and raise a reasonable exception exc_type = response.json()['exc_type'] exc_value = response.json()['exc_value'] # Only permit a specific set of exceptions exc_class = { 'ValueError': ValueError, 'NameError': NameError, 'KeyError': KeyError, }[exc_type] if isinstance(exc_value, str): raise exc_class(exc_value) else: raise exc_class(*exc_value) elif response.status_code >= 500: import pdb; pdb.set_trace() raise RuntimeError('Server error: %s' % response.body) else: response.raise_for_status() return results.Result.from_json(response.json())