summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormjtnix <mjtnix@5914aa95-5b3a-0410-a3b5-7b719e7fe9b2>2009-03-19 19:58:49 +0000
committermjtnix <mjtnix@5914aa95-5b3a-0410-a3b5-7b719e7fe9b2>2009-03-19 19:58:49 +0000
commit6c08e620b42a2e00b155006d68b2b68e31888cd7 (patch)
tree43482e2618cbfc9e891e90471cd549c1bf9c0644
parentaeff15ff87c93a96c085916150be8b9be57d5e5d (diff)
"fcl" is a command-line tool for working with freebase.com
git-svn-id: http://freebase-python.googlecode.com/svn/trunk@53 5914aa95-5b3a-0410-a3b5-7b719e7fe9b2
-rw-r--r--freebase-api/freebase/fcl/README.txt8
-rwxr-xr-xfreebase-api/freebase/fcl/__init__.py0
-rwxr-xr-xfreebase-api/freebase/fcl/commands.py688
-rwxr-xr-xfreebase-api/freebase/fcl/fbutil.py113
-rwxr-xr-xfreebase-api/freebase/fcl/fcl.py276
-rwxr-xr-xfreebase-api/freebase/fcl/inspect.py218
-rwxr-xr-xfreebase-api/freebase/fcl/mktype.py179
-rw-r--r--freebase-api/setup.py9
8 files changed, 1489 insertions, 2 deletions
diff --git a/freebase-api/freebase/fcl/README.txt b/freebase-api/freebase/fcl/README.txt
new file mode 100644
index 0000000..2e03d71
--- /dev/null
+++ b/freebase-api/freebase/fcl/README.txt
@@ -0,0 +1,8 @@
+
+This is a command-line tool for performing various freebase commands.
+
+It should be installed by default as "fcl".
+
+Try "fcl help" after installing the freebase-api library.
+
+
diff --git a/freebase-api/freebase/fcl/__init__.py b/freebase-api/freebase/fcl/__init__.py
new file mode 100755
index 0000000..e69de29
--- /dev/null
+++ b/freebase-api/freebase/fcl/__init__.py
diff --git a/freebase-api/freebase/fcl/commands.py b/freebase-api/freebase/fcl/commands.py
new file mode 100755
index 0000000..0fdc283
--- /dev/null
+++ b/freebase-api/freebase/fcl/commands.py
@@ -0,0 +1,688 @@
+# ==================================================================
+# Copyright (c) 2007,2008,2009 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 os, sys, re, time
+from fbutil import *
+
+import simplejson
+
+import freebase.rison as rison
+from freebase.api import attrdict
+
+
+def cmd_help(fb, command=None):
+ """get help on commands
+
+ %prog help [cmd]
+ """
+
+ if command is not None:
+ cmd = fb.commands[command]
+ print ' %s: %s' % (cmd.name, cmd.shortdoc)
+ print cmd.doc
+ return
+
+ print """
+the interface to this tool is loosely modeled on the
+"svn" command-line tool. many commands work slightly
+differently from their unix and svn equivalents -
+read the doc for the command first.
+
+use "%s help <subcommand>" for help on a particular
+subcommand.
+""" % fb.progpath
+
+ fb.oparser.print_help()
+
+ print 'available subcommands:'
+ cmds = sorted(fb.commands.keys())
+
+ for fk in cmds:
+ cmd = fb.commands[fk]
+ print ' %s: %s' % (cmd.name, cmd.shortdoc)
+
+
+def cmd_wikihelp(fb):
+ """get help on commands in mediawiki markup format
+
+ %prog wikihelp
+ """
+
+ print """usage: %s subcommand [ args ... ]
+
+ the interface to this tool is loosely modeled on the
+ "svn" command-line tool. many commands work slightly
+ differently from their unix and svn equivalents -
+ read the doc for the command first.
+ """ % fb.progpath
+
+ print 'subcommands:'
+ for fk in sorted(fb.commands.keys()):
+ cmd = fb.commands[fk]
+
+ # mediawiki markup cares about the difference between
+ # a blank line and a blank line with a bunch of
+ # spaces at the start of it. ewww.
+ doc = re.sub(r'\r?\n {0,8}', '\n ', cmd.doc)
+
+ print '==== %s %s: %s ====' % (fb.progpath, cmd.name, cmd.shortdoc)
+ print doc
+ print
+
+
+# this command is disabled because it relies on some svn glue that
+# was not sufficiently well thought out.
+def old_cmd_pwid(fb):
+ """show the "current working namespace id"
+
+ %prog pwid
+
+ by default, relative ids can't be resolved. however, if you
+ run the fb tool from a directory that has the svn property
+ 'freebase:id' set, relative ids will be resolved relative
+ to that id. the idea is that you can create an svn tree
+ that parallels the freebase namespace tree.
+
+ for example:
+ $ %prog pwid
+
+ $ svn propset freebase:id "/freebase" .
+
+ $ %prog pwid
+ /freebase
+
+ """
+ if fb.cwid is not None:
+ print fb.cwid
+ else:
+ print ''
+
+
+def cmd_ls(fb, path=None):
+ """list the keys in a namespace
+
+ %prog ls [id]
+ """
+ path = fb.absid(path)
+ q = {'id': path,
+ '/type/namespace/keys': [{'value': None,
+ 'namespace': {
+ 'id': None,
+ 'type': []
+ },
+ 'optional':True
+ }]
+ }
+
+ r = fb.mss.mqlread(q)
+ if r is None:
+ raise CmdException('query for id %r failed' % path)
+
+ #sys.stdout.write(' '.join([mk.value for mk in r['/type/namespace/keys']]))
+
+ for mk in r['/type/namespace/keys']:
+ print mk.value
+
+ if 0:
+ suffix = ''
+ if ('/type/namespace' in mk.namespace.type
+ or '/type/domain' in mk.namespace.type):
+ suffix = '/'
+ print mk.value, mk.namespace.id+suffix
+
+
+def cmd_mkdir(fb, path):
+ """create a new freebase namespace
+ %prog mkdir id
+
+ create a new instance of /type/namespace at the given
+ point in id space. if id already exists, it should be
+ a namespace.
+ """
+ path = fb.absid(path)
+ dir,file = dirsplit(path)
+ wq = { 'create': 'unless_exists',
+ 'key':{
+ 'connect': 'insert',
+ 'namespace': dir,
+ 'value': file
+ },
+ 'name': path,
+ 'type': '/type/namespace'
+ }
+
+ r = fb.mss.mqlwrite(wq)
+
+def cmd_ln(fb, src, dst):
+ """create a namespace key
+ %prog ln srcid dstid
+
+ create a new namespace link at dstid to the object
+ currently at srcid.
+ """
+ src = fb.absid(src)
+ dst = fb.absid(dst)
+ dir,file = dirsplit(dst)
+ wq = { 'id': src,
+ 'key':{
+ 'connect': 'insert',
+ 'namespace': dir,
+ 'value': file
+ }
+ }
+
+ r = fb.mss.mqlwrite(wq)
+
+
+def cmd_rm(fb, path):
+ """unlink a namespace key
+ %prog rm id
+
+ remove the /type/key that connects the given id to its
+ parent. id must be a path for this to make any sense.
+
+ note that is like unix 'unlink' rather than 'rm'.
+ it won't complain if the 'subdirectory' contains data,
+ since that data will still be accessible to other queries.
+ it's not like 'rm -rf' either, because it doesn't
+ disturb anything other than the one directory entry.
+ """
+ path = fb.absid(path)
+ dir,file = dirsplit(path)
+
+ wq = { 'id': path,
+ 'key':{
+ 'connect': 'delete',
+ 'namespace': dir,
+ 'value': file
+ }
+ }
+
+ r = fb.mss.mqlwrite(wq)
+
+def cmd_mv(fb, src, dst):
+ """rename srcid to dstid.
+ %prog mv srcid dstid
+
+ equivalent to:
+ $ fb ln <srcid> <dstid>
+ $ fb rm <srcid>
+ """
+ cmd_ln(fb, src, dst)
+ cmd_rm(fb, src)
+
+def cmd_cat(fb, id, include_headers=False):
+ """download a document from freebase to stdout
+ %prog cat id
+
+ equivalent to "%prog get id -".
+ """
+ return cmd_get(fb, id, localfile='-', include_headers=include_headers)
+
+def cmd_get(fb, id, localfile=None, include_headers=False):
+ """download a file from freebase
+ %prog get id [localfile]
+
+ download the document or image with the given id from freebase
+ into localfile. localfile '-' means stdout. localfile
+ defaults to a file in the current directory with the same name
+ as the last key in the path, possibly followed by a metadata
+ extension like .html or .txt.
+ """
+ id = fb.absid(id)
+ dir,file = dirsplit_unsafe(id)
+
+ def read_content(id, content_only=False):
+ c = attrdict(id=id)
+ cq = { 'id': id,
+ 'type': [],
+ '/common/document/content': None,
+ '/common/document/source_uri': None,
+ '/type/content/media_type': { 'name':None,
+ 'optional': True },
+ #'/type/content/text_encoding': { 'name':None },
+ '/type/content/blob_id':None,
+ }
+ cd = fb.mss.mqlread(cq)
+ if '/type/content' in cd.type:
+ c.media_type = cd['/type/content/media_type'].name
+ #c.text_encoding = cd['/type/content/text_encoding'].name
+ c.sha256 = cd['/type/content/blob_id']
+ return c
+
+ if content_only:
+ raise CmdException('%s is not a content id' % id)
+
+ cid = cd['/common/document/content']
+ if cid is not None:
+ return read_content(cid, content_only=True)
+
+ # in this case we don't have a content object
+ if cd['/common/document/source_uri'] is not None:
+ return None
+
+ raise CmdException('%s is not a content or document id' % id)
+
+
+ content = read_content(id)
+ log.debug('COBJ %r' % content)
+ if content is not None:
+ fileext = media_type_to_extension.get(content.media_type, None)
+ else:
+ fileext = None
+
+ if localfile == '-':
+ ofp = sys.stdout
+ else:
+ if localfile is None:
+ implicit_outfile = True
+ localfile = file
+ elif re.match(r'[/\\]$', localfile):
+ implicit_outfile = True
+ localfile = localfile + file
+ else:
+ implicit_outfile = False
+ localfile = os.path.abspath(localfile)
+
+ # add file extension based on content-type:
+ # should be an option to disable this
+ if implicit_outfile and fileext is not None:
+ localfile += '.' + fileext
+
+ # if we didn't explicitly name the output file,
+ # don't destroy an existing file
+ localfile_base = localfile
+ count = 0
+ while implicit_outfile and os.path.exists(localfile):
+ count += 1
+ localfile = '%s.%d' % (localfile_base, count)
+ ofp = open(localfile, 'wb')
+
+ body = fb.mss.trans(id)
+
+ if include_headers:
+ # XXX show content-type, what else?
+ pass
+
+ ofp.write(body)
+
+ if localfile != '-':
+ print ('%s saved (%d bytes)' % (localfile, len(body)))
+ ofp.close()
+
+
+def cmd_put(fb, localfile, id=None, content_type=None):
+ """upload a document to freebase -- EXPERIMENTAL
+ %prog put localfile [id] [content-type]
+
+ upload the document or image in localfile to given freebase
+ id. if localfile is '-' the data will be read from stdin.
+
+ if id is missing or empty, a new document will be created.
+ later the id might default to something computed from localfile
+ and any svn attributes it has.
+
+ output: a single line, the id of the document.
+ """
+ if content_type is None:
+ ext = re.sub('^.*\.([^/.]+)$', r'\1', localfile)
+ media_type = extension_to_media_type.get(ext, None)
+
+ if media_type is None:
+ raise CmdException('could not infer a media type from extension %r: please specify it'
+ % ext)
+
+ if media_type.startswith('text/'):
+ # this is a bad assumption. should sniff it?
+ text_encoding = 'utf-8'
+ content_type = '%s;charset=%s' % (media_type, text_encoding)
+ else:
+ content_type = media_type
+
+ new_id = None
+ if id is not None:
+ idinfo = fb.mss.mqlread({ 'id': id, 'type': '/common/document' })
+ if idinfo is None:
+ new_id = id
+ id = None
+
+ body = open(localfile, 'rb').read()
+ r = fb.mss.upload(body, content_type, document_id=id)
+
+ if new_id is None:
+ print r.document
+ else:
+ cmd_ln(fb, r.document, new_id)
+ print new_id
+
+
+def cmd_dump(fb, id):
+ """show all properties of a freebase object
+ %prog dump object_id
+ """
+ id = fb.absid(id)
+
+ import inspect
+
+ r = inspect.inspect_object(fb.mss, id)
+ if r is None:
+ raise CmdException('no match for id %r' % id)
+
+ for k in sorted(r.keys()):
+ vs = r[k]
+ for v in vs:
+ id = v.get('id', '')
+
+ name = '%r' % (v.get('name') or v.get('value'))
+ if name == 'None': name = ''
+
+ type = v.get('type', '')
+ if type == '/type/text':
+ extra = v.get('lang', '')
+ elif type == '/type/key':
+ extra = v.get('namespace', '')
+ else:
+ extra = ''
+
+ fb.trow(k, id, name, type, extra)
+
+
+def cmd_pget(fb, id, propid):
+ """get a property of a freebase object -- EXPERIMENTAL
+ %prog pget object_id property_id
+
+ get the property named by property_id from the object.
+
+ XXX output quoting is not well specified.
+
+ property_id must be a fully qualified id for now.
+
+ prints one line for each match.
+
+ if propid ends in '*' this does a wildcard for a particular type.
+ """
+ id = fb.absid(id)
+ proptype, propkey = dirsplit(propid)
+
+ if propkey != '*':
+ # look up the prop
+ q = { 'id': id,
+ propid: [{}],
+ }
+ r = fb.mss.mqlread(q)
+ for v in r[propid]:
+ if 'value' in v:
+ print v.value
+ else:
+ print v.id
+
+ else:
+ # look up the prop
+ q = { 'id': id,
+ '*': [{}],
+ }
+ if isinstance(proptype, basestring):
+ q['type'] = proptype
+
+ r = fb.mss.mqlread(q)
+
+ for k in sorted(r.keys()):
+ v = r[k];
+ if 'value' in v:
+ print '%s %s' % (k, v.value)
+ else:
+ print '%s %s' % (k, v.id)
+
+def cmd_pdel(fb, id, propid, oldval):
+ """delete a property of a freebase object -- EXPERIMENTAL
+ %prog pdel object_id property_id oldvalue
+
+ set the property named by property_id on the object.
+ value is an id or a json value. XXX this is ambiguous.
+
+ property_id must be a fully qualified id for now.
+
+ for now you need to provide a "oldval" argument,
+ later this tool will query and perhaps prompt if the
+ deletion is ambiguous.
+
+ prints a single line, either 'deleted' or 'missing'
+ """
+ return cmd_pset(fb, id, propid, None, oldval)
+
+
+def cmd_touch(fb):
+ """bypass any cached query results the service may have. use sparingly.
+ """
+ fb.mss.mqlflush()
+
+
+def cmd_pset(fb, id, propkey, val, oldval=None, extra=None):
+ """set a property of a freebase object -- EXPERIMENTAL
+ %prog pset object_id property_id value
+
+ set the property named by property_id on the object.
+ value is an id or a json value. XXX this is ambiguous.
+
+ property_id must be a fully qualified id for now.
+
+ if the property should be a unique property, this will
+ write with 'connect:update'. if the property may have
+ multiple, it is written with 'connect:insert'.
+
+ prints a single line, either 'inserted' or 'present'
+ """
+ id = fb.absid(id)
+
+ propid = fb.absprop(propkey)
+
+ # look up the prop
+ pq = { 'id': propid,
+ 'type': '/type/property',
+ 'name': None,
+ 'unique': None,
+ 'expected_type': {
+ 'id': None,
+ 'name': None,
+ 'default_property': None,
+ 'optional': True,
+ },
+ }
+
+ prop = fb.mss.mqlread(pq)
+
+ if prop is None:
+ raise CmdException('can\'t resolve property key %r - use an absolute id' % propid);
+
+ if propid.startswith('/type/object/') or propid.startswith('/type/value/'):
+ propkey = re.sub('/type/[^/]+/', '', propid);
+ else:
+ propkey = propid
+
+ wq = { 'id': id,
+ propkey: {
+ }
+ }
+
+ if val is None:
+ val = oldval
+ wq[propkey]['connect'] = 'delete'
+ elif prop.unique:
+ wq[propkey]['connect'] = 'update'
+ else:
+ wq[propkey]['connect'] = 'insert'
+
+ if prop.expected_type is None:
+ wq[propkey]['id'] = val
+ elif prop.expected_type.id not in value_types:
+ wq[propkey]['id'] = val
+ else:
+ wq[propkey]['value'] = val
+
+ if prop.expected_type.id == '/type/text':
+ if extra is not None:
+ lang = extra
+ else:
+ lang = '/lang/en'
+ wq[propkey]['lang'] = lang
+
+ if prop.expected_type.id == '/type/key':
+ if extra is not None:
+ wq[propkey]['namespace'] = extra
+ else:
+ raise CmdException('must specify a namespace to pset /type/key')
+
+ r = fb.mss.mqlwrite(wq)
+ print r[propkey]['connect']
+
+def cmd_login(fb, username=None, password=None):
+ """login to the freebase service
+ %prog login [username [password]]
+
+ cookies are maintained in a file so
+ they are available to the next invocation.
+ prompts for username and password if not given
+ """
+ import getpass
+ if username is None:
+ sys.stdout.write('freebase.com username: ')
+ username = sys.stdin.readline()
+ if not username:
+ raise CmdException('usernmae required for login')
+ username = re.compile('\n$').sub('', username)
+ if password is None:
+ password = getpass.getpass('freebase.com password: ')
+
+ fb.mss.username = username
+ fb.mss.password = password
+ fb.mss.login()
+
+def cmd_logout(fb):
+ """logout from the freebase service
+ %prog logout
+
+ deletes the login cookies
+ """
+ fb.cookiejar.clear(domain=fb.service_host.split(':')[0])
+
+
+def cmd_find(fb, qstr):
+ """print all ids matching a given constraint.
+
+ if the query string starts with "{" it is treated as json.
+ otherwise it is treated as o-rison.
+
+ %prog find
+ """
+ if qstr.startswith('{'):
+ q = simplejson.loads(qstr)
+ else:
+ q = rison.loads('(' + qstr + ')')
+
+ if 'id' not in q:
+ q['id'] = None
+
+ results = fb.mss.mqlreaditer(q)
+ for r in results:
+ print r.id
+
+def cmd_q(fb, qstr):
+ """run a freebase query.
+
+ if the query string starts with "{" it is treated as json.
+ otherwise it is treated as o-rison.
+
+ dump the result as json.
+
+ %prog q
+ """
+ if qstr.startswith('{'):
+ q = simplejson.loads(qstr)
+ else:
+ q = rison.loads('(' + qstr + ')')
+
+ # results could be streamed with a little more work
+ results = fb.mss.mqlreaditer(q)
+ print simplejson.dumps(list(results), indent=2)
+
+def cmd_open(fb, id):
+ """open a web browser on the given id. works on OSX only for now.
+
+ %prog open /some/id
+ """
+ os.system("open 'http://www.freebase.com/view%s'" % id)
+
+
+def cmd_log(fb, id):
+ """log changes pertaining to a given id.
+
+ INCOMPLETE
+
+ %prog log /some/id
+ """
+
+ null = None
+ true = True
+ false = False
+
+ baseq = {
+ 'type': '/type/link',
+ 'source': null,
+ 'master_property': null,
+ 'attribution': null,
+ 'timestamp': null,
+ 'operation': null,
+ 'valid': null,
+ 'sort': '-timestamp'
+ };
+
+ queries = [
+ {
+ 'target_value': { '*': null },
+ 'target': { 'id': null, 'name': null, 'optional': true },
+ },
+ {
+ 'target': { 'id': null, 'name': null },
+ }]
+
+ for i,q in list(enumerate(queries)):
+ q.update(baseq)
+ queries[i] = [q]
+
+
+ valuesfrom,linksfrom = fb.mss.mqlreadmulti(queries)
+
+ for link in linksfrom:
+ # fb.trow(link.master_property.id, ...)
+ print simplejson.dumps(link, indent=2)
+
+ for link in valuesfrom:
+ # fb.trow(link.master_property.id, ...)
+ print simplejson.dumps(link, indent=2)
+
+
diff --git a/freebase-api/freebase/fcl/fbutil.py b/freebase-api/freebase/fcl/fbutil.py
new file mode 100755
index 0000000..ed1e511
--- /dev/null
+++ b/freebase-api/freebase/fcl/fbutil.py
@@ -0,0 +1,113 @@
+# ==================================================================
+# Copyright (c) 2007,2008,2009 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 re
+
+import logging
+log = logging.getLogger()
+
+
+class FbException(Exception):
+ pass
+
+class CmdException(Exception):
+ pass
+
+media_types = {
+ 'html': ['text/html'],
+ 'txt': ['text/plain'],
+
+ 'xml': ['text/xml',
+ 'application/xml'],
+
+ 'atom': ['application/atom+xml'],
+
+ 'js': ['text/javascript',
+ 'application/javascript',
+ 'application/x-javascript'],
+
+ 'json': ['application/json'],
+
+ 'jpg': ['image/jpeg',
+ 'image/pjpeg'],
+
+ 'gif': ['image/gif'],
+
+ 'png': ['image/png'],
+ }
+
+extension_to_media_type = dict([(k,vs[0]) for k,vs in media_types.items()])
+media_type_to_extension = {}
+for k,vs in media_types.items():
+ for v in vs:
+ media_type_to_extension[v] = k
+
+
+
+
+DIRSPLIT = re.compile(r'^(.+)/([^/]+)$')
+
+def dirsplit_unsafe(id):
+ m = DIRSPLIT.match(id)
+ if m is None:
+ return (None, id)
+ dir,file = m.groups()
+ return (dir,file)
+
+def dirsplit(id):
+ dir,file = dirsplit_unsafe(id)
+ if dir == '/guid':
+ raise FbException('%r is not a freebase keypath' % (id,))
+ return (dir,file)
+
+value_types = [
+ '/type/text',
+ '/type/key',
+ '/type/rawstring',
+ '/type/float',
+ '/type/int',
+ '/type/boolean',
+ '/type/uri',
+ '/type/datetime',
+ '/type/id',
+ '/type/enumeration',
+ ]
+
+default_propkeys = {
+ 'value': '/type/value/value',
+ 'id': '/type/object/id',
+ 'guid': '/type/object/guid',
+ 'type': '/type/object/type',
+ 'name': '/type/object/name',
+ 'key': '/type/object/key',
+ 'timestamp': '/type/object/timestamp',
+ 'permission': '/type/object/permission',
+ 'creator': '/type/object/creator',
+ 'attribution': '/type/object/attribution'
+};
diff --git a/freebase-api/freebase/fcl/fcl.py b/freebase-api/freebase/fcl/fcl.py
new file mode 100755
index 0000000..a0299b8
--- /dev/null
+++ b/freebase-api/freebase/fcl/fcl.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python
+# ==================================================================
+# Copyright (c) 2007,2008,2009 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 os, sys, re, time, stat
+
+from optparse import OptionParser
+import getpass
+import cookielib
+import logging
+import simplejson
+
+
+from fbutil import FbException, CmdException, log, default_propkeys
+console = logging.StreamHandler()
+log.addHandler(console)
+
+from freebase.api import HTTPMetawebSession, MetawebError, attrdict
+
+_cookiedir = None
+if os.environ.has_key('HOME'):
+ _cookiedir = os.path.join(os.environ['HOME'], '.pyfreebase')
+
+
+class Command(object):
+ def __init__(self, module, name, func):
+ self.module = module
+ self.name = name
+ self.func = func
+
+ PROG = re.compile(r'\%prog')
+ NEWLINE = re.compile(r'(?:\r?\n)+')
+
+ # fill in self.doc
+ if isinstance(func.__doc__, basestring):
+ doc = PROG.sub('fcl', func.__doc__ + '\n')
+ self.shortdoc, self.doc = NEWLINE.split(doc, 1)
+ else:
+ self.shortdoc = '(missing documentation)'
+ self.doc = '(missing documentation)'
+
+
+
+class FbCommandHandler(object):
+
+ def __init__(self):
+ self.service_host = 'www.freebase.com'
+ self.cookiejar = None
+ self.cwid = ''
+ self.progpath = 'fcl'
+ self.commands = {}
+
+ self.cookiefile = None
+ if _cookiedir is not None:
+ self.cookiefile = os.path.join(_cookiedir, 'cookiejar')
+
+
+ def init(self):
+ if self.cookiefile is not None:
+ self.cookiejar = cookielib.LWPCookieJar(self.cookiefile)
+ if os.path.exists(self.cookiefile):
+ try:
+ self.cookiejar.load(ignore_discard=True)
+ except cookielib.LoadError:
+ log.warn('error loading cookies')
+
+ #print 'start cookies %r' % self.cookiejar
+
+ self.mss = HTTPMetawebSession(self.service_host,
+ cookiejar=self.cookiejar)
+
+
+ def absid(self, path):
+ if path is None:
+ path = ''
+ if path.startswith('/'):
+ return path
+
+ if not isinstance(self.cwid, basestring) or not self.cwid.startswith('/'):
+ # svn cwid support is disabled because it relies on some svn glue that
+ # was not sufficiently well thought out.
+ # raise CmdException("can't resolve relative id %r without cwid - see 'fcl help pwid'" % (path))
+ raise CmdException("no support for relative id %r" % (path))
+
+ if path == '' or path == '.':
+ return self.cwid
+
+ return self.cwid + '/' + path
+
+
+ def absprop(self, propkey):
+ if propkey.startswith('/'):
+ return propkey
+
+ # check schemas of /type/object and /type/value,
+ # as well as other reserved names
+ if propkey in default_propkeys:
+ return default_propkeys[propkey]
+
+ return self.absid(propkey)
+
+ def thead(self, *args):
+ strs = ['%r' % arg
+ for arg in args]
+ print '\t'.join(strs)
+
+ def trow(self, *args):
+ print '\t'.join(args)
+ return
+ strs = ['%r' % arg
+ for arg in args]
+ print '\t'.join(strs)
+
+ def save(self):
+ #print 'end cookies %r' % self.cookiejar
+ if _cookiedir and self.cookiefile.startswith(_cookiedir):
+ # create private cookiedir if needed
+ if not os.path.exists(_cookiedir):
+ os.mkdir(_cookiedir, 0700)
+ os.chmod(_cookiedir, stat.S_IRWXU)
+
+ if self.cookiejar is None:
+ return
+
+ self.cookiejar.save(ignore_discard=True)
+
+ # save the cwd and other state too
+
+
+ def import_commands(self, modname):
+ """
+ import new fb commands from a file
+ """
+ namespace = {}
+
+ pyimport = 'from %s import *' % modname
+ exec pyimport in namespace
+ mod = sys.modules.get(modname)
+
+ commands = [Command(mod, k[4:], getattr(mod, k))
+ for k in getattr(mod, '__all__', dir(mod))
+ if (k.startswith('cmd_')
+ and callable(getattr(mod, k)))]
+
+
+ for cmd in commands:
+ log.info('importing %r' % ((cmd.name, cmd.func),))
+ self.commands[cmd.name] = cmd
+
+ log.info('imported %r from %r' % (modname, mod.__file__))
+
+
+ def dispatch(self, cmd, args):
+ if cmd in self.commands:
+ try:
+ self.commands[cmd].func(self, *args)
+ except KeyboardInterrupt, e:
+ sys.stderr.write('%s\n' % (str(e),))
+ except FbException, e:
+ sys.stderr.write('%s\n' % (str(e),))
+ except CmdException, e:
+ sys.stderr.write('%s\n' % (str(e),))
+ except MetawebError, e:
+ sys.stderr.write('%s\n' % (str(e),))
+ else:
+ self.oparser.error('unknown subcommand %r, try "%s help"' % (cmd, self.progpath))
+
+ self.save()
+
+
+ def cmdline_main(self):
+ op = OptionParser(usage='%prog [options] command [args...] ')
+ self.oparser = op
+
+ op.disable_interspersed_args()
+
+ op.add_option('-d', '--debug', dest='debug',
+ default=False, action='store_true',
+ help='turn on debugging output')
+
+ op.add_option('-v', '--verbose', dest='verbose',
+ default=False, action='store_true',
+ help='verbose output')
+
+ op.add_option('-V', '--very-verbose', dest='very_verbose',
+ default=False, action='store_true',
+ help='lots of debug output')
+
+ op.add_option('-s', '--service', dest='service_host',
+ metavar='HOST',
+ default=self.service_host,
+ help='Freebase HTTP service address:port')
+
+ op.add_option('-S', '--sandbox', dest='use_sandbox',
+ default=False, action='store_true',
+ help='shortcut for --service=sandbox.freebase.com')
+
+ op.add_option('-c', '--cookiejar', dest='cookiefile',
+ metavar='FILE',
+ default=self.cookiefile,
+ help='Cookie storage file (will be created if missing)')
+ options,args = op.parse_args()
+
+ if len(args) < 1:
+ op.error('required subcommand missing')
+
+
+ loglevel = logging.WARNING
+ if options.verbose:
+ loglevel = logging.INFO
+ if options.very_verbose:
+ loglevel = logging.DEBUG
+
+ console.setLevel(loglevel)
+ log.setLevel(loglevel)
+
+ if options.use_sandbox:
+ self.service_host = 'sandbox.freebase.com'
+ else:
+ self.service_host = options.service_host
+
+ self.cookiefile = options.cookiefile
+ #self.progpath = sys.argv[0]
+
+ self.init()
+
+ self.mss.log.setLevel(loglevel)
+ self.mss.log.addHandler(console)
+
+ self.import_commands('freebase.fcl.commands')
+ self.import_commands('freebase.fcl.mktype')
+
+ cmd = args.pop(0)
+ self.dispatch(cmd, args)
+
+# entry point for script
+def main():
+ try:
+ # turn off crlf output on windows so we work properly
+ # with unix tools.
+ import msvcrt
+ msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+ msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
+ except ImportError:
+ pass
+
+ fb = FbCommandHandler()
+ fb.cmdline_main()
+
+if __name__ == '__main__':
+ main()
diff --git a/freebase-api/freebase/fcl/inspect.py b/freebase-api/freebase/fcl/inspect.py
new file mode 100755
index 0000000..8cd04d2
--- /dev/null
+++ b/freebase-api/freebase/fcl/inspect.py
@@ -0,0 +1,218 @@
+# ==================================================================
+# Copyright (c) 2007,2008,2009 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.
+# ====================================================================
+
+
+#
+#
+# wrap all the nastiness needed for a general mql inspect query
+#
+#
+
+import os, sys, re
+
+
+null = None
+true = True
+false = False
+
+inspect_query = {
+ 'name': null,
+ 'type': [],
+
+ '/type/reflect/any_master': [{
+ 'optional':true,
+ 'id': null,
+ 'name': null,
+ 'link': {
+ 'master_property': {
+ 'id': null,
+ 'schema': null
+ }
+ }
+ }],
+
+ '/type/reflect/any_reverse': [{
+ 'optional':true,
+ 'id': null,
+ 'name': null,
+ 'link': {
+ 'master_property': {
+ 'id':null,
+ 'schema': null,
+ 'expected_type': null,
+ 'reverse_property': {
+ 'id': null,
+ 'schema': null,
+ 'optional': true
+ }
+ }
+ }
+ }],
+
+ '/type/reflect/any_value': [{
+ 'optional':true,
+ 'value': null,
+ 'link': {
+ 'master_property': {
+ 'id':null,
+ 'schema': null,
+ 'expected_type': null
+ },
+ }
+ }],
+
+ 't:/type/reflect/any_value': [{
+ 'optional':true,
+ 'type': '/type/text',
+ 'value': null,
+ 'lang': null,
+ 'link': {
+ 'master_property': {
+ 'id':null,
+ 'schema': null
+ },
+ }
+ }],
+
+ '/type/object/creator': [{
+ 'optional':true,
+ 'id':null,
+ 'name':null
+ }],
+ '/type/object/timestamp': [{
+ 'optional':true,
+ 'value': null,
+ }],
+
+ '/type/object/key': [{
+ 'optional':true,
+ 'value': null,
+ 'namespace': null
+ }],
+ '/type/namespace/keys': [{
+ 'optional':true,
+ 'value': null,
+ 'namespace': null
+ }]
+}
+
+
+def transform_result(result):
+ proptypes = {}
+ props = {}
+
+ # copy a property from a /type/reflect clause
+ def pushtype(propdesc, prop):
+ tid = propdesc['schema']
+ propid = propdesc['id']
+
+ if isinstance(prop, dict):
+ prop = dict(prop)
+ if 'link' in prop:
+ prop.pop('link')
+
+ if tid not in proptypes:
+ proptypes[tid] = {}
+ if propid not in proptypes[tid]:
+ proptypes[tid][propid] = []
+
+ if propid not in props:
+ props[propid] = []
+ props[propid].append(prop)
+
+ # copy a property that isn't enumerated by /type/reflect
+ def pushprop(propid):
+ ps = result[propid]
+ if ps is None:
+ return
+
+ # hack to infer the schema from id, not always reliable!
+ schema = re.sub(r'/[^/]+$', '', propid)
+ keyprop = dict(id=propid, schema=schema)
+ for p in ps:
+ pushtype(keyprop, p)
+
+ ps = result['/type/reflect/any_master'] or []
+ for p in ps:
+ propdesc = p.link.master_property
+ pushtype(propdesc, p)
+
+ # non-text non-key values
+ ps = result['/type/reflect/any_value'] or []
+ for p in ps:
+ propdesc = p.link.master_property
+
+ # /type/text values are queried specially
+ # so that we can get the lang, so ignore
+ # them here.
+ if propdesc.expected_type == '/type/text':
+ continue
+
+ pushtype(propdesc, p)
+
+ # text values
+ ps = result['t:/type/reflect/any_value'] or []
+ for p in ps:
+ propdesc = p.link.master_property
+ pushtype(propdesc, p)
+
+ pushprop('/type/object/creator')
+ pushprop('/type/object/timestamp')
+ pushprop('/type/object/key')
+ pushprop('/type/namespace/keys')
+
+ # now the reverse properties
+ ps = result['/type/reflect/any_reverse'] or []
+ for prop in ps:
+ propdesc = prop.link.master_property.reverse_property
+
+ # synthetic property descriptor for the reverse of
+ # a property with no reverse descriptor.
+ # note the bogus id starting with '-'.
+ if propdesc is None:
+ # schema = prop.link.master_property.expected_type
+ # if schema is None:
+ # schema = 'other'
+
+ schema = 'other'
+ propdesc = dict(id='-' + prop.link.master_property.id,
+ schema=schema)
+
+ pushtype(propdesc, prop)
+
+ #return proptypes
+ return props
+
+
+def inspect_object(mss, id):
+ q = dict(inspect_query)
+ q['id'] = id
+ r = mss.mqlread(q)
+ if r is None:
+ return None
+ return transform_result(r)
diff --git a/freebase-api/freebase/fcl/mktype.py b/freebase-api/freebase/fcl/mktype.py
new file mode 100755
index 0000000..30f8df7
--- /dev/null
+++ b/freebase-api/freebase/fcl/mktype.py
@@ -0,0 +1,179 @@
+# ==================================================================
+# Copyright (c) 2007,2008,2009 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 os, sys, re, time
+from fbutil import *
+
+
+def cmd_mkobj(fb, id, typeid='/common/topic', name=''):
+ """create a new object with a given type -- EXPERIMENTAL
+ %prog mkobj new_id typeid name
+
+ create a new object with type typeid at the given
+ namespace location.
+
+ if present, name gives the display name of the new object.
+
+ """
+ id = fb.absid(id)
+ nsid, key = dirsplit(id)
+
+ typeid = fb.absid(typeid)
+
+ if name == '':
+ name = key
+
+ wq = { 'create': 'unless_exists',
+ 'id': None,
+ 'name': name,
+ 'type': typeid,
+ 'key':{
+ 'namespace': nsid,
+ 'value': key
+ },
+ }
+
+ # TODO add included types
+
+ r = fb.mss.mqlwrite(wq)
+ print r.id,r.create
+
+def cmd_mktype(fb, id, name=''):
+ """create a new type -- EXPERIMENTAL
+ %prog mktype new_id name
+
+ create a new object with type Type at the given
+ namespace location.
+
+ this doesn't create any type hints.
+
+ if present, name gives the display name of the new property
+ """
+ id = fb.absid(id)
+
+ nsid, key = dirsplit(id)
+
+ if name == '':
+ name = key
+
+ wq = { 'create': 'unless_exists',
+ 'id': None,
+ 'name': name,
+ 'type': '/type/type',
+ 'key':{
+ 'namespace': nsid,
+ 'value': key
+ },
+ }
+
+ r = fb.mss.mqlwrite(wq)
+ print r.id,r.create
+
+def mkprop(fb, typeid, key, name='', vtype=None, master_property=None):
+ """helper to create a new property
+ """
+ if name == '':
+ name = key
+
+ wq = { 'create': 'unless_exists',
+ 'id': None,
+ 'type': '/type/property',
+ 'name': name,
+ 'schema': typeid,
+ 'key': {
+ 'namespace': typeid,
+ 'value': key
+ }
+ }
+
+ if vtype is not None:
+ wq['expected_type'] = vtype
+ if master_property is not None:
+ wq['master_property'] = master_property
+
+ return fb.mss.mqlwrite(wq)
+
+
+def cmd_mkprop(fb, id, name='', vtype=None, revkey=None, revname=''):
+ """create a new property -- EXPERIMENTAL
+ %prog mkprop new_id [name] [expected_type] [reverse_property] [reverse_name]
+
+ create a new object with type Property at the given
+ location. creates both the "schema" and "key" links
+ for the property, but doesn't create any freebase property
+ hints.
+
+ if present, name gives the display name of the new property
+ """
+ id = fb.absid(id)
+ if vtype is not None:
+ vtype = fb.absid(vtype)
+
+ typeid, key = dirsplit(id)
+
+
+ r = mkprop(fb, typeid, key, name, vtype)
+
+ # write the reverse property if specified
+
+ print r.id, r.create
+
+ if revkey is None:
+ return
+
+ assert vtype is not None
+
+ rr = mkprop(fb, vtype, revkey, revname, typeid, id)
+ print rr.id, rr.create
+
+
+def cmd_publish_type(fb, typeid):
+ """try to publish a freebase type for the client
+ %prog publish_type typeid
+
+ set /freebase/type_profile/published to the /freebase/type_status
+ instance named 'Published'
+
+
+ should also try to set the domain to some namespace that
+ has type:/type/domain
+
+ """
+ id = fb.absid(typeid)
+
+ w = {
+ 'id': id,
+ '/freebase/type_profile/published': {
+ 'connect': 'insert',
+ 'type': '/freebase/type_status',
+ 'name': 'Published'
+ }
+ }
+ r = fb.mss.mqlwrite(w)
+
+ print r['/freebase/type_profile/published']['connect']
diff --git a/freebase-api/setup.py b/freebase-api/setup.py
index a584848..3459227 100644
--- a/freebase-api/setup.py
+++ b/freebase-api/setup.py
@@ -34,7 +34,7 @@ except ImportError:
setup(
name='freebase',
- version='0.2.4',
+ version='0.2.5',
author='Nick Thompson',
author_email='nix@metaweb.com',
maintainer_email='developers@freebase.com',
@@ -44,7 +44,12 @@ setup(
long_description="""A Python library providing a convenient
wrapper around the freebase.com service api, as well as some
utility functions helpful in writing clients of the api.""",
- packages=['freebase', 'freebase.api'],
+ packages=['freebase', 'freebase.api', 'freebase.fcl'],
+ entry_points = {
+ 'console_scripts': [
+ 'fcl = freebase.fcl.fcl:main'
+ ]
+ },
requires=[
"simplejson",
],