1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 __doc__ = \
21 """
22 pywurfl Query Language
23
24 pywurfl QL is a WURFL query language that looks very similar to SQL.
25
26 Language Definition
27 ===================
28
29 Select statement
30 ================
31
32 select (device|id|ua)
33 ---------------------
34
35 The select statement consists of the keyword 'select' followed by the
36 select type which can be one of these keywords: 'device', 'ua', 'id'.
37 The select statement is the first statement in all queries.
38
39 device
40 ------
41 When 'select' is followed by the keyword 'device', a device object will
42 be returned for each device that matches the 'where' expression
43 (see below).
44
45 ua
46 --
47 When 'select' is followed by the keyword 'ua', an user-agent string
48 will be returned for each device that matches the 'where' expression
49 (see below).
50
51 id
52 --
53 When 'select' is followed by the keyword 'id', a WURFL id string will be
54 returned for each device that matches the 'where' expression (see below).
55
56
57 Where statement
58 ===============
59
60 where condition
61 ---------------
62 where condition and/or condition
63 --------------------------------
64 where any/all and/or condition
65 ------------------------------
66
67 The where statement follows a select statement and can consist of the
68 following elements: 'where condition', 'any statement', 'all statement'.
69
70 Where condition
71 ---------------
72 A where condition consists of a capability name followed by a test
73 operator followed by a value. For example, "ringtone = true".
74
75 Any statement
76 -------------
77 An any statement consists of the keyword 'any' followed by a
78 parenthesized, comma delimited list of capability names, followed by
79 a test operator and then followed by a value. All capabilities
80 listed in an any statement will be 'ored' together. There must be a
81 minimum of two capabilities listed.
82
83 For example: "any(ringtone_mp3, ringtone_wav) = true".
84
85 All statement
86 -------------
87 An all statement consists of the keyword 'all' followed by a
88 parenthesized, comma delimited list of capability names, followed by
89 a test operator and then followed by a value. All capabilities
90 listed in an all statement will be 'anded' together. There must be a
91 minimum of two capabilities listed.
92
93 For example: "all(ringtone_mp3, ringtone_wav) = true".
94
95 Test operators
96 --------------
97 The following are the test operators that the query language can
98 recognize::
99
100 = != < > >= <=
101
102 Comparing strings follow Python's rules.
103
104 Values
105 ------
106 Test values can be integers, strings in quotes and the tokens
107 "true" or "false" for boolean tests.
108
109
110 Binary operators
111 ================
112
113 There are two binary operators defined in the language "and" and "or".
114 They can be used between any where statement tests and follow
115 conventional precedence rules::
116
117 ringtone=true or ringtone_mp3=false and preferred_markup="wml_1_1"
118 -- becomes --
119 (ringtone=true or (ringtone_mp3=false and preferred_markup="wml_1_1"))
120
121
122 Example Queries
123 ===============
124
125 select id where ringtone=true
126
127 select id where ringtone=false and ringtone_mp3=true
128
129 select id where rows > 3
130
131 select id where all(ringtone_mp3, ringtone_aac, ringtone_qcelp)=true
132
133 select ua where preferred_markup = "wml_1_1"
134
135
136 EBNF
137 ====
138
139 query ::= select_statement where_statement
140
141 select_statement ::= 'select' ('device' | 'id' | 'ua')
142
143 where_statement ::= 'where' + where_expression
144
145 where_expression ::= where_test (boolop where_test)*
146
147 where_test ::= (any_statement | all_statement | expr_test)
148
149 any_statement ::= 'any' '(' expr_list ')' operator expr
150
151 all_statement ::= 'all' '(' expr_list ')' operator expr
152
153 capability ::= alphanums ('_' alphanums)*
154
155 expr_test ::= expr operator expr
156
157 expr_list ::= expr (',' expr)*
158
159 expr ::= types attributes_methods_concat | capability attributes_methods_concat
160
161 attributes_methods_concat ::= ('.' method '(' method_args? ')')*
162
163 method_args ::= (method_arg (',' method_arg)*)
164
165 method_arg ::= (types | expr)
166
167 method ::= ('_' alphanums)*
168
169 operator ::= ('='|'!='|'<'|'>'|'>='|'<=')
170
171 types ::= (<quote> string <quote> | integer | boolean)
172
173 boolean ::= ('true' | 'false')
174
175 boolop ::= ('and' | 'or')
176 """
177
178 import re
179 import operator
180
181 from pyparsing import (CaselessKeyword, Forward, Group, ParseException,
182 QuotedString, StringEnd, Suppress, Word, ZeroOrMore,
183 alphanums, alphas, nums, oneOf, delimitedList)
184
185 from pywurfl.exceptions import WURFLException
186
187
188 __author__ = "Armand Lynch <lyncha@users.sourceforge.net>"
189 __contributors__ = "Gabriele Fantini <gabriele.fantini@staff.dada.net>"
190 __copyright__ = "Copyright 2006-2010, Armand Lynch"
191 __license__ = "LGPL"
192 __url__ = "http://celljam.net/"
193 __all__ = ['QueryLanguageError', 'QL']
194
195
197 """Base exception class for pywurfl.ql"""
198 pass
199
200
202 """Convert to pywurfl number type"""
203 n = toks[0]
204 try:
205 return TypeNum(int(n))
206 except ValueError, e:
207 return TypeNum(float(n))
208
209
211 """Convert to pywurfl boolean type"""
212 val = toks[0]
213 if val.lower() == 'true':
214 return TypeBool(True)
215 elif val.lower() == 'false':
216 return TypeBool(False)
217 else:
218 raise QueryLanguageError("Invalid boolean value '%s'" % val)
219
220
222 """Convert to pywurfl string type"""
223 val = toks[0]
224 return TypeStr(val)
225
226
229 self.py_value = py_value
230
232 return getattr(self.py_value, method)
233
234
237
238
241
242
244 - def substr(self, begin, end):
245 try:
246 return self.py_value[begin:end]
247 except IndexError, e:
248 return None
249
250 - def _match(self, regex, num=0, flags=0):
251 if re.compile(regex, flags).match(self.py_value, num) is None:
252 return False
253 else:
254 return True
255
256 - def match(self, regex, num=0):
257 return self._match(regex, num)
258
259 - def imatch(self, regex, num=0):
260 return self._match(regex, num, re.IGNORECASE)
261
262
265
266
269 try:
270 return self.__getitem__(i)
271 except IndexError, e:
272 return None
273
275 """
276 Defines the pywurfl query language.
277
278 @rtype: pyparsing.ParserElement
279 @return: The definition of the pywurfl query language.
280 """
281
282
283 integer = Word(nums).setParseAction(_toNum)
284 boolean = (CaselessKeyword("true") | CaselessKeyword("false")).setParseAction(_toBool)
285 string = (QuotedString("'") | QuotedString('"')).setParseAction(_toStr)
286 types = (integer | boolean | string)('value')
287
288 capability = Word(alphas, alphanums + '_')('capability')
289
290
291 select_token = CaselessKeyword("select")
292 ua_token = CaselessKeyword("ua")
293 id_token = CaselessKeyword("id")
294 device_token = CaselessKeyword("device")
295 select_type = (device_token | ua_token | id_token)("type")
296 select_statement = (select_token + select_type)("select")
297
298 expr = Forward()
299
300
301 method_arg = (types | Group(expr))
302 method_args = Group(ZeroOrMore(delimitedList(method_arg)))('method_args')
303
304
305 attribute = Word(alphas + '_', alphanums + '_')("attribute")
306 attribute_call = (attribute + Suppress('(') + method_args +
307 Suppress(')'))("attribute_call")
308
309 attribute_concat = Group(ZeroOrMore(Group(Suppress('.') + (attribute_call | attribute))))('attribute_concat')
310
311 expr << Group(types + attribute_concat | capability + attribute_concat)('expr')
312
313 binop = oneOf("= != < > >= <=", caseless=True)("operator")
314 and_ = CaselessKeyword("and")
315 or_ = CaselessKeyword("or")
316
317 expr_list = (expr + ZeroOrMore(Suppress(',') + expr))
318
319
320 any_token = CaselessKeyword("any")
321 any_expr_list = expr_list("any_expr_list")
322 any_statement = (any_token + Suppress('(') + any_expr_list + Suppress(')') +
323 binop + expr("rexpr"))('any_statement')
324
325
326 all_token = CaselessKeyword("all")
327 all_expr_list = expr_list("all_expr_list")
328 all_statement = (all_token + Suppress('(') + all_expr_list + Suppress(')') +
329 binop + expr("rexpr"))('all_statement')
330
331
332 expr_test = expr('lexpr') + binop + expr('rexpr')
333
334
335 boolop = (and_ | or_)('boolop')
336 where_token = CaselessKeyword("where")
337
338 where_test = (all_statement | any_statement | expr_test)('where_test')
339 where_expression = Forward()
340 where_expression << Group(where_test + ZeroOrMore(boolop + where_expression))('where_expression')
341
342 where_statement = where_token + where_expression
343
344
345
346
347 return select_statement + where_statement + '*' + StringEnd()
348
349
351 """
352 Returns a dictionary of operator mappings for the query language.
353
354 @rtype: dict
355 """
356
357 def and_(func1, func2):
358 """
359 Return an 'anding' function that is a closure over func1 and func2.
360 """
361 def and_tester(value):
362 """Tests a device by 'anding' the two following functions:"""
363 return func1(value) and func2(value)
364 return and_tester
365
366 def or_(func1, func2):
367 """
368 Return an 'oring' function that is a closure over func1 and func2.
369 """
370 def or_tester(value):
371 """Tests a device by 'oring' the two following functions:"""
372 return func1(value) or func2(value)
373 return or_tester
374
375 return {'=':operator.eq, '!=':operator.ne, '<':operator.lt,
376 '>':operator.gt, '>=':operator.ge, '<=':operator.le,
377 'and':and_, 'or':or_}
378
379
380 ops = get_operators()
381
382
384 """
385 Returns an exp test function.
386
387 @param lexpr: An expr
388 @type lexpr: expr
389 @param op: A binary test operator
390 @type op: string
391 @param rexpr: An expr
392 @type rexpr: expr
393
394 @rtype: function
395 """
396
397 def expr_tester(devobj):
398 lvalue = _evaluate(devobj, lexpr)
399 rvalue = _evaluate(devobj, rexpr)
400 return ops[op](lvalue.py_value, rvalue.py_value)
401
402 return expr_tester
403
404
406 """
407 Evaluate an expression with respect to a device object
408 """
409 value = None
410 if expression.keys() == ['expr']:
411 expression = expression.expr
412
413 if 'capability' in expression.keys():
414 capability = expression.capability
415 try:
416 py_value = getattr(devobj, capability)
417 except AttributeError, e:
418 raise QueryLanguageError("Invalid capability '%s'" %
419 capability)
420
421 if isinstance(py_value, bool):
422 value = TypeBool(py_value)
423 elif isinstance(py_value, int):
424 value = TypeNum(py_value)
425 elif isinstance(py_value, basestring):
426 value = TypeStr(py_value)
427 else:
428 raise QueryLanguageError("Unknown type '%s'" %
429 py_value.__class__)
430 else:
431 value = expression.value
432
433 for attribute in expression.attribute_concat:
434 py_value = None
435 if 'attribute_call' in attribute.keys():
436 method_name = attribute.attribute_call.attribute
437 method_args = []
438 for method_arg in attribute.attribute_call.method_args:
439 method_arg_value = None
440 try:
441 method_arg_value = _evaluate(devobj, method_arg.expr)
442 except AttributeError, e:
443 method_arg_value = method_arg
444
445 method_args.append(method_arg_value.py_value)
446
447 try:
448 attr = getattr(value, method_name)
449 py_value = attr(*method_args)
450 except (AttributeError, TypeError), e:
451 msg = "'%s' object has no callable attribute '%s'"
452 raise QueryLanguageError(msg %
453 (type(value.py_value).__name__,
454 method_name))
455 elif 'attribute' in attribute.keys():
456 try:
457 py_value = getattr(value, attribute.attribute)
458 except AttributeError, e:
459 raise QueryLanguageError(str(e))
460 if callable(py_value):
461 msg = "'%s' object has no attribute '%s'"
462 raise QueryLanguageError(msg %
463 (type(value.py_value).__name__,
464 attribute.attribute))
465 else:
466 raise QueryLanguageError('query syntax error')
467
468 if isinstance(py_value, bool):
469 value = TypeBool(py_value)
470 elif py_value is None:
471 value = TypeNone(py_value)
472 elif isinstance(py_value, int):
473 value = TypeNum(py_value)
474 elif isinstance(py_value, basestring):
475 value = TypeStr(py_value)
476 elif isinstance(py_value, (list, tuple)):
477 value = TypeList(py_value)
478 else:
479 raise QueryLanguageError("Unknown type '%s'" %
480 py_value.__class__)
481
482 return value
483
484
486 """
487 Combines a list of functions with binary operators.
488
489 @param funcs: A python list of function objects with descriptions of
490 binary operators interspersed.
491
492 For example [func1, 'and', func2, 'or', func3]
493 @type funcs: list
494 @rtype: function
495 """
496
497 while len(funcs) > 1:
498 try:
499 f_index = funcs.index('and')
500 op = ops['and']
501 except ValueError:
502 try:
503 f_index = funcs.index('or')
504 op = ops['or']
505 except ValueError:
506 break
507 combined = op(funcs[f_index - 1], funcs[f_index + 1])
508 funcs = funcs[:f_index-1] + [combined] + funcs[f_index + 2:]
509 return funcs[0]
510
511
513 """
514 Reduces a sequence of function objects to one function object by applying
515 a binary function recursively to the sequence::
516
517 In:
518 func = and
519 seq = [func1, func2, func3, func4]
520 Out:
521 and(func1, and(func2, and(func3, func4)))
522
523 @param func: A function that acts as a binary operator.
524 @type func: function
525 @param seq: An ordered sequence of function objects
526 @type seq: list
527 @rtype: function
528 """
529
530 if seq[1:]:
531 return func(seq[0], reduce_funcs(func, seq[1:]))
532 else:
533 return seq[0]
534
535
537 """
538 Produces a function that represents the "any" or "all" expression passed
539 in by exp::
540
541 In:
542 any(ringtone_mp3, ringtone_awb) = true
543 Out:
544 ((ringtone_mp3 = true) or (ringtone_awb = true))
545
546 @param exp: The result from parsing an 'any' or 'all' statement.
547 @type exp: pyparsing.ParseResults
548 @rtype: function
549 """
550
551 funcs = []
552 if exp.any_statement:
553 for expr in exp.any_statement.any_expr_list:
554 funcs.append(expr_test_func(expr, exp.operator, exp.rexpr))
555 func = ops['or']
556 elif exp.all_statement:
557 for expr in exp.all_statement.all_expr_list:
558 funcs.append(expr_test_func(expr, exp.operator, exp.rexpr))
559 func = ops['and']
560 return reduce_funcs(func, funcs)
561
562
564 """
565 Produces a function that encapsulates all the tests from a where
566 statement and takes a Device class or object as a parameter::
567
568 In (a result object from the following query):
569 select id where ringtone=true and any(ringtone_mp3, ringtone_awb)=true
570
571 Out:
572 def func(devobj):
573 if (devobj.ringtone == True and
574 (devobj.ringtone_mp3 == True or
575 devobj.ringtone_awb == True)):
576 return True
577 else:
578 return False
579 return func
580
581 @param ql_result: The result from calling pyparsing.parseString()
582 @rtype: function
583 """
584
585 funcs = []
586 where_test = ql_result.where_expression
587 while where_test:
588 if where_test.any_statement or where_test.all_statement:
589 func = reduce_statement(where_test)
590 else:
591 func = expr_test_func(where_test.lexpr, where_test.operator,
592 where_test.rexpr)
593
594 boolop = where_test.boolop
595 if boolop:
596 funcs.extend([func, boolop])
597 else:
598 funcs.append(func)
599 where_test = where_test.where_expression
600 return combine_funcs(funcs)
601
602
604 """
605 Return a function that can run queries against the WURFL.
606
607 @param devices: The device class hierarchy from pywurfl
608 @type devices: pywurfl.Devices
609 @rtype: function
610 """
611
612 language = define_language()
613
614 def query(qstr, instance=True):
615 """
616 Return a generator that filters the pywurfl.Devices instance by the
617 query string provided in qstr.
618
619 @param qstr: A query string that follows the pywurfl.ql language
620 syntax.
621 @type qstr: string
622 @param instance: Used to select that you want an instance instead of a
623 class.
624 @type instance: boolean
625 @rtype: generator
626 """
627 if isinstance(qstr, str):
628 raise UnicodeError(u"query must be a unicode string")
629 qstr = qstr.replace('\n', ' ').replace('\r', ' ') + '*'
630 try:
631 qres = language.parseString(qstr)
632 tester = test_generator(qres)
633 if qres.select.type == 'ua':
634 return (x.devua for x in devices.devids.itervalues()
635 if tester(x))
636 elif qres.select.type == 'id':
637 return (x.devid for x in devices.devids.itervalues()
638 if tester(x))
639 else:
640 if instance:
641 return (x() for x in devices.devids.itervalues()
642 if tester(x))
643 else:
644 return (x for x in devices.devids.itervalues()
645 if tester(x))
646 except ParseException, exception:
647 raise QueryLanguageError(str(exception))
648 setattr(devices, 'query', query)
649 return query
650