Module Gnumed.pycommon.gmCfgDB

GNUmed database-backed configuration handling.

Once your software has established database connectivity you can set up a config source from the database. You can limit the option applicability by the constraints "workplace", "user", and "cookie".

GNUmed tries to centralize configuration in the backend as much as possible (as opposed to in client-side files).

Expand source code
"""GNUmed database-backed configuration handling.

Once your software has established database connectivity you can
set up a config source from the database. You can limit the option
applicability by the constraints "workplace", "user", and "cookie".

GNUmed tries to centralize configuration in the backend as
much as possible (as opposed to in client-side files).
"""
#==================================================================
__author__ = "Karsten Hilbert <Karsten.Hilbert@gmx.net>"
__license__ = "GPL v2 or later (details at https://www.gnu.org)"

# standard modules
import sys
import logging

# gnumed modules
if __name__ == '__main__':
        sys.path.insert(0, '../../')
from Gnumed.pycommon import gmPG2


_log = logging.getLogger('gm.cfg')


_DB_CFG = None

#==================================================================
# convenience functions
#------------------------------------------------------------------
def get(option:str=None, workplace:str=None, cookie:str=None, bias:str=None, default=None):
        """Return configuration value.

        Calls get(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().get (
                option = option,
                workplace = workplace,
                cookie = cookie,
                bias = bias,
                default = default
        )

#------------------------------------------------------------------
def get4user(option:str=None, workplace:str=None, cookie:str=None, default=None):
        """Return configuration value.

        Calls get4user(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().get4user (
                option = option,
                workplace = workplace,
                cookie = cookie,
                default = default
        )

#------------------------------------------------------------------
def get4workplace(option:str=None, workplace:str=None, cookie:str=None, default=None):
        """Return configuration value.

        Calls get4workplace(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().get4workplace (
                option = option,
                workplace = workplace,
                cookie = cookie,
                default = default
        )

#------------------------------------------------------------------
def get4site(option:str=None, default=None):
        """Retrieve site-wide configuration option from backend.

        Site-wide means owner=NULL and workplace=NULL.
        """
        return __get_db_cfg_object().get4site(option = option, default = default)

#------------------------------------------------------------------
def set(owner:str=None, workplace:str=None, cookie:str=None, option:str=None, value=None) -> bool:
        """Set configuration value.

        Calls set(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().set (
                owner = owner,
                workplace = workplace,
                cookie = cookie,
                option = option,
                value = value
        )

#------------------------------------------------------------------
def delete(conn=None, pk_option:int=None):
        """Delete configuration value.

        Calls delete(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().delete (
                conn = conn,
                pk_option = pk_option
        )

#------------------------------------------------------------------
def get_all_options(order_by:str=None) -> list:
        """Return a list of all configuration items.

        Args:
                order_by: optionally, columns to order the SQL output by

        Returns:
                A list of all configuration items in the database along with their metadata.
        """
        if order_by is None:
                order_by = ''
        else:
                order_by = 'ORDER BY %s' % order_by
        cmd = 'SELECT * FROM cfg.v_cfg_options %s' % order_by
        rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
        return rows

#------------------------------------------------------------------
def log_all_options() -> None:
        _log.debug('client configuration stored in the database:')
        for opt in get_all_options(order_by = 'option, owner, workplace'):
                _log.debug('option [%s] -- owner [%s] -- workplace [%s]', opt['option'], opt['owner'], opt['workplace'])
                _log.debug(' %s: %s', type(opt['value']), opt['value'])

#==================================================================
def __get_db_cfg_object():
        """Setup a module global instance of cCfgSQL()."""
        global _DB_CFG
        if _DB_CFG is None:
                _DB_CFG = cCfgSQL()
        return _DB_CFG

#==================================================================
class cCfgSQL:
        """Handles configuration options stored in a PostgreSQL database."""

        #-----------------------------------------------
        # external API
        #-----------------------------------------------
        def get4user(self, option:str=None, workplace:str=None, cookie:str=None, default=None):
                """Retrieve configuration option from backend, biased to current user.

                Calls self.get(..., bias = 'user', ...).
                """
                return self.get (
                        option = option,
                        workplace = workplace,
                        cookie = cookie,
                        bias = 'user',
                        default = default
                )

        #-----------------------------------------------
        def get4workplace(self, option:str=None, workplace:str=None, cookie:str=None, default=None):
                """Retrieve configuration option from backend, biased to workplace.

                Calls self.get(..., bias = 'workplace', ...).
                """
                return self.get (
                        option = option,
                        workplace = workplace,
                        cookie = cookie,
                        bias = 'workplace',
                        default = default
                )

        #-----------------------------------------------
        def get4site(self, option:str=None, default=None):
                """Retrieve site-wide configuration option from backend."""
                return self.__get4site(option = option, default = default)

        #-----------------------------------------------
        def get(self, option:str=None, workplace:str=None, cookie:str=None, bias:str=None, default=None):
                """Retrieve configuration option from backend for current user.

                This method will look for option values in a
                more-specific to less-specific order, namely

                1) specific to the workplace, if given, plus the current user

                2) either
                        if bias is "user", specific to the current user, *regardless* of workplace
                        ("Did *I* set the option anywhere on this site ? If so, use that value.")
                   OR
                        if bias is "workplace", specific to the given workplace, *regardless* of user
                        "Did anyone set the option for *this workplace* ? If so, use that value."
                  OR
                        if bias is None, skip biased search

                3) explicitely not specific to any user or workplace (both being searched
                   as NULL), IOW the site-wide default

                When no value is found at all, the default (if given) is stored in the
                database as site-wide default and returned.

                Args:
                        option: the configuration item name
                        workplace: the workplace for which to retrieve the value, None = site-wide default
                        cookie: an opaque value further restricting the scope of the configuration item value search, say, a particular patient's ID
                        bias: the "direction" into which to search for config options
                                'user': When no value is found for "current_user/workplace" look for a value for "current_user" regardless of workspace.
                                'workplace': When no value is found for "current_user/workplace" look for a value for "workplace" regardless of user.
                        default: the default configuration value to use if no value is found

                Returns:
                        The configuration value found, or the default, or else None.
                """
                _log.debug('option [%s], workplace [%s], cookie [%s], bias [%s], default [%s]', option, workplace, cookie, bias, default)
                if option is None:
                        raise ValueError('<option> (name) must not be None')
                if bias not in ['user', 'workplace', None]:
                        raise ValueError('<bias> must be "user" or "workplace", or None')
                if (bias == 'workplace') and not workplace:
                        raise ValueError('if <bias> is "workplace", then <workplace> must not be None or empty')
                if (bias == 'workplace') and (workplace.strip() == ''):
                        raise ValueError('if <bias> is "workplace", then <workplace> must not be empty')

                args = {
                        'opt': option,
                        'wp': workplace,
                        'cookie': cookie
                }
                # 1) search value with explicit workplace and current user
                where_parts = [
                        'c_vco.option = %(opt)s',
                        'c_vco.owner = CURRENT_USER',
                        'c_vco.workplace = %(wp)s'
                ]
                if cookie:
                        where_parts.append('c_vco.cookie = %(cookie)s')
                cmd = 'SELECT * FROM cfg.v_cfg_options c_vco WHERE %s LIMIT 1' % (' AND '.join(where_parts))
                rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
                if rows:
                        return self.__auto_heal_pseudo_boolean_setting(setting = rows[0], default = default)

                # 2) search value with biased query
                _log.warning('no user+workplace specific value for option [%s] in database', option)
                value = self.__get_with_bias (
                        option = option,
                        workplace = workplace,
                        cookie = cookie,
                        bias = bias,
                        default = default
                )
                if value is not None:
                        return value

                # 3) search site-wide default value, or default, if given
                _log.warning('no %s-biased value for option [%s] in database, or no bias given', bias, option)
                return self.__get4site(option = option, default = default)

        #----------------------------
        def set(self, owner:str=None, workplace:str=None, cookie:str=None, option:str=None, value=None, description:str=None) -> bool:
                """Set (create or update) option+value in database.

                Any argument that is None will be set to NULL, meaning site-wide default.

                Note:
                        *value* cannot be None (and thusly map to NULL) as None is
                        used to denote "no value found" in .get*() methods

                Args:
                        owner: the user this value applies to,
                                '' -> CURRENT_USER,
                                None -> NULL=site-wide default,
                        workplace: the workplace the value applies to,
                                None -> NULL=site-wide default,
                        option: the configuration item name
                        cookie: an opaque value further restricting the scope of the configuration item value, say, a particular patient's ID
                        value: the actual configuration value, anything that can be turned into JSON will be returned as it was stored

                Returns:
                        True/False based on success.
                """
                if None in [option, value]:
                        raise ValueError('invalid arguments (option=<%s>, value=<%s>)' % (option, value))

                args = {
                        'opt': option,
                        'val': gmPG2.dbapi.extras.Json(value),
                        'wp': workplace,
                        'cookie': cookie,
                        'usr': owner,
                        'desc': description
                }
                _log.debug('value:  [%s]', value)
                _log.debug('JSON: >>>%s<<<', args['val'])
                SQL = 'SELECT cfg.set_option(%(opt)s, %(val)s, %(wp)s, %(cookie)s, %(usr)s, %(desc)s)'
                queries = [{'cmd': SQL, 'args': args}]
                try:
                        rows = gmPG2.run_rw_queries(queries = queries, return_data = True)
                        result = rows[0][0]
                except Exception:
                        _log.exception('cannot set option: [%s]=<%s>', option, value)
                        result = False
                return result

        #-----------------------------------------------
        def delete(self, conn=None, pk_option:int=None):
                """Delete configuration value from database.

                Args:
                        conn: optionally, a connection to use, note that you can only
                                delete your own options, unless conn belongs to gm-dbo
                        pk_option: primary key in cfg.cfg_item
                """
                if conn is None:
                        cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s AND owner = CURRENT_USER"
                else:
                        cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s"
                args = {'pk': pk_option}
                gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], end_tx = True)

        #-----------------------------------------------
        # helper functions
        #-----------------------------------------------
        def __auto_heal_pseudo_boolean_setting(self, setting=None, default=None):
                if not isinstance(default, bool):
                        # apparently not intended to be boolean, leave alone
                        return setting['value']

                _log.debug('current setting [%s], default [%s]', setting, default)
                if setting['value'] not in [0,1]:
                        _log.error('default suggests boolean, but current setting not 0/1, returning bool() of current value, but leaving database unchanged')
                        return bool(setting['value'])

                _log.debug('auto-healing boolean from 0/1 to False/True')
                self.set (
                        owner = setting['owner'],
                        workplace = setting['workplace'],
                        cookie = setting['cookie'],
                        option = setting['option'],
                        value = bool(setting['value'])
                )
                return bool(setting['value'])

        #-----------------------------------------------
        def __get_with_bias(self, option:str=None, workplace:str=None, cookie:str=None, bias:str=None, default=None):
                if bias is None:
                        return None

                args = {
                        'opt': option,
                        'wp': workplace,
                        'cookie': cookie
                }
                where_parts = ['c_vco.option = %(opt)s']
                if cookie:
                        where_parts.append('c_vco.cookie = %(cookie)s')
                if bias == 'user':
                        # "Did *I* set this option for *any* workplace ?"
                        where_parts.append('c_vco.owner = CURRENT_USER')
                elif bias == 'workplace':
                        # "Did *anyone* set this option for *this* workplace ?"
                        where_parts.append('c_vco.workplace = %(wp)s')
                else:
                        # well ...
                        raise ValueError('<bias> must be "user" or "workplace", or None')

                cmd = 'SELECT * FROM cfg.v_cfg_options c_vco WHERE %s LIMIT 1' % (' AND '.join(where_parts))
                rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
                if rows:
                        return self.__auto_heal_pseudo_boolean_setting(setting = rows[0], default = default)

                return None

        #-----------------------------------------------
        def __get4site(self, option:str=None, default=None):
                _log.debug('option [%s], default [%s]', option, default)
                args = {'opt': option}
                where_parts = [
                        'c_vco.owner IS NULL',
                        'c_vco.workplace IS NULL',
                        'c_vco.option = %(opt)s'
                ]
                cmd = 'SELECT * FROM cfg.v_cfg_options c_vco WHERE %s LIMIT 1' % (' AND '.join(where_parts))
                rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
                if not rows:
                        _log.warning('no site-wide default value for option [%s] in database' % option)
                        if default is None:
                                _log.warning('no default value for option [%s] supplied by caller' % option)
                                return None

                        _log.info('setting site-wide default for option [%s] to [%s]' % (option, default))
                        self.set(option = option, value = default)
                        return default

                return self.__auto_heal_pseudo_boolean_setting(setting = rows[0], default = default)

#=============================================================
# main
#=============================================================
if __name__ == "__main__":

        if len(sys.argv) < 2:
                sys.exit()

        if sys.argv[1] != 'test':
                sys.exit()

        root = logging.getLogger()
        root.setLevel(logging.DEBUG)
        #---------------------------------------------------------
        def test_get_all_options():
                for opt in get_all_options():
                        print('')
                        print('%s -- %s@%s -- "%s"' % (type(opt['value']), opt['owner'], opt['workplace'], opt['option']))
                        print('  value: %s' % opt['value'])
                        print('  descr: %s' % opt['description'])

        #---------------------------------------------------------
        def test_set():
                print('set: ', set(option = 'test', value = 'mal sehen 4'))
                print(get(option = 'test'))

        #---------------------------------------------------------
        def test_db_cfg():
                print("testing database config")
                print("=======================")

                myDBCfg = cCfgSQL()

#               print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace'))
                print("font is initially:", myDBCfg.get(option = 'font name', workplace = 'test workplace', bias = 'user'))
                print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace'))
                print("font after set():", myDBCfg.get(option = 'font name', workplace = 'test workplace', bias = 'user'))
#               print("delete() works:", myDBCfg.delete(option='font name', workplace = 'test workplace'))
                print("font after delete():", myDBCfg.get(option = 'font name', workplace = 'test workplace', bias = 'user'))
                print("font after get() with default:", myDBCfg.get(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'WingDings'))
                print("font right after get() with another default:", myDBCfg.get(option = 'font name', workplace = 'test workplace', bias = 'user', default = 'default: Courier'))
                print("set() works:", myDBCfg.set(option='font name', value="Times New Roman", workplace = 'test workplace'))
                print("font after set() on existing option:", myDBCfg.get(option = 'font name', workplace = 'test workplace', bias = 'user'))

                print("setting array option")
                print("array now:", myDBCfg.get(option = 'test array', workplace = 'test workplace', bias = 'user'))
                aList = ['val 1', 'val 2']
                print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace'))
                print("array now:", myDBCfg.get(option = 'test array', workplace = 'test workplace', bias = 'user'))
                aList = ['val 11', 'val 12']
                print("set():", myDBCfg.set(option='test array', value = aList, workplace = 'test workplace'))
                print("array now:", myDBCfg.get(option = 'test array', workplace = 'test workplace', bias = 'user'))
#               print("delete() works:", myDBCfg.delete(option='test array', workplace='test workplace'))
                print("array now:", myDBCfg.get(option = 'test array', workplace = 'test workplace', bias = 'user'))

                print("setting complex option")
                data = {1: 'line 1', 2: 'line2', 3: {1: 'line3.1', 2: 'line3.2'}, 4: 1234}
                print("set():", myDBCfg.set(option = "complex option test", value = data, workplace = 'test workplace'))
                print("complex option now:", myDBCfg.get(workplace = 'test workplace', option = "complex option test", bias = 'user'))
#               print("delete() works:", myDBCfg.delete(option = "complex option test", workplace = 'test workplace'))
                print("complex option now:", myDBCfg.get(workplace = 'test workplace', option = "complex option test", bias = 'user'))

        #---------------------------------------------------------
        gmPG2.request_login_params(setup_pool = True)

        log_all_options()
        #test_get_all_options()
        #test_set()
        #test_db_cfg()

Functions

def delete(conn=None, pk_option: int = None)

Delete configuration value.

Calls delete(…) on a module global instance of cCfgSQL().

Expand source code
def delete(conn=None, pk_option:int=None):
        """Delete configuration value.

        Calls delete(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().delete (
                conn = conn,
                pk_option = pk_option
        )
def get(option: str = None, workplace: str = None, cookie: str = None, bias: str = None, default=None)

Return configuration value.

Calls get(…) on a module global instance of cCfgSQL().

Expand source code
def get(option:str=None, workplace:str=None, cookie:str=None, bias:str=None, default=None):
        """Return configuration value.

        Calls get(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().get (
                option = option,
                workplace = workplace,
                cookie = cookie,
                bias = bias,
                default = default
        )
def get4site(option: str = None, default=None)

Retrieve site-wide configuration option from backend.

Site-wide means owner=NULL and workplace=NULL.

Expand source code
def get4site(option:str=None, default=None):
        """Retrieve site-wide configuration option from backend.

        Site-wide means owner=NULL and workplace=NULL.
        """
        return __get_db_cfg_object().get4site(option = option, default = default)
def get4user(option: str = None, workplace: str = None, cookie: str = None, default=None)

Return configuration value.

Calls get4user(…) on a module global instance of cCfgSQL().

Expand source code
def get4user(option:str=None, workplace:str=None, cookie:str=None, default=None):
        """Return configuration value.

        Calls get4user(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().get4user (
                option = option,
                workplace = workplace,
                cookie = cookie,
                default = default
        )
def get4workplace(option: str = None, workplace: str = None, cookie: str = None, default=None)

Return configuration value.

Calls get4workplace(…) on a module global instance of cCfgSQL().

Expand source code
def get4workplace(option:str=None, workplace:str=None, cookie:str=None, default=None):
        """Return configuration value.

        Calls get4workplace(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().get4workplace (
                option = option,
                workplace = workplace,
                cookie = cookie,
                default = default
        )
def get_all_options(order_by: str = None) ‑> list

Return a list of all configuration items.

Args

order_by
optionally, columns to order the SQL output by

Returns

A list of all configuration items in the database along with their metadata.

Expand source code
def get_all_options(order_by:str=None) -> list:
        """Return a list of all configuration items.

        Args:
                order_by: optionally, columns to order the SQL output by

        Returns:
                A list of all configuration items in the database along with their metadata.
        """
        if order_by is None:
                order_by = ''
        else:
                order_by = 'ORDER BY %s' % order_by
        cmd = 'SELECT * FROM cfg.v_cfg_options %s' % order_by
        rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd}])
        return rows
def log_all_options() ‑> None
Expand source code
def log_all_options() -> None:
        _log.debug('client configuration stored in the database:')
        for opt in get_all_options(order_by = 'option, owner, workplace'):
                _log.debug('option [%s] -- owner [%s] -- workplace [%s]', opt['option'], opt['owner'], opt['workplace'])
                _log.debug(' %s: %s', type(opt['value']), opt['value'])
def set(owner: str = None, workplace: str = None, cookie: str = None, option: str = None, value=None) ‑> bool

Set configuration value.

Calls set(…) on a module global instance of cCfgSQL().

Expand source code
def set(owner:str=None, workplace:str=None, cookie:str=None, option:str=None, value=None) -> bool:
        """Set configuration value.

        Calls set(...) on a module global instance of cCfgSQL().
        """
        return __get_db_cfg_object().set (
                owner = owner,
                workplace = workplace,
                cookie = cookie,
                option = option,
                value = value
        )

Classes

class cCfgSQL

Handles configuration options stored in a PostgreSQL database.

Expand source code
class cCfgSQL:
        """Handles configuration options stored in a PostgreSQL database."""

        #-----------------------------------------------
        # external API
        #-----------------------------------------------
        def get4user(self, option:str=None, workplace:str=None, cookie:str=None, default=None):
                """Retrieve configuration option from backend, biased to current user.

                Calls self.get(..., bias = 'user', ...).
                """
                return self.get (
                        option = option,
                        workplace = workplace,
                        cookie = cookie,
                        bias = 'user',
                        default = default
                )

        #-----------------------------------------------
        def get4workplace(self, option:str=None, workplace:str=None, cookie:str=None, default=None):
                """Retrieve configuration option from backend, biased to workplace.

                Calls self.get(..., bias = 'workplace', ...).
                """
                return self.get (
                        option = option,
                        workplace = workplace,
                        cookie = cookie,
                        bias = 'workplace',
                        default = default
                )

        #-----------------------------------------------
        def get4site(self, option:str=None, default=None):
                """Retrieve site-wide configuration option from backend."""
                return self.__get4site(option = option, default = default)

        #-----------------------------------------------
        def get(self, option:str=None, workplace:str=None, cookie:str=None, bias:str=None, default=None):
                """Retrieve configuration option from backend for current user.

                This method will look for option values in a
                more-specific to less-specific order, namely

                1) specific to the workplace, if given, plus the current user

                2) either
                        if bias is "user", specific to the current user, *regardless* of workplace
                        ("Did *I* set the option anywhere on this site ? If so, use that value.")
                   OR
                        if bias is "workplace", specific to the given workplace, *regardless* of user
                        "Did anyone set the option for *this workplace* ? If so, use that value."
                  OR
                        if bias is None, skip biased search

                3) explicitely not specific to any user or workplace (both being searched
                   as NULL), IOW the site-wide default

                When no value is found at all, the default (if given) is stored in the
                database as site-wide default and returned.

                Args:
                        option: the configuration item name
                        workplace: the workplace for which to retrieve the value, None = site-wide default
                        cookie: an opaque value further restricting the scope of the configuration item value search, say, a particular patient's ID
                        bias: the "direction" into which to search for config options
                                'user': When no value is found for "current_user/workplace" look for a value for "current_user" regardless of workspace.
                                'workplace': When no value is found for "current_user/workplace" look for a value for "workplace" regardless of user.
                        default: the default configuration value to use if no value is found

                Returns:
                        The configuration value found, or the default, or else None.
                """
                _log.debug('option [%s], workplace [%s], cookie [%s], bias [%s], default [%s]', option, workplace, cookie, bias, default)
                if option is None:
                        raise ValueError('<option> (name) must not be None')
                if bias not in ['user', 'workplace', None]:
                        raise ValueError('<bias> must be "user" or "workplace", or None')
                if (bias == 'workplace') and not workplace:
                        raise ValueError('if <bias> is "workplace", then <workplace> must not be None or empty')
                if (bias == 'workplace') and (workplace.strip() == ''):
                        raise ValueError('if <bias> is "workplace", then <workplace> must not be empty')

                args = {
                        'opt': option,
                        'wp': workplace,
                        'cookie': cookie
                }
                # 1) search value with explicit workplace and current user
                where_parts = [
                        'c_vco.option = %(opt)s',
                        'c_vco.owner = CURRENT_USER',
                        'c_vco.workplace = %(wp)s'
                ]
                if cookie:
                        where_parts.append('c_vco.cookie = %(cookie)s')
                cmd = 'SELECT * FROM cfg.v_cfg_options c_vco WHERE %s LIMIT 1' % (' AND '.join(where_parts))
                rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
                if rows:
                        return self.__auto_heal_pseudo_boolean_setting(setting = rows[0], default = default)

                # 2) search value with biased query
                _log.warning('no user+workplace specific value for option [%s] in database', option)
                value = self.__get_with_bias (
                        option = option,
                        workplace = workplace,
                        cookie = cookie,
                        bias = bias,
                        default = default
                )
                if value is not None:
                        return value

                # 3) search site-wide default value, or default, if given
                _log.warning('no %s-biased value for option [%s] in database, or no bias given', bias, option)
                return self.__get4site(option = option, default = default)

        #----------------------------
        def set(self, owner:str=None, workplace:str=None, cookie:str=None, option:str=None, value=None, description:str=None) -> bool:
                """Set (create or update) option+value in database.

                Any argument that is None will be set to NULL, meaning site-wide default.

                Note:
                        *value* cannot be None (and thusly map to NULL) as None is
                        used to denote "no value found" in .get*() methods

                Args:
                        owner: the user this value applies to,
                                '' -> CURRENT_USER,
                                None -> NULL=site-wide default,
                        workplace: the workplace the value applies to,
                                None -> NULL=site-wide default,
                        option: the configuration item name
                        cookie: an opaque value further restricting the scope of the configuration item value, say, a particular patient's ID
                        value: the actual configuration value, anything that can be turned into JSON will be returned as it was stored

                Returns:
                        True/False based on success.
                """
                if None in [option, value]:
                        raise ValueError('invalid arguments (option=<%s>, value=<%s>)' % (option, value))

                args = {
                        'opt': option,
                        'val': gmPG2.dbapi.extras.Json(value),
                        'wp': workplace,
                        'cookie': cookie,
                        'usr': owner,
                        'desc': description
                }
                _log.debug('value:  [%s]', value)
                _log.debug('JSON: >>>%s<<<', args['val'])
                SQL = 'SELECT cfg.set_option(%(opt)s, %(val)s, %(wp)s, %(cookie)s, %(usr)s, %(desc)s)'
                queries = [{'cmd': SQL, 'args': args}]
                try:
                        rows = gmPG2.run_rw_queries(queries = queries, return_data = True)
                        result = rows[0][0]
                except Exception:
                        _log.exception('cannot set option: [%s]=<%s>', option, value)
                        result = False
                return result

        #-----------------------------------------------
        def delete(self, conn=None, pk_option:int=None):
                """Delete configuration value from database.

                Args:
                        conn: optionally, a connection to use, note that you can only
                                delete your own options, unless conn belongs to gm-dbo
                        pk_option: primary key in cfg.cfg_item
                """
                if conn is None:
                        cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s AND owner = CURRENT_USER"
                else:
                        cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s"
                args = {'pk': pk_option}
                gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], end_tx = True)

        #-----------------------------------------------
        # helper functions
        #-----------------------------------------------
        def __auto_heal_pseudo_boolean_setting(self, setting=None, default=None):
                if not isinstance(default, bool):
                        # apparently not intended to be boolean, leave alone
                        return setting['value']

                _log.debug('current setting [%s], default [%s]', setting, default)
                if setting['value'] not in [0,1]:
                        _log.error('default suggests boolean, but current setting not 0/1, returning bool() of current value, but leaving database unchanged')
                        return bool(setting['value'])

                _log.debug('auto-healing boolean from 0/1 to False/True')
                self.set (
                        owner = setting['owner'],
                        workplace = setting['workplace'],
                        cookie = setting['cookie'],
                        option = setting['option'],
                        value = bool(setting['value'])
                )
                return bool(setting['value'])

        #-----------------------------------------------
        def __get_with_bias(self, option:str=None, workplace:str=None, cookie:str=None, bias:str=None, default=None):
                if bias is None:
                        return None

                args = {
                        'opt': option,
                        'wp': workplace,
                        'cookie': cookie
                }
                where_parts = ['c_vco.option = %(opt)s']
                if cookie:
                        where_parts.append('c_vco.cookie = %(cookie)s')
                if bias == 'user':
                        # "Did *I* set this option for *any* workplace ?"
                        where_parts.append('c_vco.owner = CURRENT_USER')
                elif bias == 'workplace':
                        # "Did *anyone* set this option for *this* workplace ?"
                        where_parts.append('c_vco.workplace = %(wp)s')
                else:
                        # well ...
                        raise ValueError('<bias> must be "user" or "workplace", or None')

                cmd = 'SELECT * FROM cfg.v_cfg_options c_vco WHERE %s LIMIT 1' % (' AND '.join(where_parts))
                rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
                if rows:
                        return self.__auto_heal_pseudo_boolean_setting(setting = rows[0], default = default)

                return None

        #-----------------------------------------------
        def __get4site(self, option:str=None, default=None):
                _log.debug('option [%s], default [%s]', option, default)
                args = {'opt': option}
                where_parts = [
                        'c_vco.owner IS NULL',
                        'c_vco.workplace IS NULL',
                        'c_vco.option = %(opt)s'
                ]
                cmd = 'SELECT * FROM cfg.v_cfg_options c_vco WHERE %s LIMIT 1' % (' AND '.join(where_parts))
                rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
                if not rows:
                        _log.warning('no site-wide default value for option [%s] in database' % option)
                        if default is None:
                                _log.warning('no default value for option [%s] supplied by caller' % option)
                                return None

                        _log.info('setting site-wide default for option [%s] to [%s]' % (option, default))
                        self.set(option = option, value = default)
                        return default

                return self.__auto_heal_pseudo_boolean_setting(setting = rows[0], default = default)

Methods

def delete(self, conn=None, pk_option: int = None)

Delete configuration value from database.

Args

conn
optionally, a connection to use, note that you can only delete your own options, unless conn belongs to gm-dbo
pk_option
primary key in cfg.cfg_item
Expand source code
def delete(self, conn=None, pk_option:int=None):
        """Delete configuration value from database.

        Args:
                conn: optionally, a connection to use, note that you can only
                        delete your own options, unless conn belongs to gm-dbo
                pk_option: primary key in cfg.cfg_item
        """
        if conn is None:
                cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s AND owner = CURRENT_USER"
        else:
                cmd = "DELETE FROM cfg.cfg_item WHERE pk = %(pk)s"
        args = {'pk': pk_option}
        gmPG2.run_rw_queries(link_obj = conn, queries = [{'cmd': cmd, 'args': args}], end_tx = True)
def get(self, option: str = None, workplace: str = None, cookie: str = None, bias: str = None, default=None)

Retrieve configuration option from backend for current user.

This method will look for option values in a more-specific to less-specific order, namely

1) specific to the workplace, if given, plus the current user

2) either if bias is "user", specific to the current user, regardless of workplace ("Did I set the option anywhere on this site ? If so, use that value.") OR if bias is "workplace", specific to the given workplace, regardless of user "Did anyone set the option for this workplace ? If so, use that value." OR if bias is None, skip biased search

3) explicitely not specific to any user or workplace (both being searched as NULL), IOW the site-wide default

When no value is found at all, the default (if given) is stored in the database as site-wide default and returned.

Args

option
the configuration item name
workplace
the workplace for which to retrieve the value, None = site-wide default
cookie
an opaque value further restricting the scope of the configuration item value search, say, a particular patient's ID
bias
the "direction" into which to search for config options 'user': When no value is found for "current_user/workplace" look for a value for "current_user" regardless of workspace. 'workplace': When no value is found for "current_user/workplace" look for a value for "workplace" regardless of user.
default
the default configuration value to use if no value is found

Returns

The configuration value found, or the default, or else None.

Expand source code
def get(self, option:str=None, workplace:str=None, cookie:str=None, bias:str=None, default=None):
        """Retrieve configuration option from backend for current user.

        This method will look for option values in a
        more-specific to less-specific order, namely

        1) specific to the workplace, if given, plus the current user

        2) either
                if bias is "user", specific to the current user, *regardless* of workplace
                ("Did *I* set the option anywhere on this site ? If so, use that value.")
           OR
                if bias is "workplace", specific to the given workplace, *regardless* of user
                "Did anyone set the option for *this workplace* ? If so, use that value."
          OR
                if bias is None, skip biased search

        3) explicitely not specific to any user or workplace (both being searched
           as NULL), IOW the site-wide default

        When no value is found at all, the default (if given) is stored in the
        database as site-wide default and returned.

        Args:
                option: the configuration item name
                workplace: the workplace for which to retrieve the value, None = site-wide default
                cookie: an opaque value further restricting the scope of the configuration item value search, say, a particular patient's ID
                bias: the "direction" into which to search for config options
                        'user': When no value is found for "current_user/workplace" look for a value for "current_user" regardless of workspace.
                        'workplace': When no value is found for "current_user/workplace" look for a value for "workplace" regardless of user.
                default: the default configuration value to use if no value is found

        Returns:
                The configuration value found, or the default, or else None.
        """
        _log.debug('option [%s], workplace [%s], cookie [%s], bias [%s], default [%s]', option, workplace, cookie, bias, default)
        if option is None:
                raise ValueError('<option> (name) must not be None')
        if bias not in ['user', 'workplace', None]:
                raise ValueError('<bias> must be "user" or "workplace", or None')
        if (bias == 'workplace') and not workplace:
                raise ValueError('if <bias> is "workplace", then <workplace> must not be None or empty')
        if (bias == 'workplace') and (workplace.strip() == ''):
                raise ValueError('if <bias> is "workplace", then <workplace> must not be empty')

        args = {
                'opt': option,
                'wp': workplace,
                'cookie': cookie
        }
        # 1) search value with explicit workplace and current user
        where_parts = [
                'c_vco.option = %(opt)s',
                'c_vco.owner = CURRENT_USER',
                'c_vco.workplace = %(wp)s'
        ]
        if cookie:
                where_parts.append('c_vco.cookie = %(cookie)s')
        cmd = 'SELECT * FROM cfg.v_cfg_options c_vco WHERE %s LIMIT 1' % (' AND '.join(where_parts))
        rows = gmPG2.run_ro_queries(queries = [{'cmd': cmd, 'args': args}])
        if rows:
                return self.__auto_heal_pseudo_boolean_setting(setting = rows[0], default = default)

        # 2) search value with biased query
        _log.warning('no user+workplace specific value for option [%s] in database', option)
        value = self.__get_with_bias (
                option = option,
                workplace = workplace,
                cookie = cookie,
                bias = bias,
                default = default
        )
        if value is not None:
                return value

        # 3) search site-wide default value, or default, if given
        _log.warning('no %s-biased value for option [%s] in database, or no bias given', bias, option)
        return self.__get4site(option = option, default = default)
def get4site(self, option: str = None, default=None)

Retrieve site-wide configuration option from backend.

Expand source code
def get4site(self, option:str=None, default=None):
        """Retrieve site-wide configuration option from backend."""
        return self.__get4site(option = option, default = default)
def get4user(self, option: str = None, workplace: str = None, cookie: str = None, default=None)

Retrieve configuration option from backend, biased to current user.

Calls self.get(…, bias = 'user', …).

Expand source code
def get4user(self, option:str=None, workplace:str=None, cookie:str=None, default=None):
        """Retrieve configuration option from backend, biased to current user.

        Calls self.get(..., bias = 'user', ...).
        """
        return self.get (
                option = option,
                workplace = workplace,
                cookie = cookie,
                bias = 'user',
                default = default
        )
def get4workplace(self, option: str = None, workplace: str = None, cookie: str = None, default=None)

Retrieve configuration option from backend, biased to workplace.

Calls self.get(…, bias = 'workplace', …).

Expand source code
def get4workplace(self, option:str=None, workplace:str=None, cookie:str=None, default=None):
        """Retrieve configuration option from backend, biased to workplace.

        Calls self.get(..., bias = 'workplace', ...).
        """
        return self.get (
                option = option,
                workplace = workplace,
                cookie = cookie,
                bias = 'workplace',
                default = default
        )
def set(self, owner: str = None, workplace: str = None, cookie: str = None, option: str = None, value=None, description: str = None) ‑> bool

Set (create or update) option+value in database.

Any argument that is None will be set to NULL, meaning site-wide default.

Note

value cannot be None (and thusly map to NULL) as None is used to denote "no value found" in .get*() methods

Args

owner
the user this value applies to, '' -> CURRENT_USER, None -> NULL=site-wide default,
workplace
the workplace the value applies to, None -> NULL=site-wide default,
option
the configuration item name
cookie
an opaque value further restricting the scope of the configuration item value, say, a particular patient's ID
value
the actual configuration value, anything that can be turned into JSON will be returned as it was stored

Returns

True/False based on success.

Expand source code
def set(self, owner:str=None, workplace:str=None, cookie:str=None, option:str=None, value=None, description:str=None) -> bool:
        """Set (create or update) option+value in database.

        Any argument that is None will be set to NULL, meaning site-wide default.

        Note:
                *value* cannot be None (and thusly map to NULL) as None is
                used to denote "no value found" in .get*() methods

        Args:
                owner: the user this value applies to,
                        '' -> CURRENT_USER,
                        None -> NULL=site-wide default,
                workplace: the workplace the value applies to,
                        None -> NULL=site-wide default,
                option: the configuration item name
                cookie: an opaque value further restricting the scope of the configuration item value, say, a particular patient's ID
                value: the actual configuration value, anything that can be turned into JSON will be returned as it was stored

        Returns:
                True/False based on success.
        """
        if None in [option, value]:
                raise ValueError('invalid arguments (option=<%s>, value=<%s>)' % (option, value))

        args = {
                'opt': option,
                'val': gmPG2.dbapi.extras.Json(value),
                'wp': workplace,
                'cookie': cookie,
                'usr': owner,
                'desc': description
        }
        _log.debug('value:  [%s]', value)
        _log.debug('JSON: >>>%s<<<', args['val'])
        SQL = 'SELECT cfg.set_option(%(opt)s, %(val)s, %(wp)s, %(cookie)s, %(usr)s, %(desc)s)'
        queries = [{'cmd': SQL, 'args': args}]
        try:
                rows = gmPG2.run_rw_queries(queries = queries, return_data = True)
                result = rows[0][0]
        except Exception:
                _log.exception('cannot set option: [%s]=<%s>', option, value)
                result = False
        return result