1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/fixx.py Sun Nov 29 14:28:14 2009 +0000
1.3 @@ -0,0 +1,273 @@
1.4 +import urllib, base64,httplib
1.5 +from restful_lib import Connection
1.6 +import simplejson as json
1.7 +
1.8 +####USAGE
1.9 +# from fixx import Fixx
1.10 +# variable = Fixx("fixx url","port","username","password")
1.11 +# variable.getAllIssues()
1.12 +# variable.getIssue("issuenumber")
1.13 +# Data should be inputted in dictionaries on all cases except the filtering/search/pagination for getting issues, which should be a list.
1.14 +# returns python objects
1.15 +#
1.16 +###
1.17 +
1.18 +
1.19 +class Fixx:
1.20 +
1.21 + def __init__(self,url,port,name,password):
1.22 + self.url=url
1.23 + self.auth = base64.encodestring('%s:%s' % (name, password))[:-1]
1.24 + self.type = "application/json"
1.25 + self.port = port
1.26 +
1.27 + ####################
1.28 + # ISSUES #
1.29 + ####################
1.30 +
1.31 + def getAllIssues(self,filterOptions = None):
1.32 + if(filterOptions is not None):
1.33 + options = "&".join(filterOptions)
1.34 + url = "/api/issues?"+options
1.35 + else:
1.36 + url = "/api/issues"
1.37 + conn = Connection(self.url)
1.38 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.39 + return json.loads(response['body'])
1.40 +
1.41 +
1.42 + def getAllIssuesForProjects(self,projectId):
1.43 + url = "/api/projects/"+projectId+"/issues"
1.44 + conn = Connection(self.url)
1.45 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.46 + return json.loads(response['body'])
1.47 +
1.48 + def getIssue(self,issueId):
1.49 + url = "/api/issues/"+issueId
1.50 + conn = Connection(self.url)
1.51 + response = conn.request_get(url, headers={'Accept':self.type, 'Content-Type':self.type, "Authorization":"Basic %s" % self.auth})
1.52 + return json.loads(response['body'])
1.53 +
1.54 + def createIssue(self,data):
1.55 + data = json.dumps(data)
1.56 + url = "/api/issues"
1.57 + conn = Connection(self.url)
1.58 + response = conn.request_post(url,body=data, headers={'Accept':self.type, 'Content-Type':self.type, "Authorization":"Basic %s" % self.auth})
1.59 + return json.loads(response['body'])
1.60 +
1.61 + def updateIssue(self,issueId,data):
1.62 + data = json.dumps(data)
1.63 + url = "/api/issues/"+issueId
1.64 + conn = Connection(self.url)
1.65 + response = conn.request_put(url, body=data, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.66 + return json.loads(response['body'])
1.67 +
1.68 + def deleteIssue(self,issueId):
1.69 + url = "/api/issues/"+issueId
1.70 + conn = Connection(self.url)
1.71 + response = conn.request_delete(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.72 + return json.loads(response['body'])
1.73 +
1.74 +
1.75 + ####################
1.76 + # PROJECTS #
1.77 + ####################
1.78 + def getAllProjects(self):
1.79 + url = "/api/projects"
1.80 + conn = Connection(self.url)
1.81 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.82 + return json.loads(response['body'])
1.83 +
1.84 + def getProject(self,projectId):
1.85 + url = "/api/projects/"+projectId
1.86 + conn = Connection(self.url)
1.87 + response = conn.request_get(url, headers={'Accept':self.type, 'Content-Type':self.type, "Authorization":"Basic %s" % self.auth})
1.88 + return json.loads(response['body'])
1.89 +
1.90 + def createProject(self,data):
1.91 + data = json.dumps(data)
1.92 + url = "/api/projects"
1.93 + conn = Connection(self.url)
1.94 + response = conn.request_post(url,body=data, headers={'Accept':self.type, 'Content-Type':self.type, "Authorization":"Basic %s" % self.auth})
1.95 + return json.loads(response['body'])
1.96 +
1.97 + def updateProject(self,projectId,data):
1.98 + data = json.dumps(data)
1.99 + url = "/api/projects/"+projectId
1.100 + conn = Connection(self.url)
1.101 + response = conn.request_put(url, args=data, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.102 + return json.loads(response['body'])
1.103 +
1.104 +
1.105 + ####################
1.106 + # RESOLUTIONS #
1.107 + ####################
1.108 + def getResolutionsForProject(self,projectId):
1.109 + url = "/api/projects/"+projectId+"/resolutions"
1.110 + conn = Connection(self.url)
1.111 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.112 + return json.loads(response['body'])
1.113 +
1.114 + def getIssueTypesForProject(self,projectId):
1.115 + url = "/api/projects/"+projectId+"/issue-types"
1.116 + conn = Connection(self.url)
1.117 + response = conn.request_get(url, args=data, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.118 + return json.loads(response['body'])
1.119 +
1.120 + ####################
1.121 + # AREAS #
1.122 + ####################
1.123 +
1.124 + def getAllAreasForProject(self,projectId):
1.125 + url = "/api/projects/"+projectId+"/areas"
1.126 + conn = Connection(self.url)
1.127 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.128 + return json.loads(response['body'])
1.129 +
1.130 +
1.131 + ####################
1.132 + # VERSIONS #
1.133 + ####################
1.134 +
1.135 + def getAllVersionsForProject(self,projectId):
1.136 + url = "/api/projects/"+projectId+"/versions"
1.137 + conn = Connection(self.url)
1.138 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.139 + return json.loads(response['body'])
1.140 +
1.141 + def getReleasedVersionsOnlyForProject(self,projectId):
1.142 + url = "/api/projects/"+projectId+"/versions?type=fixfor"
1.143 + conn = Connection(self.url)
1.144 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.145 + return json.loads(response['body'])
1.146 +
1.147 + ####################
1.148 + # COMMENTS #
1.149 + ####################
1.150 +
1.151 + def getAllCommentsForIssue(self,issueId):
1.152 + url = "/api/issues/"+issueId+"/comments"
1.153 + conn = Connection(self.url)
1.154 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.155 + return json.loads(response['body'])
1.156 +
1.157 + def addCommentToIssue(self,issueId,data):
1.158 + data = json.dumps(data)
1.159 + url = "/api/issues/"+issueId+"/comments"
1.160 + conn = Connection(self.url)
1.161 + response = conn.request_post(url, body=data,headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.162 + return json.loads(response['body'])
1.163 +
1.164 + def deleteComment(self,issueId,commentId):
1.165 + url = "/api/issues/"+issueId+"/comments/"+commentId
1.166 + conn = Connection(self.url)
1.167 + response = conn.request_delete(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.168 + return json.loads(response['body'])
1.169 +
1.170 + ####################
1.171 + # TIME LOGS #
1.172 + ####################
1.173 + def getAllTimelogsForIssue(self,issueId):
1.174 + url = "/api/issues/"+issueId+"/timelogs"
1.175 + conn = Connection(self.url)
1.176 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.177 + return json.loads(response['body'])
1.178 +
1.179 + def addTimelogToIssue(self,issueId,data):
1.180 + data = json.dumps(data)
1.181 + url = "/api/issues/"+issueId+"/timelogs"
1.182 + conn = Connection(self.url)
1.183 + response = conn.request_post(url, body=data, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.184 + return json.loads(response['body'])
1.185 +
1.186 + def deleteTimelog(self,issueId,timelogId):
1.187 + url = "/api/issues/"+issueId+"/timelogs/"+timelogId
1.188 + conn = Connection(self.url)
1.189 + response = conn.request_delete(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.190 + return json.loads(response['body'])
1.191 +
1.192 + ####################
1.193 + # TAGS #
1.194 + ####################
1.195 + def getAllTagsForIssue(self,issueId):
1.196 + url = "/api/issues/"+issueId+"/tags"
1.197 + conn = Connection(self.url)
1.198 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.199 + return json.loads(response['body'])
1.200 +
1.201 + def addTagToIssue(self,issueId,data):
1.202 + data = json.dumps(data)
1.203 + url = "/api/issues/"+issueId+"/tags"
1.204 + conn = Connection(self.url)
1.205 + response = conn.request_post(url, body=data, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.206 + return json.loads(response['body'])
1.207 +
1.208 + def deleteTag(self,issueId,tagId):
1.209 + url = "/api/issues/"+issueId+"/timelogs/"+timelogId
1.210 + conn = Connection(self.url)
1.211 + response = conn.request_delete(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.212 + return json.loads(response['body'])
1.213 +
1.214 + ####################
1.215 + # CHANGELOGS #
1.216 + ####################
1.217 + def getAllChangelogsForIssue(self,issueId):
1.218 + url = "/api/issues/"+issueId+"/changelogs"
1.219 + conn = Connection(self.url)
1.220 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.221 + return json.loads(response['body'])
1.222 +
1.223 + ####################
1.224 + # USERS #
1.225 + ####################
1.226 + def getAllUsers(self):
1.227 + url = "/users"
1.228 + conn = Connection(self.url)
1.229 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.230 + return json.loads(response['body'])
1.231 +
1.232 + def getUser(self,user):
1.233 + url = "/users/"+user
1.234 + conn = Connection(self.url)
1.235 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.236 + return json.loads(response['body'])
1.237 +
1.238 + def getUsersForProject(self,projectId):
1.239 + url = "/api/projects/"+projectId+"/users"
1.240 + conn = Connection(self.url)
1.241 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.242 + return json.loads(response['body'])
1.243 +
1.244 + def getClientsForProject(self,projectId):
1.245 + url = "/api/projects/"+projectId+"/clients"
1.246 + conn = Connection(self.url)
1.247 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.248 + return json.loads(response['body'])
1.249 +
1.250 + def getWatchersForIssue(self,issueId):
1.251 + url = "/api/issues/"+issueId+"/watchers"
1.252 + conn = Connection(self.url)
1.253 + response = conn.request_get(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.254 + return json.loads(response['body'])
1.255 +
1.256 + def addWatcherToIssue(self,issueId,userId):
1.257 + url = "/api/issues/"+issueId+"/watchers"+userId
1.258 + conn = Connection(self.url)
1.259 + response = conn.request_put(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.260 + return json.loads(response['body'])
1.261 +
1.262 + ####################
1.263 + # PRIORITIES #
1.264 + ####################
1.265 +
1.266 + def getAllPriorities(self):
1.267 + url = "/priorities"
1.268 + conn = Connection(self.url)
1.269 + response = conn.request_put(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.270 + return json.loads(response['body'])
1.271 +
1.272 + def getPriority(self,priorityId):
1.273 + url = "/priorities/"+priorityId
1.274 + conn = Connection(self.url)
1.275 + response = conn.request_put(url, headers={'Accept':self.type,'Content-Type':self.type,"Authorization":"Basic %s" % self.auth})
1.276 + return json.loads(response['body'])
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/httplib2/iri2uri.py Sun Nov 29 14:28:14 2009 +0000
2.3 @@ -0,0 +1,110 @@
2.4 +"""
2.5 +iri2uri
2.6 +
2.7 +Converts an IRI to a URI.
2.8 +
2.9 +"""
2.10 +__author__ = "Joe Gregorio (joe@bitworking.org)"
2.11 +__copyright__ = "Copyright 2006, Joe Gregorio"
2.12 +__contributors__ = []
2.13 +__version__ = "1.0.0"
2.14 +__license__ = "MIT"
2.15 +__history__ = """
2.16 +"""
2.17 +
2.18 +import urlparse
2.19 +
2.20 +
2.21 +# Convert an IRI to a URI following the rules in RFC 3987
2.22 +#
2.23 +# The characters we need to enocde and escape are defined in the spec:
2.24 +#
2.25 +# iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
2.26 +# ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
2.27 +# / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
2.28 +# / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
2.29 +# / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
2.30 +# / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
2.31 +# / %xD0000-DFFFD / %xE1000-EFFFD
2.32 +
2.33 +escape_range = [
2.34 + (0xA0, 0xD7FF ),
2.35 + (0xE000, 0xF8FF ),
2.36 + (0xF900, 0xFDCF ),
2.37 + (0xFDF0, 0xFFEF),
2.38 + (0x10000, 0x1FFFD ),
2.39 + (0x20000, 0x2FFFD ),
2.40 + (0x30000, 0x3FFFD),
2.41 + (0x40000, 0x4FFFD ),
2.42 + (0x50000, 0x5FFFD ),
2.43 + (0x60000, 0x6FFFD),
2.44 + (0x70000, 0x7FFFD ),
2.45 + (0x80000, 0x8FFFD ),
2.46 + (0x90000, 0x9FFFD),
2.47 + (0xA0000, 0xAFFFD ),
2.48 + (0xB0000, 0xBFFFD ),
2.49 + (0xC0000, 0xCFFFD),
2.50 + (0xD0000, 0xDFFFD ),
2.51 + (0xE1000, 0xEFFFD),
2.52 + (0xF0000, 0xFFFFD ),
2.53 + (0x100000, 0x10FFFD)
2.54 +]
2.55 +
2.56 +def encode(c):
2.57 + retval = c
2.58 + i = ord(c)
2.59 + for low, high in escape_range:
2.60 + if i < low:
2.61 + break
2.62 + if i >= low and i <= high:
2.63 + retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')])
2.64 + break
2.65 + return retval
2.66 +
2.67 +
2.68 +def iri2uri(uri):
2.69 + """Convert an IRI to a URI. Note that IRIs must be
2.70 + passed in a unicode strings. That is, do not utf-8 encode
2.71 + the IRI before passing it into the function."""
2.72 + if isinstance(uri ,unicode):
2.73 + (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
2.74 + authority = authority.encode('idna')
2.75 + # For each character in 'ucschar' or 'iprivate'
2.76 + # 1. encode as utf-8
2.77 + # 2. then %-encode each octet of that utf-8
2.78 + uri = urlparse.urlunsplit((scheme, authority, path, query, fragment))
2.79 + uri = "".join([encode(c) for c in uri])
2.80 + return uri
2.81 +
2.82 +if __name__ == "__main__":
2.83 + import unittest
2.84 +
2.85 + class Test(unittest.TestCase):
2.86 +
2.87 + def test_uris(self):
2.88 + """Test that URIs are invariant under the transformation."""
2.89 + invariant = [
2.90 + u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
2.91 + u"http://www.ietf.org/rfc/rfc2396.txt",
2.92 + u"ldap://[2001:db8::7]/c=GB?objectClass?one",
2.93 + u"mailto:John.Doe@example.com",
2.94 + u"news:comp.infosystems.www.servers.unix",
2.95 + u"tel:+1-816-555-1212",
2.96 + u"telnet://192.0.2.16:80/",
2.97 + u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ]
2.98 + for uri in invariant:
2.99 + self.assertEqual(uri, iri2uri(uri))
2.100 +
2.101 + def test_iri(self):
2.102 + """ Test that the right type of escaping is done for each part of the URI."""
2.103 + self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}"))
2.104 + self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}"))
2.105 + self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}"))
2.106 + self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}"))
2.107 + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))
2.108 + self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")))
2.109 + self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8')))
2.110 +
2.111 + unittest.main()
2.112 +
2.113 +
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/mimeTypes.py Sun Nov 29 14:28:14 2009 +0000
3.3 @@ -0,0 +1,57 @@
3.4 +"""
3.5 + Copyright (C) 2008 Benjamin O'Steen
3.6 +
3.7 + This file is part of python-fedoracommons.
3.8 +
3.9 + python-fedoracommons is free software: you can redistribute it and/or modify
3.10 + it under the terms of the GNU General Public License as published by
3.11 + the Free Software Foundation, either version 3 of the License, or
3.12 + (at your option) any later version.
3.13 +
3.14 + python-fedoracommons is distributed in the hope that it will be useful,
3.15 + but WITHOUT ANY WARRANTY; without even the implied warranty of
3.16 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3.17 + GNU General Public License for more details.
3.18 +
3.19 + You should have received a copy of the GNU General Public License
3.20 + along with python-fedoracommons. If not, see <http://www.gnu.org/licenses/>.
3.21 +"""
3.22 +
3.23 +__license__ = 'GPL http://www.gnu.org/licenses/gpl.txt'
3.24 +__author__ = "Benjamin O'Steen <bosteen@gmail.com>"
3.25 +__version__ = '0.1'
3.26 +
3.27 +class mimeTypes(object):
3.28 + def getDictionary(self):
3.29 + mimetype_to_extension = {}
3.30 + extension_to_mimetype = {}
3.31 + mimetype_to_extension['text/plain'] = 'txt'
3.32 + mimetype_to_extension['text/xml'] = 'xml'
3.33 + mimetype_to_extension['text/css'] = 'css'
3.34 + mimetype_to_extension['text/javascript'] = 'js'
3.35 + mimetype_to_extension['text/rtf'] = 'rtf'
3.36 + mimetype_to_extension['text/calendar'] = 'ics'
3.37 + mimetype_to_extension['application/msword'] = 'doc'
3.38 + mimetype_to_extension['application/msexcel'] = 'xls'
3.39 + mimetype_to_extension['application/x-msword'] = 'doc'
3.40 + mimetype_to_extension['application/vnd.ms-excel'] = 'xls'
3.41 + mimetype_to_extension['application/vnd.ms-powerpoint'] = 'ppt'
3.42 + mimetype_to_extension['application/pdf'] = 'pdf'
3.43 + mimetype_to_extension['text/comma-separated-values'] = 'csv'
3.44 +
3.45 +
3.46 + mimetype_to_extension['image/jpeg'] = 'jpg'
3.47 + mimetype_to_extension['image/gif'] = 'gif'
3.48 + mimetype_to_extension['image/jpg'] = 'jpg'
3.49 + mimetype_to_extension['image/tiff'] = 'tiff'
3.50 + mimetype_to_extension['image/png'] = 'png'
3.51 +
3.52 + # And hacky reverse lookups
3.53 + for mimetype in mimetype_to_extension:
3.54 + extension_to_mimetype[mimetype_to_extension[mimetype]] = mimetype
3.55 +
3.56 + mimetype_extension_mapping = {}
3.57 + mimetype_extension_mapping.update(mimetype_to_extension)
3.58 + mimetype_extension_mapping.update(extension_to_mimetype)
3.59 +
3.60 + return mimetype_extension_mapping
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/restful_lib.py Sun Nov 29 14:28:14 2009 +0000
4.3 @@ -0,0 +1,129 @@
4.4 +"""
4.5 + Copyright (C) 2008 Benjamin O'Steen
4.6 +
4.7 + This file is part of python-fedoracommons.
4.8 +
4.9 + python-fedoracommons is free software: you can redistribute it and/or modify
4.10 + it under the terms of the GNU General Public License as published by
4.11 + the Free Software Foundation, either version 3 of the License, or
4.12 + (at your option) any later version.
4.13 +
4.14 + python-fedoracommons is distributed in the hope that it will be useful,
4.15 + but WITHOUT ANY WARRANTY; without even the implied warranty of
4.16 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
4.17 + GNU General Public License for more details.
4.18 +
4.19 + You should have received a copy of the GNU General Public License
4.20 + along with python-fedoracommons. If not, see <http://www.gnu.org/licenses/>.
4.21 +"""
4.22 +
4.23 +__license__ = 'GPL http://www.gnu.org/licenses/gpl.txt'
4.24 +__author__ = "Benjamin O'Steen <bosteen@gmail.com>"
4.25 +__version__ = '0.1'
4.26 +
4.27 +import httplib2
4.28 +import urlparse
4.29 +import urllib
4.30 +import base64
4.31 +from base64 import encodestring
4.32 +
4.33 +from mimeTypes import *
4.34 +
4.35 +import mimetypes
4.36 +
4.37 +from cStringIO import StringIO
4.38 +
4.39 +class Connection:
4.40 + def __init__(self, base_url, username=None, password=None):
4.41 + self.base_url = base_url
4.42 + self.username = username
4.43 + m = mimeTypes()
4.44 + self.mimetypes = m.getDictionary()
4.45 +
4.46 + self.url = urlparse.urlparse(base_url)
4.47 +
4.48 + (scheme, netloc, path, query, fragment) = urlparse.urlsplit(base_url)
4.49 +
4.50 + self.scheme = scheme
4.51 + self.host = netloc
4.52 + self.path = path
4.53 +
4.54 + # Create Http class with support for Digest HTTP Authentication, if necessary
4.55 + self.h = httplib2.Http(".cache")
4.56 + self.h.follow_all_redirects = True
4.57 + if username and password:
4.58 + self.h.add_credentials(username, password)
4.59 +
4.60 + def request_get(self, resource, args = None, headers={}):
4.61 + return self.request(resource, "get", args, headers=headers)
4.62 +
4.63 + def request_delete(self, resource, args = None, headers={}):
4.64 + return self.request(resource, "delete", args, headers=headers)
4.65 +
4.66 + def request_head(self, resource, args = None, headers={}):
4.67 + return self.request(resource, "head", args, headers=headers)
4.68 +
4.69 + def request_post(self, resource, args = None, body = None, filename=None, headers={}):
4.70 + return self.request(resource, "post", args , body = body, filename=filename, headers=headers)
4.71 +
4.72 + def request_put(self, resource, args = None, body = None, filename=None, headers={}):
4.73 + return self.request(resource, "put", args , body = body, filename=filename, headers=headers)
4.74 +
4.75 + def get_content_type(self, filename):
4.76 + extension = filename.split('.')[-1]
4.77 + guessed_mimetype = self.mimetypes.get(extension, mimetypes.guess_type(filename)[0])
4.78 + return guessed_mimetype or 'application/octet-stream'
4.79 +
4.80 + def request(self, resource, method = "get", args = None, body = None, filename=None, headers={}):
4.81 + params = None
4.82 + path = resource
4.83 + headers['User-Agent'] = 'Basic Agent'
4.84 +
4.85 + BOUNDARY = u'00hoYUXOnLD5RQ8SKGYVgLLt64jejnMwtO7q8XE1'
4.86 + CRLF = u'\r\n'
4.87 +
4.88 + if filename and body:
4.89 + #fn = open(filename ,'r')
4.90 + #chunks = fn.read()
4.91 + #fn.close()
4.92 +
4.93 + # Attempt to find the Mimetype
4.94 + content_type = self.get_content_type(filename)
4.95 + headers['Content-Type']='multipart/form-data; boundary='+BOUNDARY
4.96 + encode_string = StringIO()
4.97 + encode_string.write(CRLF)
4.98 + encode_string.write(u'--' + BOUNDARY + CRLF)
4.99 + encode_string.write(u'Content-Disposition: form-data; name="file"; filename="%s"' % filename)
4.100 + encode_string.write(CRLF)
4.101 + encode_string.write(u'Content-Type: %s' % content_type + CRLF)
4.102 + encode_string.write(CRLF)
4.103 + encode_string.write(body)
4.104 + encode_string.write(CRLF)
4.105 + encode_string.write(u'--' + BOUNDARY + u'--' + CRLF)
4.106 +
4.107 + body = encode_string.getvalue()
4.108 + headers['Content-Length'] = str(len(body))
4.109 + elif body:
4.110 + if not headers.get('Content-Type', None):
4.111 + headers['Content-Type']='text/xml'
4.112 + headers['Content-Length'] = str(len(body))
4.113 + else:
4.114 + headers['Content-Type']='text/xml'
4.115 +
4.116 + if args:
4.117 + path += u"?" + urllib.urlencode(args)
4.118 +
4.119 + request_path = []
4.120 + if self.path != "/":
4.121 + if self.path.endswith('/'):
4.122 + request_path.append(self.path[:-1])
4.123 + else:
4.124 + request_path.append(self.path)
4.125 + if path.startswith('/'):
4.126 + request_path.append(path[1:])
4.127 + else:
4.128 + request_path.append(path)
4.129 +
4.130 + resp, content = self.h.request(u"%s://%s%s" % (self.scheme, self.host, u'/'.join(request_path)), method.upper(), body=body, headers=headers )
4.131 +
4.132 + return {u'headers':resp, u'body':content.decode('UTF-8')}
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
5.2 +++ b/talis.py Sun Nov 29 14:28:14 2009 +0000
5.3 @@ -0,0 +1,262 @@
5.4 +import urllib2
5.5 +
5.6 +from urllib2 import urlparse
5.7 +
5.8 +from restful_lib import Connection
5.9 +
5.10 +from gae_restful_lib import GAE_Connection
5.11 +
5.12 +from datetime import datetime
5.13 +
5.14 +from StringIO import StringIO
5.15 +
5.16 +from xml.etree import ElementTree as ET
5.17 +
5.18 +SPARQL_ENDPOINT = "/services/sparql"
5.19 +META_ENDPOINT = "/meta"
5.20 +CONTENT_ENDPOINT = "/items"
5.21 +JOB_REQUESTS = "/jobs"
5.22 +SNAPSHOTS = "/snapshots"
5.23 +SNAPSHOT_TEMPLATE = "/snapshots/%s"
5.24 +
5.25 +RESET_STORE_TEMPLATE = u"""<rdf:RDF
5.26 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
5.27 + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
5.28 + xmlns:bf="http://schemas.talis.com/2006/bigfoot/configuration#" >
5.29 + <bf:JobRequest>
5.30 + <rdfs:label>%s</rdfs:label>
5.31 + <bf:jobType rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#ResetDataJob"/>
5.32 + <bf:startTime>%sZ</bf:startTime>
5.33 + </bf:JobRequest>
5.34 + </rdf:RDF>"""
5.35 +
5.36 +SNAPSHOT_STORE_TEMPLATE = """<rdf:RDF
5.37 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
5.38 + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
5.39 + xmlns:bf="http://schemas.talis.com/2006/bigfoot/configuration#" >
5.40 + <bf:JobRequest>
5.41 + <rdfs:label>%s</rdfs:label>
5.42 + <bf:jobType rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#SnapshotJob"/>
5.43 + <bf:startTime>%sZ</bf:startTime>
5.44 + </bf:JobRequest>
5.45 + </rdf:RDF>"""
5.46 +
5.47 +SNAPSHOT_RESTORE_TEMPLATE = """<rdf:RDF
5.48 + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
5.49 + xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
5.50 + xmlns:bf="http://schemas.talis.com/2006/bigfoot/configuration#" >
5.51 + <bf:JobRequest>
5.52 + <rdfs:label>%s</rdfs:label>
5.53 + <bf:jobType rdf:resource="http://schemas.talis.com/2006/bigfoot/configuration#RestoreJob"/>
5.54 + <bf:snapshotUri rdf:resource="%s" />
5.55 + <bf:startTime>%sZ</bf:startTime>
5.56 + </bf:JobRequest>
5.57 + </rdf:RDF>"""
5.58 +
5.59 +class RDFFormatException(Exception):
5.60 + def __init__(self, value):
5.61 + self.value = value
5.62 + def __str__(self):
5.63 + return repr(self.value)
5.64 +
5.65 +class Store():
5.66 + def __init__(self, base_store_url, username=None, password=None):
5.67 + """ Base URL for the store should be pretty self-explanatory. E.g. something like
5.68 + "http://api.talis.com/stores/store_name"
5.69 + Only needs to enter the username/password if this class is going to tinker
5.70 + with things."""
5.71 + if base_store_url.endswith('/'):
5.72 + base_store_url = base_store_url[:-1]
5.73 +
5.74 + self.base_store_url = base_store_url
5.75 + # Split the given URL
5.76 + if base_store_url:
5.77 + self.conn = Connection(base_store_url, username=username, password=password)
5.78 +
5.79 + def does_snapshot_exist(self, snapshot_filename):
5.80 + # Test to see if snapshot exists:
5.81 + snapshot_path = SNAPSHOT_TEMPLATE % snapshot_filename
5.82 +
5.83 + response = self.conn.request(snapshot_path, method = "HEAD")
5.84 +
5.85 + if response.get('headers') and response.get('headers').get('status'):
5.86 + status = response.get('headers').get('status')
5.87 +
5.88 + if status in ['200', '204']:
5.89 + return True
5.90 + elif status.startswith('4'):
5.91 + return False
5.92 + # else: raise Error?
5.93 +
5.94 + return False
5.95 +
5.96 + def schedule_reset_data(self, label, at_time=None):
5.97 + """Will request that the store is emptied, and label the request.
5.98 + If a time is given as an ISO8601 formatted string, this will be
5.99 + the scheduled time for the snapshot. Otherwise, it will use the current time."""
5.100 + if not at_time:
5.101 + at_time=datetime.utcnow().isoformat().split('.')[0]
5.102 +
5.103 + snapshot_request = RESET_STORE_TEMPLATE % (label, at_time)
5.104 +
5.105 + return self.conn.request_post(JOB_REQUESTS, body = snapshot_request, headers={'Content-Type':'application/rdf+xml'})
5.106 +
5.107 + def schedule_snapshot_data(self, label, at_time=None):
5.108 + """Will request a snapshot be made of the store.
5.109 + If a time is given as an ISO8601 formatted string, this will be
5.110 + the scheduled time for the snapshot. Otherwise, it will use the current time."""
5.111 + if not at_time:
5.112 + at_time=datetime.utcnow().isoformat().split('.')[0]
5.113 +
5.114 + snapshot_request = SNAPSHOT_STORE_TEMPLATE % (label, at_time)
5.115 +
5.116 + return self.conn.request_post(JOB_REQUESTS, body = snapshot_request, headers={'Content-Type':'application/rdf+xml'})
5.117 +
5.118 + def schedule_snapshot_restore(self, label, snapshot_filename, at_time=None):
5.119 + """Will request that the store is restored from a snapshot. If a time is given as
5.120 + an ISO8601 formatted string, this will be the scheduled time for
5.121 + the recovery. Otherwise, it will use the current time."""
5.122 + if not at_time:
5.123 + at_time=datetime.utcnow().isoformat().split('.')[0]
5.124 +
5.125 + # Test to see if snapshot exists:
5.126 + snapshot_path = SNAPSHOT_TEMPLATE % snapshot_filename
5.127 +
5.128 + if self.does_snapshot_exist(snapshot_filename):
5.129 + snapshot_uri = "%s%s" % (self.base_store_url, snapshot_path)
5.130 + snapshot_request = SNAPSHOT_RESTORE_TEMPLATE % (label, snapshot_uri, at_time)
5.131 + return self.conn.request_post(JOB_REQUESTS, body = snapshot_request, headers={'Content-Type':'application/rdf+xml'})
5.132 +
5.133 + def submit_rdfxml(self, rdf_text):
5.134 + """Puts the given RDF/XML into the Talis Store"""
5.135 + return self._put_rdf(rdf_text, mimetype="application/rdf+xml")
5.136 +
5.137 + def _put_rdf(self, rdf_text, mimetype="application/rdf+xml"):
5.138 + """Placeholder for allowing other serialisation types to be put into a
5.139 + Talis store, whether the conversion takes place here, or if the Talis
5.140 + store starts to accept other formats."""
5.141 + if rdf_text:
5.142 + request_headers = {}
5.143 + if mimetype not in ['application/rdf+xml']:
5.144 + raise RDFFormatException("%s is not an allowed RDF serialisation format" % mimetype)
5.145 + request_headers['Content-Type'] = mimetype
5.146 + return self.conn.request_post(META_ENDPOINT, body=rdf_text, headers=request_headers)
5.147 +
5.148 + def _query_sparql_service(self, query, args={}):
5.149 + """Low-level SPARQL query - returns the message and response headers from the server.
5.150 + You may be looking for Store.sparql instead of this."""
5.151 + passed_args = {'query':query}
5.152 + passed_args.update(args)
5.153 + return self.conn.request_get(SPARQL_ENDPOINT, args=passed_args, headers={'Content-type':'application/x-www-form-urlencoded'})
5.154 +
5.155 + def _query_search_service(self, query, args={}):
5.156 + """Low-level content box query - returns the message and response headers from the server.
5.157 + You may be looking for Store.search instead of this."""
5.158 +
5.159 + passed_args = {'query':query}
5.160 + passed_args.update(args)
5.161 +
5.162 + return self.conn.request_get(CONTENT_ENDPOINT, args=passed_args, headers={'Content-type':'application/x-www-form-urlencoded'} )
5.163 +
5.164 + def _list_snapshots(self, passed_args={}):
5.165 + return self.conn.request_get(SNAPSHOTS, args=passed_args, headers={})
5.166 +
5.167 +##############################################################################
5.168 +# Convenience Functions
5.169 +##############################################################################
5.170 +
5.171 + def submit_rdfxml_from_url(self, url_to_file, headers={"Accept":"application/rdf+xml"}):
5.172 + """Convenience method - downloads the file from a given url, and then pushes that
5.173 + into the meta store. Currently, it doesn't put it through a parse-> reserialise
5.174 + step, so that it could handle more than rdf/xml on the way it but it is a
5.175 + future possibility."""
5.176 + import_rdf_connection = Connection(url_to_file)
5.177 + response = import_rdf_connection.request_get("", headers=headers)
5.178 +
5.179 + if response.get('headers') and response.get('headers').get('status') in ['200', '204']:
5.180 + request_headers = {}
5.181 +
5.182 + # Lowercase all response header fields, to make matching easier.
5.183 + # According to HTTP spec, they should be case-insensitive
5.184 + response_headers = response['headers']
5.185 + for header in response_headers:
5.186 + response_headers[header.lower()] = response_headers[header]
5.187 +
5.188 + # Set the body content
5.189 + body = response.get('body').encode('UTF-8')
5.190 +
5.191 + # Get the response mimetype
5.192 + rdf_type = response_headers.get('content-type', None)
5.193 +
5.194 + return self._put_rdf(body, mimetype=rdf_type)
5.195 +
5.196 + def sparql(self, query, args={}):
5.197 + """Performs a SPARQL query and simply returns the body of the response if successful
5.198 + - if there is an issue, such as a code 404 or 500, this method will return False.
5.199 +
5.200 + Use the _query_sparql_service method to get hold of
5.201 + the complete response in this case."""
5.202 + response = self._query_sparql_service(query, args)
5.203 + headers = response.get('headers')
5.204 +
5.205 + status = headers.get('status', headers.get('Status'))
5.206 +
5.207 + if status in ['200', 200, '204', 204]:
5.208 + return response.get('body').encode('UTF-8')
5.209 + else:
5.210 + return False
5.211 +
5.212 + def search(self, query, args={}):
5.213 + """Performs a search query and simply returns the body of the response if successful
5.214 + - if there is an issue, such as a code 404 or 500, this method will return False.
5.215 +
5.216 + Use the _query_search_service method to get hold of
5.217 + the complete response in this case."""
5.218 + response = self._query_search_service(query, args)
5.219 + headers = response.get('headers')
5.220 +
5.221 + status = headers.get('status', headers.get('Status'))
5.222 +
5.223 + if status in ['200', 200, '204', 204]:
5.224 + parsed_atom = Atom_Search_Results(response.get('body').encode('UTF-8'))
5.225 + return parsed_atom.get_item_list()
5.226 + else:
5.227 + return False
5.228 +
5.229 +class Item():
5.230 + def __init__(self):
5.231 + self.title = None
5.232 + self.link = None
5.233 +
5.234 +class Atom_Search_Results():
5.235 + def __init__(self, atom_text):
5.236 + self.load_atom_search(atom_text)
5.237 +
5.238 + def load_atom_search(self, atom_text):
5.239 + self.atom = ET.fromstring(atom_text)
5.240 +
5.241 + def get_item_list(self):
5.242 + if self.atom:
5.243 + items = []
5.244 + for item in self.atom.findall('{http://purl.org/rss/1.0/}item'):
5.245 + item_fields = Item()
5.246 + item_fields.title = item.find('{http://purl.org/rss/1.0/}title').text
5.247 + item_fields.link = item.find('{http://purl.org/rss/1.0/}link').text
5.248 + items.append(item_fields)
5.249 +
5.250 + return items
5.251 +
5.252 +class GAE_Store(Store):
5.253 + def __init__(self, base_store_url, username=None, password=None):
5.254 + """ Base URL for the store should be pretty self-explanatory. E.g. something like
5.255 + "http://api.talis.com/stores/store_name"
5.256 + The username and password will not do anything, until the Google app engine's
5.257 + fetch library handles authentication, if ever."""
5.258 + if base_store_url.endswith('/'):
5.259 + base_store_url = base_store_url[:-1]
5.260 +
5.261 + self.base_store_url = base_store_url
5.262 + # Split the given URL
5.263 + if base_store_url:
5.264 + self.conn = GAE_Connection(base_store_url, username, password)
5.265 +