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_test (boolop where_test)*
144
145 where_test := (any_test | all_test | capability_test)
146
147 any_test := 'any' capability_list operator value
148
149 all_test := 'all' capability_list operator value
150
151 capability := alphanums ('_' alphanums)*
152
153 capability_list := '(' capability, capability (',' capability)* ')'
154
155 capability_test := capability operator value
156
157 operator := ('='|'!='|'<'|'>'|'>='|'<=')
158
159 value := (<quote> string <quote> | integer | boolean)
160
161 boolean := ('true' | 'false')
162
163 boolop := ('and' | 'or')
164 """
165
166 import operator
167
168 from pyparsing import (CaselessKeyword, Forward, Group, ParseException,
169 QuotedString, StringEnd, Suppress, Word, ZeroOrMore,
170 alphanums, alphas, nums, oneOf)
171
172 from pywurfl.exceptions import BaseException
173
174
175 __author__ = "Armand Lynch <lyncha@users.sourceforge.net>"
176 __copyright__ = "Copyright 2006, Armand Lynch"
177 __license__ = "LGPL"
178 __url__ = "http://celljam.net/"
179 __all__ = ['QueryLanguageError', 'QL']
180
181
183 """Base exception class for pywurfl.ql"""
184 pass
185
186
188 """
189 Defines the pywurfl query language.
190
191 @rtype: pyparsing.ParserElement
192 @return: The definition of the pywurfl query language.
193 """
194
195
196 select_token = CaselessKeyword("select")
197 ua_token = CaselessKeyword("ua")
198 id_token = CaselessKeyword("id")
199 device_token = CaselessKeyword("device")
200 select_type = (device_token | ua_token | id_token).setResultsName("type")
201 select_clause = select_token + select_type
202 select_statement = Group(select_clause).setResultsName("select")
203
204 capability = Word(alphas, alphanums + '_').setResultsName("capability")
205 integer = Word(nums)
206 boolean = CaselessKeyword("true") | CaselessKeyword("false")
207 string = QuotedString("'") | QuotedString('"')
208 value = (integer | boolean | string).setResultsName("value")
209 binop = oneOf("= != < > >= <=", caseless=True).setResultsName("operator")
210 and_ = CaselessKeyword("and")
211 or_ = CaselessKeyword("or")
212
213
214 capabilities = (capability + Suppress(',') + capability +
215 ZeroOrMore(Suppress(',') + capability))
216 any_token = CaselessKeyword("any")
217 any_list = capabilities.setResultsName("any_caps")
218 any_test = (any_token + Suppress('(') + any_list + Suppress(')') +
219 binop + value)
220
221 all_token = CaselessKeyword("all")
222 all_list = capabilities.setResultsName("all_caps")
223 all_test = (all_token + Suppress('(') + all_list + Suppress(')') +
224 binop + value)
225
226 cap_test = capability + binop + value
227
228
229 boolop = (and_ | or_).setResultsName('boolop')
230 where_token = CaselessKeyword("where")
231
232 where_test = all_test | any_test | cap_test
233 where_expression = Forward()
234 where_expression << (Group(where_test + ZeroOrMore(boolop +
235 where_expression)
236 ).setResultsName('where'))
237
238
239
240
241
242
243
244 where_statement = where_token + where_expression
245
246
247
248
249
250 return select_statement + where_statement + '*' + StringEnd()
251
252
254 """
255 Returns a dictionary of operator mappings for the query language.
256
257 @rtype: dict
258 """
259
260 def and_(func1, func2):
261 """
262 Return an 'anding' function that is a closure over func1 and func2.
263 """
264 def and_tester(value):
265 """Tests a device by 'anding' the two following functions:"""
266 return func1(value) and func2(value)
267 and_tester.__doc__ = '\n'.join([and_tester.__doc__, func1.__doc__,
268 func2.__doc__])
269 return and_tester
270
271 def or_(func1, func2):
272 """
273 Return an 'oring' function that is a closure over func1 and func2.
274 """
275 def or_tester(value):
276 """Tests a device by 'oring' the two following functions:"""
277 return func1(value) or func2(value)
278 or_tester.__doc__ = '\n'.join([or_tester.__doc__, func1.__doc__,
279 func2.__doc__])
280 return or_tester
281
282 return {'=':operator.eq, '!=':operator.ne, '<':operator.lt,
283 '>':operator.gt, '>=':operator.ge, '<=':operator.le,
284 'and':and_, 'or':or_}
285
286
287 ops = get_operators()
288
289
291 """
292 Returns a capability test function.
293
294 @param cap: A WURFL capability
295 @type cap: string
296 @param op: A binary test operator
297 @type op: string
298 @param val: The value to test for
299 @type val: string
300
301 @rtype: function
302 """
303
304 try:
305 val = int(val)
306 except ValueError:
307 if val == 'true':
308 val = True
309 elif val == 'false':
310 val = False
311 def capability_tester(devobj):
312 return ops[op](getattr(devobj, cap), val)
313 capability_tester.__doc__ = "Test a device for %s %s %s" % (cap, op, val)
314 return capability_tester
315
316
318 """
319 Combines a list of functions with binary operators.
320
321 @param funcs: A python list of function objects with descriptions of
322 binary operators interspersed.
323
324 For example [func1, 'and', func2, 'or', func3]
325 @type funcs: list
326 @rtype: function
327 """
328
329 while len(funcs) > 1:
330 try:
331 f_index = funcs.index('and')
332 op = ops['and']
333 except ValueError:
334 try:
335 f_index = funcs.index('or')
336 op = ops['or']
337 except ValueError:
338 break
339 combined = op(funcs[f_index - 1], funcs[f_index + 1])
340 funcs = funcs[:f_index-1] + [combined] + funcs[f_index + 2:]
341 return funcs[0]
342
343
345 """
346 Reduces a sequence of function objects to one function object by applying
347 a binary function recursively to the sequence::
348
349 In:
350 func = and
351 seq = [func1, func2, func3, func4]
352 Out:
353 and(func1, and(func2, and(func3, func4)))
354
355 @param func: A function that acts as a binary operator.
356 @type func: function
357 @param seq: An ordered sequence of function objects
358 @type seq: list
359 @rtype: function
360 """
361
362 if seq[1:]:
363 return func(seq[0], reduce_funcs(func, seq[1:]))
364 else:
365 return seq[0]
366
367
369 """
370 Produces a function that represents the "any" or "all" expression passed
371 in by exp::
372
373 In:
374 any(ringtone_mp3, ringtone_awb) = true
375 Out:
376 ((ringtone_mp3 = true) or (ringtone_awb = true))
377
378 @param exp: The result from parsing an 'any' or 'all' statement.
379 @type exp: pyparsing.ParseResults
380 @rtype: function
381 """
382
383 funcs = []
384 if exp.any_caps:
385 for cap in exp.any_caps:
386 funcs.append(capability_test(cap, exp.operator, exp.value))
387 return reduce_funcs(ops['or'], funcs)
388 elif exp.all_caps:
389 for cap in exp.all_caps:
390 funcs.append(capability_test(cap, exp.operator, exp.value))
391 return reduce_funcs(ops['and'], funcs)
392
393
395 """
396 Produces a function that encapsulates all the tests from a where
397 statement that takes a Device class or object as a parameter::
398
399 In (a result object from the following query):
400 select id where ringtone=true and any(ringtone_mp3, ringtone_awb)=true
401
402 Out:
403 def func(devobj):
404 if (devobj.ringtone == true and
405 (devobj.ringtone_mp3 == true or
406 devobj.ringtone_awb == true)):
407 return True
408 else:
409 return False
410 return func
411
412 @param ql_result: The result from calling pyparsing.parseString()
413 @rtype: function
414 """
415
416 funcs = []
417 exp = ql_result.where
418 while exp:
419 if exp.any_caps or exp.all_caps:
420 func = reduce_statement(exp)
421 else:
422 func = capability_test(exp.capability, exp.operator, exp.value)
423
424 boolop = exp.boolop
425 if boolop:
426 funcs.extend([func, boolop])
427 else:
428 funcs.append(func)
429 exp = exp.where
430 return combine_funcs(funcs)
431
432
434 """
435 Return a function that can run queries against the WURFL.
436
437 @param devices: The device class hierarchy from pywurfl
438 @type devices: pywurfl.Devices
439 @rtype: function
440 """
441
442 language = define_language()
443
444 def query(qstr, instance=True):
445 """
446 Return a generator that filters the pywurfl.Devices instance by the
447 query string provided in qstr.
448
449 @param qstr: A query string that follows the pywurfl.ql language
450 syntax.
451 @type qstr: string
452 @param instance: Used to select that you want an instance instead of a
453 class.
454 @type instance: boolean
455 @rtype: generator
456 """
457 qstr = qstr.replace('\n', ' ')
458 qstr = qstr + '*'
459 try:
460 qres = language.parseString(qstr)
461 tester = test_generator(qres)
462 if qres.select.type == 'ua':
463 return (x.devua for x in devices.devids.itervalues()
464 if tester(x))
465 elif qres.select.type == 'id':
466 return (x.devid for x in devices.devids.itervalues()
467 if tester(x))
468 else:
469 if instance:
470 return (x() for x in devices.devids.itervalues()
471 if tester(x))
472 else:
473 return (x for x in devices.devids.itervalues()
474 if tester(x))
475 except ParseException, exception:
476 raise QueryLanguageError(str(exception))
477 setattr(devices, 'query', query)
478 return query
479