317 lines
15 KiB
Python
317 lines
15 KiB
Python
|
from .regex import REGEX_SEARCH_PATTERN, REGEX_SQUARE_BRACKETS
|
||
|
from .._compat import long
|
||
|
|
||
|
def to_num(num):
|
||
|
result = 0
|
||
|
try:
|
||
|
result = long(num)
|
||
|
except NameError as e:
|
||
|
result = int(num)
|
||
|
return result
|
||
|
|
||
|
class RestParser(object):
|
||
|
def __init__(self, db):
|
||
|
self.db = db
|
||
|
|
||
|
def auto_table(self, table, base='', depth=0):
|
||
|
patterns = []
|
||
|
for field in self.db[table].fields:
|
||
|
if base:
|
||
|
tag = '%s/%s' % (base, field.replace('_', '-'))
|
||
|
else:
|
||
|
tag = '/%s/%s' % (
|
||
|
table.replace('_', '-'), field.replace('_', '-'))
|
||
|
f = self.db[table][field]
|
||
|
if not f.readable:
|
||
|
continue
|
||
|
if f.type == 'id' or 'slug' in field or \
|
||
|
f.type.startswith('reference'):
|
||
|
tag += '/{%s.%s}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
elif f.type.startswith('boolean'):
|
||
|
tag += '/{%s.%s}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
elif f.type in ('float', 'double', 'integer', 'bigint'):
|
||
|
tag += '/{%s.%s.ge}/{%s.%s.lt}' % (table, field, table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
elif f.type.startswith('list:'):
|
||
|
tag += '/{%s.%s.contains}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
elif f.type in ('date', 'datetime'):
|
||
|
tag += '/{%s.%s.year}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
tag += '/{%s.%s.month}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
tag += '/{%s.%s.day}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
if f.type in ('datetime', 'time'):
|
||
|
tag += '/{%s.%s.hour}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
tag += '/{%s.%s.minute}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
tag += '/{%s.%s.second}' % (table, field)
|
||
|
patterns.append(tag)
|
||
|
patterns.append(tag+'/:field')
|
||
|
if depth > 0:
|
||
|
for f in self.db[table]._referenced_by:
|
||
|
tag += '/%s[%s.%s]' % (table, f.tablename, f.name)
|
||
|
patterns.append(tag)
|
||
|
patterns += self.auto_table(table, base=tag, depth=depth-1)
|
||
|
return patterns
|
||
|
|
||
|
def parse(self, patterns, args, vars, queries=None, nested_select=True):
|
||
|
"""
|
||
|
Example:
|
||
|
Use as::
|
||
|
|
||
|
db.define_table('person',Field('name'),Field('info'))
|
||
|
db.define_table('pet',
|
||
|
Field('ownedby',db.person),
|
||
|
Field('name'),Field('info')
|
||
|
)
|
||
|
|
||
|
@request.restful()
|
||
|
def index():
|
||
|
def GET(*args,**vars):
|
||
|
patterns = [
|
||
|
"/friends[person]",
|
||
|
"/{person.name}/:field",
|
||
|
"/{person.name}/pets[pet.ownedby]",
|
||
|
"/{person.name}/pets[pet.ownedby]/{pet.name}",
|
||
|
"/{person.name}/pets[pet.ownedby]/{pet.name}/:field",
|
||
|
("/dogs[pet]", db.pet.info=='dog'),
|
||
|
("/dogs[pet]/{pet.name.startswith}", db.pet.info=='dog'),
|
||
|
]
|
||
|
parser = db.parse_as_rest(patterns,args,vars)
|
||
|
if parser.status == 200:
|
||
|
return dict(content=parser.response)
|
||
|
else:
|
||
|
raise HTTP(parser.status,parser.error)
|
||
|
|
||
|
def POST(table_name,**vars):
|
||
|
if table_name == 'person':
|
||
|
return db.person.validate_and_insert(**vars)
|
||
|
elif table_name == 'pet':
|
||
|
return db.pet.validate_and_insert(**vars)
|
||
|
else:
|
||
|
raise HTTP(400)
|
||
|
return locals()
|
||
|
"""
|
||
|
|
||
|
re1 = REGEX_SEARCH_PATTERN
|
||
|
re2 = REGEX_SQUARE_BRACKETS
|
||
|
|
||
|
if patterns == 'auto':
|
||
|
patterns = []
|
||
|
for table in self.db.tables:
|
||
|
if not table.startswith('auth_'):
|
||
|
patterns.append('/%s[%s]' % (table, table))
|
||
|
patterns += self.auto_table(table, base='', depth=1)
|
||
|
else:
|
||
|
i = 0
|
||
|
while i < len(patterns):
|
||
|
pattern = patterns[i]
|
||
|
if not isinstance(pattern, str):
|
||
|
pattern = pattern[0]
|
||
|
tokens = pattern.split('/')
|
||
|
if tokens[-1].startswith(':auto') and re2.match(tokens[-1]):
|
||
|
new_patterns = self.auto_table(
|
||
|
tokens[-1][tokens[-1].find('[')+1:-1],
|
||
|
'/'.join(tokens[:-1]))
|
||
|
patterns = patterns[:i]+new_patterns+patterns[i+1:]
|
||
|
i += len(new_patterns)
|
||
|
else:
|
||
|
i += 1
|
||
|
if '/'.join(args) == 'patterns':
|
||
|
return self.db.Row({
|
||
|
'status': 200, 'pattern': 'list', 'error': None,
|
||
|
'response': patterns})
|
||
|
for pattern in patterns:
|
||
|
basequery, exposedfields = None, []
|
||
|
if isinstance(pattern, tuple):
|
||
|
if len(pattern) == 2:
|
||
|
pattern, basequery = pattern
|
||
|
elif len(pattern) > 2:
|
||
|
pattern, basequery, exposedfields = pattern[0:3]
|
||
|
otable = table = None
|
||
|
if not isinstance(queries, dict):
|
||
|
dbset = self.db(queries)
|
||
|
if basequery is not None:
|
||
|
dbset = dbset(basequery)
|
||
|
i = 0
|
||
|
tags = pattern[1:].split('/')
|
||
|
if len(tags) != len(args):
|
||
|
continue
|
||
|
for tag in tags:
|
||
|
if re1.match(tag):
|
||
|
tokens = tag[1:-1].split('.')
|
||
|
table, field = tokens[0], tokens[1]
|
||
|
if not otable or table == otable:
|
||
|
if len(tokens) == 2 or tokens[2] == 'eq':
|
||
|
query = self.db[table][field] == args[i]
|
||
|
elif tokens[2] == 'ne':
|
||
|
query = self.db[table][field] != args[i]
|
||
|
elif tokens[2] == 'lt':
|
||
|
query = self.db[table][field] < args[i]
|
||
|
elif tokens[2] == 'gt':
|
||
|
query = self.db[table][field] > args[i]
|
||
|
elif tokens[2] == 'ge':
|
||
|
query = self.db[table][field] >= args[i]
|
||
|
elif tokens[2] == 'le':
|
||
|
query = self.db[table][field] <= args[i]
|
||
|
elif tokens[2] == 'year':
|
||
|
query = self.db[table][field].year() == args[i]
|
||
|
elif tokens[2] == 'month':
|
||
|
query = self.db[table][field].month() == args[i]
|
||
|
elif tokens[2] == 'day':
|
||
|
query = self.db[table][field].day() == args[i]
|
||
|
elif tokens[2] == 'hour':
|
||
|
query = self.db[table][field].hour() == args[i]
|
||
|
elif tokens[2] == 'minute':
|
||
|
query = self.db[table][field].minutes() == args[i]
|
||
|
elif tokens[2] == 'second':
|
||
|
query = self.db[table][field].seconds() == args[i]
|
||
|
elif tokens[2] == 'startswith':
|
||
|
query = self.db[table][field].startswith(args[i])
|
||
|
elif tokens[2] == 'contains':
|
||
|
query = self.db[table][field].contains(args[i])
|
||
|
else:
|
||
|
raise RuntimeError("invalid pattern: %s" % pattern)
|
||
|
if len(tokens) == 4 and tokens[3] == 'not':
|
||
|
query = ~query
|
||
|
elif len(tokens) >= 4:
|
||
|
raise RuntimeError("invalid pattern: %s" % pattern)
|
||
|
if not otable and isinstance(queries, dict):
|
||
|
dbset = self.db(queries[table])
|
||
|
if basequery is not None:
|
||
|
dbset = dbset(basequery)
|
||
|
dbset = dbset(query)
|
||
|
else:
|
||
|
raise RuntimeError(
|
||
|
"missing relation in pattern: %s" % pattern)
|
||
|
elif re2.match(tag) and args[i] == tag[:tag.find('[')]:
|
||
|
ref = tag[tag.find('[')+1:-1]
|
||
|
if '.' in ref and otable:
|
||
|
table, field = ref.split('.')
|
||
|
selfld = '_id'
|
||
|
if self.db[table][field].type.startswith('reference '):
|
||
|
refs = [
|
||
|
x.name for x in self.db[otable]
|
||
|
if x.type == self.db[table][field].type]
|
||
|
else:
|
||
|
refs = [
|
||
|
x.name for x in self.db[table]._referenced_by
|
||
|
if x.tablename == otable]
|
||
|
if refs:
|
||
|
selfld = refs[0]
|
||
|
if nested_select:
|
||
|
try:
|
||
|
dbset = self.db(self.db[table][field].belongs(
|
||
|
dbset._select(self.db[otable][selfld])))
|
||
|
except ValueError:
|
||
|
return self.db.Row({
|
||
|
'status': 400, 'pattern': pattern,
|
||
|
'error': 'invalid path', 'response': None})
|
||
|
else:
|
||
|
items = [
|
||
|
item.id for item in dbset.select(
|
||
|
self.db[otable][selfld])
|
||
|
]
|
||
|
dbset = self.db(
|
||
|
self.db[table][field].belongs(items))
|
||
|
else:
|
||
|
table = ref
|
||
|
if not otable and isinstance(queries, dict):
|
||
|
dbset = self.db(queries[table])
|
||
|
dbset = dbset(self.db[table])
|
||
|
elif tag == ':field' and table:
|
||
|
# print 're3:'+tag
|
||
|
field = args[i]
|
||
|
if field not in self.db[table]:
|
||
|
break
|
||
|
# hand-built patterns should respect .readable=False as well
|
||
|
if not self.db[table][field].readable:
|
||
|
return self.db.Row({
|
||
|
'status': 418, 'pattern': pattern,
|
||
|
'error': 'I\'m a teapot', 'response': None})
|
||
|
try:
|
||
|
distinct = vars.get('distinct', False) == 'True'
|
||
|
offset = to_num(vars.get('offset', None) or 0)
|
||
|
limits = (
|
||
|
offset,
|
||
|
to_num(vars.get('limit', None) or 1000) + offset)
|
||
|
except ValueError:
|
||
|
return self.db.Row({
|
||
|
'status': 400, 'error': 'invalid limits',
|
||
|
'response': None})
|
||
|
items = dbset.select(
|
||
|
self.db[table][field], distinct=distinct,
|
||
|
limitby=limits)
|
||
|
if items:
|
||
|
return self.db.Row({
|
||
|
'status': 200, 'response': items,
|
||
|
'pattern': pattern})
|
||
|
else:
|
||
|
return self.db.Row({
|
||
|
'status': 404, 'pattern': pattern,
|
||
|
'error': 'no record found', ' response': None})
|
||
|
elif tag != args[i]:
|
||
|
break
|
||
|
otable = table
|
||
|
i += 1
|
||
|
if i == len(tags) and table:
|
||
|
if hasattr(self.db[table], '_id'):
|
||
|
ofields = vars.get(
|
||
|
'order', self.db[table]._id.name).split('|')
|
||
|
else:
|
||
|
ofields = vars.get(
|
||
|
'order', self.db[table]._primarykey[0]).split('|')
|
||
|
try:
|
||
|
orderby = [
|
||
|
self.db[table][f] if not f.startswith('~')
|
||
|
else ~self.db[table][f[1:]] for f in ofields]
|
||
|
except (KeyError, AttributeError):
|
||
|
return self.db.Row({
|
||
|
'status': 400, 'error': 'invalid orderby',
|
||
|
'response': None})
|
||
|
if exposedfields:
|
||
|
fields = [
|
||
|
field for field in self.db[table]
|
||
|
if str(field).split('.')[-1] in exposedfields and
|
||
|
field.readable]
|
||
|
else:
|
||
|
fields = [
|
||
|
field for field in self.db[table]
|
||
|
if field.readable]
|
||
|
count = dbset.count()
|
||
|
try:
|
||
|
offset = to_num(vars.get('offset', None) or 0)
|
||
|
limits = (
|
||
|
offset,
|
||
|
to_num(vars.get('limit', None) or 1000) + offset)
|
||
|
except ValueError:
|
||
|
return self.db.Row({
|
||
|
'status': 400, 'error': ' invalid limits',
|
||
|
'response': None})
|
||
|
try:
|
||
|
response = dbset.select(
|
||
|
limitby=limits, orderby=orderby, *fields)
|
||
|
except ValueError:
|
||
|
return self.db.Row({
|
||
|
'status': 400, 'pattern': pattern,
|
||
|
'error': 'invalid path', 'response': None})
|
||
|
return self.db.Row({
|
||
|
'status': 200, 'response': response,
|
||
|
'pattern': pattern, 'count': count})
|
||
|
return self.db.Row({
|
||
|
'status': 400, 'error': 'no matching pattern', 'response': None})
|