root/eridanus/atom.py

Revision 65, 8.6 kB (checked in by Jonathan Jacobs <korpse@…>, 2 years ago)

Implement Atom feed support.

Line 
1"""
2A set of tools to easily build Atom 1.0 syndication feeds.
3
4Usage sample::
5
6    from xml.etree.ElementTree import tostring
7    from epsilon.extime import Time
8    from atom import Link, Summary, Entry, Author, Feed
9
10    entries = [
11        Entry(title='Atom-Powered Robots Run Amok',
12              links=[Link(href='http://example.org/2003/12/13/atom03')],
13              id='urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a',
14              updated=Time(),
15              summary=Summary('Some text.'))
16        ]
17
18    feed = Feed(id='urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6',
19                title='Example Feed',
20                updated=Time(),
21                authors=[Author(name='John Doe')],
22                links=[Link(href='http://example.org/')],
23                entries=entries)
24
25    print tostring(f.serialize())
26"""
27try:
28    from xml.etree import ElementTree as ET
29except ImportError:
30    from elementtree import ElementTree as ET
31
32from epsilon.structlike import record
33
34
35NAMESPACE = 'http://www.w3.org/2005/Atom'
36tostring = ET.tostring
37
38
39class ElementMagic(object):
40    def __init__(self, elemName, **kw):
41        # XXX: Argh! Etree doesn't enjoy serializing XML attributes with
42        # values that are None.
43        kw = dict((k, v) for k, v in kw.iteritems() if v is not None)
44        self.elem = ET.Element(elemName, **kw)
45
46    def __getitem__(self, child):
47        elem = self.elem
48
49        if child is None:
50            return None
51        elif isinstance(child, type(elem)):
52            elem.append(child)
53        elif isinstance(child, type(self)):
54            elem.append(child.elem)
55        elif isinstance(child, basestring):
56            elem.text = child
57        elif hasattr(child, 'serialize'):
58            self[child.serialize()]
59        else:
60            try:
61                for c in iter(child):
62                    self[c]
63            except TypeError:
64                raise ValueError('Unrecognized child: %r :: %r' % (child, type(child)))
65
66        return elem
67
68E = ElementMagic
69
70
71def magicOrElement(name, obj):
72    """
73    Return an L{ElementMagic} instance, with C{name} as the element name, if
74    C{obj} is not an L{AtomElement}. Otherwise return C{obj}.
75    """
76    if isinstance(obj, AtomElement):
77        return obj
78    return E(name)[obj]
79
80
81class AtomElement(object):
82    def serialize(self):
83        """
84        Flattens the C{AtomElement} into an L{ElementTree.Element}.
85        """
86        raise NotImplementedError()
87
88
89class Person(AtomElement, record('elemName name uri email',
90    uri=None, email=None)):
91    """
92    Represents a person.
93
94    @ivar elemName: Element name to generate
95    @type name: C{str} or C{unicode}
96    @type uri: C{str} or C{unicode} or C{None}
97    @type email: C{str} or C{unicode} or C{None}
98    """
99    def serialize(self):
100        return E(self.elemName)[
101            E('name')[self.name],
102            E('uri')[self.uri],
103            E('email')[self.email]]
104
105
106class Author(Person):
107    def __init__(self, *a, **kw):
108        kw['elemName'] = 'author'
109        super(Author, self).__init__(*a, **kw)
110
111
112class Contributor(Person):
113    def __init__(self, *a, **kw):
114        kw['elemName'] = 'contributor'
115        super(Contributor, self).__init__(*a, **kw)
116
117
118class Link(AtomElement, record('href rel type hreflang title length',
119    rel=None, type=None, hreflang=None, title=None, length=None)):
120    """
121    Defines a link.
122
123    @type href: C{str} or C{unicode} or C{None}
124    @type rel: C{str} or C{unicode} or C{None}
125    @type type: C{str} or C{unicode} or C{None}
126    @type hreflang: C{str} or C{unicode} or C{None}
127    @type title: C{str} or C{unicode} or C{None}
128    @type length: C{str} or C{unicode} or C{None}
129    """
130    def serialize(self):
131        return E('link',
132            href=self.href,
133            rel=self.rel,
134            hreflang=self.hreflang,
135            title=self.title,
136            length=self.length)
137
138
139class Text(AtomElement, record('content elemName type',
140    type=None)):
141    """
142    Generic text element.
143
144    @cvar elemName: Override the elemName argument
145
146    @type content: C{str} or C{unicode}
147    @ivar elemName: Element name to generate
148    @type type: C{str}, C{unicode} or C{None}
149    """
150    def __init__(self, *a, **kw):
151        if hasattr(self, 'elemName'):
152            kw['elemName'] = self.elemName
153        super(Text, self).__init__(*a, **kw)
154
155    def serialize(self):
156        return E(self.elemName, type=self.type)[self.content]
157
158
159class Title(Text):
160    elemName = 'title'
161
162
163class Summary(Text):
164    elemName = 'summary'
165
166
167class Content(Text):
168    elemName = 'content'
169
170
171class Rights(Text):
172    elemName = 'rights'
173
174
175class Icon(Text):
176    elemName = 'icon'
177
178
179class Logo(Text):
180    elemName = 'logo'
181
182
183class Subtitle(Text):
184    elemName = 'subtitle'
185
186
187class Entry(AtomElement, record('id title updated authors content links summary categories contributors published source rights',
188    authors=None, content=None, links=None, summary=None, categories=None, contributors=None, published=None, source=None, rights=None)):
189    """
190    An indivial entry, acting as a container for metadata and data.
191
192    @type id: C{str} or C{unicode}
193    @type title: C{str}, C{unicode}, L{Title} instance or C{None}
194    @type updated: L{epsilon.extime.Time} instance
195    @type authors: C{iterable} of L{Author} instances or C{None}
196    @type content: C{str}, C{unicode}, L{Content} instance or C{None}
197    @type links: C{iterable} of L{Link} instances or C{None}
198    @type summary: C{str}, C{unicode}, L{Summary} instance or C{None}
199    @type categories: C{iterable} of L{Category} instances or C{None}
200    @type contributors: C{iterable} of L{Contributor} instances or C{None}
201    @type published: L{epsilon.extime.Time} instance or C{None}
202    @type source: L{Source} instance or C{None}
203    @type rights: C{str}, C{unicode}, L{Rights} instance or C{None}
204    """
205    def serialize(self):
206        published = None
207        if self.published is not None:
208            published = self.published.asISO8601TimeAndDate()
209
210        return E('entry')[
211            E('id')[self.id],
212            magicOrElement('title', self.title),
213            E('updated')[self.updated.asISO8601TimeAndDate()],
214            self.authors,
215            magicOrElement('content', self.content),
216            self.links,
217            magicOrElement('summary', self.summary),
218            self.categories,
219            self.contributors,
220            E('published')[published],
221            self.source,
222            magicOrElement('rights', self.rights)]
223
224
225class Generator(AtomElement, record('name uri version',
226    uri=None, version=None)):
227    """
228    Identifies the software used to generate the feed.
229
230    @type name: C{str} or C{unicode}
231    @type uri: C{str} or C{unicode}
232    @type version: C{str} or C{unicode}
233    """
234    def serialize(self):
235        return E('generator', uri=self.uri, version=self.version)[self.name]
236
237
238class Feed(AtomElement, record('id title updated authors links entries categories contributors generator icon logo rights subtitle',
239    authors=None, links=None, entries=None, categories=None, contributors=None, generator=None, icon=None, logo=None, rights=None, subtitle=None)):
240    """
241    Root element of an Atom 1.0 feed.
242
243    @type id: C{str} or C{unicode}
244    @type title: C{str}, C{unicode}, L{Title} instance or C{None}
245    @type updated: L{epsilon.extime.Time} instance
246    @type authors: C{iterable} of L{Author} instances or C{None}
247    @type links: C{iterable} of L{Link} instances or C{None}
248    @type entries: C{iterable} of L{Entry} instances or C{None}
249    @type categories: C{iterable} of L{Category} instances or C{None}
250    @type contributors: C{iterable} of L{Contributor} instances or C{None}
251    @type generator: L{Generator} instance or C{None}
252    @type icon: C{str}, C{unicode}, L{Icon} instance or C{None}
253    @type logo: C{str}, C{unicode}, L{Logo} instance or C{None}
254    @type rights: C{str}, C{unicode}, L{Rights} instance or C{None}
255    @type subtitle: C{str}, C{unicode}, L{Subtitle} instance or C{None}
256    """
257    def serialize(self):
258        return E('feed', xmlns=NAMESPACE)[
259            E('id')[self.id],
260            magicOrElement('title', self.title),
261            E('updated')[self.updated.asISO8601TimeAndDate()],
262            self.authors,
263            self.links,
264            self.entries,
265            self.categories,
266            self.contributors,
267            self.generator,
268            magicOrElement('icon', self.icon),
269            magicOrElement('logo', self.logo),
270            magicOrElement('rights', self.rights),
271            self.subtitle]
272
273
274__all__ = [
275    # Constants
276    'NAMESPACE',
277
278    # Core objects
279    'Author', 'Content', 'Contributor', 'Entry', 'Feed', 'Generator', 'Icon',
280    'Link', 'Logo', 'Rights', 'Subtitle', 'Summary', 'Title']
Note: See TracBrowser for help on using the browser.