From f653d1e3a9b740b9ec20a99023fca51aeddbf268 Mon Sep 17 00:00:00 2001 From: Zenohm Date: Sat, 14 May 2016 17:24:48 -0400 Subject: [PATCH 01/16] Consistency matters. Redesign how information is stored and accessed. This way code can be written once. A developer using this API should not have to build contingencies just because Wolfram Alpha changes its output. The API should handle that. These changes move the parsing of the tree out of the Wolfram API's returned XML and into Python dictionaries. This allows the full suite of tools that work with Python dictionaries to be used as well as simplifying and unifying how the responses should be handled. In addition, I have added consistencies in certain areas that allow information to be accessed in one, unified way; regardless of how it would otherwise have been formatted by the library. With this I want to encourage simplicity. You shouldn't have to look back through the code to figure out exactly what is being iterated over when someone decided to write iter(self) instead of simply iter(self.pods). Also moved the Client class to the top of the file where it can be immediately seen. --- wolframalpha/__init__.py | 221 ++++++++++++++++++++++++++++----------- 1 file changed, 158 insertions(+), 63 deletions(-) diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index 468e509..7e45c58 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -1,84 +1,179 @@ -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 """ - 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 = 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): + 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 _flatten(self, lists): + ''' + src: http://stackoverflow.com/a/952952/4241708 + usr: intuited + ''' + from itertools import chain + return list(chain.from_iterable(lists)) + + def __iter__(self): + return iter(self.info) + + def __len__(self): + return len(self.tree) + + @property + def results(self): + return self._flatten([pod.details for pod in self.pods if pod.primary or pod.title=='Result']) + + @property + def details(self): + return {pod.title: pod.details for pod in self.pods} + +class Pod(object): + def __init__(self, node): + self.node = node + self._handle_error() + self.title = node['@title'] + self.scanner = node['@scanner'] + self.id = node['@id'] + self.position = float(node['@position']) + self.error = node['@error'] + 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): + error_state = self.node['@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.subpods) + + def __len__(self): + return self.number_of_subpods + + @property + def details(self): + return [subpod.text for subpod in self.subpods] + +# 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 Subpod(object): + 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)) + +class Image(object): + 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'] + From 55567d473212a4a6d4f51418e6e417365c6d631c Mon Sep 17 00:00:00 2001 From: Zenohm Date: Sat, 14 May 2016 17:28:10 -0400 Subject: [PATCH 02/16] Add dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) 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={ }, From bb3ee3cdc33197c4f193aeee4c27b8ced8eb445c Mon Sep 17 00:00:00 2001 From: Zenohm Date: Sat, 14 May 2016 17:49:26 -0400 Subject: [PATCH 03/16] Documentation --- wolframalpha/__init__.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index 7e45c58..60c22b9 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -9,13 +9,18 @@ compat.fix_HTTPMessage() 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='Q59EW4-7K8AHE858R'): self.app_id = app_id 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 """ data = { 'input': query, @@ -32,6 +37,9 @@ class Client(object): 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() @@ -79,21 +87,24 @@ class Result(object): @property def results(self): + ''' Get the response to a simple, discrete query. ''' return self._flatten([pod.details 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.details 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.error = node['@error'] self.number_of_subpods = int(node['@numsubpods']) self.subpods = node['subpod'] # Allow subpods to be accessed in a consistent way, @@ -104,11 +115,10 @@ class Pod(object): self.primary = '@primary' in node and node['@primary'] != 'false' def _handle_error(self): - error_state = self.node['@error'] - if error_state == 'false': + if self.error == 'false': return - error = self.tree['error'] + error = self.node['error'] code = error['code'] msg = error['msg'] template = 'Error {code}: {msg}' @@ -122,6 +132,7 @@ class Pod(object): @property def details(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] # Needs work. At the moment this should be considered a placeholder. @@ -157,6 +168,7 @@ class Warning(object): return len(node) 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'] @@ -169,6 +181,7 @@ class Subpod(object): self.img = list(map(Image, self.img)) class Image(object): + ''' Holds information about an image included with an answer. ''' def __init__(self, node): self.node = node self.title = node['@title'] From 8ee1dd0e09c76c6594e1bd276080d3f6b0e2ba1b Mon Sep 17 00:00:00 2001 From: Zenohm Date: Sat, 14 May 2016 18:09:28 -0400 Subject: [PATCH 04/16] Update readme and add information. --- README.rst | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index c37b05d..464b8e6 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,19 @@ 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]) For more information, read the source. From 363ccf248d1ab10a6ff9e2c6049607f1c54bcbcb Mon Sep 17 00:00:00 2001 From: Zenohm Date: Sat, 14 May 2016 18:27:38 -0400 Subject: [PATCH 05/16] Noted severe change. These changes will most likely break most code that relies on this library. Using generators is great and all, but if you want to query Wolfram Alpha for more complex information with more context this library needs to be able to do that and be expandable at the same time. There's a lot more to Wolfram Alpha and the Python API should be able to access that content in a uniform way. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) 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 === From 643934b950fb97793cec95703310d749c3aea9c7 Mon Sep 17 00:00:00 2001 From: Zenohm Date: Sat, 14 May 2016 18:37:44 -0400 Subject: [PATCH 06/16] Explained limitations, offered alternative Added a section at the bottom explaining what a developer should do if they need access to information that the Wolfram Alpha API provides but that isn't built into the interface. Because of the way this update handles the API's replies the information is easy to access using builtin data types. --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 464b8e6..babdfbc 100644 --- a/README.rst +++ b/README.rst @@ -49,4 +49,6 @@ You may also query for simply the pods which have 'Result' titles or are marked print(res.results[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. From 48ff24daa90a305ae65ed5fae6011f8cebc0ef64 Mon Sep 17 00:00:00 2001 From: Zenohm Date: Sat, 14 May 2016 18:50:56 -0400 Subject: [PATCH 07/16] It's a change. :D Contrary to what I thought, this update appears to be mostly compatible with all the code listed that came before. All generators have been switched out for lists so that information can be used from one location. If this is determined to somehow have an impact despite Wolfram Alpha not being expected to return oodles of information, then the code can be easily rewritten to use generators instead of lists. Lists are simply more convenient when experimenting with the source code. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1d5fb72..e14d9e6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,4 +1,4 @@ -3.0 +2.5 === Changed to using dictionaries instead of parsing XML. From 93315583406015da30e81261e092c029f1b9350c Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 19:13:58 -0400 Subject: [PATCH 08/16] Align with original methods Rewrote the results method a bit so it returned something similar to the original result, which will make the transition easier. --- wolframalpha/__init__.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index 60c22b9..0a5e0b7 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -71,14 +71,6 @@ class Result(object): template = 'Error {code}: {msg}' raise Exception(template.format(code=code, msg=msg)) - def _flatten(self, lists): - ''' - src: http://stackoverflow.com/a/952952/4241708 - usr: intuited - ''' - from itertools import chain - return list(chain.from_iterable(lists)) - def __iter__(self): return iter(self.info) @@ -88,7 +80,7 @@ class Result(object): @property def results(self): ''' Get the response to a simple, discrete query. ''' - return self._flatten([pod.details for pod in self.pods if pod.primary or pod.title=='Result']) + return [pod for pod in self.pods if pod.primary or pod.title=='Result'] @property def details(self): From a784c88fa1bc71cfe0db2770370ebffdc2b4251c Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 19:23:50 -0400 Subject: [PATCH 09/16] Simplify naming convetion Aligns more with the original implementation. --- wolframalpha/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index 0a5e0b7..047df90 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -79,7 +79,7 @@ class Result(object): @property def results(self): - ''' Get the response to a simple, discrete query. ''' + ''' 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 @@ -123,7 +123,7 @@ class Pod(object): return self.number_of_subpods @property - def details(self): + 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] From d3046c7edeb755b642ffe034bd0bc9caf2da53c9 Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 19:24:48 -0400 Subject: [PATCH 10/16] Make tests reflect changes --- wolframalpha/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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') From 07a8a2d804f8af158b05db955cfcc3fafbadc331 Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 19:28:50 -0400 Subject: [PATCH 11/16] Update example --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index babdfbc..c0a28ed 100644 --- a/README.rst +++ b/README.rst @@ -47,7 +47,7 @@ Pod objects have `subpods` (a Subpod is a specific response with the plaintext r You may also query for simply the pods which have 'Result' titles or are marked as 'primary':: - print(res.results[0]) + 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. From 5675e1ec2c28da7467afcb7dd202919b2cdf8770 Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 19:33:27 -0400 Subject: [PATCH 12/16] Nevermind. Yeah, it'll break some stuff. --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e14d9e6..4cb1df0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,8 @@ -2.5 +3.0 === Changed to using dictionaries instead of parsing XML. +Align with Wolfram Alpha API v2.0 2.4 === From 1d8d2451bb7acd6104ee9d1c0caa870756a3bf55 Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 20:00:16 -0400 Subject: [PATCH 13/16] Fix unseen error. Praised be rubber ducky. --- wolframalpha/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index 047df90..e19f87c 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -75,7 +75,7 @@ class Result(object): return iter(self.info) def __len__(self): - return len(self.tree) + return len(self.info) @property def results(self): From 570998719bae65852a31da7934ae67707eb9428f Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 20:08:00 -0400 Subject: [PATCH 14/16] Fix unseen error. Result of one of the previous changes. Almost didn't notice it. --- wolframalpha/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index e19f87c..173334d 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -85,7 +85,7 @@ class Result(object): @property def details(self): ''' Get a simplified set of answers with some context. ''' - return {pod.title: pod.details for pod in self.pods} + return {pod.title: pod.text for pod in self.pods} class Pod(object): ''' Groups answers and information contextualizing those answers. ''' From 13c2f7700dd451fe39919018be7ed2a66283b407 Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 20:12:18 -0400 Subject: [PATCH 15/16] Update CHANGES.rst --- CHANGES.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4cb1df0..1d5fb72 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,6 @@ === Changed to using dictionaries instead of parsing XML. -Align with Wolfram Alpha API v2.0 2.4 === From 7d3453ec9aea14625208df080980484fd637a802 Mon Sep 17 00:00:00 2001 From: Isaac Smith Date: Sat, 14 May 2016 20:19:16 -0400 Subject: [PATCH 16/16] Grouped Pod and Subpod classes. --- wolframalpha/__init__.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/wolframalpha/__init__.py b/wolframalpha/__init__.py index 173334d..8ad0138 100644 --- a/wolframalpha/__init__.py +++ b/wolframalpha/__init__.py @@ -127,6 +127,19 @@ class Pod(object): ''' 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): @@ -159,19 +172,6 @@ class Warning(object): def __len__(self): return len(node) -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)) - class Image(object): ''' Holds information about an image included with an answer. ''' def __init__(self, node):