Merge pull request #7 from Zenohm/master

Restructuring
This commit is contained in:
Jason R. Coombs 2016-09-03 10:55:50 -04:00 committed by GitHub
commit 43f656f296
5 changed files with 188 additions and 74 deletions

View File

@ -1,3 +1,8 @@
3.0
===
Changed to using dictionaries instead of parsing XML.
2.4 2.4
=== ===

View File

@ -8,15 +8,15 @@ v2.0 API. This project is hosted on `Github
Installation Installation
============ ============
This library is released to PyPI, so the easiest way to install it is to use This library is released to PyPI - the Python Package Index, so the easiest way to install it is to use
easy_install:: pip::
easy_install wolframalpha
or pip::
pip install wolframalpha 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 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 also simply extract the 'wolframalpha' directory an appropriate location in
your Python path. 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') 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: for pod in res.pods:
do_something_with(pod) 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)::
print(next(res.results).text) for pod in res.pods:
for sub in pod:
print(sub.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. For more information, read the source.

View File

@ -33,6 +33,7 @@ setup_params = dict(
namespace_packages=name.split('.')[:-1], namespace_packages=name.split('.')[:-1],
install_requires=[ install_requires=[
'six', 'six',
'xmltodict',
], ],
extras_require={ extras_require={
}, },

View File

@ -1,84 +1,184 @@
from xml.etree import ElementTree as etree
from six.moves import urllib from six.moves import urllib
import xmltodict
from . import compat from . import compat
compat.fix_HTTPMessage() 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): class Client(object):
""" """
Wolfram|Alpha v2.0 client 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 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( data = {
input=query, 'input': query,
appid=self.app_id, 'appid': self.app_id
)) }
if assumption:
data.update({'assumption': assumption})
query = urllib.parse.urlencode(data)
url = 'https://api.wolframalpha.com/v2/query?' + query url = 'https://api.wolframalpha.com/v2/query?' + query
resp = urllib.request.urlopen(url) resp = urllib.request.urlopen(url)
assert resp.headers.get_content_type() == 'text/xml' assert resp.headers.get_content_type() == 'text/xml'
assert resp.headers.get_param('charset') == 'utf-8' assert resp.headers.get_param('charset') == 'utf-8'
return Result(resp) 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']

View File

@ -27,7 +27,7 @@ def test_basic(API_key):
res = client.query('30 deg C in deg F') res = client.query('30 deg C in deg F')
assert len(res.pods) > 0 assert len(res.pods) > 0
results = list(res.results) 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(): def test_invalid_app_id():
client = wolframalpha.Client('abcdefg') client = wolframalpha.Client('abcdefg')