Gnumed.pycommon.gmCfg

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                                  initial = sql_return_type,
    168                                  instead = '',
    169                                  template_initial = '::%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                          initial = cookie,
    180                          instead = 'vco.cookie is null',
    181                          template_initial = '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                          initial = cookie,
    211                          instead = 'vco.cookie is null',
    212                          template_initial = '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