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