├── LICENSE ├── README.md └── pybase.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 Intuit Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Quickbase-Python-SDK 2 | =================== 3 | 4 | Python bindings for the QuickBase API 5 | 6 | QBConn variables: 7 | 8 | error: the numerical error code returned by an API call. 0 is no error, negative values are internal to this library 9 | tables: a dictionary containing tablename:tableID pairs 10 | 11 | [constructor] QBConn(QB_URL,QB_APPID[,QB_TOKEN, QB_REALM]): 12 | 13 | Makes a connection to the QuickBase specified by QB_URL and QB_APPID. Uses QB_TOKEN and QB_REALM if specified. 14 | Note: QB_URL should have a trailing slash. ex. "https://intuitcorp.quickbase.com/db/"; 15 | 16 | authenticate(username,password): 17 | 18 | Authenticates username and password with QuickBase and stores the returned ticket. The tables variable is populated on success 19 | 20 | sql(querystr): 21 | Performs a query() after translating a simple SQL-style string to QuickBase's query format 22 | 23 | Example: qb.sql("SELECT * FROM users WHERE name`EX`John\_Doe OR role`EX`fakeperson") #The \_ represents a space. This is a very basic function that doesn't use state machines. Note: field and table names will not have spaces 24 | Example: qb.sql("SELECT firstname|lastname FROM users WHERE paid`EX`true ORDER BY lastname ASC LIMIT 100") 25 | Example: qb.sql("DELETE FROM assets WHERE value`BF`0") 26 | Please contribute any improvents you make on this function back to this repo. It would make life so much easier for all QuickBase+Python users :) 27 | 28 | request(params,url_ext): 29 | 30 | Takes a dict of param:value pairs, adds ticket, token, and realm (if specified) and makes an API call to the base URL+url_extension 31 | 32 | getFields(tableID): 33 | 34 | Returns a dict containing the fields of a table as fieldname:fieldID pairs 35 | 36 | addRecord(tableID,data): 37 | 38 | Adds a record with data specified by the data dict of fieldname:value pairs to tableID 39 | 40 | editRecord(tableID,rid,newdata[,options]): 41 | 42 | Updates a record (rid) in table (tableID) with the data given by newdata fieldname:value pairs 43 | 44 | deleteRecord(tableID,rid): 45 | 46 | Deletes record specified by rid from table specified by tableID 47 | 48 | purgeRecords(tableID,query): 49 | 50 | Deletes records from tableID that match the QuickBase-style query 51 | 52 | _getTables(): 53 | 54 | Returns a dict containing a QuickBase app's tables as tablename:tableID pairs. This is run automatically after a successful authenticate call 55 | 56 | query(tableID,query): 57 | 58 | Returns a list of dicts containing fieldname:value pairs that represent rows returned by the query. record ID will always be specified by the "rid" key 59 | -------------------------------------------------------------------------------- /pybase.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | #import urllib.request, urllib.parse, urllib.error #Use this for Python > 3 4 | import urllib #Use this line instead of the previous for Python < 3.0 5 | import xml.etree.ElementTree as elementree 6 | import re 7 | import string 8 | 9 | class QBConn: 10 | def __init__(self,url,appid,token=None, user_token=None,realm=""): 11 | 12 | self.url = url 13 | self.token = token 14 | self.user_token = user_token 15 | self.appid = appid 16 | self.ticket = None 17 | self.realm = realm #This allows one QuickBase realm to proxy for another 18 | self.error = 0 #Set after every API call. A non-zero value indicates an error. A negative value indicates an error with this library 19 | self.tables = {} 20 | 21 | def authenticate(self,username=None,password=None): 22 | 23 | if self.user_token: 24 | self.tables = self._getTables() 25 | return 26 | 27 | 28 | params = {'act':'API_Authenticate','username':username,'password':password} 29 | resp = self.request(params,'main') 30 | if self.error != 0: 31 | return 32 | else: 33 | self.ticket = resp.find("ticket").text 34 | self.tables = self._getTables() 35 | 36 | #Adds the appropriate fields to the request and sends it to QB 37 | #Takes a dict of parameter:value pairs and the url extension (main or your table ID, mostly) 38 | def request(self,params,url_ext): 39 | url = self.url 40 | url += url_ext 41 | 42 | if self.user_token: 43 | params['usertoken'] = self.user_token 44 | else: 45 | params['ticket'] = self.ticket 46 | 47 | 48 | params['apptoken'] = self.token 49 | params['realmhost'] = self.realm 50 | #urlparams = urllib.parse.urlencode(params) #Use this line for Python > 3 51 | urlparams = urllib.urlencode(params) #use this line for < Python 3 52 | #resp = urllib.request.FancyURLopener().open(url+"?"+urlparams).read() #Use this line for Python > 3 53 | resp = urllib.FancyURLopener().open(url+"?"+urlparams).read() #use this line for < Python 3 54 | if re.match('^\<\?xml version=',resp.decode("utf-8")) == None: 55 | print("No useful data received") 56 | self.error = -1 #No XML data returned 57 | else: 58 | tree = elementree.fromstring(resp) 59 | self.error = int(tree.find('errcode').text) 60 | return tree 61 | 62 | #Creates a record with the given data in the table specified by tableID 63 | #Takes a tableID (you can get this using qb.tables["yourtable"]) 64 | #Also takes a dict containing field name:field value pairs 65 | def addRecord(self,tableID,data): 66 | fields = self.getFields(tableID) 67 | params = {'act':'API_AddRecord'} 68 | for field in data: 69 | if field in fields: 70 | params["_fid_"+fields[field]] = data[field] 71 | return self.request(params,tableID) 72 | 73 | #Updates a reord with the given data 74 | #Takes the record's table ID, record ID, a dict containing field:newvalue pairs, and an optional dict with param:value pairs 75 | def editRecord(self,tableID,rid,newdata,options={}): 76 | params = {'act':'API_EditRecord','rid':rid} 77 | fields = self.getFields(tableID) 78 | for key,value in list(newdata.items()): 79 | if key.isdigit(): 80 | params["_fid_"+key] = value 81 | else: 82 | if key in fields: 83 | params["_fid_"+fields[key]] = value 84 | params = dict(params,**options) 85 | return self.request(params,tableID) 86 | 87 | #Deletes the record specified by rid from the table given by tableID 88 | def deleteRecord(self,tableID,rid): 89 | params = {'act':'API_DeleteRecord','rid':rid} 90 | return self.request(params,tableID) 91 | 92 | #Deletes every record from tableID selected by query 93 | def purgeRecords(self,tableID,query): 94 | params = {'act':'API_PurgeRecords','query':query} 95 | return self.request(params,tableID) 96 | 97 | #Returns a dict containing fieldname:fieldid pairs 98 | #Field names will have spaces replaced with not spaces 99 | def getFields(self,tableID): 100 | params = {'act':'API_GetSchema'} 101 | schema = self.request(params,tableID) 102 | fields = schema.find('table').find('fields') 103 | fieldlist = {} 104 | for field in fields: 105 | label = field.find('label').text.lower().replace(' ','') 106 | fieldlist[label] = field.attrib['id'] 107 | return fieldlist 108 | 109 | #Returns a dict of tablename:tableID pairs 110 | #This is called automatically after successful authentication 111 | def _getTables(self): 112 | if self.appid == None: 113 | return {} 114 | params = {'act':'API_GetSchema'} 115 | schema = self.request(params,self.appid) 116 | chdbs = schema.find('table').find('chdbids') 117 | tables = {} 118 | for chdb in chdbs: 119 | tables[chdb.attrib['name'][6:]] = chdb.text 120 | return tables 121 | 122 | #Executes a query on tableID 123 | #Returns a list of dicts containing fieldname:value pairs. record ID will always be specified by the "rid" key 124 | def query(self,tableID,query): 125 | params = dict(query) 126 | params['act'] = "API_DoQuery" 127 | params['includeRids'] = '1' 128 | params['fmt'] = "structured" 129 | records = self.request(params,tableID).find('table').find('records') 130 | data = [] 131 | fields = {fid:name for name,fid in list(self.getFields(tableID).items())} 132 | for record in records: 133 | temp = {} 134 | temp['rid'] = record.attrib['rid'] 135 | for field in record: 136 | if(field.tag == "f"): 137 | temp[fields[field.attrib['id']]] = field.text 138 | data.append(temp) 139 | return data 140 | 141 | #Emulates the syntax of basic (SELECT,DELETE) SQL queries 142 | #Example: qb.sql("SELECT * FROM users WHERE name`EX`John\_Doe OR role`EX`fakeperson") #The \_ represents a space. This is a very basic function that doesn't use state machines. Note: field and table names will not have spaces 143 | #Example: qb.sql("SELECT firstname|lastname FROM users WHERE paid`EX`true ORDER BY lastname ASC LIMIT 100") 144 | #Example: qb.sql("DELETE FROM assets WHERE value`BF`0") 145 | #I encourage you to modify this to suit your needs. Please contribute this back to the Python-QuickBase-SDK repository. Give QuickBase the API it deserves... 146 | def sql(self,querystr): 147 | tokens = querystr.split(" ") 148 | if tokens[0] == "SELECT": 149 | query = {} 150 | tid = self.tables[tokens[3]] 151 | tfields = self.getFields(tid) 152 | if tokens[1] != "*": 153 | clist = "" 154 | for field in tokens[1].split("|"): 155 | clist += tfields[field]+"." 156 | query['clist'] = clist[:len(clist)-1] 157 | if len(tokens) > 4: 158 | try: 159 | where = tokens.index("WHERE") 160 | querystr = "" 161 | for i in range(where+1,len(tokens)): 162 | if (i-where+1)%2 == 0: 163 | filt = tokens[i].split("`") 164 | querystr += "{'"+tfields[filt[0]]+"'."+filt[1]+".'"+filt[2].replace("\_"," ")+"'}" 165 | elif tokens[i] == "AND" or tokens[i] == "OR": 166 | querystr += tokens[i] 167 | else: 168 | break 169 | query['query'] = querystr 170 | except ValueError: 171 | pass 172 | except: 173 | print("SQL error near WHERE") 174 | self.error = -2 175 | return 176 | 177 | try: 178 | orderby = tokens.index("ORDER")+1 179 | orderings = tokens[orderby+1].split("|") 180 | slist = "" 181 | for ordering in orderings: 182 | slist += tfields[ordering]+"." 183 | query['slist'] = slist[:len(slist)-1] 184 | query['options'] = (query['options']+"." if 'options' in query else "")+"sortorder-"+("A" if tokens[orderby+2] == "ASC" else "D") 185 | except ValueError: 186 | pass 187 | except: 188 | print("SQL error near ORDER") 189 | self.error = -2 190 | return 191 | 192 | try: 193 | limit = tokens[tokens.index("LIMIT")+1] 194 | limit = limit.split(",") 195 | if(len(limit) > 1): 196 | query['options'] = (query['options']+"." if 'options' in query else "")+"skp-"+limit[0]+".num-"+limit[1] 197 | else: 198 | query['options'] = (query['options']+"." if 'options' in query else "")+"num-"+limit[0] 199 | except ValueError: 200 | pass 201 | except: 202 | print("SQL error near LIMIT") 203 | self.error = -2 204 | return 205 | 206 | return self.query(tid,query) 207 | 208 | elif tokens[0] == "DELETE": 209 | tid = self.tables[tokens[2]] 210 | tfields = self.getFields(tid) 211 | where = 3 212 | querystr = "" 213 | for i in range(where+1,len(tokens)): 214 | if (i-where+1)%2 == 0: 215 | filt = tokens[i].split("`") 216 | querystr += "{'"+tfields[filt[0]]+"'."+filt[1]+".'"+filt[2]+"'}" 217 | elif tokens[i] == "AND" or tokens[i] == "OR": 218 | querystr += tokens[i] 219 | else: 220 | break 221 | return self.purgeRecords(tid,querystr) 222 | 223 | 224 | 225 | --------------------------------------------------------------------------------