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  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   
180 -class QueryLanguageError(BaseException):
181 """Base exception class for pywurfl.ql""" 182 pass
183 184
185 -def define_language():
186 """ 187 Defines the pywurfl query language. 188 189 @rtype: pyparsing.ParserElement 190 @return: The definition of the pywurfl query language. 191 """ 192 193 # Select statement 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 # Any test 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 # All test 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 # Capability test 224 cap_test = capability + binop + value 225 226 # WHERE statement 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 #where_expression << (Group(where_test + ZeroOrMore(boolop + 237 # where_expression) + 238 # StringEnd()).setResultsName('where')) 239 #where_expression = (Group(where_test + ZeroOrMore(boolop + where_test) + 240 # StringEnd()).setResultsName('where')) 241 242 where_statement = where_token + where_expression 243 244 # Mon Jan 1 12:35:56 EST 2007 245 # If there isn't a concrete end to the string pyparsing will not parse 246 # query correctly 247 248 return select_statement + where_statement + '*' + StringEnd()
249 250
251 -def get_operators():
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
288 -def capability_test(cap, op, val):
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
315 -def combine_funcs(funcs):
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
342 -def reduce_funcs(func, seq):
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
366 -def reduce_statement(exp):
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
392 -def test_generator(ql_result):
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
431 -def QL(devices):
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