Package Gnumed :: Package pycommon :: Module gmCfg
[frames] | no frames]

Source Code for Module Gnumed.pycommon.gmCfg

  1  """GNUmed configuration handling. 
  2   
  3  This source of configuration information is supported: 
  4   
  5   - database tables 
  6   
  7  Theory of operation: 
  8   
  9  It is helpful to have a solid log target set up before importing this 
 10  module in your code. This way you will be able to see even those log 
 11  messages generated during module import. 
 12   
 13  Once your software has established database connectivity you can 
 14  set up a config source from the database. You can limit the option 
 15  applicability by the constraints "workplace", "user", and "cookie". 
 16   
 17  The basic API for handling items is get()/set(). 
 18  The database config objects auto-sync with the backend. 
 19   
 20  @copyright: GPL v2 or later 
 21  """ 
 22  # TODO: 
 23  # - optional arg for set -> type 
 24  #================================================================== 
 25  __author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>" 
 26   
 27  # standard modules 
 28  import sys, pickle, decimal, logging, re as regex 
 29   
 30   
 31  # gnumed modules 
 32  if __name__ == '__main__': 
 33          sys.path.insert(0, '../../') 
 34  from Gnumed.pycommon import gmPG2, gmTools 
 35   
 36   
 37  _log = logging.getLogger('gm.cfg') 
 38   
 39  # don't change this without knowing what you do as 
 40  # it will already be in many databases 
 41  cfg_DEFAULT = "xxxDEFAULTxxx" 
 42  #================================================================== 
43 -def get_all_options(order_by=None):
44 45 if order_by is None: 46 order_by = '' 47 else: 48 order_by = 'ORDER BY %s' % order_by 49 50 cmd = """ 51 SELECT * FROM ( 52 53 SELECT 54 vco.*, 55 cs.value 56 FROM 57 cfg.v_cfg_options vco 58 JOIN cfg.cfg_string cs ON (vco.pk_cfg_item = cs.fk_item) 59 60 UNION ALL 61 62 SELECT 63 vco.*, 64 cn.value::text 65 FROM 66 cfg.v_cfg_options vco 67 JOIN cfg.cfg_numeric cn ON (vco.pk_cfg_item = cn.fk_item) 68 69 UNION ALL 70 71 SELECT 72 vco.*, 73 csa.value::text 74 FROM 75 cfg.v_cfg_options vco 76 JOIN cfg.cfg_str_array csa ON (vco.pk_cfg_item = csa.fk_item) 77 78 UNION ALL 79 80 SELECT 81 vco.*, 82 cd.value::text 83 FROM 84 cfg.v_cfg_options vco 85 JOIN cfg.cfg_data cd ON (vco.pk_cfg_item = cd.fk_item) 86 87 ) as option_list 88 %s""" % order_by 89 90 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd}], get_col_idx = False) 91 92 return rows
93 #================================================================== 94 # FIXME: make a cBorg around this
95 -class cCfgSQL:
96 97 # def __init__(self): 98 # pass 99 100 #----------------------------------------------- 101 # external API 102 #-----------------------------------------------
103 - def get(self, option=None, workplace=None, cookie=None, bias=None, default=None, sql_return_type=None):
104 return self.get2 ( 105 option = option, 106 workplace = workplace, 107 cookie = cookie, 108 bias = bias, 109 default = default, 110 sql_return_type = sql_return_type 111 )
112 #-----------------------------------------------
113 - def get2(self, option=None, workplace=None, cookie=None, bias=None, default=None, sql_return_type=None):
114 """Retrieve configuration option from backend. 115 116 @param bias: Determine the direction into which to look for config options. 117 118 'user': When no value is found for "current_user/workplace" look for a value 119 for "current_user" regardless of workspace. The corresponding concept is: 120 121 "Did *I* set this option anywhere on this site ? If so, reuse the value." 122 123 'workplace': When no value is found for "current_user/workplace" look for a value 124 for "workplace" regardless of user. The corresponding concept is: 125 126 "Did anyone set this option for *this workplace* ? If so, reuse that value." 127 128 @param default: if no value is found for the option this value is returned 129 instead, also the option is set to this value in the backend, if <None> 130 a missing option will NOT be created in the backend 131 @param sql_return_type: a PostgreSQL type the value of the option is to be 132 cast to before returning, if None no cast will be applied, you will 133 want to make sure that sql_return_type and type(default) are compatible 134 """ 135 if None in [option, workplace]: 136 raise ValueError('neither <option> (%s) nor <workplace> (%s) may be [None]' % (option, workplace)) 137 if bias not in ['user', 'workplace']: 138 raise ValueError('<bias> must be "user" or "workplace"') 139 140 # does this option exist ? 141 cmd = "select type from cfg.cfg_template where name=%(opt)s" 142 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': {'opt': option}}]) 143 if len(rows) == 0: 144 # not found ... 145 if default is None: 146 # ... and no default either 147 return None 148 _log.info('creating option [%s] with default [%s]' % (option, default)) 149 success = self.set(workplace = workplace, cookie = cookie, option = option, value = default) 150 if not success: 151 # ... but cannot create option with default value either 152 _log.error('creating option failed') 153 return default 154 155 cfg_table_type_suffix = rows[0][0] 156 args = { 157 'opt': option, 158 'wp': workplace, 159 'cookie': cookie, 160 'def': cfg_DEFAULT 161 } 162 163 if cfg_table_type_suffix == 'data': 164 sql_return_type = '' 165 else: 166 sql_return_type = gmTools.coalesce ( 167 value2test = sql_return_type, 168 return_instead = '', 169 template4value = '::%s' 170 ) 171 172 # 1) search value with explicit workplace and current user 173 where_parts = [ 174 'vco.owner = CURRENT_USER', 175 'vco.workplace = %(wp)s', 176 'vco.option = %(opt)s' 177 ] 178 where_parts.append(gmTools.coalesce ( 179 value2test = cookie, 180 return_instead = 'vco.cookie is null', 181 template4value = 'vco.cookie = %(cookie)s' 182 )) 183 cmd = "select vco.value%s from cfg.v_cfg_opts_%s vco where %s limit 1" % ( 184 sql_return_type, 185 cfg_table_type_suffix, 186 ' and '.join(where_parts) 187 ) 188 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 189 if len(rows) > 0: 190 if cfg_table_type_suffix == 'data': 191 return pickle.loads(str(rows[0][0])) 192 return rows[0][0] 193 194 _log.warning('no user AND workplace specific value for option [%s] in config database' % option) 195 196 # 2) search value with biased query 197 if bias == 'user': 198 # did *I* set this option on *any* workplace ? 199 where_parts = [ 200 'vco.option = %(opt)s', 201 'vco.owner = CURRENT_USER', 202 ] 203 else: 204 # did *anyone* set this option on *this* workplace ? 205 where_parts = [ 206 'vco.option = %(opt)s', 207 'vco.workplace = %(wp)s' 208 ] 209 where_parts.append(gmTools.coalesce ( 210 value2test = cookie, 211 return_instead = 'vco.cookie is null', 212 template4value = 'vco.cookie = %(cookie)s' 213 )) 214 cmd = "select vco.value%s from cfg.v_cfg_opts_%s vco where %s" % ( 215 sql_return_type, 216 cfg_table_type_suffix, 217 ' and '.join(where_parts) 218 ) 219 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 220 if len(rows) > 0: 221 # set explicitely for user/workplace 222 self.set ( 223 workplace = workplace, 224 cookie = cookie, 225 option = option, 226 value = rows[0][0] 227 ) 228 if cfg_table_type_suffix == 'data': 229 return pickle.loads(str(rows[0][0])) 230 return rows[0][0] 231 232 _log.warning('no user OR workplace specific value for option [%s] in config database' % option) 233 234 # 3) search value within default site policy 235 where_parts = [ 236 'vco.owner = %(def)s', 237 'vco.workplace = %(def)s', 238 'vco.option = %(opt)s' 239 ] 240 cmd = "select vco.value%s from cfg.v_cfg_opts_%s vco where %s" % ( 241 sql_return_type, 242 cfg_table_type_suffix, 243 ' and '.join(where_parts) 244 ) 245 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}]) 246 if len(rows) > 0: 247 # set explicitely for user/workplace 248 self.set ( 249 workplace = workplace, 250 cookie = cookie, 251 option = option, 252 value = rows[0]['value'] 253 ) 254 if cfg_table_type_suffix == 'data': 255 return pickle.loads(str(rows[0]['value'])) 256 return rows[0]['value'] 257 258 _log.warning('no default site policy value for option [%s] in config database' % option) 259 260 # 4) not found, set default ? 261 if default is None: 262 _log.warning('no default value for option [%s] supplied by caller' % option) 263 return None 264 _log.info('setting option [%s] to default [%s]' % (option, default)) 265 success = self.set ( 266 workplace = workplace, 267 cookie = cookie, 268 option = option, 269 value = default 270 ) 271 if not success: 272 return None 273 274 return default
275 #-----------------------------------------------
276 - def getID(self, workplace = None, cookie = None, option = None):
277 """Get config value from database. 278 279 - unset arguments are assumed to mean database defaults except for <cookie> 280 """ 281 # sanity checks 282 if option is None: 283 _log.error("Need to know which option to retrieve.") 284 return None 285 286 alias = self.__make_alias(workplace, 'CURRENT_USER', cookie, option) 287 288 # construct query 289 where_parts = [ 290 'vco.option=%(opt)s', 291 'vco.workplace=%(wplace)s' 292 ] 293 where_args = { 294 'opt': option, 295 'wplace': workplace 296 } 297 if workplace is None: 298 where_args['wplace'] = cfg_DEFAULT 299 300 where_parts.append('vco.owner=CURRENT_USER') 301 302 if cookie is not None: 303 where_parts.append('vco.cookie=%(cookie)s') 304 where_args['cookie'] = cookie 305 where_clause = ' and '.join(where_parts) 306 cmd = """ 307 select vco.pk_cfg_item 308 from cfg.v_cfg_options vco 309 where %s 310 limit 1""" % where_clause 311 312 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], return_data=True) 313 if len(rows) == 0: 314 _log.warning('option definition for [%s] not in config database' % alias) 315 return None 316 return rows[0][0]
317 #----------------------------
318 - def set(self, workplace = None, cookie = None, option = None, value = None):
319 """Set (insert or update) option value in database. 320 321 Any parameter that is None will be set to the database default. 322 323 Note: you can't change the type of a parameter once it has been 324 created in the backend. If you want to change the type you will 325 have to delete the parameter and recreate it using the new type. 326 """ 327 # sanity checks 328 if None in [option, value]: 329 raise ValueError('invalid arguments (option=<%s>, value=<%s>)' % (option, value)) 330 331 rw_conn = gmPG2.get_connection(readonly=False) 332 333 alias = self.__make_alias(workplace, 'CURRENT_USER', cookie, option) 334 335 opt_value = value 336 sql_type_cast = '' 337 if isinstance(value, str): 338 sql_type_cast = '::text' 339 elif isinstance(value, bool): 340 opt_value = int(opt_value) 341 elif isinstance(value, (float, int, decimal.Decimal, bool)): 342 sql_type_cast = '::numeric' 343 elif isinstance(value, list): 344 # there can be different syntaxes for list types so don't try to cast them 345 pass 346 elif isinstance(value, buffer): 347 # can go directly into bytea 348 pass 349 else: 350 try: 351 opt_value = gmPG2.dbapi.Binary(pickle.dumps(value)) 352 sql_type_cast = '::bytea' 353 except pickle.PicklingError: 354 _log.error("cannot pickle option of type [%s] (key: %s, value: %s)", type(value), alias, str(value)) 355 raise 356 except: 357 _log.error("don't know how to store option of type [%s] (key: %s, value: %s)", type(value), alias, str(value)) 358 raise 359 360 cmd = 'select cfg.set_option(%%(opt)s, %%(val)s%s, %%(wp)s, %%(cookie)s, NULL)' % sql_type_cast 361 args = { 362 'opt': option, 363 'val': opt_value, 364 'wp': workplace, 365 'cookie': cookie 366 } 367 try: 368 rows, idx = gmPG2.run_rw_queries(link_obj=rw_conn, queries=[{'cmd': cmd, 'args': args}], return_data=True) 369 result = rows[0][0] 370 except: 371 _log.exception('cannot set option') 372 result = False 373 374 rw_conn.commit() # will rollback if transaction failed 375 rw_conn.close() 376 377 return result
378 379 #-------------------------------------------
380 - def getAllParams(self, user = None, workplace = cfg_DEFAULT):
381 """Get names of all stored parameters for a given workplace/(user)/cookie-key. 382 This will be used by the ConfigEditor object to create a parameter tree. 383 """ 384 # if no workplace given: any workplace (= cfg_DEFAULT) 385 where_snippets = [ 386 'cfg_template.pk=cfg_item.fk_template', 387 'cfg_item.workplace=%(wplace)s' 388 ] 389 where_args = {'wplace': workplace} 390 391 # if no user given: current db user 392 if user is None: 393 where_snippets.append('cfg_item.owner=CURRENT_USER') 394 else: 395 where_snippets.append('cfg_item.owner=%(usr)s') 396 where_args['usr'] = user 397 398 where_clause = ' and '.join(where_snippets) 399 400 cmd = """ 401 select name, cookie, owner, type, description 402 from cfg.cfg_template, cfg.cfg_item 403 where %s""" % where_clause 404 405 # retrieve option definition 406 rows, idx = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': where_args}], return_data=True) 407 return rows
408 409 #----------------------------
410 - def delete(self, conn=None, pk_option=None):
411 if conn is None: 412 # without a gm-dbo connection you can only delete your own options :-) 413 cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s AND owner = CURRENT_USER" 414 else: 415 cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s" 416 args = {'pk': pk_option} 417 gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], end_tx = True)
418 #----------------------------
419 - def delete_old(self, workplace = None, cookie = None, option = None):
420 """ 421 Deletes an option or a whole group. 422 Note you have to call store() in order to save 423 the changes. 424 """ 425 if option is None: 426 raise ValueError('<option> cannot be None') 427 428 if cookie is None: 429 cmd = """ 430 delete from cfg.cfg_item where 431 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and 432 owner = CURRENT_USER and 433 workplace = %(wp)s and 434 cookie is Null 435 """ 436 else: 437 cmd = """ 438 delete from cfg.cfg_item where 439 fk_template=(select pk from cfg.cfg_template where name = %(opt)s) and 440 owner = CURRENT_USER and 441 workplace = %(wp)s and 442 cookie = %(cookie)s 443 """ 444 args = {'opt': option, 'wp': workplace, 'cookie': cookie} 445 gmPG2.run_rw_queries(queries=[{'cmd': cmd, 'args': args}]) 446 return True
447 #----------------------------
448 - def __make_alias(self, workplace, user, cookie, option):
449 return '%s-%s-%s-%s' % (workplace, user, cookie, option)
450 #===================================================================
451 -def getDBParam(workplace = None, cookie = None, option = None):
452 """Convenience function to get config value from database. 453 454 will search for context dependant match in this order: 455 - CURRENT_USER_CURRENT_WORKPLACE 456 - CURRENT_USER_DEFAULT_WORKPLACE 457 - DEFAULT_USER_CURRENT_WORKPLACE 458 - DEFAULT_USER_DEFAULT_WORKPLACE 459 460 We assume that the config tables are found on service "default". 461 That way we can handle the db connection inside this function. 462 463 Returns (value, set) of first match. 464 """ 465 466 # FIXME: depending on set store for user ... 467 468 if option is None: 469 return (None, None) 470 471 # connect to database (imports gmPG2 if need be) 472 dbcfg = cCfgSQL() 473 474 # (set_name, user, workplace) 475 sets2search = [] 476 if workplace is not None: 477 sets2search.append(['CURRENT_USER_CURRENT_WORKPLACE', None, workplace]) 478 sets2search.append(['CURRENT_USER_DEFAULT_WORKPLACE', None, None]) 479 if workplace is not None: 480 sets2search.append(['DEFAULT_USER_CURRENT_WORKPLACE', cfg_DEFAULT, workplace]) 481 sets2search.append(['DEFAULT_USER_DEFAULT_WORKPLACE', cfg_DEFAULT, None]) 482 # loop over sets 483 matchingSet = None 484 result = None 485 for set in sets2search: 486 result = dbcfg.get( 487 workplace = set[2], 488 user = set[1], 489 option = option, 490 cookie = cookie 491 ) 492 if result is not None: 493 matchingSet = set[0] 494 break 495 _log.debug('[%s] not found for [%s@%s]' % (option, set[1], set[2])) 496 497 # cleanup 498 if matchingSet is None: 499 _log.warning('no config data for [%s]' % option) 500 return (result, matchingSet)
501 #-------------------------------------------------------------
502 -def setDBParam(workplace = None, user = None, cookie = None, option = None, value = None):
503 """Convenience function to store config values in database. 504 505 We assume that the config tables are found on service "default". 506 That way we can handle the db connection inside this function. 507 508 Omitting any parameter (or setting to None) will store database defaults for it. 509 510 - returns True/False 511 """ 512 # connect to database 513 dbcfg = cCfgSQL() 514 # set value 515 success = dbcfg.set( 516 workplace = workplace, 517 user = user, 518 option = option, 519 value = value 520 ) 521 522 if not success: 523 return False 524 return True
525 #============================================================= 526 # main 527 #============================================================= 528 if __name__ == "__main__": 529 530 if len(sys.argv) < 2: 531 sys.exit() 532 533 if sys.argv[1] != 'test': 534 sys.exit() 535 536 root = logging.getLogger() 537 root.setLevel(logging.DEBUG) 538 #---------------------------------------------------------
539 - def test_get_all_options():
540 for opt in get_all_options(): 541 print('%s (%s): %s (%s@%s)' % (opt['option'], opt['type'], opt['value'], opt['owner'], opt['workplace']))
542 # print(u' %s' % opt['description']) 543 # print(u' %s on %s' % (opt['owner'], opt['workplace'])) 544 #---------------------------------------------------------
545 - def test_db_cfg():
546 print("testing database config") 547 print("=======================") 548 549 myDBCfg = cCfgSQL() 550 551 print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace')) 552 print("font is initially:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')) 553 print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace')) 554 print("font after set():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')) 555 print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace')) 556 print("font after delete():", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')) 557 print("font after get() with default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'WingDings')) 558 print("font right after get() with another default:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'default: Courier')) 559 print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace')) 560 print("font after set() on existing option:", myDBCfg.get2(option = 'font name', workplace = 'test workplace', bias = 'user')) 561 562 print("setting array option") 563 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')) 564 aList = ['val 1', 'val 2'] 565 print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace')) 566 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')) 567 aList = ['val 11', 'val 12'] 568 print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace')) 569 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')) 570 print("delete() works:", myDBCfg.delete(option='test array', workplace='test workplace')) 571 print("array now:", myDBCfg.get2(option = 'test array', workplace = 'test workplace', bias = 'user')) 572 573 print("setting complex option") 574 data = {1: 'line 1', 2: 'line2', 3: {1: 'line3.1', 2: 'line3.2'}, 4: 1234} 575 print("set():", myDBCfg.set(option = "complex option test", value = data, workplace = 'test workplace')) 576 print("complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user')) 577 print("delete() works:", myDBCfg.delete(option = "complex option test", workplace = 'test workplace')) 578 print("complex option now:", myDBCfg.get2(workplace = 'test workplace', option = "complex option test", bias = 'user'))
579 580 #--------------------------------------------------------- 581 test_get_all_options() 582 # try: 583 # test_db_cfg() 584 # except: 585 # _log.exception('test suite failed') 586 # raise 587 588 #============================================================= 589