#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
Provides base classes for accessing MythTV
"""

from MythStatic import *

import os, re, socket, MySQLdb
import xml.dom.minidom as minidom
from datetime import datetime
from urllib import urlopen


class DictData( object ):
    """
    Basic class for providing management of data resembling dictionaries.
    Must be subclassed for use.
    """
    # Requires two lists of equal length to describe input fields
    #   The first list is one of field names
    #   The second list is one of field types
    #     0: integer
    #     1: float
    #     2: bool
    #     3: string
    #     4: date -- processed by datetime.fromtimestamp(int(value))
    #   The second list can be a single string 'Pass'
    #               to pass all values untouched
    
    #requires:
    #  field_order
    #  field_type

    ### class emulation functions
    def __getattr__(self,name):
        # function for class attribute access of data fields
        # accesses real attributes, falls back to data fields, and errors
        if name in self.__dict__:
            return self.__dict__[name]
        elif name in self.field_order:
            return self.data[name]
        else:
            raise AttributeError("'DictData' object has no attribute '%s'"%name)

    def __setattr__(self,name,value):
        # function for class attribute access of data fields
        # sets value in data fields, falling back to real attributes
        if name in self.field_order:
            self.data[name] = value
        else:
            self.__dict__[name] = value

    ### container emulation functions
    def __getitem__(self,key):
        # function for list-like and dist-like access to content
        # as a side effect of dual operation, dict data cannot be indexed
        #       keyed off integer values
        if isinstance(key,str):
            if key in self.data:
                return self.data[key]
            else:
                raise KeyError("'DictData' object has no key '%s'" %key)
        elif isinstance(key,int):
            if key in range(0,len(self.field_order)):
                if self.field_order[key] in self.data:
                    return self.data[self.field_order[key]]
            else:
                raise IndexError("Index out of bounds")
        raise TypeError('Data access must use string or integer')

    def __setitem__(self,key,value):
        # function for list-like and dist-like access to content
        # does not allow setting of data not already in field_order
        if isinstance(key,str):
            if key in self.field_order:
                self.data[key] = value
            else:
                raise KeyError("'DictData' does not allow new data")
        elif isinstance(key,int):
            if key in range(0,len(self.field_order)):
                self.data[self.field_order[key]] = value
            else:
                raise IndexError("'DictData' does not allow new data")
        else:
            raise KeyError('Value not usable in DictData')
            
    def __contains__(self,item):
        return bool(item in self.data.keys())

    def __iter__(self):
        self.field = 0
        self.itermode = 0
        return self
        
    def iterkeys(self):
        self.field = 0
        self.itermode = 1
        return self
        
    def itervalues(self):
        self.field = 0
        self.itermode = 0
        return self
    
    def iteriterms(self):
        self.field = 0
        self.itermode = 2
        return self

    def next(self):
        if self.field == len(self.field_order):
            del self.field
            del self.itermode
            raise StopIteration
        self.field += 1
        if self.itermode == 0:
            return self.data[self.field_order[self.field-1]]
        elif self.itermode == 1:
            return self.field_order[self.field-1]
        elif self.itermode == 2:
            return (self.field_order[self.field-1],
                    self.data[self.field_order[self.field-1]])
        
    def keys(self):
        return self.field_order
        
    def has_key(self,item):
        return self.__contains__(item)
        
    def values(self):
        return list(self)
        
    def get(self,key):
        return self.data[key]
        
    def items(self):
        pairs = []
        for field in self.field_order:
            pairs.append((field, self.data[field]))
        return tuple(pairs)
        
    def pop(self,key):
        raise NotImplementedError
    
    def popitem(self):
        raise NotImplementedError
        
    def update(self, *args, **keywords):
        # accepts database, iterable, or keywords to update the data
        if self.data is None: self.data = {}
        self.data.update(apply(self._distill, args, keywords))

    ### additional management functions
    def _setDefs(self):
        self.data = {}
        self.field = 0
        
    def __init__(self, data):
        self._setDefs()
        self.data.update(self._distill(self._process(data)))

    def _process(self, data):
        data = list(data)
        for i in range(0, len(data)):
            if data[i] == '':
                data[i] = None
            elif self.field_type == 'Pass':
                data[i] = data[i]
            elif self.field_type[i] == 0:
                data[i] = int(data[i])
            elif self.field_type[i] == 1:
                data[i] = float(data[i])
            elif self.field_type[i] == 2:
                data[i] = bool(data[i])
            elif self.field_type[i] == 3:
                data[i] = data[i]
            elif self.field_type[i] == 4:
                data[i] = datetime.fromtimestamp(int(data[i]))
        return tuple(data)

    def _distill(self, *args, **keywords):
        # accepts dict, iterable, or keywords and outputs a distilled dict
        data = {}
        for arg in args:
            if isinstance(arg,dict):
                for key in arg.keys():
                    data[key] = arg[key]
            elif isinstance(arg, list) or isinstance(arg, tuple):
                if len(arg) == len(self.field_order):
                    data.update(zip(self.field_order, arg))
                else:
                    for key,value in arg:
                        data[key] = value
        for key in keywords.keys():
            data[key] = keywords[key]
        for key in data:
            if key not in self.field_order:
                del data[key]
        return data

    def _import(self, *args, **keywords):
        if self.data is None: self.data = {}
        self.data.update(apply(self._distill, args, keywords))

    def joinInt(self,high,low):
        """
        Returns a single long from a pair of signed integers.
        """
        return (high + (low<0))*2**32 + low

    def listFields(self):
        """
        Returns a tuple of all available object fields.
        """
        return tuple(self.data.keys())

class DBData( DictData ):
    """
    Altered DictData adding several functions for dealing with individual rows.
    Must be subclassed for use.
    """
    field_type = 'Pass'

    def _setDefs(self):
        self.__dict__['field_order'] = []
        DictData._setDefs(self)

    def __init__(self,data=None,db=None):
        self._setDefs()
        self.db = MythDBConn(db)
        self.field_order = self.db.tablefields[self.table]
        
        if data is not None:
            self._import(self,data)

    def _pull(self):
        """Updates table with data pulled from database."""
        c = self.db.cursor()
        c.execute("""SELECT * FROM %s WHERE %s""" \
                % (self.table, self.where), self.wheredat)
        data = c.fetchone()
        c.close()
        if data is None:
            return
        self._import(data)

class DBDataWrite( DBData ):
    """
    Altered DBData, with support for writing back to the database.
    'defaults' dictionary allows default values when creating creating new
            and updating old entries
    Must be subclassed for use.
    """
    defaults = None
    def _sanitize(self,data):
        """Remove fields from dictionary that are not in database table."""
        for key in data:
            if key not in self.field_order:
                del data[key]
        if self.defaults is None:
            return data
        for key in self.defaults:
            if key in data:
                if self.defaults[key] is None:
                    del data[key]
                elif data[key] is None:
                    data[key] = self.defaults[key]
        return data

    def create(self,data):
        """
        Creates a new database entry using given dictionary.
        Only works on an uninitialized object.
        """
        if len(self.data) > 0:
            raise MythError('DBDataWrite object already bound to '+\
                                                    'existing instance')
        self._insert(self, data)
        data = self._sanitize(self.data)
        c = self.db.cursor()
        fields = ', '.join(data.keys())
        format_string = ', '.join(['%s' for d in data.values()])
        c.execute("""   INSERT INTO %s (%s)
                        VALUES(%s)"""\
                    % (self.table, fields, format_string), data.values())
        intid = c.lastrowid
        c.close()
        return intid

    def _push(self):
        """
        Updates database with data from local table.
        """
        if (self.where is None) or (self.wheredat is None):
            return
        c = self.db.cursor()
        data = self._sanitize(self.data)
        format_string = ', '.join(['%s = %%s' % d for d in data])
        sql_values = data.values()
        sql_values.extend(self.wheredat)

        c.execute("""   UPDATE %s SET %s WHERE %s"""\
                % (self.table, format_string, self.where), sql_values)
        c.close()
        self._pull()
        
    def update(self, *args, **keywords):
        apply(self._import, args, keywords)
        self._push()

    def delete(self):
        """
        Delete video entry from database.
        """
        if (self.where is None) or \
                (self.wheredat is None) or \
                (self.data is None):
            return
        c = self.db.cursor()
        c.execute("""   DELETE FROM %s WHERE %s"""\
                % (self.table, self.where), self.wheredat)
        c.close()

class DBDataCRef( DictData ):
    """
    Altered DictData for managing lists of crossreferenced data.
    """
    field_type = 'Pass'

    class SubData( DictData ):
        def _setDefs(self):
            self.__dict__['field_order'] = []
            DictData._setDefs(self)
        def __repr__(self):
            return str(tuple(self.data.values()))
        def __init__(self,data,fields):
            self._setDefs()
            self.field_order = fields
            self.field_type = 'Pass'
            self._import(data)

    def _setDefs(self):
        self.__dict__['field_order'] = []
        DictData._setDefs(self)
    def __repr__(self):
        return self.data.keys()

    def __init__(self,where,db=None):
        self._setDefs()
        self.db = MythDBConn(db)
        self.w_dat = tuple(where)
        
        if self.t_add is None: self.t_add = []
        if self.r_add is None: self.r_add = []
        
        tf = '%s.%s' % (self.table, self.t_field)
        rf = '%s.%s' % (self.ref, self.r_field)
        
        c = self.db.cursor()
        vals = ','.join([tf]+self.t_add+self.r_add)
        where = ' AND '.join('%s=%%s' % d for d in self.w_field)
        join = '%s=%s' % (tf,rf)
        c.execute("""SELECT %s FROM %s JOIN %s ON %s WHERE %s""" % \
                (vals, self.table, self.ref, join, where), self.w_dat)
        for entry in c.fetchall():
            self.field_order.append(entry[0])
            self.data[entry[0]] = self.SubData(entry[1:],self.t_add+self.r_add)

    def add(self,data):
        if len(data) != len(self.t_add+self.r_add):
            return

        # check for existing
        for dat in self.data:
            if data == dat.data.values():
                return

        c = self.db.cursor()
        
        # search for existing table data, add if needed
        tdata = data[:len(self.t_add)]
        where = ' AND '.join('%s=%%s' % d for d in self.t_add)
        count = c.execute("""SELECT %s FROM %s WHERE %s""" % \
                        (self.t_field, self.table, where), tdata)
        if count == 1:
            id = c.fetchone()[0]
        else:
            fields = ', '.join(self.t_add)
            format = ', '.join(['%s' for d in tdata])
            c.execute("""INSERT INTO %s (%s) VALUES(%s)"""\
                    % (self.table, fields, format), tdata)
            id = c.lastrowid
            
        # add cross reference
        rdata = data[-len(self.r_add):]+id+self.w_dat
        rfields = self.r_add+self.r_field+self.w_field
        fields = ', '.join(rfields)
        format = ', '.join(['%s' for d in rdata])
        c.execute("""INSERT INTO %s (%s) VALUES(%s)"""\
                    % (self.ref, fields, format), rdata)
        c.close()

        # add local entry
        self.field_order.append(id)
        self.data[id] = self.SubData(id,data)

    def delete(self,cref):
        if cref not in self.data:
            return

        c = self.db.cursor()
        wdata = cref+self.w_dat
        wfields = self.r_field+self.w_field
        where = ' AND '.join('%s=%ss' % d for d in wfields)
        c.execute("""DELETE FROM %s WHERE %s""" \
                        % (self.ref, where), wdata)

        if c.execute("""SELECT * FROM %s WHERE %s=%%s""" \
                            % (self.ref, self.r_field), cref) == 0:
            c.execute("""DELETE FROM %s WHERE %s=%%s"""\
                            % (self.table, self.t_field), cref)
        c.close()

        # remove local entry
        del self.data[cref]

class MythDBConn( object ):
    """
    Basic connection to the mythtv database
    Offers connection caching to prevent multiple connections
    """
    db = None

    class _TableFields(object):
        """Provides a dictionary-like list of table fieldnames"""
        def __init__(self, ident): self._ident = ident
        def __getattr__(self,key):
            if key in self.__dict__:
                return self.__dict__[key]
            elif key in MYTHDBCONN[self._ident][2]:
                return MYTHDBCONN[self._ident][2][key]
            else:
                return self.__getitem__(key)
        def __getitem__(self,key):
            if key in MYTHDBCONN[self._ident][2]:
                return MYTHDBCONN[self._ident][2][key]
            table_names = []
            c = MYTHDBCONN[self._ident][0].cursor()
            try:
                c.execute("DESC %s" % (key,))
            except:
                return None
            
            for name in c.fetchall():
                table_names.append(name[0])
            c.close()
            MYTHDBCONN[self._ident][2][key] = tuple(table_names)
            return table_names

    class _Settings(object):
        """Provides dictionary-like list of hosts"""
        class __Settings(object):
            """Provides dictionary-like list of settings"""
            def __init__(self,ident,host):
                self.data = {}
                self._ident = ident
                self._host = host
            def __getattr__(self,key):
                if key in self.__dict__:
                    return self.__dict__[key]
                elif key in self.data:
                    return self.data[key]
                else:
                    return self.__getitem__(key)
            def __getitem__(self,key):
                if key in self.data:
                    return self.data[key]
                if self._host == 'NULL':
                    where = 'IS NULL'
                    wheredat = (key,)
                else:
                    where = 'LIKE(%s)'
                    wheredat = (key, self._host)
                c = MYTHDBCONN[self._ident][0].cursor()
                c.execute("""SELECT data FROM settings WHERE value LIKE(%%s)
                    AND hostname %s LIMIT 1""" % where, wheredat)
                row = c.fetchone()
                c.close()
                if row:
                    self.data[key] = row[0]
                    return row[0]
                else:
                    return None
        def __init__(self, ident): self._ident = ident
        def __getattr__(self,key):
            if key in self.__dict__:
                return self.__dict__[key]
            elif key in MYTHDBCONN[self._ident][3]:
                return MYTHDBCONN[self._ident][3][key]
            else:
                return self.__getitem__(key)
        def __getitem__(self,key):
            if key not in MYTHDBCONN[self._ident][3]:
                MYTHDBCONN[self._ident][3][key] =\
                             self.__Settings(self._ident,key)
            return MYTHDBCONN[self._ident][3][key]

    def __init__(self, args=None):
        if args is not None:
            if isinstance(args, dict):
                # use connection settings from dictionary
                self.dbconn = args
            else:
                # load existing database connection
                self.dbconn = args.dbconn
            if not self._check_dbconn(self.dbconn):
                raise MythError("Required information for database "+\
                                            "connection missing")
        else:
            # try to read from config.xml
            config_files = [ os.path.expanduser('~/.mythtv/config.xml') ]
            if 'MYTHCONFDIR' in os.environ:
                config_files.append('%s/config.xml' %os.environ['MYTHCONFDIR'])

            dbconn = {'DBHostName':0,   'DBName':0, 'DBUserName':0,
                      'DBPassword':0,   'USN':0,    'SecurityPin':0}

            for config_file in config_files:
                try:
                    config = minidom.parse(config_file)
                except:
                    continue

                for key in dbconn:
                    dbconn[key] = None

                for token in config.getElementsByTagName('Configuration')[0].\
                                getElementsByTagName('UPnP')[0].\
                                getElementsByTagName('MythFrontend')[0].\
                                getElementsByTagName('DefaultBackend')[0].\
                                childNodes:
                    if token.nodeType == token.TEXT_NODE:
                        continue
                    try:
                        if token.tagName in dbconn:
                            dbconn[token.tagName] = token.childNodes[0].data
                    except:
                        pass

                if self._check_dbconn(dbconn):
                    #log.Msg(INFO, 'Using config %s', config_file)
                    self.dbconn = dbconn
                    break
            else:
                raise MythError("Could not find database login settings")

        self.ident = "sql://%s@%s/" % \
                (self.dbconn['DBName'],self.dbconn['DBHostName'])
        if self.ident in MYTHDBCONN:
            # reuse existing database connection and update count
            self.db = MYTHDBCONN[self.ident][0]
            MYTHDBCONN[self.ident][1] += 1
        else:
            # attempt new connection
            try:
                self.db = MySQLdb.connect(  user=  self.dbconn['DBUserName'],
                                            host=  self.dbconn['DBHostName'],
                                            passwd=self.dbconn['DBPassword'],
                                            db=    self.dbconn['DBName'],
                                            use_unicode=True,
                                            charset='utf8')
                #log.Msg(INFO, 'DB Connection info (host:%s, name:%s,'+
                #            ' user:%s, pass:%s)',   self.dbconn['DBHostName'],
                #                                    self.dbconn['DBName'],
                #                                    self.dbconn['DBUserName'],
                #                                    self.dbconn['DBPassword'])
            except:
                raise MythError("Connection failed for '%s'@'%s' to database "
                         "%s using password %s" % ( self.dbconn['DBUserName'],
                                                    self.dbconn['DBHostName'],
                                                    self.dbconn['DBName'],
                                                    self.dbconn['DBPassword']))
            # check schema version
            sver = self._check_schema('DBSchemaVer',SCHEMA_VERSION)
            if sver:
                #log.Msg(CRITICAL, 'DB speaks schema version %d, '+
                #                'but we speak version %d',
                #                sver, SCHEMA_VERSION)
                self.db.close()
                self.db = None
                raise MythError('Mismatched schema version')

            # add connection to cache
            MYTHDBCONN[self.ident] = [self.db, 1, {}, {}]

        # connect to table name cache
        self.tablefields = self._TableFields(self.ident)
        self.settings = self._Settings(self.ident)

    def _check_dbconn(self,dbconn):
        if      dbconn['DBHostName'] is None or \
                dbconn['DBName']     is None or \
                dbconn['DBUserName'] is None or \
                dbconn['DBPassword'] is None:
            return False
        else:
            return True

    def _check_schema(self,value,local):
        c = self.db.cursor()
        c.execute("""SELECT data FROM settings WHERE value LIKE(%s)""",(value,))
        sver = int(c.fetchone()[0])
        c.close()

        if local == sver:
            return 0
        else:
            return sver

    def getStorageGroup(self, groupname=None, hostname=None):
        """
        Returns tuple of dictionaries containing storage group directories
            with the fields 'id', 'groupname', 'hostname', and 'dirname'
        Takes an optional groupname and hostname for filtering
        """
        c = self.db.cursor()
        where = []
        wheredat = []
        if groupname:
            where.append("groupname=%s")
            wheredat.append(groupname)
        if hostname:
            where.append("hostname=%s")
            wheredat.append(hostname)
        if len(where):
            where = 'WHERE '+' AND '.join(where)
            c.execute("""SELECT * FROM storagegroup %s ORDER BY id"""\
                        % where, wheredat)
        else:
            c.execute("""SELECT * FROM storagegroup ORDER BY id""")

        ret = []
        for row in c.fetchall():
            ret.append(StorageGroup(row,self))
        return tuple(ret)

    def cursor(self): return self.db.cursor()

    def close(self): self.__del__()

    def __del__(self):
        if self.db is not None:
            # decrement use count
            MYTHDBCONN[self.ident][1] -= 1
            self.db = None

            if not MYTHDBCONN[self.ident][1]:
                # database connection no longer in use, close and delete it
                MYTHDBCONN[self.ident][0].close()
                del MYTHDBCONN[self.ident]


class MythBEConn( object ):
    """
    Basic class for mythbackend socket connections
    """

    db = None

    def __init__(self, backend=None, type='Monitor'):
        self.db = MythDBConn()
        if backend is None:
            # use master backend, no sanity checks, these should always be set
            self.host = self.db.settings.NULL.MasterServerIP
            self.port = self.db.settings.NULL.MasterServerPort
        else:
            if re.match('(?:\d{1,3}\.){3}\d{1,3}',backend):
                # process ip address
                c = self.db.cursor()
                if c.execute("""SELECT hostname FROM settings WHERE
                        value='BackendServerIP' AND
                        data=%s""", backend) == 0:
                    c.close()
                    raise MythError('Backend not found in database: '+backend)
                backend = c.fetchone()[0]
                c.close()
            self.host = self.db.settings[backend].BackendServerIP
            if not self.host:
                    raise MythError('Backend not found in database: '+backend)
            self.port = self.db.settings[backend].BackendServerPort
            if not self.port:
                raise MythError('Backend port not found in database: '+backend)
        self.port = int(self.port)

        try:
            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.socket.settimeout(10)
            self.socket.connect((self.host, self.port))
            self.check_version()
            self.announce(type)
        except socket.error, e:
            #log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d '+
            #            '(is the backend running)', self.host, self.port)
            raise MythError("Couldn't connect to %s:%d "+
                        "(is the backend running)" % (self.host,self.port))
        except:
            raise

    def __del__(self):
        self.backendCommand('DONE')
        self.socket.shutdown(1)
        self.socket.close()

    def close(self): self.__del__()

    def check_version(self):
        res = self.backendCommand('MYTH_PROTO_VERSION %s' \
                    % PROTO_VERSION).split(BACKEND_SEP)
        if res[0] == 'REJECT':
            #log.Msg(CRITICAL, 'Backend has version %s '+
            #        'and we speak version %s', res[1], PROTO_VERSION)
            raise MythError('Backend has version %s '+
                    'and we speak version %s' % (res[1], PROTO_VERSION))

    def announce(self,type):
        res = self.backendCommand('ANN %s %s 0' % (type, socket.gethostname()))
        if res != 'OK':
            #log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res)
            raise MythError('Unexpected answer to ANN command: %s' % res)
        #else:
            #log.Msg(INFO, 'Successfully connected mythbackend at %s:%d',
            #                self.host, self.port)

    def backendCommand(self, data):
        """
        Sends a formatted command via a socket to the mythbackend.

        Returns the result from the backend.
        """
        def recv():
            """
            Reads the data returned from the backend.
            """
            # The first 8 bytes of the response gives us the length
            data = self.socket.recv(8)
            try:
                length = int(data)
            except:
                return u''
            data = []
            while length > 0:
                chunk = self.socket.recv(length)
                length = length - len(chunk)
                data.append(chunk)
            try:
                return unicode(''.join(data),'utf8')
            except:
                return u''.join(data)

        command = u'%-8d%s' % (len(data), data)
        #log.Msg(DEBUG, 'Sending command: %s', command)
        self.socket.send(command)
        return recv()

    def joinInt(self,high,low):
        """
        Returns a single long from a pair of signed integers
        """
        return (high + (low<0))*2**32 + low

    def splitInt(self,integer):
        """
        Returns a pair of signed integers from a single long
        """
        return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32



class MythXMLConn( object ):
    def __init__(self, backend=None):
        db = MythDBConn()
        if backend is None:
            # use master backend
            self.host = db.settings.NULL.MasterServerIP
            self.port = 6544 #self.db.settings.NULL.MasterServerPort
        elif re.match('(?:\d{1,3}\.){3}\d{1,3}',backend):
            # process ip address
            self.host = backend
            c = db.cursor()
            if c.execute("""SELECT hostname FROM settings WHERE
                    value='BackendServerIP' AND
                    data=%s""", self.host) == 0:
                backend = None
            else:
                backend = c.fetchone()[0]
            self.port = 6544 #self.db.settings[backend].BackendServerPort
            c.close()
        else:
            # assume given a hostname
            self.host = db.settings[backend].BackendServerIP
            if not self.host:
                # try a truncated hostname
                backend = backend.split('.')[0]
                self.host = db.setting[backend].BackendServerIP
                if not self.host:
                    raise MythError('Unable to find requested backend.')
            self.port = 6544 #self.db.setting[backend].BackendServerPort
        db.close()

    def _query(self, path=None, **keyvars):
        url = 'http://%s:%d/' % (self.host, self.port)
        if path is not None:
            url += 'Myth/%s/' % path
        if len(keyvars) > 0:
            fields = []
            for key in keyvars:
                fields.append('%s=%s' % (key, keyvars[key]))
            url += '?'+'&'.join(fields)
        print 'opening %s' % url
        ufd = urlopen(url)
        res = minidom.parseString(ufd.read())
        ufd.close()
        return res
    

class MythLog( object ):
    """
    A simple logging class
    """
    def __init__(self, level, format, instance):
        self.log = logging.getLogger(instance)
        self.log.setLevel(level)
        self.ch = logging.StreamHandler()
        self.ch.setFormatter(logging.Formatter(format))
        self.log.addHandler(self.ch)

    def Msg(self, level, msg, *args, **kwargs):
        self.log.log(level, msg, *args, **kwargs)

class MythError(Exception):
    """
    A simple exception class
    """
    def __init__(self, message):
        self.message = message

    def __repr__(self):
        print ': ' + self.message

class StorageGroup(DBData):
    """
    Represents a single storage group entry
    """
    table = 'storagegroup'
    where = 'id=%s'
    
    def __str__(self):
        return u"%s@%s%s" % (self.groupname,self.hostname,self.dirname)

    def __repr__(self): return self.__str__().encode('utf-8')

    def __init__(self,data,db=None):
        DBData.__init__(self,data,db)
        self.wheredat = (self.id)
        if self.hostname == socket.gethostname():
            self.local = True
        elif os.access(self.dirname, os.F_OK):
            self.local = True
        else:
            self.local = False


