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