
'''
Name: srd_recordset.py

Overview: Provides methods and properties for working with tables.

Description:
    
    
Notes:

Author: Doug Crane
        May, 2012

Modifications:

'''

__author__ = 'Doug Crane'
__version__ = '1.0'

import sys
import os
import pdb
import itertools

import arcpy

from srd_exception import *
import srd_featureclass_info
import srd_logging
    
__all__ = ['SRD_Recordset']
        
# ====================================================================
class SRD_Recordset(object):
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, recordsetPath):
        '''
        Initialize

        Variables:
            fullPath - full path to the dataset
            rawPath - same as datasetPath
            parentDir - parent directory for the dataset.

            fieldNames - list of all field names from table or feature class.
                         Note that all field names are converted to lower case.
                         
            fieldTypes - dictionary of field types associated each field The
                         key for the dictionary is based on the lowercase field
                         name. The field type is returned in uppercase.

            fieldEditable - dictionary of field editable status associated each
                            field The key for the dictionary is based on the
                            lowercase field name. The value of the entry is either
                            True or False depending on editablility.

            fieldInfo -     A dictionary that contains the field properties for each field
                            in the feature class. The dictionary is keyed using the lowercase
                            name of the field. The properties are a dictionary containing each
                            field property. For boolean properties such as Editable the property
                            is stored as a true boolean and not a string. The properties dictionary
                            is keyed using lowercase name of the property.

                            Property keys:
                                aliasname
                                domain
                                required
                                isnullable
                                length
                                precision
                                scale
                                type
                                defaultNullReplacement
            
        Parameters:
            recordsetPath - path to the recordset you wish to process.

        Examples:
            import srd.srd_recordset as srd_rs

            featureClass = r'D:\testData\avi\polygon'
            rsObj = srd_rs.SRD_Recordset(featureClass)

            print rsObj.fieldInfo['sp1']
            print rsObj.fieldNames
            print rsObj.fullPath
            print rsObj.parentDir
        
        '''

        # Make sure that dataset exists 
        if not arcpy.Exists(recordsetPath):
            raise SRD_Exception('Dataset: %s not found' % recordsetPath)

        self._logChn = srd_logging.SRD_Log()
        
        self.rawPath = recordsetPath

        # Obtain some other path information in case we need it.
        utilFC = srd_featureclass_info.SRD_FeatureClassInfo(recordsetPath)
        self.fullPath = utilFC.fullPath
        self.parentDir = utilFC.parentDir

        self.fieldNames = []
        self.fieldTypes = {}
        self.fieldEditable = {}
        self.fieldInfo = {}

        # OID field stored for reference in case is needed as key.
        desc = arcpy.Describe(self.fullPath)
        if desc.hasOID:
            self.oidFieldName = desc.OIDFieldName
        else:
            self.oidFieldName = ''
            
        self.fieldValues = {}

        # Populate recordset information.
        self.setFieldInfo()

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def setFieldInfo(self):
        '''

        Builds a dictionary for each field containing all field information
        properties for the field.
        
        Parameters:
            
        Returns:        

            A dictionary that contains the field properties for each field
            in the feature class. The dictionary is keyed using the lowercase
            name of the field. The properties are a dictionary containing each
            field property. For boolean properties such as Editable the property
            is stored as a true boolean and not a string. The properties dictionary
            is keyed using lowercase name of the property.

            Property keys:
                aliasname
                domain
                required
                isnullable
                length
                precision
                scale
                type
                defaultNullReplacement

        Example:            
            
        '''

        self.fieldInfo = {}
        self.fieldNames = []

        fieldList = arcpy.ListFields(self.fullPath)

        # Each field has a set of field properties that is a dictionary
        # keyed by the lowercase name of the property.
        
        for field in fieldList:
            # Ignore certain fields that should be left alone.
##            if not field.type.upper() in ['GEOMETRY', 'BLOB', 'RASTER']:
            fieldProperties = {}
            fieldName = field.name.lower()
            self.fieldNames.append(fieldName)

            fieldProperties['aliasname'] = field.aliasName
            fieldProperties['domain'] = field.domain
            fieldProperties['editable'] = field.editable
            fieldProperties['required'] = field.required
            fieldProperties['isnullable'] = field.isNullable
            fieldProperties['length'] = field.length
            fieldProperties['precision'] = field.precision
            fieldProperties['scale'] = field.scale
            fieldProperties['type'] = field.type
            
            fieldProperties['defaultNullReplacementVal'] = self.getDefaultNullReplacement(field.type)

            # Store the field properties for the field.
            self.fieldInfo[fieldName] = fieldProperties

            # Build some redundant info for quick reference if needed by user.
            self.fieldTypes[field.name.lower()] = field.type.upper()
            self.fieldEditable[field.name.lower()] = field.editable
        
        return self.fieldInfo
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getFieldTypes(self):
        '''
        Returns dictionary of field types keyed on lowercase field name
        '''
        return self.fieldTypes

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getIsEditable(self):
        '''
        Returns dictionary of field is editable values keyed on lowercase field name
        '''
        return self.fieldEditable
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getIsNullable(self):
        '''
        Returns dictionary of field is nullable values keyed on lowercase field name
        '''
        
        d = {}
        for fieldName in self.fieldNames:
            d[fieldName] = self.fieldInfo[fieldName]['isnullable']

        return d
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getDomain(self):
        '''
        Returns dictionary of field domain associations keyed on lowercase field name
        '''
        
        d = {}
        for fieldName in self.fieldNames:
            d[fieldName] = self.fieldInfo[fieldName]['domain']

        return d
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getRecordsAsDict(self, fieldList=[], keyFieldList=[], fieldNameToUpper=False, replaceNulls=False, stripBlanks=False):
        '''
        Returns a dictionary of records for the recordset. Each entry in the dictionary is
        a dictionary of values based on the field name. This is just a wrapper that calls
        _getRecords.

        Parameters:
            fieldList - list of field names to read. If not passed then all fields will be read.
            
            keyFieldList - list of fields to use a key for the recordset dictionary. If not passed
                           then the OID of the recordset is used. If it is a multi-field key then
                           the key will be generated by concatenating the string values of each field
                           into a key. If it is just a single field then its native value will be used
                           as the key.
            
            fieldNameToUpper - by default the field dictionary uses the lowercase name of the
                               field as its key. Set to True if you wish the uppercase name used.

            replaceNulls - by default Null values are returned as None. Set to True to replace
                           nulls with either an empty string or 0.

            stripBlanks - by default string field values will be returned as is. Set to True if
                          you wish the all trailing and leading blanks stripped from string fields.

        '''

        return self._getRecords(fieldList, keyFieldList, fieldNameToUpper, replaceNulls, stripBlanks)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getRecordsAsList(self, fieldList=[], fieldNameToUpper=False, replaceNulls=False, stripBlanks=False):
        '''
        Returns a list of records for the recordset. Each entry in the dictionary is
        a dictionary of values based on the field name. 

        Parameters:
            fieldList - list of field names to read. If not passed then all fields will be read.
                        
            fieldNameToUpper - by default the field dictionary uses the lowercase name of the
                               field as its key. Set to True if you wish the uppercase name used.

            replaceNulls - by default Null values are returned as None. Set to True to replace
                           nulls with either an empty string or 0.

            stripBlanks - by default string field values will be returned as is. Set to True if
                          you wish the all trailing and leading blanks stripped from string fields.

        '''

        keyFieldList = []
        rsDict = self._getRecords(fieldList, keyFieldList, fieldNameToUpper, replaceNulls, stripBlanks)

        return rsDict.values()
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _getRecords(self, fieldList=[], keyFieldList=[], fieldNameToUpper=False, replaceNulls=False, stripBlanks=False):
        '''
        Returns a dictionary of records for the recordset. Each entry in the dictionary is
        a dictionary of values based on the field name. This is the main method for obtaining
        data from the recordset and is used by getRecordsAsDict and getRecordsAsList

        Parameters:
            fieldList - list of field names to read. If not passed then all fields will be read.
            
            keyFieldList - list of fields to use a key for the recordset dictionary. If not passed
                           then the OID of the recordset is used. If it is a multi-field key then
                           the key will be generated by concatenating the string values of each field
                           into a key. If it is just a single field then its native value will be used
                           as the key.
            
            fieldNameToUpper - by default the field dictionary uses the lowercase name of the
                               field as its key. Set to True if you wish the uppercase name used.

            replaceNulls - by default Null values are returned as None. Set to True to replace
                           nulls with either an empty string or 0.

            stripBlanks - by default string field values will be returned as is. Set to True if
                          you wish the all trailing and leading blanks stripped from string fields.

        '''


        # Build list of field names that will be used to generate the record
        readFieldList = []
        if fieldList:
            # Restrict to fields requested by user.
            for fieldName in fieldList:
                if fieldName.lower() in self.fieldNames:
                    readFieldList.append(fieldName.lower())
                else:
                    raise SRD_Exception('Field: %s not found in recordset' % fieldName)
        else:
            # No field list passed so use all fields.
            readFieldList = self.fieldNames[:]

        if fieldNameToUpper:
            readFieldList = [fieldName.upper() for fieldName in readFieldList]
            
        # Ensure that all the fields that will be used to build
        # the record key exist in the recordset.
        if keyFieldList:
            for fieldName in keyFieldList:
                if not fieldName.lower() in self.fieldNames:
                    raise SRD_Exception('Key Field: %s not found in recordset' % fieldName)
        else:
            if self.oidFieldName:
                keyFieldList = (self.oidFieldName,)

        # If we will be using uppercase field names for record key then make
        # sure our key field list is also upper case since we may be referencing
        # these values directly from the record dictionary.
        if fieldNameToUpper:
            keyFieldList = [fieldName.upper() for fieldName in keyFieldList]

        selCnt = int(arcpy.GetCount_management(self.fullPath).getOutput(0))
        arcpy.SetProgressor('step', 'Reading Records...', 0, selCnt, 1)

        # Define dictionary to contain all records.
        recordsetDict = {}

        # Use the rawPath as this may indicate a feature/table layer that
        # has a selection query applied to it.
        with arcpy.da.SearchCursor(self.rawPath, readFieldList) as cursor:
            for row in cursor:
                # Define dictionary for current record.
                recDict = self.getRecord(row, readFieldList, replaceNulls, stripBlanks)

                # Build the record key based on set of field names provided by user.
                recKey = ''
                for keyFieldName in keyFieldList:
                    if row.isNull(keyFieldName):
                        keyVal = self.fieldInfo[keyFieldName.lower()]['defaultNullReplacementVal']
                    else:
                        keyVal = row.getValue(keyFieldName)

                    # If a concatenated key then convert all fields to string
                    # otherwise use its native format.
                    if len(keyFieldList) > 1:
                        recKey += str(keyVal)
                    else:
                        recKey = keyVal

                recordsetDict[recKey] = recDict
            
                arcpy.SetProgressorPosition()

        arcpy.ResetProgressor()

        return recordsetDict
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getRecord(self, row, fieldList, replaceNulls=False, stripBlanks=False, fieldNamesToLower=False):
        '''
        Returns a dictionary of field values for the record. Each entry in the dictionary is
        a dictionary of values based on the field name. 
        

        Parameters:
            row - row from the table or feature class to extract field values from.
            
            fieldList - list of field names to read.
                        Note that the names in this list will be used as the key for the dictionary
                        so if you want them in a certain upper/lower case then pass in that particular
                        case.
            
            replaceNulls - by default Null values are returned as None. Set to True to replace
                           nulls with either an empty string or 0.

            stripBlanks - by default string field values will be returned as is. Set to True if
                          you wish the all trailing and leading blanks stripped from string fields.

        '''

        # Define dictionary for current record.

        # Set names to lower case if requested.
        if fieldNamesToLower:
            fieldList = [fieldName.lower() for fieldName in fieldList]
        
        recDict = dict(itertools.izip(fieldList, row))

        # May want to make sure no nulls or leading/trailing blanks.
        if replaceNulls or stripBlanks:
            # Read each requested field into the record dictioanry.
            for fieldName,fieldVal in recDict.items():

                # Ignore any special cursor.da fields.
                if not '@' in fieldName:
                    # Perform Null replacement if required.
                    if replaceNulls:
                        if fieldVal is None:
                            fieldVal = self.fieldInfo[fieldName.lower()]['defaultNullReplacementVal']

                    # Strip leading and trailing blanks if requested.
                    if stripBlanks:
                        if self.fieldInfo[fieldName.lower()]['type'].upper() == 'STRING':
                            if not fieldVal is None:
                                fieldVal = fieldVal.strip()
                        
                # Update record dictionary.
                recDict[fieldName] = fieldVal

        return recDict
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getRecordNoDA(self, row, fieldList, replaceNulls=False, stripBlanks=False, fieldNamesToLower=False):
        '''
        Returns a dictionary of field values for the record. Each entry in the dictionary is
        a dictionary of values based on the field name. Row is based on Non Data-Access cursor.
        

        Parameters:
            row - row from the table or feature class to extract field values from.
            
            fieldList - list of field names to read.
                        Note that the names in this list will be used as the key for the dictionary
                        so if you want them in a certain upper/lower case then pass in that particular
                        case.
            
            replaceNulls - by default Null values are returned as None. Set to True to replace
                           nulls with either an empty string or 0.

            stripBlanks - by default string field values will be returned as is. Set to True if
                          you wish the all trailing and leading blanks stripped from string fields.

        '''

        # Define dictionary for current record.

        # Set names to lower case if requested.
        if fieldNamesToLower:
            fieldList = [fieldName.lower() for fieldName in fieldList]

        recDict = {}
        for fieldName in fieldList:
            try:
                fldVal = row.getValue(fieldName)
            except:
                self._logChn.logMsg('Field: %s could not be read, defaulting to None' % fieldName)
                fldVal = None
                
            recDict[fieldName] = fldVal

        # May want to make sure no nulls or leading/trailing blanks.
        if replaceNulls or stripBlanks:
            # Read each requested field into the record dictioanry.
            for fieldName,fieldVal in recDict.items():

                # Ignore any special cursor.da fields.
                if not '@' in fieldName:
                    # Perform Null replacement if required.
                    if replaceNulls:
                        if fieldVal is None:
                            fieldVal = self.fieldInfo[fieldName.lower()]['defaultNullReplacementVal']

                    # Strip leading and trailing blanks if requested.
                    if stripBlanks:
                        if self.fieldInfo[fieldName.lower()]['type'].upper() == 'STRING':
                            if not fieldVal is None:
                                fieldVal = fieldVal.strip()
                        
                # Update record dictionary.
                recDict[fieldName] = fieldVal

        return recDict

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def replaceNulls(self, fieldNameList = [], excludeFields = False):
        '''
        Replaces all Null values by replacing them with an empty string
        or the value 0.

        Parameters:
            fieldNameList - optional list of fields that will be the
                            only ones considered when replacing Null
                            values.
                            
            excludeFields - Determines how the fields in the field name
                            list will be used. If set to True then the list
                            of fields represent fields to exclude from any
                            Null replacement. By default this value is False so
                            the names in the list represent the only set
                            of names to consider for Null replacement. Note
                            that any Raster, Blog, OID, and Geometry columns are
                            always excluded since we can not directly assign values
                            to these types of fields.
                                
        Examples:

            import srd_recordset
            
            fcPath = r'D:\testData\srd_recordset_tst.gdb\null_test_out'
            rsObj = srd_recordset.SRD_Recordset(fcPath)
            
            # Replace Nulls for all editable fields:
            rsObj.replaceNulls()

            # Only replace Nulls for the TST_DATE field:
            rsObj.replaceNulls(['TST_DATE',])

            # Only replace Nulls for all fields except the TST_DATE field:
            rsObj.replaceNulls(['TST_DATE',], True)
            
        '''

        nullReplacementDict = {}

        # Make sure all the field names in list provided are in lowercase.
        fieldNames = [fieldName.lower() for fieldName in fieldNameList]

        for fieldName in self.fieldNames:
            # If user has passed list of fields use this.
            if fieldNames:
                # List may represent fields to exclude.
                if excludeFields:
                    if fieldName in fieldNames:
                        addFieldToList = False
                    else:
                        addFieldToList = True
                else:
                    if fieldName in fieldNames:
                        addFieldToList = True
                    else:
                        addFieldToList = False
            else:
                # No field list passed by user so use all fields.
                addFieldToList = True
                    
            # Only consider field if it is editable.
            if not self.fieldInfo[fieldName]['editable']:
                self._logChn.logMsg('Field: %s not editable, will not consider for Null replacement' % fieldName)
                addFieldToList = False
                
            if addFieldToList:
                defaultValue = self.fieldInfo[fieldName.lower()]['defaultNullReplacementVal']
                nullReplacementDict[fieldName] = defaultValue
        
        selCnt = int(arcpy.GetCount_management(self.fullPath).getOutput(0))
        arcpy.SetProgressor('step', 'Replacing NULLs...', 0, selCnt, 1)

        # Use the list of field names from the dictionary to determine fields to read.
        fieldNameList = nullReplacementDict.keys()
        with arcpy.da.UpdateCursor(self.rawPath, fieldNameList) as cursor:
            for row in cursor:
                for idx in range(0, len(fieldNameList)):
                    fieldName = fieldNameList[idx]
                    if row[idx] is None:
                        row[idx] = nullReplacementDict[fieldName]
                cursor.updateRow(row)
                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getDefaultNullReplacement(self, fldType):
        '''
        Based on the field type return a default non-null value.
        '''

        if fldType.upper() == 'Smallinteger'.upper():
            defaultValue = 0
        elif fldType.upper() == 'Integer'.upper():
            defaultValue = 0
        elif fldType.upper() == 'Single'.upper():
            defaultValue = 0.0
        elif fldType.upper() == 'Double'.upper():
            defaultValue = 0.0
        elif fldType.upper() == 'Double'.upper():
            defaultValue = 0.0
        elif fldType.upper() == 'Date'.upper():
            defaultValue = 0.0
        elif fldType.upper() == 'String'.upper():
            defaultValue = ''
        else:
            # All other types of fields will be ignored by
            # flagging with a None value.
            defaultValue = None

        return defaultValue
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def stripBlanks(self, fieldNameList = [], excludeFields = False):
        '''
        Strips all leading and trailing spaces from text field values.
        You may choose to process all text fields in table or select the
        fields for processing using the fieldNameList.
        
        Parameters:
            fieldNameList - optional list of fields that will be the
                            only ones considered when replacing Null
                            values.
                            
            excludeFields - Determines how the fields in the field name
                            list will be used. If set to True then the list
                            of fields represent fields to exclude from any
                            Blank stripping. By default this value is False so
                            the names in the list represent the only set
                            of names to consider for Blank stripping. 
                                
        Examples:
            import srd.srd_recordset as srd_rs
            
            fcPath = r'D:\testData\srd_recordset_tst.gdb\blank_test_out'
            rsObj = srd_rs.SRD_Recordset(fcPath)

            # Strip Trailing/Leading Blanks for all editable text fields:
            rsObj.stripBlanks()

            # Strip Trailing/Leading Blanks for TST_CHR field only:
            rsObj.stripBlanks(['TST_CHR',])

            # Strip Trailing/Leading Blanks for all text fields except the TST_CHR:
            rsObj.stripBlanks(['TST_CHR',], True)
        '''

        # Make sure all the field names in list provided are in lowercase.
        fieldNames = [fieldName.lower() for fieldName in fieldNameList]

        # List of fields that will have blanks stripped from them.
        # Note we will only consider fields defined as a String.
        blankStripList = []
        
        for fieldName in self.fieldNames:
            # If user has passed list of fields use this.
            if fieldNames:
                # List may represent fields to exclude.
                if excludeFields:
                    if fieldName in fieldNames:
                        addFieldToList = False
                    else:
                        addFieldToList = True
                else:
                    if fieldName in fieldNames:
                        addFieldToList = True
                    else:
                        addFieldToList = False
            else:
                # No field list passed by user so use all fields.
                addFieldToList = True
                    
            # Only consider field if it is editable.
            if self.fieldInfo[fieldName]['type'].upper() == 'String'.upper():
                if not self.fieldInfo[fieldName]['editable']:
                    self._logChn.logMsg('Field: %s not editable, will not consider for processing' % fieldName)
                    addFieldToList = False

            # Only consider text fields.
            if addFieldToList:
               if self.fieldInfo[fieldName]['type'].upper() == 'String'.upper():
                    blankStripList.append(fieldName)

        if not blankStripList:
            self._logChn.logWarning('\n***\n*** No String fields in feature class, nothing to strip...\n***')
            return False
        
        selCnt = int(arcpy.GetCount_management(self.fullPath).getOutput(0))
        arcpy.SetProgressor('step', 'Stripping Leading-Trailing Blanks...', 0, selCnt, 1)

        with arcpy.da.UpdateCursor(self.rawPath, blankStripList) as cursor:
            for row in cursor:
                for idx in range(0, len(blankStripList)):
                    if row[idx] is None:
                        row[idx] = ''
                    else:
                        row[idx] = row[idx].strip()
                    
                cursor.updateRow(row)
                arcpy.SetProgressorPosition()
                
        arcpy.ResetProgressor()

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def dumpInfo(rsObj):

    for fieldName in rsObj.fieldNames:
        print fieldName
        for fieldAtt,fieldVal in rsObj.fieldInfo[fieldName].items():
            print '   %s - %s' % (fieldAtt, fieldVal)

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def tstNulls():

    srcFC = r'D:\testData\util_tests\recordset.gdb\null_test_src'
    outFC = r'D:\testData\util_tests\recordset.gdb\null_test_out'

    arcpy.env.overwriteOutput = True
    
    arcpy.CopyFeatures_management(srcFC, outFC)

    rsObj = SRD_Recordset(outFC)

    rsObj.replaceNulls()

    print rsObj.getFieldTypes()

    print rsObj.getIsNullable()
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def tstBlanks():

    srcFC = r'D:\testData\util_tests\recordset.gdb\rec_tst_fields'
    outFC = r'D:\testData\util_tests\recordset.gdb\rec_tst_fields_out'

    arcpy.env.overwriteOutput = True
    
    arcpy.CopyFeatures_management(srcFC, outFC)

    rsObj = SRD_Recordset(outFC)

    rsObj.stripBlanks()

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def tstNulls2():

    srcFC = r'D:\testData\util_tests\recordset.gdb\null_test_src'
    outFC = r'D:\testData\util_tests\recordset.gdb\null_test_out'

    arcpy.env.overwriteOutput = True
    
    arcpy.CopyFeatures_management(srcFC, outFC)

    import srd_recordset
    
    rsObj = srd_recordset.SRD_Recordset(outFC)
    
    # Replace Nulls for all editable fields:
    ##rsObj.replaceNulls()

    # Only replace Nulls for the TST_DATE field:
    ##rsObj.replaceNulls(['TST_DATE',])

    # Only replace Nulls for all fields except the TST_DATE field:
    rsObj.replaceNulls(['TST_DATE',], True)

# --------------------------------------------------------------------
if __name__ == '__main__':


    tblPathArg = r'D:\testData\util_tests\recordset.gdb\test'
    tblPathArg = r'D:\testData\util_tests\recordset.gdb\test_strip_blanks'
    tblPathArg = r'D:\testData\util_tests\recordset.gdb\rec_tst_fields'
    
    rsObj = SRD_Recordset(tblPathArg)
        
##        pdb.set_trace()
##    rsDict = rsObj.getRecordsAsDict(replaceNulls=True, stripBlanks=True)

    tblPathArg = r'D:\testData\util_tests\recordset.gdb\test'
    fieldList = ('NULL_INT', 'NULL_TEXT', 'NULL_FLOAT')
##    rsList = rsObj.getRecordsAsList(fieldList, fieldNameToUpper=True, stripBlanks=True, replaceNulls=True)

    
    fieldList = ('BLANK_TEST',)
##    rsList = rsObj.getRecordsAsList(fieldList, fieldNameToUpper=True, stripBlanks=True, replaceNulls=True)

##    pdb.set_trace()
        
    tstBlanks()
