Added migration to add extra info to book.
[wolnelektury.git] / lib / librarian / dcparser.py
1 # -*- coding: utf-8 -*-
2 from xml.parsers.expat import ExpatError
3 from datetime import date
4 import time
5
6 # Import ElementTree from anywhere
7 try:
8     import xml.etree.ElementTree as etree # Python >= 2.5
9 except ImportError:
10     try:
11         import elementtree.ElementTree as etree # effbot's pure Python module
12     except ImportError:
13         import lxml.etree as etree # ElementTree API using libxml2
14
15
16 # ==============
17 # = Converters =
18 # ==============
19 class Person(object):
20     """Single person with last name and a list of first names."""
21     def __init__(self, last_name, *first_names):
22         self.last_name = last_name
23         self.first_names = first_names
24     
25     
26     def __eq__(self, right):
27         return self.last_name == right.last_name and self.first_names == right.first_names
28     
29     
30     def __unicode__(self):
31         if len(self.first_names) > 0:
32             return '%s, %s' % (self.last_name, ' '.join(self.first_names))
33         else:
34             return self.last_name
35     
36     
37     def __repr__(self):
38         return 'Person(last_name=%r, first_names=*%r)' % (self.last_name, self.first_names)
39
40
41 def str_to_unicode(value, previous):
42     return unicode(value)
43
44
45 def str_to_unicode_list(value, previous):
46     if previous is None:
47         previous = []
48     previous.append(str_to_unicode(value, None))
49     return previous
50
51
52 def str_to_person(value, previous):
53     comma_count = value.count(',')
54     
55     if comma_count == 0:
56         last_name, first_names = value, []
57     elif comma_count == 1:
58         last_name, first_names = value.split(',')
59         first_names = [name for name in first_names.split(' ') if len(name)]
60     else:
61         raise ValueError("value contains more than one comma: %r" % value)
62     
63     return Person(last_name.strip(), *first_names)
64
65
66 def str_to_date(value, previous):
67     try:
68         t = time.strptime(value, '%Y-%m-%d')
69     except ValueError:
70         t = time.strptime(value, '%Y')
71     return date(t[0], t[1], t[2])
72
73
74 # ==========
75 # = Parser =
76 # ==========
77 class ParseError(Exception):
78     def __init__(self, message):
79         super(ParseError, self).__init__(message)
80
81
82 class XMLNamespace(object):
83     '''Represents XML namespace.'''
84     
85     def __init__(self, uri):
86         self.uri = uri
87
88     def __call__(self, tag):
89         return '{%s}%s' % (self.uri, tag)
90
91     def __contains__(self, tag):
92         return tag.startswith(str(self))
93
94     def __repr__(self):
95         return 'XMLNamespace(%r)' % self.uri
96     
97     def __str__(self):
98         return '%s' % self.uri
99
100
101 class BookInfo(object):
102     RDF = XMLNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
103     DC = XMLNamespace('http://purl.org/dc/elements/1.1/')
104     
105     mapping = {
106         DC('creator')        : ('author', str_to_person),
107         DC('title')          : ('title', str_to_unicode),
108         DC('subject.period') : ('epoch', str_to_unicode),
109         DC('subject.type')   : ('kind', str_to_unicode),
110         DC('subject.genre')  : ('genre', str_to_unicode),
111         DC('date')           : ('created_at', str_to_date),
112         DC('date.pd')        : ('released_to_public_domain_at', str_to_date),
113         DC('contributor.translator') : ('translator', str_to_person),
114         DC('contributor.technical_editor') : ('technical_editor', str_to_person),
115         DC('publisher')      : ('publisher', str_to_unicode),
116         DC('source')         : ('source_name', str_to_unicode),
117         DC('source.URL')     : ('source_url', str_to_unicode),
118         DC('identifier.url') : ('url', str_to_unicode),
119         DC('relation.hasPart') : ('parts', str_to_unicode_list),
120     }
121
122     @classmethod
123     def from_string(cls, xml):
124         from StringIO import StringIO
125         return cls.from_file(StringIO(xml))
126     
127     @classmethod
128     def from_file(cls, xml_file):
129         book_info = cls()
130         
131         try:
132             tree = etree.parse(xml_file)
133         except ExpatError, e:
134             raise ParseError(e)
135
136         description = tree.find('//' + book_info.RDF('Description'))
137         book_info.wiki_url = description.get(cls.RDF('about'), None)
138         
139         if description is None:
140             raise ParseError('no Description tag found in document')
141         
142         for element in description.findall('*'):
143             book_info.parse_element(element) 
144         
145         return book_info
146
147     def parse_element(self, element):
148         try:
149             attribute, converter = self.mapping[element.tag]
150             setattr(self, attribute, converter(element.text, getattr(self, attribute, None)))
151         except KeyError:
152             pass
153
154     def to_xml(self):
155         """XML representation of this object."""
156         etree._namespace_map[str(self.RDF)] = 'rdf'
157         etree._namespace_map[str(self.DC)] = 'dc'
158         
159         root = etree.Element(self.RDF('RDF'))
160         description = etree.SubElement(root, self.RDF('Description'))
161         
162         if self.wiki_url:
163             description.set(self.RDF('about'), self.wiki_url)
164         
165         for tag, (attribute, converter) in self.mapping.iteritems():
166             if hasattr(self, attribute):
167                 e = etree.Element(tag)
168                 e.text = unicode(getattr(self, attribute))
169                 description.append(e)
170         
171         return unicode(etree.tostring(root, 'utf-8'), 'utf-8')
172
173     def to_dict(self):
174         etree._namespace_map[str(self.RDF)] = 'rdf'
175         etree._namespace_map[str(self.DC)] = 'dc'
176         
177         result = {'about': self.wiki_url}
178         for tag, (attribute, converter) in self.mapping.iteritems():
179             if hasattr(self, attribute):
180                 result[attribute] = unicode(getattr(self, attribute))
181         
182         return result
183
184
185 def parse(file_name):
186     return BookInfo.from_file(file_name)
187
188
189 if __name__ == '__main__':
190     import sys
191     
192     info = parse(sys.argv[1])
193     for attribute, _ in BookInfo.mapping.values():
194         print '%s: %r' % (attribute, getattr(info, attribute, None))
195