1 | import re |
---|
2 | from .regex import REGEX_SEARCH_PATTERN, REGEX_SQUARE_BRACKETS |
---|
3 | from .._compat import long |
---|
4 | |
---|
5 | |
---|
6 | def to_num(num): |
---|
7 | result = 0 |
---|
8 | try: |
---|
9 | result = long(num) |
---|
10 | except NameError as e: |
---|
11 | result = int(num) |
---|
12 | return result |
---|
13 | |
---|
14 | |
---|
15 | class RestParser(object): |
---|
16 | def __init__(self, db): |
---|
17 | self.db = db |
---|
18 | |
---|
19 | def auto_table(self, table, base="", depth=0): |
---|
20 | patterns = [] |
---|
21 | for field in self.db[table].fields: |
---|
22 | if base: |
---|
23 | tag = "%s/%s" % (base, field.replace("_", "-")) |
---|
24 | else: |
---|
25 | tag = "/%s/%s" % (table.replace("_", "-"), field.replace("_", "-")) |
---|
26 | f = self.db[table][field] |
---|
27 | if not f.readable: |
---|
28 | continue |
---|
29 | if f.type == "id" or "slug" in field or f.type.startswith("reference"): |
---|
30 | tag += "/{%s.%s}" % (table, field) |
---|
31 | patterns.append(tag) |
---|
32 | patterns.append(tag + "/:field") |
---|
33 | elif f.type.startswith("boolean"): |
---|
34 | tag += "/{%s.%s}" % (table, field) |
---|
35 | patterns.append(tag) |
---|
36 | patterns.append(tag + "/:field") |
---|
37 | elif f.type in ("float", "double", "integer", "bigint"): |
---|
38 | tag += "/{%s.%s.ge}/{%s.%s.lt}" % (table, field, table, field) |
---|
39 | patterns.append(tag) |
---|
40 | patterns.append(tag + "/:field") |
---|
41 | elif f.type.startswith("list:"): |
---|
42 | tag += "/{%s.%s.contains}" % (table, field) |
---|
43 | patterns.append(tag) |
---|
44 | patterns.append(tag + "/:field") |
---|
45 | elif f.type in ("date", "datetime"): |
---|
46 | tag += "/{%s.%s.year}" % (table, field) |
---|
47 | patterns.append(tag) |
---|
48 | patterns.append(tag + "/:field") |
---|
49 | tag += "/{%s.%s.month}" % (table, field) |
---|
50 | patterns.append(tag) |
---|
51 | patterns.append(tag + "/:field") |
---|
52 | tag += "/{%s.%s.day}" % (table, field) |
---|
53 | patterns.append(tag) |
---|
54 | patterns.append(tag + "/:field") |
---|
55 | if f.type in ("datetime", "time"): |
---|
56 | tag += "/{%s.%s.hour}" % (table, field) |
---|
57 | patterns.append(tag) |
---|
58 | patterns.append(tag + "/:field") |
---|
59 | tag += "/{%s.%s.minute}" % (table, field) |
---|
60 | patterns.append(tag) |
---|
61 | patterns.append(tag + "/:field") |
---|
62 | tag += "/{%s.%s.second}" % (table, field) |
---|
63 | patterns.append(tag) |
---|
64 | patterns.append(tag + "/:field") |
---|
65 | if depth > 0: |
---|
66 | for f in self.db[table]._referenced_by: |
---|
67 | tag += "/%s[%s.%s]" % (table, f.tablename, f.name) |
---|
68 | patterns.append(tag) |
---|
69 | patterns += self.auto_table(table, base=tag, depth=depth - 1) |
---|
70 | return patterns |
---|
71 | |
---|
72 | def parse(self, patterns, args, vars, queries=None, nested_select=True): |
---|
73 | """ |
---|
74 | Example: |
---|
75 | Use as:: |
---|
76 | |
---|
77 | db.define_table('person',Field('name'),Field('info')) |
---|
78 | db.define_table('pet', |
---|
79 | Field('ownedby',db.person), |
---|
80 | Field('name'),Field('info') |
---|
81 | ) |
---|
82 | |
---|
83 | @request.restful() |
---|
84 | def index(): |
---|
85 | def GET(*args,**vars): |
---|
86 | patterns = [ |
---|
87 | "/friends[person]", |
---|
88 | "/{person.name}/:field", |
---|
89 | "/{person.name}/pets[pet.ownedby]", |
---|
90 | "/{person.name}/pets[pet.ownedby]/{pet.name}", |
---|
91 | "/{person.name}/pets[pet.ownedby]/{pet.name}/:field", |
---|
92 | ("/dogs[pet]", db.pet.info=='dog'), |
---|
93 | ("/dogs[pet]/{pet.name.startswith}", db.pet.info=='dog'), |
---|
94 | ] |
---|
95 | parser = db.parse_as_rest(patterns,args,vars) |
---|
96 | if parser.status == 200: |
---|
97 | return dict(content=parser.response) |
---|
98 | else: |
---|
99 | raise HTTP(parser.status,parser.error) |
---|
100 | |
---|
101 | def POST(table_name,**vars): |
---|
102 | if table_name == 'person': |
---|
103 | return db.person.validate_and_insert(**vars) |
---|
104 | elif table_name == 'pet': |
---|
105 | return db.pet.validate_and_insert(**vars) |
---|
106 | else: |
---|
107 | raise HTTP(400) |
---|
108 | return locals() |
---|
109 | """ |
---|
110 | |
---|
111 | if patterns == "auto": |
---|
112 | patterns = [] |
---|
113 | for table in self.db.tables: |
---|
114 | if not table.startswith("auth_"): |
---|
115 | patterns.append("/%s[%s]" % (table, table)) |
---|
116 | patterns += self.auto_table(table, base="", depth=1) |
---|
117 | else: |
---|
118 | i = 0 |
---|
119 | while i < len(patterns): |
---|
120 | pattern = patterns[i] |
---|
121 | if not isinstance(pattern, str): |
---|
122 | pattern = pattern[0] |
---|
123 | tokens = pattern.split("/") |
---|
124 | if tokens[-1].startswith(":auto") and re.match( |
---|
125 | REGEX_SQUARE_BRACKETS, tokens[-1] |
---|
126 | ): |
---|
127 | new_patterns = self.auto_table( |
---|
128 | tokens[-1][tokens[-1].find("[") + 1 : -1], "/".join(tokens[:-1]) |
---|
129 | ) |
---|
130 | patterns = patterns[:i] + new_patterns + patterns[i + 1 :] |
---|
131 | i += len(new_patterns) |
---|
132 | else: |
---|
133 | i += 1 |
---|
134 | if "/".join(args) == "patterns": |
---|
135 | return self.db.Row( |
---|
136 | {"status": 200, "pattern": "list", "error": None, "response": patterns} |
---|
137 | ) |
---|
138 | for pattern in patterns: |
---|
139 | basequery, exposedfields = None, [] |
---|
140 | if isinstance(pattern, tuple): |
---|
141 | if len(pattern) == 2: |
---|
142 | pattern, basequery = pattern |
---|
143 | elif len(pattern) > 2: |
---|
144 | pattern, basequery, exposedfields = pattern[0:3] |
---|
145 | otable = table = None |
---|
146 | if not isinstance(queries, dict): |
---|
147 | dbset = self.db(queries) |
---|
148 | if basequery is not None: |
---|
149 | dbset = dbset(basequery) |
---|
150 | i = 0 |
---|
151 | tags = pattern[1:].split("/") |
---|
152 | if len(tags) != len(args): |
---|
153 | continue |
---|
154 | for tag in tags: |
---|
155 | if re.match(REGEX_SEARCH_PATTERN, tag): |
---|
156 | tokens = tag[1:-1].split(".") |
---|
157 | table, field = tokens[0], tokens[1] |
---|
158 | if not otable or table == otable: |
---|
159 | if len(tokens) == 2 or tokens[2] == "eq": |
---|
160 | query = self.db[table][field] == args[i] |
---|
161 | elif tokens[2] == "ne": |
---|
162 | query = self.db[table][field] != args[i] |
---|
163 | elif tokens[2] == "lt": |
---|
164 | query = self.db[table][field] < args[i] |
---|
165 | elif tokens[2] == "gt": |
---|
166 | query = self.db[table][field] > args[i] |
---|
167 | elif tokens[2] == "ge": |
---|
168 | query = self.db[table][field] >= args[i] |
---|
169 | elif tokens[2] == "le": |
---|
170 | query = self.db[table][field] <= args[i] |
---|
171 | elif tokens[2] == "year": |
---|
172 | query = self.db[table][field].year() == args[i] |
---|
173 | elif tokens[2] == "month": |
---|
174 | query = self.db[table][field].month() == args[i] |
---|
175 | elif tokens[2] == "day": |
---|
176 | query = self.db[table][field].day() == args[i] |
---|
177 | elif tokens[2] == "hour": |
---|
178 | query = self.db[table][field].hour() == args[i] |
---|
179 | elif tokens[2] == "minute": |
---|
180 | query = self.db[table][field].minutes() == args[i] |
---|
181 | elif tokens[2] == "second": |
---|
182 | query = self.db[table][field].seconds() == args[i] |
---|
183 | elif tokens[2] == "startswith": |
---|
184 | query = self.db[table][field].startswith(args[i]) |
---|
185 | elif tokens[2] == "contains": |
---|
186 | query = self.db[table][field].contains(args[i]) |
---|
187 | else: |
---|
188 | raise RuntimeError("invalid pattern: %s" % pattern) |
---|
189 | if len(tokens) == 4 and tokens[3] == "not": |
---|
190 | query = ~query |
---|
191 | elif len(tokens) >= 4: |
---|
192 | raise RuntimeError("invalid pattern: %s" % pattern) |
---|
193 | if not otable and isinstance(queries, dict): |
---|
194 | dbset = self.db(queries[table]) |
---|
195 | if basequery is not None: |
---|
196 | dbset = dbset(basequery) |
---|
197 | dbset = dbset(query) |
---|
198 | else: |
---|
199 | raise RuntimeError("missing relation in pattern: %s" % pattern) |
---|
200 | elif ( |
---|
201 | re.match(REGEX_SQUARE_BRACKETS, tag) |
---|
202 | and args[i] == tag[: tag.find("[")] |
---|
203 | ): |
---|
204 | ref = tag[tag.find("[") + 1 : -1] |
---|
205 | if "." in ref and otable: |
---|
206 | table, field = ref.split(".") |
---|
207 | selfld = "_id" |
---|
208 | if self.db[table][field].type.startswith("reference "): |
---|
209 | refs = [ |
---|
210 | x.name |
---|
211 | for x in self.db[otable] |
---|
212 | if x.type == self.db[table][field].type |
---|
213 | ] |
---|
214 | else: |
---|
215 | refs = [ |
---|
216 | x.name |
---|
217 | for x in self.db[table]._referenced_by |
---|
218 | if x.tablename == otable |
---|
219 | ] |
---|
220 | if refs: |
---|
221 | selfld = refs[0] |
---|
222 | if nested_select: |
---|
223 | try: |
---|
224 | dbset = self.db( |
---|
225 | self.db[table][field].belongs( |
---|
226 | dbset._select(self.db[otable][selfld]) |
---|
227 | ) |
---|
228 | ) |
---|
229 | except ValueError: |
---|
230 | return self.db.Row( |
---|
231 | { |
---|
232 | "status": 400, |
---|
233 | "pattern": pattern, |
---|
234 | "error": "invalid path", |
---|
235 | "response": None, |
---|
236 | } |
---|
237 | ) |
---|
238 | else: |
---|
239 | items = [ |
---|
240 | item.id |
---|
241 | for item in dbset.select(self.db[otable][selfld]) |
---|
242 | ] |
---|
243 | dbset = self.db(self.db[table][field].belongs(items)) |
---|
244 | else: |
---|
245 | table = ref |
---|
246 | if not otable and isinstance(queries, dict): |
---|
247 | dbset = self.db(queries[table]) |
---|
248 | dbset = dbset(self.db[table]) |
---|
249 | elif tag == ":field" and table: |
---|
250 | # print 're3:'+tag |
---|
251 | field = args[i] |
---|
252 | if field not in self.db[table]: |
---|
253 | break |
---|
254 | # hand-built patterns should respect .readable=False as well |
---|
255 | if not self.db[table][field].readable: |
---|
256 | return self.db.Row( |
---|
257 | { |
---|
258 | "status": 418, |
---|
259 | "pattern": pattern, |
---|
260 | "error": "I'm a teapot", |
---|
261 | "response": None, |
---|
262 | } |
---|
263 | ) |
---|
264 | try: |
---|
265 | distinct = vars.get("distinct", False) == "True" |
---|
266 | offset = to_num(vars.get("offset", None) or 0) |
---|
267 | limits = ( |
---|
268 | offset, |
---|
269 | to_num(vars.get("limit", None) or 1000) + offset, |
---|
270 | ) |
---|
271 | except ValueError: |
---|
272 | return self.db.Row( |
---|
273 | {"status": 400, "error": "invalid limits", "response": None} |
---|
274 | ) |
---|
275 | items = dbset.select( |
---|
276 | self.db[table][field], distinct=distinct, limitby=limits |
---|
277 | ) |
---|
278 | if items: |
---|
279 | return self.db.Row( |
---|
280 | {"status": 200, "response": items, "pattern": pattern} |
---|
281 | ) |
---|
282 | else: |
---|
283 | return self.db.Row( |
---|
284 | { |
---|
285 | "status": 404, |
---|
286 | "pattern": pattern, |
---|
287 | "error": "no record found", |
---|
288 | " response": None, |
---|
289 | } |
---|
290 | ) |
---|
291 | elif tag != args[i]: |
---|
292 | break |
---|
293 | otable = table |
---|
294 | i += 1 |
---|
295 | if i == len(tags) and table: |
---|
296 | if hasattr(self.db[table], "_id"): |
---|
297 | ofields = vars.get("order", self.db[table]._id.name).split("|") |
---|
298 | else: |
---|
299 | ofields = vars.get( |
---|
300 | "order", self.db[table]._primarykey[0] |
---|
301 | ).split("|") |
---|
302 | try: |
---|
303 | orderby = [ |
---|
304 | self.db[table][f] |
---|
305 | if not f.startswith("~") |
---|
306 | else ~self.db[table][f[1:]] |
---|
307 | for f in ofields |
---|
308 | ] |
---|
309 | except (KeyError, AttributeError): |
---|
310 | return self.db.Row( |
---|
311 | { |
---|
312 | "status": 400, |
---|
313 | "error": "invalid orderby", |
---|
314 | "response": None, |
---|
315 | } |
---|
316 | ) |
---|
317 | if exposedfields: |
---|
318 | fields = [ |
---|
319 | field |
---|
320 | for field in self.db[table] |
---|
321 | if str(field).split(".")[-1] in exposedfields |
---|
322 | and field.readable |
---|
323 | ] |
---|
324 | else: |
---|
325 | fields = [field for field in self.db[table] if field.readable] |
---|
326 | count = dbset.count() |
---|
327 | try: |
---|
328 | offset = to_num(vars.get("offset", None) or 0) |
---|
329 | limits = ( |
---|
330 | offset, |
---|
331 | to_num(vars.get("limit", None) or 1000) + offset, |
---|
332 | ) |
---|
333 | except ValueError: |
---|
334 | return self.db.Row( |
---|
335 | { |
---|
336 | "status": 400, |
---|
337 | "error": " invalid limits", |
---|
338 | "response": None, |
---|
339 | } |
---|
340 | ) |
---|
341 | try: |
---|
342 | response = dbset.select( |
---|
343 | limitby=limits, orderby=orderby, *fields |
---|
344 | ) |
---|
345 | except ValueError: |
---|
346 | return self.db.Row( |
---|
347 | { |
---|
348 | "status": 400, |
---|
349 | "pattern": pattern, |
---|
350 | "error": "invalid path", |
---|
351 | "response": None, |
---|
352 | } |
---|
353 | ) |
---|
354 | return self.db.Row( |
---|
355 | { |
---|
356 | "status": 200, |
---|
357 | "response": response, |
---|
358 | "pattern": pattern, |
---|
359 | "count": count, |
---|
360 | } |
---|
361 | ) |
---|
362 | return self.db.Row( |
---|
363 | {"status": 400, "error": "no matching pattern", "response": None} |
---|
364 | ) |
---|