diff --git a/CHANGES.rst b/CHANGES.rst index bb5eed5..1d5fb72 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +3.0 +=== + +Changed to using dictionaries instead of parsing XML. + 2.4 === diff --git a/README.rst b/README.rst index c37b05d..c0a28ed 100644 --- a/README.rst +++ b/README.rst @@ -8,15 +8,15 @@ v2.0 API. This project is hosted on `Github Installation ============ -This library is released to PyPI, so the easiest way to install it is to use -easy_install:: - - easy_install wolframalpha - -or pip:: +This library is released to PyPI - the Python Package Index, so the easiest way to install it is to use +pip:: pip install wolframalpha +or easy_install:: + + easy_install wolframalpha + If you don't have these tools or you prefer not to use setuptools, you may also simply extract the 'wolframalpha' directory an appropriate location in your Python path. @@ -34,13 +34,21 @@ Then, you can send queries, which return Result objects:: res = client.query('temperature in Washington, DC on October 3, 2012') -Result objects have `pods` attribute (a Pod is an answer from Wolfram Alpha):: +Result objects have `pods` (a Pod is an answer group from Wolfram Alpha):: for pod in res.pods: do_something_with(pod) -You may also query for simply the pods which have 'Result' titles:: +Pod objects have `subpods` (a Subpod is a specific response with the plaintext reply and some additional info):: + + for pod in res.pods: + for sub in pod: + print(sub.text) - print(next(res.results).text) +You may also query for simply the pods which have 'Result' titles or are marked as 'primary':: + + print(res.results[0].text[0]) + +The interface as it is now does not have code built for accessing every piece of information that the Wolfram Alpha API could return. As such, every class has a copy of the original structure that it is supposed to parse. This copy is placed in a variable called node for every class but the Result class, whose variable is named tree. If there is information from the Wolfram Alpha API that you need for your program that this interface does not provide an exact function for then you can still gain access to that information through the previously mentioned variables; you'll just have to handle the API directly until the functionality you seek is built. For more information, read the source. diff --git a/setup.py b/setup.py index 87ee157..f68239e 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ setup_params = dict( namespace_packages=name.split('.')[:-1], install_requires=[ 'six', + 'xmltodict', ], extras_require={ }, diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index 468e509..8ad0138 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -1,84 +1,184 @@ -from xml.etree import ElementTree as etree from six.moves import urllib +import xmltodict from . import compat compat.fix_HTTPMessage() -class Result(object): - def __init__(self, stream): - self.tree = etree.parse(stream) - self._handle_error() - - def _handle_error(self): - error = self.tree.find('error') - if not error: - return - - code = error.find('code').text - msg = error.find('msg').text - tmpl = 'Error {code}: {msg}' - raise Exception(tmpl.format(code=code, msg=msg)) - - def __iter__(self): - return (Pod(node) for node in self.tree.findall('pod')) - - def __len__(self): - return len(self.tree) - - @property - def pods(self): - return list(iter(self)) - - @property - def results(self): - return (pod for pod in self if pod.title=='Result') - -class Pod(object): - def __init__(self, node): - self.node = node - self.__dict__.update(node.attrib) - - def __iter__(self): - return (Content(node) for node in self.node.findall('subpod')) - - @property - def main(self): - "The main content of this pod" - return next(iter(self)) - - @property - def text(self): - return self.main.text - - @property - def img(self): - return self.main.img - -class Content(object): - def __init__(self, node): - self.node = node - self.__dict__.update(node.attrib) - self.text = node.find('plaintext').text - self.img = node.find('img').attrib['src'] class Client(object): """ Wolfram|Alpha v2.0 client + + Pass an ID to the object upon instantiation, then + query Wolfram Alpha using the query method. """ - def __init__(self, app_id): + def __init__(self, app_id='Q59EW4-7K8AHE858R'): self.app_id = app_id - def query(self, query): + def query(self, query, assumption=None): """ - Query Wolfram|Alpha with query using the v2.0 API + Query Wolfram|Alpha using the v2.0 API + Allows for assumptions to be included. + See: http://products.wolframalpha.com/api/documentation.html#6 """ - query = urllib.parse.urlencode(dict( - input=query, - appid=self.app_id, - )) + data = { + 'input': query, + 'appid': self.app_id + } + if assumption: + data.update({'assumption': assumption}) + + query = urllib.parse.urlencode(data) url = 'https://api.wolframalpha.com/v2/query?' + query resp = urllib.request.urlopen(url) assert resp.headers.get_content_type() == 'text/xml' assert resp.headers.get_param('charset') == 'utf-8' return Result(resp) + +class Result(object): + ''' + Handles processing the response for the programmer. + ''' + def __init__(self, stream): + self.tree = xmltodict.parse(stream, dict_constructor=dict)['queryresult'] + self._handle_error() + self.info = [] + try: + self.pods = list(map(Pod, self.tree['pod'])) + self.info.append(self.pods) + except KeyError: + self.pods = None + try: + self.assumptions = list(map(Assumption, self.tree['assumptions'])) + self.info.append(self.assumptions) + except KeyError: + self.assumptions = None + try: + self.warnings = list(map(Warning, self.tree['warnings'])) + self.info.append(self.warnings) + except KeyError: + self.warnings = None + + def _handle_error(self): + error_state = self.tree['@error'] + if error_state == 'false': + return + + error = self.tree['error'] + code = error['code'] + msg = error['msg'] + template = 'Error {code}: {msg}' + raise Exception(template.format(code=code, msg=msg)) + + def __iter__(self): + return iter(self.info) + + def __len__(self): + return len(self.info) + + @property + def results(self): + ''' Get the pods that hold the response to a simple, discrete query. ''' + return [pod for pod in self.pods if pod.primary or pod.title=='Result'] + + @property + def details(self): + ''' Get a simplified set of answers with some context. ''' + return {pod.title: pod.text for pod in self.pods} + +class Pod(object): + ''' Groups answers and information contextualizing those answers. ''' + def __init__(self, node): + self.node = node + self.error = node['@error'] + self._handle_error() + self.title = node['@title'] + self.scanner = node['@scanner'] + self.id = node['@id'] + self.position = float(node['@position']) + self.number_of_subpods = int(node['@numsubpods']) + self.subpods = node['subpod'] + # Allow subpods to be accessed in a consistent way, + # as a list, regardless of how many there are. + if type(self.subpods) != list: + self.subpods = [self.subpods] + self.subpods = list(map(Subpod, self.subpods)) + self.primary = '@primary' in node and node['@primary'] != 'false' + + def _handle_error(self): + if self.error == 'false': + return + + error = self.node['error'] + code = error['code'] + msg = error['msg'] + template = 'Error {code}: {msg}' + raise Exception(template.format(code=code, msg=msg)) + + def __iter__(self): + return iter(self.subpods) + + def __len__(self): + return self.number_of_subpods + + @property + def text(self): + ''' Simply get the text from each subpod in this pod and return it in a list. ''' + return [subpod.text for subpod in self.subpods] + +class Subpod(object): + ''' Holds a specific answer or additional information relevant to said answer. ''' + def __init__(self, node): + self.node = node + self.title = node['@title'] + self.text = node['plaintext'] + self.img = node['img'] + # Allow images to be accessed in a consistent way, + # as a list, regardless of how many there are. + if type(self.img) != list: + self.img = [self.img] + self.img = list(map(Image, self.img)) + +# Needs work. At the moment this should be considered a placeholder. +class Assumption(object): + def __init__(self, node): + self.assumption = node + #self.description = self.assumption[0]['desc'] # We only care about our given assumption. + + def __iter__(self): + return iter(self.assumption) + + def __len__(self): + return len(self.assumption) + + @property + def text(self): + text = self.template.replace('${desc1}', self.description) + try: + text = text.replace('${word}', self.word) + except: + pass + return text[:text.index('. ') + 1] + +# Needs work. At the moment this should be considered a placeholder. +class Warning(object): + def __init__(self, node): + self.node = node + + def __iter__(self): + return iter(node) + + def __len__(self): + return len(node) + +class Image(object): + ''' Holds information about an image included with an answer. ''' + def __init__(self, node): + self.node = node + self.title = node['@title'] + self.alt = node['@alt'] + self.height = node['@height'] + self.width = node['@width'] + self.src = node['@src'] + diff --git a/wolframalpha/test_client.py b/wolframalpha/test_client.py index c017d6f..4ce9756 100644 --- a/wolframalpha/test_client.py +++ b/wolframalpha/test_client.py @@ -27,7 +27,7 @@ def test_basic(API_key): res = client.query('30 deg C in deg F') assert len(res.pods) > 0 results = list(res.results) - assert results[0].text == '86 °F (degrees Fahrenheit)' + assert results[0].text == ['86 °F (degrees Fahrenheit)'] def test_invalid_app_id(): client = wolframalpha.Client('abcdefg')