| 1 | """ |
|---|
| 2 | A set of tools to easily build Atom 1.0 syndication feeds. |
|---|
| 3 | |
|---|
| 4 | Usage 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 | """ |
|---|
| 27 | try: |
|---|
| 28 | from xml.etree import ElementTree as ET |
|---|
| 29 | except ImportError: |
|---|
| 30 | from elementtree import ElementTree as ET |
|---|
| 31 | |
|---|
| 32 | from epsilon.structlike import record |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | NAMESPACE = 'http://www.w3.org/2005/Atom' |
|---|
| 36 | tostring = ET.tostring |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | class 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 | |
|---|
| 68 | E = ElementMagic |
|---|
| 69 | |
|---|
| 70 | |
|---|
| 71 | def 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 | |
|---|
| 81 | class AtomElement(object): |
|---|
| 82 | def serialize(self): |
|---|
| 83 | """ |
|---|
| 84 | Flattens the C{AtomElement} into an L{ElementTree.Element}. |
|---|
| 85 | """ |
|---|
| 86 | raise NotImplementedError() |
|---|
| 87 | |
|---|
| 88 | |
|---|
| 89 | class 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 | |
|---|
| 106 | class Author(Person): |
|---|
| 107 | def __init__(self, *a, **kw): |
|---|
| 108 | kw['elemName'] = 'author' |
|---|
| 109 | super(Author, self).__init__(*a, **kw) |
|---|
| 110 | |
|---|
| 111 | |
|---|
| 112 | class Contributor(Person): |
|---|
| 113 | def __init__(self, *a, **kw): |
|---|
| 114 | kw['elemName'] = 'contributor' |
|---|
| 115 | super(Contributor, self).__init__(*a, **kw) |
|---|
| 116 | |
|---|
| 117 | |
|---|
| 118 | class 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 | |
|---|
| 139 | class 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 | |
|---|
| 159 | class Title(Text): |
|---|
| 160 | elemName = 'title' |
|---|
| 161 | |
|---|
| 162 | |
|---|
| 163 | class Summary(Text): |
|---|
| 164 | elemName = 'summary' |
|---|
| 165 | |
|---|
| 166 | |
|---|
| 167 | class Content(Text): |
|---|
| 168 | elemName = 'content' |
|---|
| 169 | |
|---|
| 170 | |
|---|
| 171 | class Rights(Text): |
|---|
| 172 | elemName = 'rights' |
|---|
| 173 | |
|---|
| 174 | |
|---|
| 175 | class Icon(Text): |
|---|
| 176 | elemName = 'icon' |
|---|
| 177 | |
|---|
| 178 | |
|---|
| 179 | class Logo(Text): |
|---|
| 180 | elemName = 'logo' |
|---|
| 181 | |
|---|
| 182 | |
|---|
| 183 | class Subtitle(Text): |
|---|
| 184 | elemName = 'subtitle' |
|---|
| 185 | |
|---|
| 186 | |
|---|
| 187 | class 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 | |
|---|
| 225 | class 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 | |
|---|
| 238 | class 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'] |
|---|