"""birdnotes.py: Represents an XML file conforming to birdnotes.rnc For documentation, see: http://www.nmt.edu/~shipman/aba/doc/pyims/ """ #================================================================ # Imports #---------------------------------------------------------------- import sys, os, stat, datetime, re from lxml import etree as et import terrapos import txny import abbr as abbrModule import rnc #================================================================ # Manifest constants #---------------------------------------------------------------- SCHEMA_RNG = "birdnotes.rng" YEAR_PAT = re.compile ( r'[12]' # Matches 1 or 2 r'\d{3}' # Matches three digits r'$' ) # End anchor: insure a complete match YYYY_MM_XML_PAT = re.compile ( r'[12]' # Matches 1 or 2 r'\d{3}' # Matches three digits r'\-' # Matches a hyphen r'[01]' # Matches 0 or 1 r'\d' # Matches any digit r'\.xml' # Matches a period followed by 'xml' r'$' ) # End anchor, insure a full match # - - - - - c l a s s B i r d N o t e S e t class BirdNoteSet: """Represents a NOTE_SET_N element. Exports: BirdNoteSet ( txny ): [ (txny is a taxonomy as a Txny object) and (period is the set's period title as a string) -> return a new, empty BirdNoteSet using that taxonomy ] .txny: [ as passed to constructor, read-only ] .newestTime: [ modification epoch time of the most recently modified source file read, or None ] .period: [ the set's period title, initially "", read/write ] .genDays(): [ generate the daily sets in self as DayNotes objects ] .addDay ( dayNotes ): [ dayNotes is a DayNotes object -> self := self with dayNotes added ] .readFile ( fileName ): [ fileName is a string -> if fileName names a readable file that validates against birdnotes.rnc -> self := self with the contents of that file added else -> raise IOError ] .writeFile ( fileName ): [ fileName is a string -> if fileName names a file that can be created -> that file := contents of self else -> raise IOError ] State/Invariants: .__dayList: [ a list containing self's daily sets as DayNotes objects ] """ # - - - B i r d N o t e S e t . _ _ i n i t _ _ def __init__ ( self, txny ): """Constructor for BirdNoteSet """ self.txny = txny self.period = "" self.newestTime = None self.__dayList = [] # - - - B i r d N o t e S e t . a d d D a y def addDay ( self, dayNotes ): """Add a new day's notes to self. """ self.__dayList.append ( dayNotes ) # - - - B i r d N o t e S e t . g e n D a y s def genDays ( self ): """Generate the DayNotes elements of self. """ for day in self.__dayList: yield day raise StopIteration # - - - B i r d N o t e S e t . r e a d F i l e def readFile ( self, fileName ): """Read content from an XML file. """ #-- 1 -- # [ if fileName does not exist -> # raise IOError # else if (self.newestTime is None) or # (self.newestTime < modification time of fileName) -> # self.newestTime := modification time of fileName # else -> I ] self.__fileTime ( fileName ) #-- 2 -- # [ if fileName names a readable, well-formed XML file # that validates against SCHEMA_RNG -> # noteSet := the root element of that tree # else -> # raise IOError ] noteSet = self.__validate ( fileName ) #-- 3 -- # [ if noteSet has an rnc.PERIOD_A attribute -> # self.period := that attribute # else -> raise IOError ] try: self.period = noteSet.attrib[rnc.PERIOD_A] except KeyError: raise IOError, ( "The %s element must have a %s " "attribute" % (rnc.NOTE_SET_N, rnc.PERIOD_A) ) #-- 4 -- # [ dayList := list of DAY_NOTES_N children of noteSet ] dayList = noteSet.xpath ( rnc.DAY_NOTES_N ) #-- 5 -- # [ self := self with content added from all nodes # in dayList ] for node in dayList: self.__readDayNotes ( node ) # - - - B i r d N o t e S e t . _ _ f i l e T i m e def __fileTime ( self, fileName ): """Update self.__newestTime [ fileName is a string -> if fileName does not exist -> raise IOError else if (self.newestTime is None) or (self.newestTime < modification time of fileName) -> self.newestTime := modification time of fileName else -> I ] """ #-- 1 -- # [ if fileName names a file that does not exist or is # unreadable -> # raise IOError # else -> # modTime := modification timestamp of that file ] if os.path.exists ( fileName ): status = os.stat ( fileName ) modTime = status[stat.ST_MTIME] else: raise IOError, "No such file: '%s'" % fileName #-- 2 -- if ( (self.newestTime is None) or (self.newestTime < modTime) ): self.newestTime = modTime # - - - B i r d N o t e S e t . _ _ v a l i d a t e def __validate ( self, fileName ): """Build an XML tree and validate it against the schema. [ fileName is a string -> if (SCHEMA_RNG names a readable, well-formed RNG bird notes schema) and (fileName names a readable XML bird notes file that validates against that schema) -> return the root node of a document representing that bird notes file as an et.Element ] """ #-- 1 -- # [ if SCHEMA_RNG names a readable, well-formed XML file -> # schemaDoc := a new et.ElementTree representing # that file # else -> raise IOError ] try: schemaDoc = et.parse ( SCHEMA_RNG ) except et.XMLSyntaxError: raise IOError, ( "Schema file '%s' is not " "well-formed XML." % SCHEMA_RNG ) except IOError, detail: raise IOError, ( "Can't read schema file '%s': %s" % (SCHEMA_RNG, str(detail)) ) #-- 2 -- # [ if schemaDoc is a valid Relax NG schema -> # schema := an et.RelaxNG instance representing schemaDoc # else -> raise IOError ] try: schema = et.RelaxNG ( schemaDoc ) except et.RelaxNGParseError, detail: raise IOError, ( "File '%s' is not a valid " "RNG schema: %s " % (SCHEMA_RNG, str(detail)) ) #-- 3 -- # [ if fileName names a readable, well-formed XML file -> # doc := that file as an et.ElementTree # else -> raise IOError ] try: doc = et.parse ( fileName ) except et.XMLSyntaxError: raise IOError, ( "File '%s' is not well-formed XML." % fileName ) except IOError, detail: raise IOError, ( "Can't read file '%s': %s" % (fileName, str(detail)) ) #-- 4 -- # [ if doc fails to validate against schema -> # raise IOError # else -> I ] if not schema.validate ( doc ): raise IOError, ( "File %s is not a valid bird notes " "file: %s" % (fileName, schema.error_log) ) #-- 5 -- # [ return the root element of doc ] return doc.getroot() # - - - B i r d N o t e S e t . _ _ r e a d D a y N o t e s def __readDayNotes ( self, dayNode ): """Convert a DAY_NOTES_N node to a DayNotes object. [ dayNode is a DAY_NOTES_N node as a DOM Element -> if dayNode is valid -> self.__dayList +:= a new DayNotes element made from dayNode else -> raise IOError ] """ #-- 1 -- # [ if dayNode is a valid DAY_NOTES_N node -> # dayNotes := a DayNotes object representing dayNode # else -> raise IOError ] dayNotes = DayNotes.readNode ( self, self.txny, dayNode ) #-- 2 -- self.addDay ( dayNotes ) # - - - B i r d N o t e S e t . w r i t e F i l e def writeFile ( self, fileName ): """Translate back to XML. """ #-- 1 -- # [ tree := an et.ElementTree instance with root element # rnc.NOTE_SET_N # root := that root element as an et.Element instance, with # its rnc.PERIOD_A attribute set to self.period ] root = et.Element ( rnc.NOTE_SET_N ) root.attrib[rnc.PERIOD_A] = self.period tree = et.ElementTree ( root ) #-- 2 -- # [ root := root with content added for all DayNote # instances in self ] for dayNotes in self.genDays(): dayNotes.writeNode ( root ) #-- 3 -- # [ if fileName names a file that can be created new -> # that file := tree as XML # else -> raise IOError ] tree.write ( fileName, pretty_print=True ) # - - - - - c l a s s D a y N o t e s - - - - - class DayNotes: """Represents one day's notes within one state. Exports: DayNotes ( noteSet, regionCode, date, daySummary, dayLoc ): [ (noteSet is the containing BirdNoteSet object) and (regionCode is the enclosing region as a case-insensitive US postal state code or the foreign equivalent, e.g., "nm") and (date is the fieldwork date as "YYYY-MM-DD") and (daySummary is a summary of the field day as a DaySummary) and (dayLoc is the default location as a Loc instance) -> return a new DayNotes object representing those values ] .noteSet: [ as passed to constructor, read-only ] .regionCode: [ as passed to constructor, read-only ] .date: [ as passed to constructor, read-only ] .daySummary: [ as passed to constructor, read-only ] .dayLoc: [ as passed to constructor, read-only ] .title(): [ return date+region+dayLoc.name ] .defaultLoc(): [ return the day's default location as a Loc instance ] .lookupLoc ( locCode ): [ locCode is a location code as a string -> if locCode is defined in self (case-sensitive) -> return that location as a Loc instance else -> raise KeyError ] .addForm ( birdForm): [ birdForm is a set of one or more sightings of the same kind of bird as a BirdForm object -> self := self with birdForm added ] .genForms(): [ generate the bird records in self in phylogenetic order, with records assigned to the same taxon sorted by English name ] .genFormsSeq(): [ generate the bird records in self in the order they were added ] .writeNode ( parent ): [ parent is an et.Element instance -> parent := parent with a new et.Element node added representing self return that new et.Element ] DayNotes.readNode ( noteSet, txny, dayNode ): [ (noteSet is a BirdNoteSet instance) and (txny is a Txny object) and (dayNode is a DAY_NOTES_N et.Element) -> if all the taxa under dayNode are defined in txny -> return a new DayNotes object made from dayNode else -> raise IOError ] State/Invariants: .__numberAdded: [ the number of BirdForm objects that have ever been added to self ] .__seqMap: [ a dictionary whose keys are integers defining the order in which BirdForm objects were added, and each corresponding value is that BirdForm object ] .__txMap: [ a dictionary whose keys are tuples (txKey, name) where txKey is the form's taxonomic key in self.noteSet.txny and name is the full English name, and each corresponding value is the BirdForm object for that name ] """ # - - - D a y N o t e s . t i t l e def title ( self ): """Return the full daily title. """ return ( "%s: %s: %s" % (self.date, self.regionCode.upper(), self.dayLoc.name) ) # - - - D a y N o t e s . d e f a u l t L o c def defaultLoc ( self ): """Return self's default location. """ return self.daySummary.defaultLoc() # - - - D a y N o t e s . l o o k u p L o c def lookupLoc ( self, locCode ): """Lookup a location code, return a Loc instance. """ return self.daySummary.lookupLoc ( locCode ) # - - - D a y N o t e s . a d d F o r m def addForm ( self, newForm ): """Add a new BirdForm object to self. """ #-- 1 -- # [ seqNo := self.__numberAdded + 1 # self.__numberAdded +:= 1 ] self.__numberAdded += 1 seqNo = self.__numberAdded #-- 2 -- # [ phyloKey := (taxonomic key of newForm, # English name of newForm) ] phyloKey = ( newForm.birdId.taxon.txKey, str(newForm.birdId) ) #-- 2 -- self.__seqMap[seqNo] = self.__txMap[phyloKey] = newForm # - - - D a y N o t e s . g e n F o r m s def genForms ( self ): """Generate contained forms in phylogenetic order. """ #-- 1 -- # [ keyList := keys from self.txMap in ascending order ] keyList = self.__txMap.keys() keyList.sort() #-- 2 -- # [ generate the values from self.__txMap in order by the # elements of keyList ] for key in keyList: yield self.__txMap[key] #-- 3 -- raise StopIteration # - - - D a y N o t e s . g e n F o r m s S e q def genFormsSeq ( self ): """Generate contained forms in phylogenetic order. """ #-- 1 -- # [ keyList := keys from self.txMap in ascending order ] keyList = self.__seqMap.keys() keyList.sort() #-- 2 -- # [ generate the values from self.__txMap in order by the # elements of keyList ] for key in keyList: yield self.__seqMap[key] #-- 3 -- raise StopIteration # - - - D a y N o t e s . _ _ i n i t _ _ def __init__ ( self, noteSet, regionCode, date, daySummary, dayLoc=None ): #-- 1 -- # [ self := self with all initial invariants established ] self.noteSet = noteSet self.regionCode = regionCode self.date = date self.daySummary = daySummary self.dayLoc = dayLoc self.__numberAdded = 0 self.__seqMap = {} self.__txMap = {} # - - - D a y N o t e s . w r i t e N o d e def writeNode ( self, parent ): """Generate the XML for a day-notes. """ #-- 1 -- # [ attributes := a dictionary mapping rnc.STATE_A to # self.regionCode, rnc.DATE_A to self.date, and # rnc.DAY_LOC_A to self.dayLoc or to None if # self.dayLoc matches self.daySummary.defaultLoc ] attributes = { rnc.STATE_A: self.regionCode, rnc.DATE_A: self.date } if self.dayLoc.code != self.daySummary.defaultLoc().code: attributes[rnc.DAY_LOC_A] = self.dayLoc.code #-- 2 -- # [ parent := parent with a new rnc.DAY_NOTES_N node added # with attributes=(attributes) # selfNode := that node ] selfNode = et.SubElement ( parent, rnc.DAY_NOTES_N, attributes ) #-- 3 -- # [ selfNode := selfNode with a new rnc.DAY_SUMMARY_N # child appended representing self.daySummary ] self.daySummary.writeNode ( selfNode ) #-- 4 -- # [ selfNode := selfNode with new rnc.FORM_N nodes added # representing the values from self.__seqMap in order # according to the keys from self.__seqMap ] sequenceKeyList = self.__seqMap.keys() sequenceKeyList.sort() for sequenceKey in sequenceKeyList: #-- 4 body -- # [ sequenceKey is a key in self.__seqMap -> # selfNode := selfNode with a new rnc.FORM_N node # added representing self.__seqMap[sequenceKey] ] birdForm = self.__seqMap[sequenceKey] birdForm.writeNode ( selfNode ) # - - - D a y N o t e s . r e a d N o d e (static method) def readNode ( noteSet, txny, dayNode ): """Convert an rnc.DAY_NOTES_N node to a DayNotes instance. """ #-- 1 -- # [ regionCode := rnc.STATE_A attribute from dayNode # date := rnc.DATE_A attribute from dayNode ] regionCode = dayNode.attrib [ rnc.STATE_A ] date = dayNode.attrib [ rnc.DATE_A ] #-- 2 -- # [ if dayNode has an rnc.DAY_LOC_A attribute -> # dayLocCode := that attribute # else -> # dayLocCode := None ] try: dayLocCode = dayNode.attrib [ rnc.DAY_LOC_A ] except KeyError: dayLocCode = None #-- 3 -- # [ if dayNode has a valid rnc.DAY_SUMMARY_N child # element -> # daySummary := a new DaySummary instance made # from that child element # else -> raise IOError ] summaryNode = dayNode.xpath ( rnc.DAY_SUMMARY_N )[0] daySummary = DaySummary.readNode ( summaryNode ) #-- 4 -- # [ if dayLocCode is None -> # dayLoc := default location from daySummary # else if dayLocCode is defined in daySummary -> # dayLoc := dayLocCode's location from daySummary # else -> # raise IOError ] if dayLocCode is None: dayLoc = daySummary.defaultLoc() else: try: dayLoc = daySummary.lookupLoc ( dayLocCode ) except KeyError: raise IOError, ( "%s '%s' is not defined in the " "%s element." % (rnc.DAY_LOC_A, dayLocCode, rnc.DAY_SUMMARY_N) ) #-- 5 -- # [ dayNotes := a new DayNotes instance with # noteSet=noteSet, regionCode=regionCode, date=date, # daySummary=daySummary, and dayLoc=dayLoc ] dayNotes = DayNotes ( noteSet, regionCode, date, daySummary, dayLoc ) #-- 6 -- # [ if the subtree rooted in dayNode conforms to # birdnotes.rnc -> # self := self with BirdForm objects added representing # all rnc.FORM_N children of dayNode ] # else -> # self := (anything) # raise IOError ] for formNode in dayNode.getiterator ( rnc.FORM_N ): #-- 6 body -- # [ formNode is an et.Element -> # if formNode roots a valid rnc.FORM_N subtree # in the context of txny -> # self := self with a BirdForm object added # representing formNode # else -> raise IOError ] dayNotes.readForm ( txny, formNode ) #-- 7 -- return dayNotes readNode = staticmethod ( readNode ) # - - - D a y N o t e s . r e a d F o r m def readForm ( self, txny, formNode ): """Translate a form node to a FormNode instance. [ (txny is a Txny instance) and (formNode is an et.Element) -> if formNode roots a valid rnc.FORM_N subtree in the context of txny -> self := self with a BirdForm object added representing formNode else -> raise IOError ] """ #-- 1 -- # [ if formNode is a valid rnc.FORM_N element -> # birdForm := a BirdForm instance representing that # element # else -> raise IOError ] birdForm = BirdForm.readNode ( txny, self, formNode ) #-- 2 -- # [ self := self with birdForm added ] self.addForm ( birdForm ) # - - - - - c l a s s D a y S u m m a r y class DaySummary: """Represents a day-summary element. Exports: DaySummary ( defaultLocCode ): [ defaultLocCode is a location code as a string -> return a new DaySummary instance with that default location code ] .defaultLocCode: [ as passed to constructor ] .defaultLoc(): [ return the default location as a Loc instance ] .addLoc ( loc ): [ loc is a location as a Loc instance -> if self has no location with the same code -> self := self with that location added else -> raise KeyError ] .lookupLoc ( locCode ): [ locCode is a location code as a string -> if locCode is defined in self (case-sensitive) -> return that location as a Loc instance else -> raise KeyError ] .genLocs(): [ generate the locations in self as a sequence of Loc instances in ascending order by code ] .route: [ if self has a route description -> that description as a Narrative instance else -> None ] .weather: [ if self has a weather description -> that description as a Narrative instance else -> None ] .missed: [ if self has a missed-species description -> that description as a Narrative instance else -> None ] .film: [ if self has a film description -> that description as a Narrative instance else -> None ] .notes: [ if self has general notes -> those notes as a Narrative instance else -> None ] DaySummary.readNode ( node ): # Static method [ node is an et.Element -> if node conforms to birdnotes.rnc -> return a new DaySummaryclass instance that represents that node else -> raise IOError ] .writeNode ( parent ): [ parent is an et.Element -> parent := parent with a new et.Element added representing self return that new et.Element ] State/Invariants: .__locCodeMap: [ a dictionary whose values are the locations in self as Loc instances, and each the corresponding key is the location code ] """ # - - - D a y S u m m a r y . _ _ i n i t _ _ def __init__ ( self, defaultLocCode ): """Constructor """ self.defaultLocCode = defaultLocCode self.route = None self.weather = None self.missed = None self.film = None self.notes = None self.__locCodeMap = {} # - - - D a y S u m m a r y . d e f a u l t L o c def defaultLoc ( self ): """Return self's default location. """ #-- 1 -- # [ if self.defaultLocCode is a valid location code in # self -> # return the corresponding Loc instance # else -> raise KeyError ] return self.lookupLoc ( self.defaultLocCode ) # - - - D a y S u m m a r y . a d d L o c def addLoc ( self, loc ): """Add a location. """ if self.__locCodeMap.has_key ( loc.code ): raise KeyError, ( "Duplicate location code '%s'" % loc.code ) self.__locCodeMap[loc.code] = loc # - - - D a y S u m m a r y . l o o k u p L o c def lookupLoc ( self, locCode ): """Lookup a location by its code. """ #-- 1 -- # [ if self has a location whose code matches locCode -> # return the corresponding location as a Loc instance # else -> raise KeyError ] return self.__locCodeMap [ locCode ] # - - - D a y S u m m a r y . g e n L o c s def genLocs ( self ): """Generate the locations in self. """ #-- 1 -- # [ codeList := keys of self.__locCodeMap, in ascending # order ] codeList = self.__locCodeMap.keys() codeList.sort() #-- 2 -- # [ generate values of self.__locCodemap in order by # codeList ] for code in codeList: yield self.__locCodeMap[code] #-- 3 -- raise StopIteration # - - - D a y S u m m a r y . r e a d N o d e # @staticmethod def readNode ( node ): """Convert from XML. """ #-- 1 -- # [ defaultLocCode := node's rnc.DEFAULT_LOC_A # attribute ] defaultLocCode = node.attrib[rnc.DEFAULT_LOC_A] #-- 2 -- # [ daySummary := a new DaySummary instance for # default location code (defaultLocCode) ] daySummary = DaySummary ( defaultLocCode ) #-- 3 -- # [ locNodeList := rnc.LOC_N children of node ] locNodeList = node.xpath ( rnc.LOC_N ) #-- 4 -- # [ daySummary := daySummary with locations added # from elements of locNodeList ] for locNode in locNodeList: #-- 4 body -- # [ locNode is an rnc.LOC_N node -> # self := self with a location added made # from locNode ] loc = Loc.readNode ( locNode ) daySummary.addLoc ( loc ) #-- 5 -- # [ daySummary := daySummary with information added from # any day-annotation children of (node) ] daySummary.dayAnnotation ( node ) #-- 6 -- # [ if defaultLocCode is a valid location code in # self -> # I # else -> # raise IOError ] try: test = daySummary.lookupLoc ( defaultLocCode ) except KeyError: raise IOError, ( "Default location code '%s' is not " "defined." % defaultLocCode ) #-- 7 -- return daySummary readNode = staticmethod ( readNode ) # - - - D a y S u m m a r y . d a y A n n o t a t i o n def dayAnnotation ( self, node ): """Read day-annotation content. [ node is an rnc.DAY_SUMMARY node as an et.Element -> self := self with information added from any day-annotation children of (node) ] """ #-- 1 -- # [ if node has at least one rnc.ROUTE_N child -> # self.route := an Annotation instance representing # that child's content # else -> I ] self.route = Narrative.readChild ( node, rnc.ROUTE_N ) #-- 2 -- # [ if node has at least one rnc.WEATHER_N child -> # self.weather := an Annotation instance representing # that child's content # else -> I ] self.weather = Narrative.readChild ( node, rnc.WEATHER_N ) #-- 3 -- # [ if node has at least one rnc.MISSED_N child -> # self.missed := an Annotation instance representing # that child's content # else -> I ] self.missed = Narrative.readChild ( node, rnc.MISSED_N ) #-- 4 -- # [ if node has at least one rnc.FILM_N child -> # self.film := an Annotation instance representing # that child's content # else -> I ] self.film = Narrative.readChild ( node, rnc.FILM_N ) #-- 5 -- # [ paraNodeList := list of all rnc.PARA_N children # of node ] paraNodeList = node.xpath ( rnc.PARA_N ) #-- 6 -- # [ if paraNodeList is non-empty -> # self.notes := a new Narrative instance with paragraphs # added made from the elements of paraNodeList # else -> I ] if len(paraNodeList) > 0: #-- 6.1 -- # [ self.notes := a new, empty Narrative instance ] self.notes = Narrative() #-- 6.2 -- # [ narrative := narrative with paragraphs added # made from the elements of paraNodeList ] for paraNode in paraNodeList: para = Paragraph.readNode ( paraNode ) self.notes.addPara ( para ) # - - - D a y S u m m a r y . w r i t e N o d e def writeNode ( self, parent ): """Translate self to XML. """ #-- 1 -- # [ parent := parent with a new rnc.DAY_SUMMARY_N node # added with its rnc.DEFAULT_LOC_A attribute set to # self.defaultLocCode # result := that new node ] result = et.SubElement ( parent, rnc.DAY_SUMMARY_N ) result.attrib[rnc.DEFAULT_LOC_A] = self.defaultLocCode #-- 2 -- # [ result := result with rnc.LOC_N nodes added, made # from the locations in self ] for loc in self.genLocs(): locNode = loc.writeNode ( result ) #-- 3 -- # [ if self.route is not None -> # result := result with an rnc.ROUTE_N node added # containing self.route's narrative # else -> I ] if self.route: routeNode = et.SubElement ( result, rnc.ROUTE_N ) self.route.writeNode ( routeNode ) #-- 4 -- # [ if self.weather is not None -> # result := result with an rnc.WEATHER_N node added # containing self.weather's narrative # else -> I ] if self.weather: weatherNode = et.SubElement ( result, rnc.WEATHER_N ) self.weather.writeNode ( weatherNode ) #-- 5 -- # [ if self.missed is not None -> # result := result with an rnc.MISSED_N node added # containing self.missed's narrative # else -> I ] if self.missed: missedNode = et.SubElement ( result, rnc.MISSED_N ) self.missed.writeNode ( missedNode ) #-- 6 -- # [ if self.film is not None -> # result := result with an rnc.FILM_N node added # containing self.film's narrative # else -> I ] if self.film: filmNode = et.SubElement ( result, rnc.FILM_N ) self.film.writeNode ( filmNode ) #-- 7 -- # [ if self.notes is not None -> # result := result with rnc.PARA_N nodes added # from self.notes # else -> I ] if self.notes: self.notes.writeNode ( result ) # - - - - - c l a s s L o c class Loc: """Represents one location. Exports: Loc ( code, name, text=None ): [ (code is a locality code as a string) and (name is the locality's name as a string) and (text is the locality's full description as a string, or None) -> return a new Loc instance with those values ] .code: [ as passed to constructor, read-only ] .name: [ as passed to constructor, read-only ] .text: [ as passed to constructor, read-write ] .addGps ( gps ): [ gps is a waypoint as a Gps instance -> self := self with gps added ] .genGps(): [ generate the waypoints in self in the order they were added ] Loc.readNode ( node ): # Static method [ node is an rnc.LOC_N node as an et.Element -> return a new Loc instance made from node ] .writeNode ( parent ): [ parent is an et.Element -> parent := parent with self's content added as a new rnc.LOC_N child element return that new child element ] State/Invariants: .__gpsList: [ list of waypoints in self in the order they were added, as Gps instances ] """ # - - - L o c . _ _ i n i t _ _ def __init__ ( self, code, name, text=None ): """Constructor for Loc. """ self.code = code self.name = name self.text = text self.__gpsList = [] # - - - L o c . a d d G p s def addGps ( self, gps ): """Add one waypoint. """ self.__gpsList.append ( gps ) # - - - L o c . g e n G p s def genGps ( self ): """Generate the waypoints in self. """ return iter(self.__gpsList) # - - - L o c . r e a d N o d e # @staticmethod def readNode ( node ): """Convert XML to a Loc instance. """ #-- 1 -- # [ code := rnc.CODE_A attribute from node # name := rnc.NAME_A attribute from node ] code = node.attrib[rnc.CODE_A] name = node.attrib[rnc.NAME_A] #-- 2 -- # [ loc := a new Loc instance with code=(code) and # name=(name) and no text ] loc = Loc ( code, name ) #-- 3 -- # [ gpsNodeList := a list containing all rnc.GPS_N # children of node # lastGps := None ] gpsNodeList = node.xpath ( rnc.GPS_N ) #-- 4 -- # [ if gpsNodeList is non-empty -> # loc := loc with Gps instances added, made from # elements of gpsNodeList # lastGps := the last Gps instance added # else -> I ] for gpsNode in gpsNodeList: lastGps = Gps.readNode ( gpsNode ) loc.addGps ( lastGps ) #-- 5 -- # [ if (node has any nonblank text children) and # (lastGps is None) -> # loc.text := all text children, minus leading and # trailing space # else if node has any nonblank text children -> # lastGps.tail := all text children, minus leading # and trailing space # else -> I ] textList = node.xpath ( 'text()' ) s = "".join(textList).strip() if s: loc.text = s #-- 6 -- return loc readNode = staticmethod ( readNode ) # - - - L o c . w r i t e N o d e def writeNode ( self, parent ): """Build a loc element and its subtree. """ #-- 1 -- # [ parent := parent with a new rnc.LOC_N node added # result := that node ] result = et.SubElement ( parent, rnc.LOC_N ) #-- 2 -- # [ result := result with an rnc.CODE_A attribute added # made from self.code and an rnc.NAME_A from self.name ] result.attrib [ rnc.CODE_A ] = self.code result.attrib [ rnc.NAME_A ] = self.name #-- 3 -- # [ if self.__gpsList is empty -> # lastGpsNode := None # else -> # result := result with rnc.GPS_N children added, # made from self.__gpsList # lastGpsNode := the last such child added ] lastGpsNode = None for gps in self.__gpsList: lastGpsNode = gps.writeNode ( result ) #-- 4 -- if self.text: if lastGpsNode is None: result.text = self.text else: lastGpsNode.tail = self.text #-- 5 -- return result # - - - - - c l a s s G p s class Gps: """Represents a GPS waypoint. Exports: Gps ( waypoint, text=None ): [ (waypoint is a lat-long as a string) and (text is a description as a string, or None) -> if waypoint is a valid lat-long string -> return a new Gps instance with those values else -> raise ValueError ] .waypoint: [ as passed to constructor, read-only ] .text: [ as passed to constructor, read-write ] .latLon: [ a terrapos.LatLon instance representing self.waypoint ] Gps.readNode ( node ): # Static method [ node is an rnc.GPS_N node as an et.Element -> if node conforms to birdnotes.rnc -> return a new Gps instance representing node else -> raise IOError ] .writeNode ( parent ): [ parent is an et.Element instance -> parent := parent with a new rnc.GPS_N node added representing self return that new node ] """ # - - - G p s . _ _ i n i t _ _ def __init__ ( self, waypoint, text=None ): """Constructor for Gps """ #-- 1 -- self.waypoint = waypoint self.text = text #-- 2 -- # [ if waypoint is a valid lat-lon as a string -> # self.latLon := a terrapos.LatLon instance # representing waypoint # else -> raise ValueError ] self.latLon = terrapos.scanLatLon ( waypoint ) # - - - G p s . r e a d N o d e # @staticmethod def readNode ( node ): """Convert from XML """ waypoint = node.attrib [ rnc.WAYPOINT_A ] text = node.text return Gps ( waypoint, text ) readNode = staticmethod ( readNode ) # - - - G p s . w r i t e N o d e def writeNode ( self, parent ): """Convert to XML """ #-- 1 -- # [ parent := parent with a new rnc.GPS_N child added # with rnc.WAYPOINT_A attribute set to self.waypoint ] gpsNode = et.SubElement ( parent, rnc.GPS_N ) gpsNode.attrib [ rnc.WAYPOINT_A ] = self.waypoint #-- 2 -- # [ if self.text is not None -> # gpsNode := gpsNode with its content set to self.text # else -> I ] if self.text is not None: gpsNode.text = self.text #-- 3 -- return gpsNode # - - - - - c l a s s B i r d F o r m class BirdForm: """Represents one or more sightings of a single kind of bird. Exports: BirdForm ( txny, dayNotes, ab6, rel=None, alt=None, notable=None ): [ (txny is a taxonomy as a txny.Txny instance) and (dayNotes is a DayNotes instance) and (ab6 is a six-letter bird code as a string) and (rel is a relationship code or None) and (alt is a six-letter bird code or None) and (notable is true iff the sighting is notable) -> return a new BirdForm instance with those values, containing no sightings ] .dayNotes: [ as passed to constructor, read-only ] .ab6: [ as passed to constructor, read-only ] .rel: [ as passed to constructor, read-only ] .alt: [ as passed to constructor, read-only ] .notable: [ as passed to constructor, read-only ] .birdId: [ an abbr.BirdId instance representing (ab6, rel, alt) ] .__len__(self): [ return number of sightings in self ] .__getitem__(self, n): [ n is an integer -> if n is the index of a sighting in self -> return that as a Sighting instance else -> raise KeyError ] .addSighting ( sighting ): [ sighting is a Sighting instance -> self := self with sighting added ] .genSightings(): [ generate sightings in self as a sequence of Sighting instances in the order they were added ] .locGroup: [ if self has locality information -> a LocGroup instance representing the locality else -> None ] .sightNotes: [ if self has any sighting notes -> a SightNotes instance representing those notes else -> None ] .getLoc(): [ return the effective locality data for self as a Loc instance ] BirdForm.readNode ( txny, dayNotes, node ): # Static method [ (txny is a bird taxonomy as a txny.Txny instance) and (dayNotes is the parent DayNotes instance) and (node is an et.Element) -> if node is an rnc.FORM_N node conforming to birdnotes.rnc -> return a new BirdForm instance representing node else -> raise IOError ] .writeNode ( parent ): [ parent is an et.Element -> parent := parent with a new rnc.FORM_N node added representing self return that new node ] State/Invariants: .__sightingList: [ a list containing the Sighting instances in self in the order they were added ] """ # - - - B i r d F o r m . _ _ l e n _ _ def __len__ ( self ): """Return the sighting count for self. """ return len ( self.__sightingList ) # - - - B i r d F o r m . _ _ g e t i t e m _ _ def __getitem__ ( self, n ): """Return the (n)th sighting in self """ return self.__sightingList[n] # - - - B i r d F o r m . a d d S i g h t i n g def addSighting ( self, sighting ): """Add a sighting to self. """ self.__sightingList.append ( sighting ) # - - - B i r d F o r m . g e n S i g h t i n g s def genSightings ( self ): """Generate the sightings in self. """ return iter ( self.__sightingList ) # - - - B i r d F o r m . _ _ i n i t _ _ def __init__ ( self, txny, dayNotes, ab6, rel=None, alt=None, notable=1 ): """Constructor for BirdForm """ #-- 1 -- self.txny = txny self.dayNotes = dayNotes self.ab6 = ab6 self.rel = rel self.alt = alt self.notable = notable self.locGroup = None self.sightNotes = None self.__sightingList = [] #-- 2 -- # [ if (ab6, rel, alt) are valid in txny -> # self.birdId := a BirdId instance representing # that bird identity # else -> # raise IOError ] try: self.birdId = abbrModule.BirdId ( txny, ab6, rel, alt ) except KeyError: offender = ( "%s%s%s" % (ab6, rel or "", alt or "") ) raise IOError, ( "Code '%s' undefined." % offender ) # - - - B i r d F o r m . g e t L o c G r o u p def getLocGroup ( self ): """Get the effective locality. """ #-- 1 -- # [ parentGroup := a LocGroup instance representing the # default locality for self.dayNotes ] parentGroup = LocGroup ( self.dayNotes.defaultLoc() ) #-- 2 -- # [ if self.locGroup is None -> # return parentGroup # else -> # return a new LocGroup instance made from # self.locGroup inheriting from parentGroup ] if self.locGroup: return self.locGroup.inherit ( parentGroup ) else: return parentGroup # - - - B i r d F o r m . r e a d N o d e # @staticmethod def readNode ( txny, dayNotes, node ): """Convert from XML """ #-- 1 -- # [ birdForm := a BirdForm instance with parent dayNotes # made from node's taxon-group ] birdForm = BirdForm.getTaxonGroup ( txny, dayNotes, node ) #-- 2 -- # [ flocList := node's rnc.FLOC_N children as et.Element # instances ] flocList = node.xpath ( rnc.FLOC_N ) #-- 3 -- # [ if flocList is empty -> # birdForm := birdForm with a single Sighting child # added, made from node's age-sex-group, loc-group, # and sighting-notes content # else -> # birdForm := birdForm with loc-group and sighting-notes # added from node, and Sighting children added, made # from the elements of flocList ] if len(flocList) == 0: birdForm.singleSighting ( dayNotes, node ) else: birdForm.multiSighting ( dayNotes, node, flocList ) #-- 4 -- return birdForm readNode = staticmethod ( readNode ) # - - - B i r d F o r m . g e t T a x o n G r o u p # @staticmethod def getTaxonGroup ( txny, dayNotes, node ): """Convert the XML taxon-group pattern to a BirdForm instance. [ (txny is a bird taxonomy as a txny.Txny instance) and (dayNotes is the parent DayNotes instance) and (node is an et.Element) -> return a new BirdForm instance made from node's taxon-group content ] """ #-- 1 -- # [ ab6 := node's rnc.AB6_A attribute # rel := node's rnc.REL_A attribute, defaulting to None # alt := node's rnc.ALT_A attribute, defaulting to None # notable := node's rnc.NOTABLE_A attribute, defaulting # to None ] ab6 = node.attrib [ rnc.AB6_A ] rel = node.attrib.get ( rnc.REL_A, None ) alt = node.attrib.get ( rnc.ALT_A, None ) notable = node.attrib.get ( rnc.NOTABLE_A, None ) #-- 2 -- # [ birdForm := a new BirdForm instance using ab6, rel, # alt, and notable ] birdForm = BirdForm ( txny, dayNotes, ab6, rel, alt, notable ) #-- 3 -- return birdForm getTaxonGroup = staticmethod(getTaxonGroup) # - - - B i r d F o r m . s i n g l e S i g h t i n g def singleSighting ( self, dayNotes, node ): """Build a Sighting child for the single-sighting case. [ (dayNotes is the parent DayNotes instance) and (node is an rnc.FORM_N node) -> self := self with a single Sighting child added, made from node's age-sex-group, loc-group, and sighting-notes content ] """ #-- 1 -- # [ sighting := a new, empty Sighting instance with # self as the parent ] sighting = Sighting ( self ) #-- 2 -- # [ if node has any valid loc-group content -> # sighting.locGroup := that content as a LocGroup # instance # else if node loc-group content with a loc that is # not in dayNotes -> # raise IOError # else -> # sighting.locGroup := None ] sighting.locGroup = LocGroup.readNode ( node, dayNotes ) #-- 3 -- # [ if node has any age-sex-group content -> # sighting.ageSexGroup := a new AgeSexGroup # instance representing that content # else -> # sighting.ageSexGroup := None ] sighting.ageSexGroup = AgeSexGroup.readNode ( node ) #-- 4 -- # [ if node has any sighting-notes content -> # sighting.sightNotes := a new SightNotes instance # representing that content # else -> # sighting.sightNotes := None ] sighting.sightNotes = SightNotes.readNode ( node ) #-- 5 -- # [ self := self with sighting added ] self.addSighting(sighting) # - - - B i r d F o r m . m u l t i S i g h t i n g def multiSighting ( self, dayNotes, formNode, flocList ): """Read a form with floc children. [ (dayNotes is the parent DayNotes instance) and (formNode is an rnc.FORM_N et.Element) and (flocList is a list of its rnc.FLOC_N children) -> self := self with loc-group and sighting-notes added from node, and Sighting children added, made from the elements of flocList ] """ #-- 1 -- # [ if formNode has any loc-group content -> # self.locGroup := that content as a LocGroup instance # else -> # self.locGroup := None ] self.locGroup = LocGroup.readNode ( formNode, dayNotes ) #-- 2 -- # [ if formNode has any sighting-notes content -> # self.sightNotes := that content as a SightNotes # instance # else -> # self.sightNotes := None ] self.sightNotes = SightNotes.readNode ( formNode ) #-- 3 -- # [ self := self with child Sighting instances added # made from the elements of flocList ] for flocNode in flocList: self.readFloc ( flocNode, dayNotes ) # - - - B i r d F o r m . r e a d F l o c def readFloc ( self, flocNode, dayNotes ): """Read one floc element. [ (flocNode is an rnc.FLOC_N node as an et.Element) and (dayNotes is a DayNotes instance) -> self := self with a Sighting instance added, made from flocNode, with parent dayNotes ] """ #-- 1 -- # [ sighting := a new, empty Sighting instance with # self as its parent ] sighting = Sighting ( self ) #-- 2 -- # [ if flocNode has any age-sex-group content -> # sighting.ageSexGroup := an AgeSexGroup instance # representing that content # else -> # sighting.ageSexGroup := None ] sighting.ageSexGroup = AgeSexGroup.readNode ( flocNode ) #-- 3 -- # [ if flocNode has any loc-group content -> # sighting.locGroup := a LocGroup instance # representing that content # else -> # sighting.locGroup := None ] sighting.locGroup = LocGroup.readNode ( flocNode, dayNotes ) #-- 4 -- # [ if flocNode has any sighting-notes content -> # sighting.sightNotes := a SightNotes instance # representing that content # else -> # sighting.sightNotes := None ] sighting.sightNotes = SightNotes.readNode ( flocNode ) #-- 5 -- # [ self := self with sighting added ] self.addSighting ( sighting ) # - - - B i r d F o r m . w r i t e N o d e def writeNode ( self, parent ): """Add self's content to the parent node. """ #- 1 -- # [ parent := parent with a new rnc.FORM_N node added # formnode := that new node ] formNode = et.SubElement ( parent, rnc.FORM_N ) #-- 2 -- # [ formNode +:= self's taxon-group content ] self.__writeTaxonGroup ( formNode ) #-- 3 -- # [ if self has only one sighting -> # formNode +:= form node with age-sex-group, loc-group, # and sighting-notes content added from that sighting # else -> # formNode := formNode + (loc-group and sighting-notes # from self) + (rnc.FLOC_N children added made from # self's sightings ] if self.nSightings() == 1: self.writeSingle ( formNode ) else: self.writeMulti ( formNode ) # - - - B i r d F o r m . _ _ w r i t e T a x o n G r o u p def __writeTaxonGroup ( self, formNode ): """Attach taxon-group content to formNode. [ formNode is an et.Element -> formNode +:= self's taxon-group content ] """ #-- 1 -- # [ formNode +:= an rnc.AB6_A attribute made from self.ab6 ] formNode.attrib [ rnc.AB6_A ] = self.ab6 #-- 2 -- # [ if self.rel -> # formNode := formNode with an rnc.REL_A attribute # made from self.rel and an rnc.ALT_A attribute # made from self.alt # else -> I ] if self.rel: formNode.attrib [ rnc.REL_A ] = self.rel formNode.attrib [ rnc.ALT_A ] = self.alt #-- 3 -- if self.notable: formNode.attrib [ rnc.NOTABLE_A ] = self.notable # - - - B i r d F o r m . w r i t e S i n g l e def writeSingle ( self, formNode ): """Generate XML for a single sighting. [ formNode is an et.Element -> formNode +:= form node with age-sex-group, loc-group, and sighting-notes content added from that sighting ] """ #-- 1 -- sighting = self.__sightingList[0] #-- 1 -- # [ if sighting.ageSexGroup -> # formNode := formNode with age-sex-group content # added from sighting # else -> I ] if sighting.ageSexGroup: sighting.ageSexGroup.writeNode ( formNode ) #-- 2 -- # [ if sighting.locGroup -> # formNode := formNode with loc-group content # added from sighting # else -> I ] if sighting.locGroup: sighting.locGroup.writeNode ( formNode ) #-- 3 -- # [ if sighting.sightNotes -> # formNode := formNode with sighting-notes content # added from sighting # else -> I ] if sighting.sightNotes: sighting.sightNotes.writeNode ( formNode ) # - - - B i r d F o r m . w r i t e M u l t i def writeMulti ( self, formNode ): """Generate XML for multiple sightings. [ formNode is an et.Element -> formNode := formNode + (loc-group and sighting-notes from self) + (rnc.FLOC_N children made from self's sightings ] """ #-- 1 -- # [ if self.locGroup -> # formNode +:= self.locGroup as XML # else -> I ] if self.locGroup: self.locGroup.writeNode ( formNode ) #-- 2 -- # [ if self.sightNotes -> # formNode +:= self.sightNotes as XML # else -> I ] if self.sightNotes: self.sightNotes.writeNode ( formNode ) #-- 3 -- # [ formNode +:= rnc.FLOC_N children made from self's # sightings ] for sighting in self.genSightings(): sighting.writeNode ( formNode ) # - - - - - c l a s s S i g h t i n g class Sighting: """Represents one sighting of one or more similar birds. Exports: Sighting ( birdForm ): [ birdForm is a BirdForm instance -> return a new, empty Sighting instance with birdForm as its parent ] .birdForm: [ as passed to constructor, read-only ] .locGroup: [ if self has locality content -> a LocGroup instance representing that content else -> None ] .ageSexGroup: [ if self has any age-sex-group content -> an AgeSexGroup instance representing that content else -> None ] .sightNotes: [ if self has any sighting-notes content -> a SightNotes instance representing that content else -> None ] .getLocGroup(): [ return self's locality as a LocGroup object ] .writeNode ( formNode ): [ formNode is an et.Element -> formNode +:= a new rnc.FLOC_N child made from self ] """ # - - - S i g h t i n g . _ _ i n i t _ _ def __init__ ( self, birdForm ): """Constructor for Sighting. """ self.birdForm = birdForm self.locGroup = None self.ageSexGroup = None self.sightNotes = None # - - - S i g h t i n g . g e t L o c G r o u p def getLocGroup ( self ): """Find self's effective location. """ #-- 1 -- # [ parentGroup := a LocGroup representing the # effective locality of self.birdForm ] parentGroup = self.birdForm.getLocGroup() #-- 2 -- # [ if self.locGroup is None -> # return parentGroup # else -> # return a new LocGroup made from self.locGroup, # inheriting from parentGroup ] if self.locGroup is None: return parentGroup else: return self.locGroup.inherit ( parentGroup ) # - - - S i g h t i n g . w r i t e N o d e def writeNode ( self, formNode ): """Convert self to XML. [ formNode is an et.Element -> formNode +:= a new rnc.FLOC_N child made from self ] """ #-- 1 -- # [ formNode := formNode with a new rnc.FLOC_N child added # flocNode := that new child ] flocNode = et.SubElement ( formNode, rnc.FLOC_N ) #-- 2 -- # [ if self.ageSexGroup -> # flocNode +:= age-sex-group content from # self.ageSexGroup # else -> I ] if self.ageSexGroup: self.ageSexGroup.writeNode ( flocNode ) #-- 3 -- # [ simile ] if self.locGroup: self.locGroup.writeNode ( flocNode ) #-- 4 -- if self.sightNotes: self.sightNotes.writeNode ( flocNode ) # - - - - - c l a s s L o c G r o u p class LocGroup: """Represents locality information for sightings. Exports: LocGroup ( loc=None, gps=None, locDetail=None ): [ (loc is a locality as a Loc instance or None) and (gps is a Gps instance or None) and (locDetail is a Narrative instance or None) -> return a new LocGroup instance with those values ] .loc: [ as passed to constructor, read-only ] .gps: [ as passed to constructor, read-only ] .locDetail: [ as passed to constructor, read-only ] LocGroup.readNode ( node, dayNotes ): # Static method [ (node is an et.Element) and (dayNotes is a DayNotes instance) -> if node has any valid loc-group content -> return a new LocGroup instance representing that content else if node has loc-group content but there are any location codes undefined in dayNotes -> raise IOError else -> return None ] .inherit ( parentGroup ): [ parentGroup is a LocGroup instance -> return a new LocGroup whose attributes come from parentGroup except when they are overriden by corresponding attributes of self ] .writeNode ( node ): [ node is an et.Element -> node := node with self added as XML ] """ # - - - L o c G r o u p . _ _ i n i t _ _ def __init__ ( self, loc=None, gps=None, locDetail=None ): """Constructor for LocGroup """ self.loc = loc self.gps = gps self.locDetail = locDetail # - - - L o c G r o u p . r e a d N o d e # @staticmethod def readNode ( node, dayNotes ): """Extract loc-group content from a node. """ #-- 1 -- # [ if node has an rnc.LOC_A attribute defined in dayNotes -> # locCode := that attribute # else if node has an rnc.LOC_A attribute not defined in # dayNotes -> # raise IOError # else -> # locCode := None ] locCode = node.attrib.get ( rnc.LOC_A, None ) #-- 2 -- # [ if dayNotes has a definition for location code locCode -> # loc := that definition as a Loc instance # else -> # loc := None ] if locCode is not None: try: loc = dayNotes.lookupLoc ( locCode ) except KeyError: raise IOError, ( "Location code '%s' is used but " "not defined for date %s." % (locCode, dayNotes.date) ) else: loc = None #-- 2 - # [ gps := node's rnc.GPS_A attribute, default None ] gps = node.attrib.get ( rnc.GPS_A, None ) #-- 2 -- # [ if node has an rnc.LOC_DETAIL_N child -> # locDetail := the content of that child as a # Narrative instance # else -> # locDetail := None ] detailChildList = node.xpath ( rnc.LOC_DETAIL_N ) if len(detailChildList) > 0: locDetail = Narrative.readNode ( detailChildList[0] ) else: locDetail = None #-- 3 -- if (loc or gps or locDetail): return LocGroup ( loc, gps, locDetail ) else: return None readNode = staticmethod ( readNode ) # - - - L o c G r o u p . i n h e r i t def inherit ( self, parentGroup ): """Implement locality inheritance. """ #-- 1 -- # [ if self.loc is None -> # loc := parentGroup.loc # else -> # loc := self.loc ] loc = self.loc or parentGroup.loc #-- 2 -- # [ simile ] gps = self.gps or parentGroup.gps locDetail = self.locDetail or parentGroup.locDetail #-- 3 -- return LocGroup ( loc, gps, locDetail ) # - - - L o c G r o u p . w r i t e N o d e def writeNode ( self, node ): """Add loc-group content to node. """ #-- 1 -- # [ if self.locCode -> # node := node with an rnc.LOC_A attribute added # from self.locCode # else -> I ] if self.loc: node.attrib [ rnc.LOC_A ] = self.loc.code #-- 2 -- # [ if self.gps -> # node := node with an rnc.GPS_A attribute added # from self.gps # else -> I ] if self.gps: node.attrib [ rnc.GPS_A ] = self.gps #-- 3 -- # [ if self.locDetail -> # node := node with a new child rnc.LOC_DETAIL_N # node added made from self.locDetail ] if self.locDetail: detail = et.SubElement ( node, rnc.LOC_DETAIL_N ) self.locDetail.writeNode ( detail ) # - - - - - c l a s s A g e S e x G r o u p class AgeSexGroup: """Represents assorted details of birds sighted. Exports: AgeSexGroup ( age=None, sex=None, q=None, count=None, fide=None ): [ (age is an age code or None) and (sex is a sex code or None) and (q is a countability flag or None) and (count is a count description string or None) and (fide is an attribution or None) -> return a new AgeSexGroup instance with those values ] AgeSexGroup.readNode ( node ): # Static method [ node is an et.Element -> if node has any age-sex-group content -> return an AgeSexGroup instance representing that content else -> return None ] .writeNode ( parent ): [ parent is an et.Element -> parent := parent with self's age-sex-group content attached ] """ # - - - A g e S e x G r o u p . _ _ i n i t _ _ def __init__ ( self, age=None, sex=None, q=None, count=None, fide=None ): """Constructor for AgeSexGroup. """ self.age = age self.sex = sex self.q = q self.count = count self.fide = fide # - - - A g e S e x G r o u p . r e a d N o d e # @staticmethod def readNode ( node ): """Check for age-sex-group content. """ age = node.attrib.get ( rnc.AGE_A, None ) sex = node.attrib.get ( rnc.SEX_A, None ) q = node.attrib.get ( rnc.Q_A, None ) count = node.attrib.get ( rnc.COUNT_A, None ) fide = node.attrib.get ( rnc.FIDE_A, None ) if sex or age or q or count or fide: return AgeSexGroup ( age, sex, q, count, fide ) else: return None readNode = staticmethod ( readNode ) # - - - A g e S e x G r o u p . w r i t e N o d e def writeNode ( self, parent ): """Translate self to XML. """ #-- 1 -- # [ if self.age is true -> # parent := parent with an rnc.AGE_A attribute (self.age) # else -> I ] if self.age: parent.attrib [ rnc.AGE_A ] = self.age #-- 2 -- # [ simile ] if self.sex: parent.attrib [ rnc.SEX_A ] = self.sex #-- 3 -- if self.q: parent.attrib [ rnc.Q_A ] = self.q #-- 4 -- if self.count: parent.attrib [ rnc.COUNT_A ] = self.count #-- 5 -- if self.fide: parent.attrib [ rnc.FIDE_A ] = self.fide # - - - - - c l a s s S i g h t N o t e s class SightNotes: """Represents content from the sighting-notes pattern. Exports: SightNotes(): [ return a new, empty SightNotes instance ] .desc: [ description as a Narrative or None, read/write ] .behavior: [ behavior as a Narrative or None, read/write ] .voc: [ vocalizations as a Narrative or None, read/write ] .breeding: [ breeding evidence as a Narrative or None, read/write ] .addPhoto ( photo ): [ photo is a Photo instance -> self := self with photo added ] .genPhotos(): [ generate photos in self as a sequence of Photo instances ] .addPara ( para ): [ para is a Paragraph instance -> self := self with para added to its general notes ] .notes: [ self's general notes as a Narrative instance ] SightNotes.readNode ( node ): # Static [ node is an et.Element -> if node has any sighting-notes content -> return a new SightNotes instance with that content else -> return None ] .writeNode ( parent ): [ parent is an et.Element -> parent := parent with content added representing self ] State/Invariants: .__photoList: [ list of Photo instances in the order they were added ] """ # - - - S i g h t N o t e s . _ _ i n i t _ _ def __init__ ( self ): """Constructor for SightNotes """ self.desc = None self.behavior = None self.voc = None self.breeding = None self.notes = None self.__photoList = [] # - - - S i g h t N o t e s . a d d P h o t o def addPhoto ( self, photo ): """Add one photo reference. """ self.__photoList.append ( photo ) # - - - S i g h t N o t e s . g e n P h o t o s def genPhotos ( self ): """Generate the photo objects in self. """ return iter(self.__photoList) # - - - S i g h t N o t e s . a d d P a r a def addPara ( self, para ): """Add a paragraph of notes. """ #-- 1 -- # [ if self.notes is None -> # self.notes := a new, empty Narrative instance # else -> I ] if self.notes is None: self.notes = Narrative() #-- 2 -- # [ self.notes := self.notes with para added as its new # last element ] self.notes.addPara ( para ) # - - - S i g h t N o t e s . r e a d N o d e # @staticmethod def readNode ( node ): """Extract sighting-tones from node, if any. """ #-- 1 -- # [ sightNotes := a new, empty SightNotes instance ] sightNotes = SightNotes() #-- 2 -- # [ if node has an rnc.DESC_N child -> # sightNotes.desc := the narrative content of that # child as a Narrative instance # else -> # sightNotes.desc := None ] sightNotes.desc = Narrative.readChild ( node, rnc.DESC_N ) #-- 3 -- # [ if node has an rnc.BEHAVIOR_N child -> # sightNotes.behavior := the narrative content of # that child as a Narrative instance # else -> # sightNotes.behavior := None ] sightNotes.behavior = Narrative.readChild ( node, rnc.BEHAVIOR_N ) #-- 4 -- # [ if node has an rnc.VOC_N child -> # sightNotes.voc := the narrative content of # that child as a Narrative instance # else -> # sightNotes.voc := None ] sightNotes.voc = Narrative.readChild ( node, rnc.VOC_N ) #-- 5 -- # [ if node has an rnc.BREEDING_N child -> # sightNotes.breeding := the narrative content of # that child as a Narrative instance # else -> # sightNotes.breeding := None ] sightNotes.breeding = Narrative.readChild ( node, rnc.BREEDING_N ) #-- 6 -- # [ if node has any rnc.PHOTO_N children -> # sightNotes := sightNotes with Photo instances # added representing those children # else -> I ] photoNodeList = node.xpath ( rnc.PHOTO_N ) for photoNode in photoNodeList: photo = Photo.readNode ( photoNode ) sightNotes.addPhoto ( photo ) #-- 7 -- # [ if node has any rnc.PARA_N children -> # sightNotes.notes := a new Narrative instance # representing all those children # else -> I ] paraNodeList = node.xpath ( rnc.PARA_N ) sightNotes.notes = Narrative() for paraNode in paraNodeList: # Just because you're paraNode doesn't mean they're # not out to get you. para = Paragraph.readNode ( paraNode ) sightNotes.notes.addPara ( para ) #-- 8 -- return sightNotes readNode = staticmethod(readNode) # - - - S i g h t N o t e s . w r i t e N o d e def writeNode ( self, parent ): """Translate to XML. """ #-- 1 -- # [ if self.desc is not None -> # parent := parent with a new rnc.DESC_N child # attached containing self.desc # else -> I ] self.writeChild ( parent, rnc.DESC_N, self.desc ) #-- 2 -- # [ simile ] self.writeChild ( parent, rnc.BEHAVIOR_N, self.behavior ) #-- 3 -- self.writeChild ( parent, rnc.VOC_N, self.voc ) #-- 4 -- self.writeChild ( parent, rnc.BREEDING_N, self.breeding ) #-- 5 -- if len(self.__photoList): for photo in self.__photoList: photo.writeNode ( parent ) #-- 6 -- if self.notes: self.notes.writeNode ( parent ) # - - - S i g h t N o t e s . w r i t e C h i l d def writeChild ( self, parent, childName, narr ): """Attach a child and put narrative into it. [ (parent is an et.Element) and (childName is an element name as a string) and (narr is a Narrative instance or None) -> if narr is not None -> parent := parent with a new child named (childName) attached, containing narr ] else -> I ] """ #-- 1 -- if narr is None: return #-- 2 -- # [ parent := parent with a new childName child added # child := that new child ] See . child = et.SubElement ( parent, childName ) #-- 3 -- # [ child := child with narr added ] narr.writeNode ( child ) # - - - - - c l a s s P h o t o class Photo: """Represents one photo with birds in. Exports: Photo ( catNo, url=None, text=None ): [ (catNo is a catalog number as a string) and (url is a link to the image, or None if no image) and (text is comment text as a string) -> return a new Photo instance with those values ] Photo.readNode): # Static.readNode method [ node is an et.Element -> if node conforms to birdnotes.rnc -> return a new Photo instance representing node else -> raise ValueError ] .writeNode ( parent ): [ parent is an et.Element -> parent := parent with a new rnc.PHOTO_N node sighting.sightNotes = SightNotes.readNode ( flocNode ) added representing self ] """ # - - - P h o t o . _ _ i n i t _ _ def __init__ ( self, catNo, url=None, text=None ): """Constructor for Photo. """ self.catNo = catNo self.url = url self.text = text # - - - P h o t o . r e a d N o d e # @staticmethod def readNode ( node ): """Convert from XML. """ #-- 1 -- # [ catNo := node's rnc.CAT_NO_A attribute ] catNo = node.attrib [ rnc.CAT_NO_A ] #-- 2 -- # [ if node has an rnc.URL_A attribute -> # url := that attribute's value # else -> # url := None ] url = node.attrib.get ( rnc.URL_A, None ) #-- 3 -- return Photo ( catNo, url, node.text ) readNode = staticmethod ( readNode ) # - - - P h o t o . w r i t e N o d e def writeNode ( self, parent ): """Convert to XML. """ #-- 1 -- # [ parent := parent with a new rnc.PHOTO_N child # photoNode := that new child ] photoNode = et.SubElement ( parent, rnc.PHOTO_N ) #-- 2 -- # [ photoNode := photoNode with self.catNo added as # an rnc.CAT_NO_A attribute and self.text added # as content (which may be None) ] photoNode.attrib [ rnc.CAT_NO_A ] = self.catNo photoNode.text = self.text #-- 3 -- if self.url: photoNode.attrib [ rnc.URL_A ] = self.url # - - - - - c l a s s N a r r a t i v e class Narrative: """Represents an instance of the narrative pattern. Exports: Narrative(): [ return a new, empty Narrative instance ] .addPara ( p ): [ p is a Paragraph instance -> self := self with p added as its new last paragraph ] .__len__(self): [ return the number of paragraphs in self ] .genParas(): [ generate the contents of self as a sequence of Paragraph instances ] .writeNode ( parent ): [ parent is an et.Element -> if self's content has only one paragraph -> parent := parent with that content added else -> parent := parent with self's content added as a sequence of rnc.PARA_N elements ] Narrative.readNode(parent): # Static method [ parent is an et.Element -> return a new Narrative instance made from narrative children of parent ] Narrative.readChild(parent, childName): # Static method [ (parent is an et.Element) and (childName is a node name as a string) -> if parent has at least one child named (childName) -> return a new Narrative element representing the narrative content of that child else -> return None ] State/Invariants: .__paraList: [ list of component Paragraph instances ] """ # - - - N a r r a t i v e . _ _ i n i t _ _ def __init__ ( self ): """Constructor for Narrative. """ self.__paraList = [] # - - - N a r r a t i v e . a d d P a r a def addPara ( self, p ): """Add a paragraph to self. """ self.__paraList.append ( p ) # - - - N a r r a t i v e . _ _ l e n _ _ def __len__ ( self ): """Return the length of self in paragraphs. """ return len(self.__paraList) # - - - N a r r a t i v e . _ _ g e t i t e m _ _ def __getitem__ ( self, k ): """Get one paragraph. """ return self.__paraList[k] # - - - N a r r a t i v e . g e n P a r a s def genParas ( self ): """Return an iterator over self's paragraphs. """ return iter ( self.__paraList ) # - - - N a r r a t i v e . w r i t e N o d e def writeNode ( self, parent ): """Output self's content as XML. """ #-- 1 -- if len(self.__paraList) > 1: #-- 1.1 -- # [ parent := parent with XML added for each element # of self.__paraList ] for para in self.__paraList: para.writeNode ( parent ) elif len(self.__paraList) == 1: #-- 1.2 -- # [ parent := parent with XML added for the first # element of self.__paraList ] solePara = self.__paraList[0] solePara.writeContent ( parent ) # - - - N a r r a t i v e . r e a d N o d e # @staticmethod def readNode ( parent ): """Create a Narrative instance from XML narrative content. """ #-- 1 -- # [ result := a new, empty Narrative instance ] result = Narrative() #-- 2 -- # [ if (parent has no element children) or # (parent's first element child is not rnc.PARA_N) -> # result := result with one Paragraph instance added # containing para-content from parent # else -> # result := result with Paragraph instances added, # made from the rnc.PARA_N children of parent ] if ( ( len(parent) == 0 ) or ( parent[0].tag != rnc.PARA_N ) ): #-- 2.1 -- # [ result := result with one Paragraph instance added # containing para-content from parent ] paragraph = Paragraph.readNode ( parent ) result.addPara ( paragraph ) else: #-- 2.2 -- # [ parent's element children are all rnc.PARA_N -> # result := result with Paragraph instances added, # made from those children ] for child in parent: paragraph = Paragraph.readNode ( child ) result.addPara ( paragraph ) #-- 3 -- return result readNode = staticmethod(readNode) # - - - N a r r a t i v e . r e a d C h i l d # @staticmethod def readChild ( node, childName ): """Look for a child with narrative content. """ #-- 1 -- # [ childList := list of all children named childName ] childList = node.xpath ( childName ) #-- 2 -- # [ if childList is nonempty -> # return a Narrative instance representing the # narrative content of childList[0] # else -> return None ] if len(childList) > 0: return Narrative.readNode ( childList[0] ) else: return None readChild = staticmethod ( readChild ) # - - - - - c l a s s P a r a g r a p h class Paragraph: """Represents one paragraph of mixed content. Exports: Paragraph(): [ return a new, empty Paragraph instance ] .addContent ( tag, text ): [ tag is the wrapper element's name as a string, or None if the text is not wrapped -> self := self with text added, wrapped in tag if there is one ] .getContent(): [ generate the content in self as a sequence of (tag, text) tuples as in the arguments to .addContent ] .writeNode ( parent ): [ parent is an et.Element -> parent := parent with content added representing self ] Paragraph.readNode(node): [ node is an et.Element containing para-content -> return a new Paragraph instance representing that content ] State/Invariants: .__phraseList: [ self's content as a list of tuples (tag, text) where tag is None for untagged text, or the enclosing element if tagged ] """ # - - - P a r a g r a p h . _ _ i n i t _ _ def __init__ ( self ): """Constructor for Paragraph. """ self.__phraseList = [] # - - - P a r a g r a p h . a d d C o n t e n t def addContent ( self, tag, text ): """Add another phrase to the phrase list. """ self.__phraseList.append ( (tag, text) ) # - - - P a r a g r a p h . g e n C o n t e n t def genContent ( self ): """Generate the content of self. """ for tag, text in self.__phraseList: yield (tag, text ) raise StopIteration # - - - P a r a g r a p h . w r i t e N o d e def writeNode ( self, parent ): """Attach a new paragraph to the parent node. """ #-- 1 - # [ parent := parent with a new rnc.PARA_N child added # para := that child ] para = et.SubElement ( parent, rnc.PARA_N ) #-- 2 -- # [ para := para with XML content made from # self.__phraseList ] self.writeContent ( para ) # - - - P a r a g r a p h . w r i t e C o n t e n t def writeContent ( self, parent ): """Convert self.__phraseList to XML. [ parent is an et.Element -> parent := parent with XML content made from self.__phraseList ] """ #-- 1 -- # [ pos := position of first marked-up phrase in # self.__phraseList, or past the end if there are # no marked-up phrases # parent.text := concatenation of text parts of all # initial unmarked phrases in self.__phraseList ] pos = 0; textList = [] while pos < len(self.__phraseList): markup, s = self.__phraseList[pos] if markup is None: textList.append ( s ) pos += 1 else: break parent.text = "".join(textList) #-- 2 -- # [ parent := parent with child elements made from marked-up # elements of self.__phraseList[pos:], each with # its tail made from unmarked following elements ] while pos < len(self.__phraseList): #-- 3 body -- # [ pos := position in self.__phraseList after # pos where the next marked-up element is # parent := parent with a child element made from # the phrase at self.__phraseList[pos] with # its tail made from any following unmarked # phrases ] pos = self.__writeParaChild ( parent, pos ) # - - - P a r a g r a p h . _ _ w r i t e P a r a C h i l d def __writeParaChild ( self, para, pos ): """Add one child element to a para element. [ (para is an et.Element) and (0 <= pos < len(self.__phraseList)) and (self.__phraseList[pos] is a marked-up phrase) -> para := para with a child element made from the phrase at self.__phraseList[pos] with its tail made from any following unmarked phrases ] return the position in self.__phraseList after pos where the next marked-up element is, if any, otherwise len(self.__phraseList) ] """ #-- 1 -- # [ markup := self.__phraseList[pos][0] # s := self.__phraseList[pos][1] # pos := pos + 1 ] markup, s = self.__phraseList[pos] pos += 1 #-- 2 -- # [ para := para with a new child added whose # tag is (markup), whose text is (s), and whose # tail is None # child := that new child ] child = et.SubElement ( para, markup ) child.text = s #-- 2 -- # [ if self.__phraseList[pos:] has any leading unmarked # phrases -> # pos := pos advanced past those phrases # child.tail := concatenation of all text from # those phrases ] # else -> I ] if pos < len(self.__phraseList): textList = [] while pos < len(self.__phraseList): markup, s = self.__phraseList[pos] if markup is not None: break textList.append ( s ) pos += 1 if len(textList) > 0: child.tail = "".join(textList) #-- 3 -- return pos # - - - P a r a g r a p h . r e a d N o d e # @staticmethod def readNode ( node ): """Convert para-content to a Paragraph instance. """ #-- 1 -- # [ result := a new, empty Paragraph ] result = Paragraph() #-- 2 -- # [ if node.text is not None -> # result := result with an unmarked phrase added # containing (node.text) # else -> I ] if node.text is not None: result.addPhrase ( None, node.text) #-- 3 -- # [ result := result with content added from children, # if any ] for child in node: result.addPhrase (child.tag, child.text) if child.tail is not None: result.addPhrase (None, child.tail) #-- 4 -- return result readNode = staticmethod ( readNode ) # - - - P a r a g r a p h . a d d P h r a s e def addPhrase ( self, tag, text ): """Add one phrase to self.__phraseList. """ self.__phraseList.append ( (tag, text) ) # - - - - - c l a s s B i r d N o t e T r e e class BirdNoteTree(object): '''Represents a complete tree of monthly files. Exports: BirdNoteTree ( txny, rootDir='.' ): [ (txny is a taxonomy as a Txny instance) and (rootDir is the path name of a data file tree) -> if rootDir and its subdirectories can be examined -> return a new BirdNoteTree representing the data files rooted in rootDir with names of the form "YYYY/YYYY-MM.xml" else -> raise IOError ] .txny: [ as passed to constructor, read-only ] .rootDir: [ as passed to constructor, read-only ] .genMonths(startDate=None, endDate=None, startSeason=None, endSeason=None): [ (startDate is an inclusive starting date as a datetime.date, or None for no starting cutoff) and (endDate is an inclusive ending date as a datetime.date or None for no ending cutoff) and (startSeason is an inclusive starting month and day as a datetime.date or None for no starting cutoff) and (endSeason is an inclusive ending month and day as a datetime.date or None for no ending cutoff) -> if all data files in self are readable and valid against self.txny -> generate a sequence of BirdNoteSet instances representing files that may contain dates in the range [startDate,endDate] and days of the year in the range [startSeason, endSeason] ] State/Invariants: .__monthList: [ list of months as "YYYY-MM" representing data files whose names match "YYYY/YYYY-MM.xml", in ascending order ] ''' # - - - B i r d N o t e T r e e . _ _ i n i t _ _ def __init__ ( self, txny, rootDir='.' ): '''Constructor ''' #-- 1 -- self.txny = txny self.rootDir = rootDir self.__monthList = [] #-- 2 -- # [ if rootDir can be read -> # yyyyList := list of subdirectories of rootDir # with four-digit names, sorted # else -> raise IOError ] yyyyList = sorted ( [ dirName for dirName in os.listdir ( rootDir ) if YEAR_PAT.match ( dirName ) ] ) #-- 3 -- # [ if all subdirectories of self.rootDir whose names are in # yyyyList can be read -> # self.__monthList +:= the "YYYY-MM" part of the # names of files in those subdirectories whose names # match YYYY_MM_XML_PAT # else -> raise IOError ] for yyyy in yyyyList: #-- 3 body -- # [ if subdirectory (yyyy) of self.rootDir can be read -> # self.__monthList +:= the "YYYY-MM" part of # the names of files in that subdirectory # whose names match YYYY_MM_XML_PAT # else -> raise IOError ] self.__findMonths ( yyyy ) #-- 4 -- # [ self.__monthList := self.__monthList sorted into # ascending order ] self.__monthList.sort() # - - - B i r d N o t e T r e e . _ _ f i n d M o n t h s def __findMonths ( self, yyyy ): '''Scan one year directory for month files. [ (self.rootDir is as invariant) and (yyyy is a year number as a 4-digit string) -> if subdirectory (yyyy) of self.rootDir can be read -> self.__monthList +:= the "YYYY-MM" part of the names of files in that subdirectory whose names match YYYY_MM_XML_PAT else -> raise IOError ] ''' #-- 1 -- # [ subDirPath := path to subdirectory (yyyy) of # directory (self.rootDir) ] subDirPath = os.path.join ( self.rootDir, yyyy ) #-- 2 -- # [ if directory (subDirPath) can be read -> # fileList := list of names in that directory that # match YYYY_MM_XML_PAT # else -> raise IOError ] fileList = [ fileName for fileName in os.listdir ( subDirPath ) if YYYY_MM_XML_PAT.match ( fileName ) ] #-- 3 -- # [ self.__monthList +:= the "YYYY-MM" parts of # the elements of fileList ] for fileName in fileList: self.__monthList.append ( fileName[:7] ) # - - - B i r d N o t e T r e e . g e n M o n t h s def genMonths ( self, startDate=None, endDate=None, startSeason=None, endSeason=None ): '''Generate BirdNoteSet instances from the tree ''' #-- 1 -- # [ if all month files corresponding to elements of # self.__monthList are readable and valid -> # generate a sequence of BirdNoteSet instances # representing those files in the same order # else -> # generate zero or more BirdNoteSet instances # raise IOError ] for monthKey in self.__monthList: #-- 1 body -- # [ if the month for monthKey cannot contain any # records in the date interval (startDate, endDate) # or the season interval (startSeason, endSeason) -> # I # else if the month file corresponding to monthKey is # readable and valid -> # yield a BirdNoteSet instances representing # that file # else -> # raise IOError ] if self.__timeFilter ( monthKey, startDate, endDate, startSeason, endSeason ): yield self.__readMonth ( monthKey ) #-- 2 -- raise StopIteration # - - - B i r d N o t e S e t . _ _ t i m e F i l t e r def __timeFilter ( self, monthKey, startDate, endDate, startSeason, endSeason ): '''Does this month contain records of interest? [ (monthKey is a month name as "YYYY-MM") and (startDate is an inclusive starting date as a datetime.date, or None for no starting cutoff) and (endDate is an inclusive ending date as a datetime.date or None for no ending cutoff) and (startSeason is an inclusive starting month and day as a datetime.date or None for no starting cutoff) and (endSeason is an inclusive ending month and day as a datetime.date or None for no ending cutoff) -> if monthKey's month might contain records in those date and day-of-year ranges -> return True else -> return False ] ''' #-- 1 -- # [ firstOfThis := a datetime.date instance representing # the first day of monthKey's month # firstOfNext := a datetime.date instance representing # the first day of the month following monthKey ] #-- # NB: Positions within a YYYY-MM string: # 0 1 2 3 4 5 6 7 # Y Y Y Y - M M #-- yyyy = int(monthKey[:4]) mm = int(monthKey[5:]) firstOfThis = datetime.date(yyyy, mm, 1) if mm==12: firstOfNext = datetime.date(yyyy+1, 1, 1) else: firstOfNext = datetime.date(yyyy, mm+1, 1) #-- 2 -- if ( (startDate is not None) and (startDate >= firstOfNext) ): return False #-- 3 -- if ( (endDate is not None) and (endDate < firstOfThis) ): return False #-- 4 -- if startSeason is not None: start = startSeason.replace ( yyyy ) if start >= firstOfNext: return False #-- 5 -- if endSeason is not None: end = endSeason.replace ( yyyy ) if end < firstOfThis: return False #-- 6 -- return True # - - - B i r d N o t e T r e e . _ _ r e a d M o n t h def __readMonth ( self, monthKey ): '''Convert one month's data file into a BirdNoteSet. [ monthKey is a month as "YYYY-MM" -> if the month file corresponding to monthKey is readable and valid -> return a BirdNoteSet instance representing that file else -> raise IOError ] ''' #-- 1 -- # [ birdNoteSet := a new, empty BirdNoteSet instance using # self.txny for its taxonomy # inFileName := path name for the month file corresponding # to monthKey ] birdNoteSet = BirdNoteSet ( self.txny ) inFileName = os.path.join ( self.rootDir, ( "%s/%s.xml" % (monthKey[:4], monthKey) ) ) #-- 2 -- # [ if inFileName names a readable file valid against # birdnotes.rnc -> # birdNoteSet := birdNoteSet with that file added # else -> raise IOError ] birdNoteSet.readFile ( inFileName ) #-- 3 -- return birdNoteSet # - - - - - c l a s s F l a t S i g h t i n g class FlatSighting(object): '''Represents a Sighting with its context. Exports: FlatSighting(sighting): [ sighting is a Sighting instance -> return a new FlatSighting representing sighting and its context ] .txKey: [ taxonomic key number for the smallest taxon that contains the bird form for sighting ] .abbr: [ first or only bird code, stripped ] .rel: [ if this sighting is for a single form -> "" else -> relationship code as in BirdId.rel ] .abbr2: [ if this sighting is for a single form -> "" else -> second bird code, stripped ] .eng: [ English name as "Generic[, Specific]" ] .age: [ age code or "" if unknown ] .sex: [ sex code or "" if unknown ] .q: [ questionable/uncountable flag or "" ] .count: [ count field as in AgeSexGroup.count, or "" ] .date: [ date as "YYYY-MM-DD" ] .regionCode: [ region code as in DayNotes.regionCode ] .locName: [ locality name string ] .observer: [ if seen by the primary observer -> "" else -> observer's name ] .delimited(delimiter='\t'): [ delimiter is a string -> return self as a string containing all the attributes above in the same order, with (delimiter) between each attribute value ] ''' # - - - F l a t S i g h t i n g . _ _ i n i t _ _ def __init__ ( self, sighting ): '''Flatten a Sighting instance. ''' birdForm = sighting.birdForm dayNotes = birdForm.dayNotes birdId = birdForm.birdId self.txKey = birdId.taxon.txKey self.abbr = birdId.abbr self.rel = birdId.rel or '' self.abbr2 = birdId.abbr2 or '' self.eng = birdId.engComma() ageSex = sighting.ageSexGroup if ageSex is None: self.age = '' self.sex = '' self.q = '' self.count = '' self.observer = '' else: self.age = ageSex.age or '' self.sex = ageSex.sex or '' self.q = ageSex.q or '' self.count = ageSex.count or '' self.observer = ageSex.fide or '' self.date = dayNotes.date self.regionCode = dayNotes.regionCode.upper() self.locName = self.sanitize(sighting.getLocGroup().loc.name) # - - - F l a t S i g h t i n g . s a n i t i z e def sanitize ( self, s ): '''Substitute ASCII for Unicode characters. So far the only Unicode that occurs is ñ. ''' return s.replace ( u'\xf1', 'n' ) # - - - F l a t S i g h t i n g . d e l i m i t e d def delimited(self, delimiter='\t'): '''Return a delimited record ''' L = (self.txKey, self.abbr, self.rel, self.abbr2, self.eng, self.age, self.sex, self.q, self.count, self.date, self.regionCode, self.locName, self.observer) return delimiter.join(L)