diff options
| author | kientzle <kientzle@5914aa95-5b3a-0410-a3b5-7b719e7fe9b2> | 2007-09-17 22:42:39 +0000 |
|---|---|---|
| committer | kientzle <kientzle@5914aa95-5b3a-0410-a3b5-7b719e7fe9b2> | 2007-09-17 22:42:39 +0000 |
| commit | a4ad385a68926aa60c2b37fe1100de410558dda3 (patch) | |
| tree | cc2500b23e014c6eda03bd05889273a32d78110c /trunk | |
| parent | 668822faea55bc371b42e84e97e667f96a65a8ae (diff) | |
Initial import of sources copied from Metaweb internal source control.
git-svn-id: http://freebase-python.googlecode.com/svn/trunk@2 5914aa95-5b3a-0410-a3b5-7b719e7fe9b2
Diffstat (limited to 'trunk')
| -rw-r--r-- | trunk/COPYRIGHT | 31 | ||||
| -rw-r--r-- | trunk/README | 39 | ||||
| -rw-r--r-- | trunk/examples/get_albums.py | 100 | ||||
| -rwxr-xr-x | trunk/freebase/__init__.py | 2 | ||||
| -rwxr-xr-x | trunk/freebase/api/__init__.py | 2 | ||||
| -rw-r--r-- | trunk/freebase/api/session.py | 391 | ||||
| -rwxr-xr-x | trunk/freebase/rison.py | 303 | ||||
| -rw-r--r-- | trunk/setup.py | 43 | ||||
| -rw-r--r-- | trunk/test/test_all.py | 89 |
9 files changed, 1000 insertions, 0 deletions
diff --git a/trunk/COPYRIGHT b/trunk/COPYRIGHT new file mode 100644 index 0000000..a76c9c1 --- /dev/null +++ b/trunk/COPYRIGHT @@ -0,0 +1,31 @@ +All source and examples in this project are subject to the +following copyright, unless specifically stated otherwise +in the file itself: + +======================================================================== +Copyright (c) 2007, Metaweb Technologies, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + +THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS +``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB +TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 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. +======================================================================== diff --git a/trunk/README b/trunk/README new file mode 100644 index 0000000..5bf8bf2 --- /dev/null +++ b/trunk/README @@ -0,0 +1,39 @@ +description='Python client library for Freebase API service', +maintainer_email='developers@freebase.com', +url='http://www.freebase.com/', + +INSTALL: + + /usr/bin/python setup.py install + +SYNOPSIS: + + #!/usr/bin/python + + from freebase.api import HTTPMetawebSession, MetawebError + + mss = HTTPMetawebSession('www.freebase.com') + + query = { 'type':'/music/artist', + 'name':'Sting', + 'album':[], + } + + result = mss.mqlread(query) + + for album in result.album: + print album +TESTING: + + cd test + /usr/bin/python test_all.py -v + + test_login (__main__.TestFreebase) ... ok + test_read (__main__.TestFreebase) ... ok + test_write (__main__.TestFreebase) ... ok + + ---------------------------------------------------------------------- + Ran 3 tests in 0.691s + + OK + diff --git a/trunk/examples/get_albums.py b/trunk/examples/get_albums.py new file mode 100644 index 0000000..9d2e46f --- /dev/null +++ b/trunk/examples/get_albums.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# ======================================================================== +# Copyright (c) 2007, Metaweb Technologies, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# +# THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB +# TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 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. +# ======================================================================== + +from freebase.api import HTTPMetawebSession, MetawebError + +#username='username' +#password='password' + +query = { 'type':'/music/artist', + 'name':'Sting', + 'album':[], + } +#credentials = freebase.login(username, password) + +mss = HTTPMetawebSession('www.freebase.com') +result = mss.mqlread(query) + +for album in result.album: + print album + + +# output : +#The Dream of the Blue Turtles +#Bring on the Night (disc 1) +#Bring on the Night (disc 2) +#...Nothing Like the Sun +#...Nada Como el Sol +#The Soul Cages +#Acoustic Live in Newcastle +#All This Time +#Mad About You +#Ten Summoner's Tales +#Demolition Man +#Fields of Gold: The Best of Sting 1984-1994 +#The Living Sea +#Let Your Soul Be Your Pilot +#Mercury Falling +#Brand New Day +#Desert Rose +#...All This Time +#Still Be Love in the World +#Sacred Love +#Stolen Car +#1991-10-02: Hollywood Bowl, Los Angeles, CA, USA +#Best Ballads +#Brand New Day (bonus disc) +#Brand New Day: The Remixes +#Dolphins +#Englishman in New York +#Five Live! +#Fortress: The London Symphony Orchestra performs Sting +#I Shall Be Released (disc 2) +#I Shall Be Released (disc 3) +#I Shall Be Released (disc 4) +#I Shall Be Released (disc 5) +#I'm So Happy I Can't Stop Crying +#If I Ever Lose My Faith in You +#If You Love Somebody Set Them Free +#Live at the BBC 1st Dec 2001 +#Live at the Universal Amphiteater: 10/29/99 +#Live in Central Park (disc 2) +#Mercury Falling (bonus disc) +#Moon Walking +#My Funny Friend and Me +#My Funny Valentine +#Send Your Love Remix Album +#Sting at the Movies +#Ten Summoners Tales (DTS) +#They Dance Alone (Gueca Solo) +#This Cowboy Song +#Unplugged (disc 1: The Rehearsal) +#Unplugged (disc 2: Complete Live) +#When We Dance +#You Still Touch Me diff --git a/trunk/freebase/__init__.py b/trunk/freebase/__init__.py new file mode 100755 index 0000000..d289e09 --- /dev/null +++ b/trunk/freebase/__init__.py @@ -0,0 +1,2 @@ + +pass diff --git a/trunk/freebase/api/__init__.py b/trunk/freebase/api/__init__.py new file mode 100755 index 0000000..959259f --- /dev/null +++ b/trunk/freebase/api/__init__.py @@ -0,0 +1,2 @@ + +from session import HTTPMetawebSession, MetawebError diff --git a/trunk/freebase/api/session.py b/trunk/freebase/api/session.py new file mode 100644 index 0000000..b3ec825 --- /dev/null +++ b/trunk/freebase/api/session.py @@ -0,0 +1,391 @@ +# ======================================================================== +# Copyright (c) 2007, Metaweb Technologies, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# +# THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB +# TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 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. +# ======================================================================== + +# +# declarations for external metaweb api. +# +# +# from metaweb.api import HTTPMetawebSession +# +# mss = HTTPMetawebSession('sandbox.freebase.com') +# print mss.mqlread([dict(name=None, type='/type/type')]) +# +# +# + +import os, sys, re +import urllib2 +import cookielib +import simplejson +from urllib import quote as urlquote +import pprint +import socket + +import logging +log = logging.getLogger() +log.setLevel(logging.DEBUG) +console = logging.StreamHandler() +console.setLevel(logging.INFO) +# add the handler to the root logger +log.addHandler(console) + + +__all__ = ['MetawebError', 'MetawebSession', 'HTTPMetawebSession'] + +__version__ = '0.1' + +import simplejson.encoder +simplejson.JSONEncoder.item_separator = ',' +simplejson.JSONEncoder.key_separator = ':' +#delattr(simplejson.encoder.ESCAPE_DCT, '/') + +def urlencode_weak(s): + return urlquote(s, safe='{}[],"/:$') + + +# from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/361668 +class attrdict(dict): + """A dict whose items can also be accessed as member variables. + + >>> d = attrdict(a=1, b=2) + >>> d['c'] = 3 + >>> print d.a, d.b, d.c + 1 2 3 + >>> d.b = 10 + >>> print d['b'] + 10 + + # but be careful, it's easy to hide methods + >>> print d.get('c') + 3 + >>> d['get'] = 4 + >>> print d.get('a') + Traceback (most recent call last): + TypeError: 'int' object is not callable + """ + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self.__dict__ = self + + + +# TODO expose the common parts of the result envelope +class MetawebError(Exception): + """ + an error report from the metaweb service. + """ + pass + + + +# TODO right now this is a completely unnecessary superclass. +# is there enough common behavior between session types +# to justify it? +class MetawebSession(object): + """ + MetawebSession is the base class for MetawebSession, subclassed for + different connection types. Only http is available externally. + + This is more of an interface than + """ + + # interface definition here... + + +class HTTPMetawebSession(MetawebSession): + """ + a MetawebSession is a request/response queue. + + this version uses the HTTP api, and is synchronous. + """ + # share cookies across sessions, so that different sessions can + # see each other's writes immediately. + _cookies = cookielib.CookieJar() + _cookiespy = urllib2.HTTPCookieProcessor(_cookies) + + def __init__(self, service_url, username=None, password=None, prev_session=None): + """ + create a new MetawebSession for interacting with the Metaweb. + + a new session will inherit state from prev_session if present, + """ + super(HTTPMetawebSession, self).__init__() + + assert not service_url.endswith('/') + if not '/' in service_url: # plain host:port + service_url = 'http://' + service_url + + self.service_url = service_url + + self.username = username + self.password = password + + self.tid = None + + if prev_session: + self.service_url = prev.service_url + + def _httpreq(self, service_path, method='GET', body=None, form=None, + headers=None, **kws): + """ + make an http request to the service. + + form arguments are encoded in the url, even for POST, if a non-form + content-type is given for the body. + """ + + if method == 'POST': + assert body is not None or form is not None + elif method == 'GET': + assert body is None + else: + assert 0, 'unknown method %s' % method + + log.debug('HTTPREQ: %s %s %s %s', service_path, method, kws, self._cookies) + + url = self.service_url + service_path + + if headers is None: + headers = [] + + # XXX This is a lousy way to parse Content-Type, where is + # the library? + ct = None + for k,v in headers: + if k.lower() == 'content-type': + ct = v.split(';')[0] + break + + if body is not None: + # if body is provided, content-type had better be too + assert ct is not None + + if form is not None: + qstr = '&'.join(['%s=%s' % (urlencode_weak(k), urlencode_weak(v)) + for k,v in form.items()]) + if method == 'POST': + # put the args on the url if we're putting something else + # in the body. this is used to add args to raw uploads. + if body is not None: + url += '?' + qstr + else: + if ct is None: + # XXX encoding and stuff + ct = 'application/x-www-form-urlencoded' + headers.append(('content-type', ct)) + + if ct == 'multipart/form-encoded': + # XXX fixme + raise NotImplementedError + elif ct == 'application/x-www-form-urlencoded': + body = qstr + else: + # for all methods other than POST, use the url + url += '?' + qstr + + req = urllib2.Request(url, body, dict(headers), **kws) + + self.opener = urllib2.build_opener(self._cookiespy) + + # assure the service that this isn't a CSRF form submission + self.opener.addheaders = [('User-agent', + 'python freebase.api-%s' % __version__), + ('X-Metaweb-Request', 'Python')] + + if self.tid is not None: + self.opener.addheaders.append(('X-Metaweb-TID', self.tid)) + + try: + if headers: + self.opener.addheaders += headers + resp = self.opener.open(req) + + except socket.error, e: + log.error('SOCKET FAILURE: %s', e.fp.read()) + raise MetawebError, 'failed contacting %s: %s' % (url, str(e)) + + except urllib2.HTTPError, e: + if e.code == 400 and e.info().type == 'application/json': + r = self._readjson(e.fp) + for msg in r.messages: + if msg.get('level', 'error') == 'error': + raise MetawebError(u'%s %s %r' % (msg.get('code',''), msg.message, msg.info)) + + # some 400 we don't recognize. hmm. + raise MetawebError(u'%r' % r) + raise MetawebError, 'request failed: %s: %r %r' % (url, str(e), e.fp.read()) + + for header in resp.info().headers: + log.debug('HTTP HEADER %s', header) + name, value = re.split("[:\n\r]", header, 1) + if name.lower() == 'x-metaweb-tid': + self.tid = value.strip() + #if name.lower().startswith('set-cookie'): + # if value.startswith(' metaweb-user'): + # pass + # else: + # self.saved_headers[name] = value.strip() + # log.debug('HTTP VALUE %s', value) + + return resp + + def _readjson(self, fp): + # TODO really this should be accomplished by hooking + # simplejson to create attrdicts instead of dicts. + def struct2attrdict(st): + """ + copy a json structure, turning all dicts into attrdicts. + + copying descends instances of dict and list, including subclasses. + """ + if isinstance(st, dict): + return attrdict([(k,struct2attrdict(v)) for k,v in st.items()]) + if isinstance(st, list): + return [struct2attrdict(li) for li in st] + return st + + json = fp.read() + if json == '': + log.error('the empty string is not valid json') + raise MetawebError('the empty string is not valid json') + + try: + r = simplejson.loads(json) + except ValueError, e: + log.error('error parsing json string %r' % json) + raise MetawebError, 'error parsing JSON string: %s' % e + + return struct2attrdict(r) + + def _mqlresult(self, r): + if r.code != '/api/status/ok': + for msg in r.messages: + log.error('mqlread failed: %s %s' % (msg.code, msg.message)) + raise MetawebError, 'query failed: %s' % r.messages[0].code + + return r.result + + def login(self): + """sign in to the service""" + + assert self.username is not None + assert self.password is not None + + log.debug('LOGIN USERNAME: %s', self.username) + + try: + resp = self._httpreq('/api/account/login', 'POST', + form=dict(username=self.username, + password=self.password)) + except urllib2.HTTPError, e: + raise MetawebError("login error: %s", e) + + r = self._readjson(resp) + + if r.code != '/api/status/ok': + raise MetawebError(u'%s %r' % (r.get('code',''), r.messages)) + + log.debug('LOGIN RESP: %r', r) + log.debug('LOGIN INFO: %s', resp.info()) + log.debug('LOGIN COOKIES: %s', self._cookies) + + + def mqlreaditer(self, sq): + """read a structure query""" + + cursor = True + + while 1: + subq = dict(query=[sq], cursor=cursor, escape=False) + qstr = simplejson.dumps(dict(c0=subq)) + + service = '/api/service/mqlread' + + resp = self._httpreq(service, form=dict(queries=qstr)) + r = self._readjson(resp) + r = r['c0'] + + for item in self._mqlresult(r): + yield item + + if r['cursor']: + cursor = r['cursor'] + else: + break + + def mqlread(self, sq): + """read a structure query""" + subq = dict(query=sq, cursor=True, escape=False) + qstr = simplejson.dumps(dict(c0=subq)) + + log.debug('MQLREAD: %s', qstr) + + service = '/api/service/mqlread' + + resp = self._httpreq(service, form=dict(queries=qstr)) + r = self._readjson(resp) + r = r['c0'] + return self._mqlresult(r) + + def trans(self, guid): + """translate blob from guid """ + url = '/api/trans/raw/' + urlquote(guid) + resp = self._httpreq(url) + content = resp.read() + return content + + def mqlwrite(self, sq): + """do a mql write""" + query = dict(query=sq, escape=False) + qstr = simplejson.dumps(query) + + log.debug('MQLWRITE: %s', qstr) + + service = '/api/service/mqlwrite' + resp = self._httpreq(service, 'POST', + form=dict(query=qstr)) + r = self._readjson(resp) + return self._mqlresult(r) + + def upload(self, body, content_type): + """upload to the metaweb""" + + log.debug('UPLOAD %s', content) + + service = '/api/service/upload' + resp = self._httpreq(service, 'POST', + headers={'content-type': content_type}, + body=qstr) + r = self._readjson(resp) + return self._mqlresult(r) + + +if __name__ == '__main__': + mss = HTTPMetawebSession('sandbox.freebase.com') + + print mss.mqlread([dict(name=None, type='/type/type')]) diff --git a/trunk/freebase/rison.py b/trunk/freebase/rison.py new file mode 100755 index 0000000..468d505 --- /dev/null +++ b/trunk/freebase/rison.py @@ -0,0 +1,303 @@ + +# +# based on javascript openlaszlo-json +# +# Author: Oliver Steele +# Copyright: Copyright 2006 Oliver Steele. All rights reserved. +# Homepage: http:#osteele.com/sources/openlaszlo/json +# License: MIT License. +# Version: 1.0 +# + +# hacked by nix for use in uris +# ported to python by nix +# + + + +# +# TODO +# +# switch to unicode +# fall through to simplejson if first char is not in '!(' +# + + +import os, sys, re +#import simplejson +simplejson = None + +class ParserException(Exception): + pass + +class Parser(object): + WHITESPACE = '' + #WHITESPACE = " \t\n\r\f" + + # we divide the uri-safe glyphs into three sets + # <rison> - used by rison ' ! : ( ) , + # <identifier> - common in strings - _ . / ~ + # <reserved> - not common in strings, reserved * @ $ + # + # <rison> and <reserved> classes are illegal in ids. + # ids also can't look like numbers. + + not_idchar = "'!:(),*@$"; + not_idstart = "-0123456789"; + + idrx = ('[^' + not_idstart + not_idchar + + '][^' + not_idchar + ']*') + + # regexp to check for valid rison ids + id_ok_re = re.compile('^' + idrx + '$', re.M) + + # regexp to find the end of an id when parsing + next_id_re = re.compile(idrx, re.M) + + def parse_json(self, str): + if len(str) > 0 and str[0] not in '!(': + return simplejson.loads(str) + return self.parse(str) + + def parse(self, str): + self.string = str + self.index = 0 + + value = self.readValue() + if self.next(): + raise ParserException("extra characters at the end of the string") + return value + + def readValue(self): + c = self.next() + + if c == '!': + return self.parse_bang() + if c == '(': + return self.parse_open_paren() + if c == "'": + return self.parse_single_quote() + if c in '-0123456789': + return self.parse_number() + + # fell through table, parse as an id + s = self.string + i = self.index-1 + + m = self.next_id_re.match(s, i) + if m: + id = m.group(0) + self.index = i + len(id) + return id # a string + + if c: + raise ParserException("invalid character: '" + c + "'") + raise ParserException("empty expression") + + def parse_array(self): + ar = [] + while 1: + c = self.next() + if c == ')': + return ar + + if c is None: + raise ParserException("unmatched '!('") + + if len(ar): + if c != ',': + raise ParserException("missing ','") + elif c == ',': + raise ParserException("extra ','") + else: + self.index -= 1 + n = self.readValue() + ar.append(n) + + return ar + + def parse_bang (self): + s = self.string + c = s[self.index] + self.index += 1 + if c is None: + raise ParserException('"!" at end of input') + if c not in self.bangs: + raise ParserException('unknown literal: "!' + c + '"') + x = self.bangs[c] + if callable(x): + return x(self) + + return x + + + def parse_open_paren (self): + count = 0 + o = {} + + while 1: + c = self.next() + if c == ')': + return o + if count: + if c != ',': + raise ParserException("missing ','") + elif c == ',': + raise ParserException("extra ','") + else: + self.index -= 1 + k = self.readValue() + + if self.next() != ':': + raise ParserException("missing ':'") + v = self.readValue() + + o[k] = v + count += 1 + + + def parse_single_quote(self): + s = self.string + i = self.index + start = i + segments = [] + + while 1: + if i >= len(s): + raise ParserException('unmatched "\'"') + + c = s[i] + i += 1 + if c == "'": + break + + if c == '!': + if start < i-1: + segments.append(s[start:i-1]) + c = s[i] + i += 1 + if c in "!'": + segments.append(c) + else: + raise ParserException('invalid string escape: "!'+c+'"') + + start = i + + + if start < i-1: + segments.append(s[start:i-1]) + self.index = i + return ''.join(segments) + + + # Also any number start (digit or '-') + def parse_number(self): + s = self.string + i = self.index + start = i-1 + state = 'int' + permittedSigns = '-' + transitions = { + 'int+.': 'frac', + 'int+e': 'exp', + 'frac+e': 'exp' + } + + while 1: + if i >= len(s): + i += 1 + break + + c = s[i] + i += 1 + + if '0' <= c and c <= '9': + continue + + if permittedSigns.find(c) >= 0: + permittedSigns = '' + continue + + state = transitions.get(state + '+' + c.lower(), None) + if state is None: + break + if state == 'exp': + permittedSigns = '-' + + self.index = i - 1 + s = s[start:self.index] + if s == '-': + raise ParserException("invalid number") + if re.search('[.e]', s): + return float(s) + return int(s) + + # return the next non-whitespace character, or undefined + def next(self): + l = len(self.string) + s = self.string + i = self.index + + while 1: + if i == len(s): + return None + c = s[i] + i += 1 + if c not in self.WHITESPACE: + break + + self.index = i + return c + + + bangs = { + 't': True, + 'f': False, + 'n': None, + '(': parse_array + } + + +def loads(s): + return Parser().parse(s) + +if __name__ == '__main__': + p = Parser() + + rison_examples = [ + "(a:0,b:1)", + "(a:0,b:foo,c:'23skidoo')", + "!t", + "!f", + "!n", + "''", + "0", + "1.5", + "-3", + "1e30", + "1e-30", + "G.", + "a", + "'0a'", + "abc def", + "()", + "(a:0)", + "(id:!n,type:/common/document)", + "!()", + "!(!t,!f,!n,'')", + "'-h'", + "a-z", + "'wow!!'", + "domain.com", + "'user@domain.com'", + "'US $10'", + "'can!'t'", + ]; + + for s in rison_examples: + print + print '*'*70 + print + print s + + print '%r' % (p.parse(s),) diff --git a/trunk/setup.py b/trunk/setup.py new file mode 100644 index 0000000..220bb50 --- /dev/null +++ b/trunk/setup.py @@ -0,0 +1,43 @@ +#!/usr/bin/python +# ======================================================================== +# Copyright (c) 2007, Metaweb Technologies, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# +# THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB +# TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 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. +# ======================================================================== + +#from distutils.core import setup +from setuptools import setup + +setup( + name='freebase', + version='0.1.0', + description='Python client library for Freebase API service', + maintainer_email='developers@freebase.com', + license='BSD', + url='http://www.freebase.com/', + packages=['freebase'] +) + + diff --git a/trunk/test/test_all.py b/trunk/test/test_all.py new file mode 100644 index 0000000..23c4c22 --- /dev/null +++ b/trunk/test/test_all.py @@ -0,0 +1,89 @@ +#!/usr/bin/python +# ======================================================================== +# Copyright (c) 2007, Metaweb Technologies, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * 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. +# +# THIS SOFTWARE IS PROVIDED BY METAWEB TECHNOLOGIES AND CONTRIBUTORS +# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL METAWEB +# TECHNOLOGIES OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 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. +# ======================================================================== + + +import unittest +import sys +from freebase.api import HTTPMetawebSession + +USERNAME = 'username' +PASSWORD = 'password' +API_HOST = 'sandbox.freebase.com' +TEST_QUERY = {'id': 'null', 'name': 'Sting'} + +class TestFreebase(unittest.TestCase): + + def test_login(self): + form_values = { 'username' : USERNAME, + 'password' : PASSWORD } + + mss = HTTPMetawebSession(API_HOST, username=USERNAME, + password=PASSWORD) + mss.login() + + # check mss._cookies? + + def test_read(self): + query = {'type':'/music/artist','id':[{}],'name':'Sting', 'album':[{}]} + + mss = HTTPMetawebSession(API_HOST, username=USERNAME, + password=PASSWORD) + result = mss.mqlread(query) + self.assertNotEqual(None, result) + self.assert_(result.has_key('id')) + self.assert_(result.has_key('type')) + self.assert_(result.has_key('name')) + self.assert_(result.has_key('album')) + self.assertEqual([].__class__, result['album'].__class__) + self.assert_( result['album'].__len__() > 0) + self.assertEqual( 'Sting', result['name']) + self.assertEqual( '#9202a8c04000641f8000000000092a01', result['id'][0]['value'], str(result['id'])) + + def test_write(self): + read_query = {'type':'/music/artist','name':'Yanni\'s Cousin Tom', 'id':{}} + mss = HTTPMetawebSession(API_HOST, username=USERNAME, + password=PASSWORD) + result = mss.mqlread(read_query) + self.assertEqual(None, result) + + write_query = {'create':'unless_exists', 'type':'/music/artist','name':'Yanni'} + + mss.login() + write_result = mss.mqlwrite(write_query) + self.assertNotEqual(None, write_result) + self.assert_(write_result.has_key('create')) + self.assert_(write_result.has_key('type')) + self.assert_(write_result.has_key('name')) + self.assertEqual('existed', write_result['create']) + self.assertEqual('Yanni', write_result['name']) + self.assertEqual('/music/artist', write_result['type']) + +if __name__ == '__main__': + unittest.main() + |
