Package pywurfl :: Module ql
[hide private]
[frames] | no frames]

Source Code for Module pywurfl.ql

  1  # pywurfl QL - Wireless Universal Resource File Query Language in Python 
  2  # Copyright (C) 2006 Armand Lynch 
  3  # 
  4  # This library is free software; you can redistribute it and/or modify it 
  5  # under the terms of the GNU Lesser General Public License as published by the 
  6  # Free Software Foundation; either version 2.1 of the License, or (at your 
  7  # option) any later version. 
  8  # 
  9  # This library is distributed in the hope that it will be useful, but WITHOUT 
 10  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
 11  # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 
 12  # details. 
 13  # 
 14  # You should have received a copy of the GNU Lesser General Public License 
 15  # along with this library; if not, write to the Free Software Foundation, Inc., 
 16  # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 17  # 
 18  # Armand Lynch <lyncha@users.sourceforge.net> 
 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   
182 -class QueryLanguageError(BaseException):
183 """Base exception class for pywurfl.ql""" 184 pass
185 186
187 -def define_language():
188 """ 189 Defines the pywurfl query language. 190 191 @rtype: pyparsing.ParserElement 192 @return: The definition of the pywurfl query language. 193 """ 194 195 # Select statement 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 # Any test 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 # All test 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 # Capability test 226 cap_test = capability + binop + value 227 228 # WHERE statement 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 #where_expression << (Group(where_test + ZeroOrMore(boolop + 239 # where_expression) + 240 # StringEnd()).setResultsName('where')) 241 #where_expression = (Group(where_test + ZeroOrMore(boolop + where_test) + 242 # StringEnd()).setResultsName('where')) 243 244 where_statement = where_token + where_expression 245 246 # Mon Jan 1 12:35:56 EST 2007 247 # If there isn't a concrete end to the string pyparsing will not parse 248 # query correctly 249 250 return select_statement + where_statement + '*' + StringEnd()
251 252
253 -def get_operators():
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
290 -def capability_test(cap, op, val):
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
317 -def combine_funcs(funcs):
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
344 -def reduce_funcs(func, seq):
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
368 -def reduce_statement(exp):
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
394 -def test_generator(ql_result):
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
433 -def QL(devices):
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