# This file is part of Pantry, command-line nutrient analysis.
# 
# Version 23, released Friday, November 09, 2007.
# 
# Copyright 2007 Omari Norman.
# 
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the
# Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# 
# The above copyright notice and this permission notice shall
# be included in all copies or substantial portions of the
# Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# 
# Pantry's "built-in" XML validator is a modified version of
# xmlproc.  xmlproc is Copyright 1998-2000 by Lars Marius
# Garshol, Oslo, Norway.
# 
# All Rights Reserved
# 
# Permission to use, copy, modify, and distribute this
# software and its documentation for any purpose and without
# fee is hereby granted, provided that the above copyright
# notice appear in all copies and that modified copies are
# clearly marked as such.
# 
# LARS MARIUS GARSHOL DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL LARS MARIUS
# GARSHOL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF
# USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

""" Objects that contain foods.

Foods are kept together in Cabinet instances. There are five types:
Shelf, Temp, Txt, Xml, and Zip. Zip is read-only. Each of these is a
dictionary mixin, so it has many of the usual dictionary methods.
The keys in the dictionaries are the food's hashes. For all Cabinet
types, use the addFood method to add additional foods, because
addFood checks for duplicate foods and does other handy things.

Using getFood or __getitem__ returns a brand new food every time you use
it, even if the key is the same. 

Ordinarily to read in an existing Cabinet instance, just use
getCabinet. It figures out what sort of instance you want and it picks
the right class and returns it for you. If you want to create a
specific kind of File instance, you can instantiate the exact one
you want. """

from __future__ import division
from xml.sax.handler import ContentHandler
import UserDict

class _Base(UserDict.DictMixin):
    """ Base class for objects that hold several Food objects. 

    Subclassed from this class are classes that are specific to
    how the foods are stored; use those.
    
    Subclasses must follow a specific protocol. The constructor must
    create a dictionary for each item in _CABATTRS; this may be done
    like this:
        for item in _CABATTRS:
            setattr(self, '_' + item, {})

    In addition, subclasses must define: 
    
        self.writeable, which is
        true or false depending upon whether this instance may be
        written to.

        self.filename
        
        self.changed, which is toggled to True if the contents of
        the cabinet have changed

        self._staleNuts - nutrients need to be updated

        self._staleGrams - grams need to be updated

        self._totalNuts - total nutrients in the cabinet
        self._totalGrams - total weight of the cabinet
        
    Constructors must accept exactly two parameters:
        filename
        options """

    def __delitem__(self, key):
        for attr in _CABATTRS:
            try:
                del getattr(self, '_' + attr)[key]
            except KeyError:
                pass
        self.changed = True
        self._staleNuts = True
        self._staleGrams = True

    def __getitem__(self, key):
        return self.getFood(key, True)

    def __str__(self):
        return 'File instance holding %u foods.' % len(self)

    def keys(self):
        return self._traits.keys()

    def addFood(self, food, oldKey, options = 0):
        """ Adds a food to this file.

        If incrementOrder is True, addFood will search the foods 
        currently in this file for other foods with identical values
        for both the date and meal traits. The result will be sorted
        and if the highest food's order trait matches '^[0-9]{4}$',
        then addFood will take the first three digits, strip any leading
        zeroes, and increment by one. A trailing zero is then
        added, and a leading zero is added if necessary to make the
        result four digits. 
        
        If there are no foods with identical date and meal traits,
        then the new food's order trait is set to 0010.

        The new food's order trait is set to the
        result. If the new food had an order trait, it is lost.
        
        oldKey: if this new food is replacing another food in the 
        cabinet, set oldKey equal to the old food's key. Otherwise,
        set oldKey to None."""

        if options & INCREMENT:
            import re
            import foods
            food = food.copy()
            traits = food.traits
            dateTraitNo = TRAITSBYNAME['date']
            mealTraitNo = TRAITSBYNAME['meal']
            orderTraitNo = TRAITSBYNAME['order']
            keys = self.search(
                    {dateTraitNo:traits.get(dateTraitNo, ''),
                    mealTraitNo:traits.get(mealTraitNo, '')},
                    EXACTMATCH)
            
            matchingFoods = [ self[key] for key in keys ]
            if matchingFoods:
                matchingFoods.sort(foods.CmpFoods(((orderTraitNo, 
                    foods.ASCENDING),), {}))
                topOrder = matchingFoods[-1].traits.get(orderTraitNo, '')
                if re.search(r'^[0-9]{4}$', topOrder):
                    newTopInt = int(topOrder[:3].lstrip('0')) + 1
                    newTopStr = u"%03u0" % newTopInt
                    food.traits[orderTraitNo] = newTopStr
                else:
                    food.traits[orderTraitNo] = '0010'
            else:
                food.traits[orderTraitNo] = '0010'

        mutex = 0
        while True:
            hash = food.hash
            if hash in self._traits:
                if not options & MUTATE:
                    raise DuplicateFoodError(self.filename, food)
                if mutex == 0:
                    food = food.copy()
                food, mutex = food.mutant
            else:
                break

        for attr in _CABATTRS:
            try:
                getattr(self, '_' + attr)[hash] = food.data[attr]
            except KeyError:
                pass
        if oldKey:
            _Base.__delitem__(self, oldKey)

        self.changed = True
        self._staleNuts = True
        self._staleGrams = True
        return food.traits.get(TRAITSBYNAME['order'], None), mutex

    def getFood(self, key, needNuts):
        """ Get a food from this cabinet. """
        import foods

        data = {}
        attrs = _CABATTRS[:]
        for attr in attrs:
            try:
                data[attr] = getattr(self, '_%s' % attr)[key]
            except KeyError:
                pass
        if not needNuts and 'ingredients' not in data:
            data['nutsPer100g'] = {}

        try:
            result = foods.Food(**data)
        except TypeError:
            result = foods.Recipe(**data)
        return result

    def _isWriteable(self):
        """ False if File instance is read-only, True if read-write. """
        return self._writeable

    writeable = property(fget = _isWriteable, doc = _isWriteable.__doc__)

    def search(self, patterns, options):
        """ Returns keys of foods matching search terms.

        Search terms are contained in traits, a mapping object.  The
        keys are the trait numbers to search; the values are
        regexps.  Only set the traits you want to search on; that
        is, if you only want to search for names, then use
        searchTraits = {TRAITSBYNAME['name']:'^Captain Crunch$'}.

        Options are set using global constants IGNORECASE and EXACTMATCH.
        Combine these using a bitwise OR (|). If EXACTMATCH is not
        set, then the function uses regular expressions and the traits
        dict must contain regular expressions. Otherwise, the items
        in the traits dictionary must exactly match the traits in the 
        File instance in order to be returned.

        If EXACTMATCH is set, IGNORECASE has no effect.
        
        """
        import re
        import foods

        class FoodFilter:
            def __init__(self, patterns, options):
                self.patterns = patterns
                if options & EXACTMATCH:
                    self.matcher = self.exactMatcher
                else:
                    flag = 0
                    if options & IGNORECASE:
                        flag = re.IGNORECASE
                    for patternNo, pattern in self.patterns.items():
                        self.patterns[patternNo] = re.compile(pattern, flag)
                    self.matcher = self.reMatcher

            def __call__(self, traits):
                for patternNo, pattern in self.patterns.items():
                    if not self.matcher(patternNo, traits.get(patternNo, '')):
                        return False
                return True

            def reMatcher(self, traitNo, value):
                return self.patterns[traitNo].search(value)

            def exactMatcher(self, traitNo, value):
                return self.patterns[traitNo] == value

        ff = FoodFilter(patterns, options)
        return [key for key in self.keys() if ff(self._traits[key])]

    def _getNuts(self):
        """ Returns mapping for sum of all nuts in this instance. """
        import foods
        if self._staleNuts:
            self._totalNuts = foods.nutSum(self.values())
            self._staleNuts = False
        return self._totalNuts

    nuts = property(fget = _getNuts, doc = _getNuts.__doc__)

    def _getGrams(self):
        """ Returns total gram weight of all foods in this instance.
        """
        if self._staleGrams:
            self._totalGrams = sum([food.grams for food in self.values()])
            self._staleGrams = False
        return self._totalGrams

    grams = property(fget = _getGrams, doc = _getGrams.__doc__)

class Shelf(_Base):
    """ File instance stored in a shelf; the "native" version.
    """

    class _ShelfGetter(UserDict.DictMixin):
        """ Fetches and sets items from the underlying shelf. 
        
        When setting items, they first go into a cache.
        Use save to flush the cache to the shelf."""

        def __init__(self, shelf, part):
            self.shelf = shelf
            self.part = part
            self.cache = {}

        def __getitem__(self, key):
            try:
                result = self.cache[key]
            except KeyError:
                result = self.shelf['%s:%s' % (self.part, key)]
            if self.part == 'i':
                result = getIngredients(result)
            return result
        
        def __setitem__(self, key, value):
            if self.part == 'i':
                value = getData(value)
            self.cache[key] = value

        def __delitem__(self, key):
            try:
                del self.shelf['%s:%s' % (self.part, key)]
            except KeyError:
                pass
            try:
                del self.cache[key]
            except KeyError:
                pass

        def save(self):
            for key, value in self.cache.items():
                self.shelf['%s:%s' % (self.part, key)] = value

    def __init__(self, filename, options):

        import shelve
        import os
        from bsddb.db import DBAccessError
        from anydbm import error as anydbmError
        import version
        import errno
        self.changed = False

        try:
            self.shelf = shelve.open(filename=filename, flag='w')
        except DBAccessError, error:
            if error.args[0] == 13:
                # permission denied
                self.shelf = shelve.open(filename, 'r')
                self.writeable = False
            else:
                raise
        except anydbmError, error:
            if error.args[0] == "need 'c' or 'n' flag to open new db":
                if not options & CREATE:
                    raise IOError(2, "No such file or directory")
                self.shelf = shelve.open(filename, 'c')
                self.writeable = True
            else:
                raise
        else:
            self.writeable = True

        self._traits = self.shelf.get('traits', {})
        self._arbUnits = self.shelf.get('arbUnits', {})
        self._nutsPer100g = self._ShelfGetter(self.shelf, 'n')
        self._ingredients = self._ShelfGetter(self.shelf, 'i')
        shelfVersion = self.shelf.setdefault('VERSION', version.VERSION)
        self.filename = os.path.abspath(filename)

        self._staleNuts = True
        self._staleGrams = True
        if not ACCEPT_VERSION <= shelfVersion <= version.VERSION:
            raise ShelfVersionError(self.filename, ACCEPT_VERSION, 
                    shelfVersion, version.VERSION)

    def getFood(self, key, needNuts):
        result = _Base.getFood(self, key, needNuts)
        result.traits[TRAITSBYNAME['origFile']] = self.filename
        return result

    def save(self):
        if not self.writeable:
            raise ReadOnlyError
        if self.changed:
            self.shelf['traits'] = self._traits
            self.shelf['arbUnits'] = self._arbUnits
            self._nutsPer100g.save()
            self._ingredients.save()

class Temp(_Base):
    """ File instance that starts empty and vanishes when destroyed. """

    def __init__(self, *narf, **narf2):
        for attr in _CABATTRS:
            setattr(self, '_' + attr, {})
        self.writeable = True
        self.filename = None
        self.changed = False
        self._staleNuts = True
        self._staleGrams = True

class Zip(_Base):
    """ Holds the contents of a zipped sr database. """

    class WrapTable(dict):
        """ Return an object to wrap a database table."""

        def __init__(self, table, fields, keys, eol, fieldSep, stringQuote):
            """ Set up table wrapper.

            Parameters:
                table: string containing the database table
                fields: sequence of field names
                keys: sequence of keys, must be in fieldList
                eol: string used to indicate end of line
                fieldSep: string used to separate fields
                stringQuote: character used to quote strings """

            dict.__init__(self)
            table = table.replace(stringQuote, u'')
            lines = table.rstrip(eol).split(eol)
            columnNumbersAndFieldNames = zip(range(len(fields)), fields)
            for line in lines:
                fileRow=line.split(fieldSep) 
                resultRow = {}
                resultRow = dict([(fieldName, fileRow[columnNumber])
                    for columnNumber, fieldName
                    in columnNumbersAndFieldNames])
                
                resultKey = [ resultRow[key] for key in keys ]
                self[tuple(resultKey)] = resultRow

    def __init__(self, filename, options):
        import zipfile
        import os
        import errno

        import foods

        for attr in _CABATTRS:
            setattr(self, '_' + attr, {})
        self.writeable = False
        self.filename = os.path.abspath(filename)
        self.changed = False
        self._staleNuts = True
        self._staleGrams = True

        # wrap tables
        z = zipfile.ZipFile(filename, mode = 'r')
        tables={}
        for name, (fields, keys) in _TABLES.items():
            # convert to unicode; apparently sr is encoded in latin1
            txtRaw = z.read(name.upper() + '.txt')
            txtUni = unicode(txtRaw, _ENCODING)

            tables[name]=self.WrapTable(txtUni,
                    fields = fields,
                    keys = keys,
                    eol = _EOL, fieldSep = _FIELDSEP,
                    stringQuote = _STRINGQUOTE)
        allFoodsNdb = {}
        
        # make foods, with traits
        for (ndbNo,), rowFood in tables['food_des'].items():
            group = tables['fd_group'][(rowFood['fdGrpCd'],)]['fdGrpDesc']
            name = rowFood['longDesc']
            traits = {'name':name, 
                    'group':group,
                    'unit':u'g', 
                    'qty':u'100',
                    'origFile':filename}
            if rowFood['refDesc']:
                traits['refDesc'] = rowFood['refDesc']
            if rowFood['pctRefuse']:
                traits['pctRefuse'] = rowFood['pctRefuse']
            traits = dict([(TRAITSBYNAME[name], value)
                for name, value in traits.items()])
            allFoodsNdb[ndbNo] = foods.Food(traits, {}, {})

        # add nutrients
        for (ndbNo, nutrNo), rowData in tables['nut_data'].items():
            nutrNo = int(nutrNo)
            if '.' in rowData['nutrVal']:
                nutVal = float(rowData['nutrVal'])
            else:
                nutVal = int(rowData['nutrVal'])
            allFoodsNdb[ndbNo].nutsPer100g[nutrNo] = nutVal

        # add arbitrary units
        for (ndbNo, seq), rowUnit in tables['weight'].items():
            # NDB no 07011, seq 3 in SR20 has an
            # empty value for the amount. Also, the description
            # simply is not filled in. Therefore, skip
            # any rows that don't have a value for amount.
            if rowUnit['amount'] != '' and float(rowUnit['amount']) == 1:
                unitDesc = rowUnit['measureDesc']
                if '.' in rowUnit['gmWgt']:
                    unitGrams = float(rowUnit['gmWgt'])
                else:
                    unitGrams = int(rowUnit['gmWgt'])
                allFoodsNdb[ndbNo].arbUnits[unitDesc] = unitGrams

        # add foods
        for food in allFoodsNdb.values():
            while True:
                try:
                    self.addFood(food, None)
                except DuplicateFoodError:
                    food, mutex = food.mutant
                else:
                    break

    def getFood(self, key, needNuts):
        result = _Base.getFood(self, key, needNuts)
        result.traits[TRAITSBYNAME['origFile']] = self.filename
        return result

class Xml(_Base):
    """ File instance from XML file. """

    def __init__(self, filename, options):
        import xmlCheck
        import errno
        import os.path
        import xml.sax

        for attr in _CABATTRS:
            setattr(self, '_' + attr, {})
        self.changed = False
        self.filename = os.path.abspath(filename)
        newfile = False
        self._staleNuts = True
        self._staleGrams = True

        try:
            fh = open(self.filename, 'r')
        except IOError, error:
            if error.errno == errno.ENOENT:
                if not options & CREATE:
                    raise 
                self.data = ''
                return
            else:
                raise

        data = fh.read()
        fh.close()

        if (os.path.getsize(filename) < MAXSIZE and not options & SKIPVALID
                or options & BIGVALID):
            try:
                xmlCheck.xmlCheck(data, PANTRYDTD)
            except xmlCheck.BadXMLError, e:
                raise InvalidXMLError(filename, 'Pantry', e)

        xml.sax.parseString(data, _XmlHandler(self))

    def getFood(self, key, needNuts):
        result = _Base.getFood(self, key, needNuts)
        result.traits[TRAITSBYNAME['origFile']] = self.filename
        return result

    def save(self):
        """ Save the foods to the XML file. """ 
        from io import puni
        from xml.sax import saxutils
        import foods
        import errno
        if not self.changed:
            return
        try:
            fh = open(self.filename, 'w')
        except IOError, e:
            if e.errno == errno.EACCES:
                raise ReadOnlyError
            else:
                raise
        toFile = puni(fh, 'utf-8')

        toFile(u'<?xml version="1.0" encoding="%s"?>\n' % toFile.encoding)
        toFile(u'<pantry>\n')
        for food in self.values():
            if hasattr(food, 'ingredients'):
                toFile(u'<recipe ')
                traits = food.traits.copy()
                try:
                    directions = traits.pop(TRAITSBYNAME['directions'])
                except KeyError:
                    directions = None
                for traitNo, traitValue in traits.items():
                    if traitNo != TRAITSBYNAME['origFile']:
                        toFile(u'%s=%s ' % (TRAITSBYNO[traitNo],
                            saxutils.quoteattr(traitValue)))
                toFile(u'>\n')

                for ingredient in food.ingredients:
                    toFile(u'    <ingredient ')
                    for attr in ('name', 'qty', 'unit', 'comment'):
                        traitNo = TRAITSBYNAME[attr]
                        traitValue = ingredient.traits.get(traitNo, '')
                        if traitValue:
                            toFile(u'%s=%s ' % (attr, 
                                saxutils.quoteattr(traitValue)))
                    origFile = ingredient.traits[TRAITSBYNAME['origFile']]
                    toFile(u'file=%s />\n' % saxutils.quoteattr(origFile))

                for unitName, unitGrams in food.arbUnits.items():
                    toFile(u'    <unit name=%s grams=%s />\n' %
                             (saxutils.quoteattr(unitName), 
                                 saxutils.quoteattr(str(unitGrams))))

                if directions:
                    toFile(u'    <directions>\n%s\n    </directions>\n' % 
                        directions)
                toFile(u'</recipe>\n\n')

            else:
                toFile(u'<food ')
                for traitNo, traitValue in traits.items():
                    traitName = TRAITSBYNO[traitNo]
                    if traitName != 'origFile':
                        toFile(u'%s=%s ' % (traitName, 
                            saxutils.quoteattr(traitValue)))
                toFile(u'>\n')
                for nutNo, nutVal in food.nuts.items():
                    toFile(u'    <nutrient name=%s units=%s amount=%s />\n' % 
                            (saxutils.quoteattr(NUTSBYNO[nutNo]['name']), 
                                saxutils.quoteattr(NUTSBYNO[nutNo]['units']), 
                                saxutils.quoteattr(str(nutVal))))
                    
                for unitName, unitGrams in food.arbUnits.items():
                    toFile(u'    <unit name=%s grams=%s />\n' %
                             (saxutils.quoteattr(unitName), 
                                 saxutils.quoteattr(str(unitGrams))))
                toFile(u'</food>\n\n')

        toFile(u'</pantry>\n')
        fh.close()

class _XmlHandler(ContentHandler):
    import foods
    import uniconv

    def __init__(self, cabinet):
        self.cabinet = cabinet
        self.files = {}
        self.inDirections = False
    
    def startElement(self, name, attrs):
        if name == 'food':
            self.traits = dict([(TRAITSBYNAME[attrName], attrValue)
                for attrName, attrValue
                in attrs.items()])
            self.traits[TRAITSBYNAME['origFile']] = self.cabinet.filename
            self.nuts = {}
            self.arbUnits = {}

            self.locator = ('food named %s' 
                    % self.traits.get(TRAITSBYNAME['name'],
                        '<unnamed food>'))

        elif name == 'nutrient':
            nutName = attrs['name']
            try:
                nutrVal = self.uniconv.convertUni(attrs['amount'])
            except self.uniconv.UnconvertableError:
                raise NutrientValueError(self.cabinet.filename,
                        self.locator, nutName, attrs['amount'])

            units = attrs['units']
            try:
                nutNo = NUTSBYNAME[nutName]['no']
            except KeyError:
                raise NoNutrientError(self.cabinet.filename, 
                        self.locator, nutName)
            if units not in NUTSBYNAME[nutName]['units']:
                if units == '%':
                    dv = NUTSBYNO[nutNo]['dv']
                    if not dv:
                        raise NoPercentageError(self.cabinet.filename,
                                self.locator, NUTSBYNO[nutNo]['name'])
                    nutrVal = nutrVal / 100 * dv
                else:
                    raise BadUnitsError(self.cabinet.filename,
                            self.locator, nutName, 
                            units, NUTSBYNAME[nutName]['units'])
            self.nuts[nutNo] = nutrVal

        if name == 'recipe':
            self.ingredients = []
            self.arbUnits = {}
            self.traits = dict([(TRAITSBYNAME[attr], value) for attr, value
                in attrs.items()])
        
            self.locator = self.traits.get(TRAITSBYNAME['name'],
                '<unnamed>')

        elif name == 'ingredient':
            locator = (u'Recipe %s, ingredient %s' %
                    (self.traits[TRAITSBYNAME['name']], attrs['name']))
            try:
                source = self.files[attrs['file']]
            except KeyError:
                try:
                    self.files[attrs['file']] = getCabinet(attrs['file'], 0)
                except CabinetOpenError, e:
                    raise SourceFileError(self.cabinet.filename, 
                            attrs['file'], e)
                else:
                    source = self.files[attrs['file']]
            foodKeys = source.search({TRAITSBYNAME['name']:attrs['name']}, 
                    EXACTMATCH)
            if len(foodKeys) != 1:
                raise FoodSearchError(self.cabinet.filename, locator,
                        attrs['name'], source, foodKeys)
            foodKey = foodKeys[0]

            ingredient = source.getFood(foodKey, True)

            try:
                comment = attrs['comment']
            except KeyError:
                pass
            else:
                ingredient.traits[TRAITSBYNAME['comment']] = comment

            unit = attrs['unit']
            if unit not in ingredient.units:
                raise IngredientUnitError(self.cabinet.filename,
                    locator, unit, ingredient.units)
            ingredient.traits[TRAITSBYNAME['unit']] = unit
                
            qty = attrs['qty']
            try:
                self.uniconv.convertUni(qty)
            except self.uniconv.UnconvertableError:
                raise IngredientQtyError(self.cabinet.filename,
                        locator, qty)
            ingredient.traits[TRAITSBYNAME['qty']] = qty
            self.ingredients.append(ingredient)

        elif name == 'unit':
            try:
                unitVal = self.uniconv.convertUni(attrs['grams'])
            except self.uniconv.UnconvertableError:
                raise UnitValueError(self.cabinet.filename,
                        self.locator, attrs['name'], attrs['grams'])
            if not unitVal:
                raise UnitValueError(self.cabinet.filename,
                        self.locator, attrs['name'], attrs['grams'])
            self.arbUnits[attrs['name']] = unitVal
        
        elif name == 'directions':
            self.inDirections = True
            self.traits[TRAITSBYNAME['directions']] = u''

        elif name in _HTMLTAGS:
            self.traits[TRAITSBYNAME['directions']] += u'<%s' % name
            for attrName, attrValue in attrs.items():
                self.rTraits[TRAITSBYNAME['directions']] += (u' %s=%s' %
                    (attrName, saxutils.quoteattr(attrValue)))
            self.traits[TRAITSBYNAME['directions']] += u'>'

    def endElement(self, name):
        if name == 'food':
            food = self.foods.Food(traits = self.traits, 
                arbUnits = self.arbUnits, nutsPer100g = self.nuts)
        if name == 'recipe':
            food = self.foods.Recipe(traits = self.traits,
                    arbUnits = self.arbUnits,
                    ingredients = self.ingredients)

        if name == 'food' or name == 'recipe':
            locator = ('food named %s' % 
                food.traits.get(TRAITSBYNAME['name'], '<unknown food>'))
            if food.traits[TRAITSBYNAME['unit']] not in food.units:
                raise NoUnitError(self.cabinet.filename, locator,
                        food.traits[TRAITSBYNAME['unit']],
                        food.units)

            for traitName in self.foods.FLOATTRAITS:
                try:
                    traitStr = food.traits[TRAITSBYNAME[traitName]]
                except KeyError:
                    continue
                try:
                    self.uniconv.convertUni(traitStr)
                except self.uniconv.UnconvertableError:
                    raise TraitValueError(self.cabinet.filename,
                            locator, traitName, traitStr)
                    
            if name == 'food':
                # convert to nuts per 100 g
                food.nutsPer100g = dict([(nutNo, nutrVal / food.grams * 100)
                    for nutNo, nutrVal in food.nutsPer100g.items()])
                
            self.cabinet.addFood(food, None)

        elif name == 'directions':
            self.inDirections = False

        elif name in _HTMLTAGS:
            self.traits[TRAITSBYNAME['directions']] += u'</%s>' % name

    def characters(self, content):
        if self.inDirections:
            self.traits[TRAITSBYNAME['directions']] += content

class Txt(_Base):
    """ Holds a File instance derived from a plain text file. """
    COLS = {'source':0, 'name':1, 'qty':2, 'unit':3,
            'date':4, 'meal':5, 'group':6, 'comment':7,
            'options':8 }
    TRAITCOLS = {'name':1, 'qty':2, 'unit':3,
            'date':4, 'meal':5, 'group':6, 'comment':7 }

    def __init__(self, filename, options):
        import re
        import os
        import sys
        import locale
        import errno
        import foods
        import uniconv

        COLS = Txt.COLS
        TRAITCOLS = Txt.TRAITCOLS

        for attr in _CABATTRS:
            setattr(self, '_' + attr, {})
        self.origFiles = {}
        newfile = False
        self.changed = False
        self._staleNuts = True
        self._staleGrams = True
        self.defaultEncoding = locale.getdefaultlocale()[1]

        self.filename = os.path.abspath(filename)
        try:
            fh = open(self.filename, 'r')
        except IOError, error:
            if error.errno == errno.ENOENT:
                if not options & CREATE:
                    raise 
                self.lines = []
                return
            else:
                raise

        data = fh.read()
        data = unicode(data, self.defaultEncoding)
        self.lines = data.splitlines(True)
        fh.close()

        # split lines into fields, pad lines
        # that are missing fields on the end
        splitLines = []
        lineNo = 0
        for line in self.lines:
            lineNo += 1

            # strip comments
            line = re.sub(r'(?<!\\)#.*$', '', line)
            if re.search(r'^\s*$', line):
                # skip blank lines
                splitLines.append([])
            else:
                splitLineRaw = re.split(r'(?<!\\):', line.rstrip())
                splitLine = []
                for field in splitLineRaw:
                    # raise error if any backslashes not followed by
                    # ':|#|\'
                    if re.search(r'\\(?=:|#|\\)', field):
                        raise BadBackslashError('line number %s' % lineNo)

                    # replace \:, \\, and \#
                    for char in (':', '\\', '#'):
                        newField = field.replace('\\%s' % char, char)
                    splitLine.append(newField)

                # substitute blanks for empty fields
                if len(splitLine) < 2:
                    raise MissingFieldsError('line number %s' % lineNo)
                while len(splitLine) < len(COLS):
                    splitLine.append('')
                splitLines.append(splitLine)

        sources = {}
        lineNo = 0
        for splitLine in splitLines:
            lineNo += 1
            if splitLine == []:
                continue

            sourceName = splitLine[COLS['source']]
            try:
                source = sources[sourceName]
            except KeyError:
                try:
                    sources[sourceName] = getCabinet(sourceName, 0)
                except CabinetOpenError, e:
                    raise SourceFileError(self.filename, sourceName, e)
                source = sources[sourceName]

            traits = dict([(TRAITSBYNAME[colName], splitLine[colNo])
                for colName, colNo in TRAITCOLS.items() 
                if splitLine[colNo] != ''])
            sName = traits.pop(TRAITSBYNAME['name'])
            traits[TRAITSBYNAME['order']] = '%04u' % lineNo

            options = 0
            for letter in splitLine[COLS['options']]:
                if letter == 'i':
                    options = options | IGNORECASE
                elif letter == 'x':
                    options = options | EXACTMATCH
                else:
                    raise UnrecognizedOptionError('line number %s' % lineNo,
                            letter)

            foodKeys = source.search({TRAITSBYNAME['name']:sName}, options)

            if len(foodKeys) != 1:
                raise FoodSearchError(self.filename, 
                        'line number %s' % lineNo, sName,
                        source, foodKeys)

            food = source.getFood(foodKeys[0], True)
            for traitNo, traitValue in traits.items():
                if traitNo == TRAITSBYNAME['unit']:
                    unitMatches = listSearch(food.units, traitValue, options)
                    if len(unitMatches) != 1:
                        raise UnitSearchError(self.filename,
                                'line number %s' % lineNo, traitValue)
                    food.traits[traitNo] = unitMatches[0]
                elif traitNo == TRAITSBYNAME['qty']:
                    try:
                        uniconv.convertUni(traitValue)
                    except uniconv.UnconvertableError:
                        raise QtyError(self.filename, 
                                'line number %s' % lineNo, traitValue)
                    food.traits[traitNo] = traitValue
                else:
                    food.traits[traitNo] = traitValue

            _Base.addFood(self, food, None, False)

    def __delitem__(self, key):
        lineNoToDelStr = self[key].traits['order']
        lineNoToDel = int(lineNoToDelStr.lstrip('0'))

        # decrement line number of all subsequent foods
        for food in self.values():
            afterLineNoStr = food.traits['order']
            afterLineNo = int(afterLineNoStr.lstrip('0'))
            if afterLineNo > lineNoToDel:
                afterLineNo -= 1
                food.setTraits({'order': 
                    u'%04u' % afterLineNo})
        
        del self.lines[lineNoToDel - 1]
        del self.origFiles[key]
        _Base.__delitem__(self, key)

    def save(self):
        import errno
        if not self.changed:
            return
        data = ''.join(self.lines).encode(self.defaultEncoding)
        try:
            fh = open(self.filename, 'w')
        except IOError, e:
            if e.errno == errno.EACCES:
                raise ReadOnlyError
            else:
                raise
        fh.write(data)
        fh.close()

    def addFood(self, food, oldKey):
        import os

        food = food.copy()
        if oldKey:
            food.traits[TRAITSBYNAME['order']] = \
                    self[oldKey].traits[TRAITSBYNAME['order']]
        else:
            food.traits[TRAITSBYNAME['order']] = \
                    (u'%04u' % (len(self.lines) + 1))

        line = []
        for traitName in ('origFile', 'name', 'qty', 'unit', 'date',
                'meal', 'group', 'comment'):
            line.append(food.traits.get(TRAITSBYNAME[traitName], ''))
        line.append('x\n')

        for char in (':', '#', '\\'):
            line = [ field.replace(char, '\\%s' % char) for field in line ]

        if oldKey:
            lineNo = int(food.traits['order'])
            self.lines[lineNo - 1] = ':'.join(line)
        else:
            self.lines.append(':'.join(line))

        return _Base.addFood(self, food, oldKey)

class GoalReporter(UserDict.DictMixin):
    """ Tells how foods compare against given set of goals.

    A dictionary mixin. The keys are the hashes of the foods. The
    values are Data objects. Also has a totals property, which
    returns the same thing.

    All values returned are strings, not numbers.

    nutrientAmount is typically a string that evaluates to a number.
    However, it is ?? if the nutrient does not exist for this food.

    pctGoal is typically a string that evaluates to a number.
    It may also be:
        ?? if the nutrient does not exist for this food
        ++ if the goal is 0 and nutrientAmt > 0
        -- if the goal is 0 and nutrientAmt < 0
        NA is goal is None

    pctTotal is typically a string that evaluates to a number.
    It may also be:
        ?? if the nutrient does not exist for this food
    """

    def __init__(self, goals, cabinet):
        """ goals: list of (goalNo, goalAmount) tuples. """ 
        self.goals = goals
        self._cabinet = cabinet

    def _getReport(self, food):
        """ Gets a report for food.
        
        Food is anything with a nuts attribute. """
        totalNuts = self._cabinet.nuts
        result = []
        for goalNo, goalAmount in self.goals:
            item = {}
            item['no'] = goalNo
            try:
                nutAmtNo = food.nuts[goalNo]
            except KeyError:
                item['nutAmt'] = '??'
                item['pctGoal'] = '??'
                item['pctTotal'] = '??'
            else:
                item['nutAmt'] = '%.0f' % nutAmtNo
                if nutAmtNo == 0:
                    item['pctGoal'] = '0'
                    item['pctTotal'] = '0'
                else:
                    try:
                        item['pctGoal'] = '%.0f' % (nutAmtNo / 
                                goalAmount * 100)
                    except TypeError:
                        # Goal is None
                        item['pctGoal'] = 'NA'
                    except ZeroDivisionError:
                        if nutAmtNo > 0:
                            item['pctGoal'] = '++'
                        else:
                            item['pctGoal'] = '--'
                    amtTotal = totalNuts[goalNo]
                    item['pctTotal'] = '%.0f' % (nutAmtNo / 
                        amtTotal * 100)
            result.append(item)
        return result

    def __getitem__(self, key):
        return self._getReport(self._cabinet[key])

    def _getTotals(self):
        return self._getReport(self._cabinet)

    totals = property(fget = _getTotals, doc = _getTotals.__doc__)

def getCabinet(filename, options):
    """ Get a File object appropriate for filename. """

    import os
    import errno

    fileExt = os.path.splitext(filename)[1].lower()
    for ext, type in (('.zip', Zip), ('.txt', Txt), ('.xml', Xml)):
        if fileExt == ext:
            cabinet = type
            break
        cabinet = Shelf
    try:
        result = cabinet(filename, options)
    except IOError, e:
        raise CabinetOpenIOError(filename, e)
    return result

def listSearch(sList, sStr, options):
    """ Returns list of items from sList that match sStr.
    
    Options are set using the global constants IGNORECASE
    and EXACTMATCH."""

    if options & EXACTMATCH:
        return [ item for item in sList if item == sStr ]
    else:
        import re
        flag = 0
        if options & IGNORECASE:
            flag = re.IGNORECASE
        sRE = re.compile(sStr, flag)

        return [ item for item in sList if sRE.search(item) ]

def getIngredients(ingredients):
    """ Returns list of food or recipe objects.

    ingredients argument is a list of raw food data items;
    getIngredients instantiates the foods. """
    import foods

    result = []
    for ingredient in ingredients:
        args = {}
        args['traits'] = ingredient['traits']
        args['arbUnits'] = ingredient['arbUnits']
        if 'ingredients' in ingredient:
            args['ingredients'] = getIngredients(ingredient['ingredients'])
            result.append(foods.Recipe(**args))
        else:
            args['nutsPer100g'] = ingredient['nutsPer100g']
            result.append(foods.Food(**args))
    return result

def getData(ingredients):
    """ Returns list of Python primitives for ingredients.

    ingredients argument is a list of Food or Recipe objects;
    getData returns dicts for traits, arbUnits, and ingredients or
    nuts (as appropriate). """

    result = []
    for ingredient in ingredients:
        data = ingredient.data
        if 'ingredients' in data:
            data['ingredients'] = getData(data['ingredients'])
        result.append(data)
    return result

### EXCEPTIONS
class Error(Exception):
    pass

class DuplicateFoodError(Error):
    """ Cannot add food as it duplicates one already in file. """
    def __init__(self, filename, food):
        self.msg = [u'Cannot add food %s: duplicate' 
                % food.traits.get(TRAITSBYNAME['name'], '<unnamed>')]

class CabinetOpenError(Error):
    """ Root class for all errors that can occur when opening a cabinet.

    Subclasses must define a msg attribute--a unicode indicating
    the nature of the error. In addition, subclasses must define a 
    filename attribute, which is a unicode indicating the filename
    in which the error occurred. """
    
    def __init__(self, filename):
        self.msg = []

class ShelfVersionError(CabinetOpenError):
    """ Shelf version is too new or too old. """
    def __init__(self, filename, acceptVersion, shelfVersion, currVersion):
        CabinetOpenError.__init__(self, filename)
        self.msg.append(u'File created with incompatible Pantry version.')
        self.msg.append(u'This version accepts files created with Pantry'
                + ' versions %i through %i.' % (acceptVersion, currVersion))
        self.msg.append(u'The file was created with Pantry version %i.' 
                % shelfVersion)

class SourceFileError(CabinetOpenError):
    """ A problem occurred in opening the source file for Recipes or Txt. """
    def __init__(self, filename, sourceFile, lowerException):
        CabinetOpenError.__init__(self, filename)
        self.msg.append(u'Could not open source file %s' % sourceFile)
        self.msg.extend(lowerException.msg)

class XMLError(CabinetOpenError):
    """ XML is invalid, the wrong type, not well formed, etc.

    Subclasses have specific errors. XMLError should be used as a base
    class only if the problem is with the XML itself--that is, the XML
    is not valid, it is not XML at all, or it is not a Pantry XML file.
    If the XML is perfectly valid but there are other problems with it 
    (e.g. the foods in it are wrong) do not subclass this. """
    def __init__(self, filename):
        CabinetOpenError.__init__(self, filename)
        self.msg.append('Could not read XML:')

class UnknownXMLError(XMLError):
    """ Root element of XML cannot be determined. """

    def __init__(self, filename):
        XMLError.__init__(self, filename)
        self.msg.append(u'Root element cannot be determined.')

class InvalidXMLError(XMLError):
    """ XML file is invalid. """

    def __init__(self, filename, xmlType, errorInst):
        """ xmlType: unicode, either Foods or Recipes as appropriate. """
        XMLError.__init__(self, filename)
        self.msg.append(u'File does not validate as a %s file.' % xmlType)
        self.msg.extend(errorInst.msg)

class WrongXMLError(XMLError):
    """ XML is not Pantry. """

    def __init__(self, filename):
        XMLError.__init__(self, filename)
        self.msg.append('XML file is not a Pantry file.')

class InvalidDataError(CabinetOpenError):
    def __init__(self, locator):
        self.msg = [u'Invalid data in file at location: %s' % locator]

class CabinetOpenIOError(CabinetOpenError):
    def __init__(self, filename, e):
        CabinetOpenError.__init__(self, filename)
        self.msg.append(e.strerror)

class NoNutrientError(InvalidDataError):
    """ Nutrient name given in XML file does not exist. """
    def __init__(self, filename, locator, nutName):
        InvalidDataError.__init__(self, locator)
        self.msg.append('%s is an invalid nutrient name.' % nutName)

class BadUnitsError(InvalidDataError):
    """ Nutrient units given do not match the permissible value. """
    def __init__(self, filename, locator, nutName, badUnits, goodUnits):
        InvalidDataError.__init__(self, locator)
        self.msg.append('Units for nutrient %s is incorrect.' % nutName)
        self.msg.append('Given units: %s Correct units: %s' % 
                (badUnits, goodUnits))

class NutrientValueError(InvalidDataError):
    """ Nutrient value cannot be converted by convertUni. """
    def __init__(self, filename, locator, nutName, nutrVal):
        InvalidDataError.__init__(self, locator)
        self.msg.append('Nutrient value invalid.')
        self.msg.append('Name: %s Given value: %s' % (nutName, nutrVal))

class UnitValueError(InvalidDataError):
    def __init__(self, filename, locator, unitName, unitVal):
        InvalidDataError.__init__(self, locator)
        self.msg.append('Unit value invalid.')
        self.msg.append('Name: %s Given value: %s' % (unitName, unitVal))

class FoodSearchError(InvalidDataError):
    """ Search for foods for ingredient or Txt returned != 1 match. """
    def __init__(self, filename, locator, pattern, source, keys):
        InvalidDataError.__init__(self, locator)
        self.msg.append('Search pattern %s did not match exactly one food.'
                % pattern)
        self.msg.append('Matches:')
        if len(keys) > 1:
            matchingNames = [source.getFood(key, False).traits.get(
                TRAITSBYNAME['name'], '<unnamed food>')
                    for key in keys]
            self.msg.append(u'\n'.join(matchingNames))
        else:
            self.msg.append(u'<none>')

class IngredientUnitError(InvalidDataError):
    """ Unit given for ingredient is invalid. """
    def __init__(self, filename, locator, badUnit, availUnits):
        InvalidDataError.__init__(self, locator)
        self.msg.append('Unit %s is not available for this food.' % badUnit)
        self.msg.append('Available units: %s' % '\n'.join(availUnits))

class IngredientQtyError(InvalidDataError):
    """ Qty given for food cannot be converted to a number. """
    def __init__(self, filename, locator, qty):
        InvalidDataError.__init__(self, locator)
        self.msg.append('%s is not a valid quantity.' % qty)

class QtyError(InvalidDataError):
    """ Qty for an ingredient or FoodsTxt food would not convert to float. """
    def __init__(self, filename, locator, badQty):
        InvalidDataError.__init__(self, locator)
        self.msg.extend('qty %s is not a number.' % badQty)

class UnitSearchError(InvalidDataError):
    """ Search for unit for ingredient or for Foods Txt has != 1 match. """
    def __init__(self, filename, locator, badUnit):
        InvalidDataError.__init__(self, locator)
        self.msg.extend('unit %s does not have exactly one match.' % badUnit)

class FoodInitError(InvalidDataError):
    """ Could not initialize food in XML or Recipes file. """
    def __init__(self, filename, locator):
        InvalidDataError.__init__(self, locator)

class NoPercentageError(FoodInitError):
    """ Percent given for nutrient, but there is no %DV. """
    def __init__(self, filename, locator, nutName):
        FoodInitError.__init__(self, filename, locator)
        self.msg.append('No %%DV is available for nutrient %s.' % nutName)

class NoUnitError(FoodInitError):
    """ Unit given in XML file does not exist for this food. """
    def __init__(self, filename, locator, badUnit, availUnits):
        FoodInitError.__init__(self, filename, locator)
        self.msg.append('Unit %s not available for this food.' % badUnit)
        self.msg.append('Available units:\n%s' % '\n'.join(availUnits))

class TraitValueError(FoodInitError):
    """ Given trait will not convert to a float. """
    def __init__(self, filename, locator, traitName, badValue):
        FoodInitError.__init__(self, filename, locator)
        self.msg.append('Value %s given for trait %s is not a number.' %
                (badValue, traitName))

class TxtError(InvalidDataError):
    """ Problems parsing Txt files. """
    def __init__(self, locator):
        InvalidDataError.__init__(self, locator)

class BadBackslashError(TxtError):
    def __init__(self, locator):
        TxtError.__init__(self, locator)
        self.msg.append('Backslash not followed by "#", ":", or "\\"')

class MissingFieldsError(TxtError):
    def __init__(self, locator):
        TxtError.__init__(self, locator)
        self.msg.append('Each line must have at least two fields.')

class UnrecognizedOptionError(TxtError):
    def __init__(self, locator, option):
        TxtError.__init__(self, locator)
        self.msg.append('Option %s not recognized.' % option)

class ReadOnlyError(Error):
    pass

### CONSTANTS
# cabinet open
SKIPVALID = 2
BIGVALID = 4
CREATE = 8

MAXSIZE = 307200

# search 
IGNORECASE = 1
EXACTMATCH = 2

# food add
MUTATE = 2
INCREMENT = 4

ACCEPT_VERSION = 22 

from foods import TRAITSBYNAME, TRAITSBYNO, ABSOLUTES, NUTSBYNO, NUTSBYNAME

PANTRYDTD = """
<!ELEMENT pantry        (food | recipe)*>
<!ELEMENT food          (nutrient+, unit*) >
<!ELEMENT nutrient      EMPTY>
<!ELEMENT recipe        (ingredient+, unit*, directions?)>
<!ELEMENT yield         EMPTY>
<!ELEMENT unit          EMPTY>
<!ELEMENT directions    (p+)>
<!ELEMENT p             (#PCDATA) >
<!ELEMENT ingredient    EMPTY>

<!ATTLIST food
    name                CDATA #IMPLIED
    group               CDATA #IMPLIED
    pctRefuse           CDATA #IMPLIED
    refDesc             CDATA #IMPLIED 
    date                CDATA #IMPLIED
    meal                CDATA #IMPLIED
    qty                 CDATA #REQUIRED 
    comment             CDATA #IMPLIED
    order               CDATA #IMPLIED
    unit                CDATA #REQUIRED >

<!ATTLIST nutrient
    name                CDATA #REQUIRED
    units               CDATA #REQUIRED 
    amount              CDATA #REQUIRED >

<!ATTLIST recipe
    name                CDATA #IMPLIED
    group               CDATA #IMPLIED
    pctRefuse           CDATA #IMPLIED
    refDesc             CDATA #IMPLIED 
    date                CDATA #IMPLIED
    meal                CDATA #IMPLIED
    qty                 CDATA #REQUIRED 
    comment             CDATA #IMPLIED
    order               CDATA #IMPLIED
    unit                CDATA #REQUIRED 
    yieldGrams          CDATA #IMPLIED
    yieldDesc           CDATA #IMPLIED
    servings            CDATA #IMPLIED >

<!ATTLIST ingredient
    name                    CDATA #REQUIRED
    qty                     CDATA #REQUIRED
    unit                    CDATA #REQUIRED
    file                    CDATA #REQUIRED
    comment                 CDATA #IMPLIED >

<!ATTLIST unit
    grams               CDATA #REQUIRED
    name                CDATA #REQUIRED >""".strip()

_CABATTRS = ['traits', 'nutsPer100g', 'arbUnits', 'ingredients']
_HTMLTAGS = ['p']
# Constants from sr:
# 
# EOL: end of line character in SR table files.
# STRINGQUOTE: used to quote string fields in SR.
# FIELDSEP: SR field separator.
# ENCODING: hmm...
# 
# TABLES is a dict: 
#     key: table name
# 
#     value: 2-tuple of tuples. value[0] consists of strings, 
#     with each string being a field name (in the order that they
#     appear in the table.) value[1] consists of strings, with each 
#     string being the name of a key field.
# 
#     Many key/value pairs in fields are commented out. This way
#     FileSrZip does not import these tables. In addition,
#     FileSrZip is unaffected if trailing fields are omitted.
#     Thus trailing fields are often omitted to save RAM. """

_EOL = '\r\n'
_STRINGQUOTE = '~'
_FIELDSEP = '^'
_ENCODING = 'iso-8859-1'

# fields. Key: table name. Value: 2-tuple; first tuple
# are the fields; second tuple is the keys.
# It does no harm to omit the last fields of a table, so
# these are omitted where possible to save RAM.
_TABLES = {
    #'food_des':(('ndbNo', 'fdGrpCd', 'longDesc', 'shrtDesc', 'comName', 
    #    'manufacName', 'survey', 'refDesc', 'pctRefuse', 'sciName',
    #    'nFactor', 'proFactor', 'fatFactor', 'choFactor'), ('ndbNo',)),

    'food_des':(('ndbNo', 'fdGrpCd', 'longDesc', 'shrtDesc', 'comName', 
        'manufacName', 'survey', 'refDesc', 'pctRefuse'), ('ndbNo',)),

    'fd_group':(( 'fdGrpCd', 'fdGrpDesc' ), ('fdGrpCd',)),

    # There is a last field in SR, 'cc', but it has not been populated
    # in SR 19. That's what the docs say anyway. It seems there is data
    # there but although it is alphanumeric, it is not surrounded by
    # tildes like all the other strings are. This causes import errors.
    # I have simply omitted the last field, which should not cause any
    # problems. (The last field is 'cc', for 'confidence code.'
    #
    # Subsequently additional fields omitted as well, to save RAM.

    #'nut_data':(( 'ndbNo', 'nutrNo', 'nutrVal', 'numDataPts', 'stdError',
    #    'srcCd', 'derivCd', 'refNdbNo', 'addNutrMark', 'numStudies',
    #    'min', 'max', 'df', 'loweb', 'upeb', 'statcmt'), 
    #    ('ndbNo', 'nutrNo')),

    'nut_data':(( 'ndbNo', 'nutrNo', 'nutrVal'), ('ndbNo', 'nutrNo')),

    #'nutr_def':(( 'nutrNo', 'units', 'tagName', 'nutrDesc', 'numDec',
    #    'srOrder'), ('nutrNo',)),

    #'nutr_def':(( 'nutrNo', 'units', 'tagName', 'nutrDesc'), ('nutrNo',)),

    #'src_cd':(( 'srcCd', 'srcCdDesc'), ('srcCd',)),

    #'deriv_cd':(( 'derivCd', 'derivDesc'), ('derivCd',)),

    #'weight':(( 'ndbNo', 'seq', 'amount', 'measureDesc', 'gmWgt', 'numDataPts',
    #    'stdDev' ), ('ndbNo', 'seq')),

    'weight':(( 'ndbNo', 'seq', 'amount', 'measureDesc', 'gmWgt'), 
        ('ndbNo', 'seq')),

    #'footnote':(( 'ndbNo', 'footntNo', 'footntType', 
    #   'nutrNo', 'footntTxt' ),
    #    ('ndbNo', 'footntNo')),
    #
    #'data_Src':(( 'dataSrcId', 'authors', 'title', 'year', 'journal', 
    #   'volCity', 'issueState', 'startPage', 'endPage' ), ('dataSrcId',)),
    #
    #'datSrcLn':(( 'ndbNo', 'nutrNo', 'dataSrcId' ), ('ndbNo', 'nutrNo'))
    }
