diff --git a/docs/api.rst b/docs/api.rst index 120da92..d482876 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,4 +6,10 @@ API .. automodule:: genrss .. autoclass:: GenRSS + :members: + +.. autoclass:: Item + :members: + +.. autoclass:: Enclosure :members: \ No newline at end of file diff --git a/docs/installation.rst b/docs/installation.rst index 2f6b129..0b5ef61 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -24,7 +24,7 @@ Install GenRSS Use the following command to install GenRSS: -``pip install genrss`` +``pip install -U genrss`` GenRSS is now installed. Check out :ref:`quickstart` diff --git a/genrss/__init__.py b/genrss/__init__.py index d6ea987..64a6c06 100644 --- a/genrss/__init__.py +++ b/genrss/__init__.py @@ -1,19 +1,13 @@ import mimetypes from lxml.etree import Element, CDATA, tostring -from typing import Optional, List, TypeVar, Dict, Any +from typing import Optional, List, TypeVar, Dict, Any, Union from datetime import datetime -from collections import namedtuple import pytz __all__ = ('GenRSS', 'Enclosure',) ElementT = TypeVar('ElementT') - -Enclosure = namedtuple('Enclosure', ('url', 'size', 'type')) -Enclosure.__new__.__defaults__ = (None, None, None) -Enclosure.__doc__ = 'Creates information for enclosure tag.' - RSS_DEFAULT_GENERATOR = f'Generated by genrss for python' @@ -37,6 +31,101 @@ def create_element(name: str, text: Any = None, children: List[ElementT] = None, return el +class Enclosure: + """Data for enclosure tag for rss. + + :param url: Absolute url to file + :param size: File size + :param type: File mime type + """ + + def __init__(self, url: str, size=None, type=None): + self.url = url + self.size = size or 0 + self.type = type or mimetypes.guess_type(self.url)[0] + + def to_element(self) -> ElementT: + """Returns item element for xml.""" + return create_element('enclosure', url=self.url, length=str(self.size), + type=self.type) + + @staticmethod + def from_dict(data: Dict[str, Union[str, int]]): + """Makes enclosure data from dict.""" + return Enclosure(data.get('url'), data.get('size'), data.get('type')) + + +class Item: + """Data for item tag for rss. + + :param title: Title of this particular item + :param description: Content for the item. Can contain html but + link and image urls must be absolute path including hostname + :param url: Url to the item. This could be a blog entry + :param guid: A unique string feed readers use to know if an item + is new or has already been seen. If you use a guid never change + it. If you don't provide a guid then your item urls must be unique + :param author: If included it is the name of the item's creator. + If not provided the item author will be the same as the feed + author. This is typical except on multi-author blogs + :param categories: If provided, each array item will be added as a + category element + :param enclosure: An enclosure object + :param pub_date: The date and time of when the item was created. + Feed readers use this to determine the sort order. Some readers + will also use it to determine if the content should be presented + as unread + """ + + def __init__(self, title: str, **kwargs): + self.title: str = title + self.description: str = kwargs.pop('description', '') + self.url: Optional[str] = kwargs.pop('url', None) + self.guid: Optional[str] = kwargs.pop('guid', None) + self.author: Optional[str] = kwargs.pop('author', None) + self.categories: List[str] = kwargs.pop('categories', []) + self.enclosure: Optional[Enclosure] = kwargs.pop('enclosure', None) + self.pub_date: Optional[datetime] = kwargs.pop('pub_date', None) + + if isinstance(self.enclosure, dict): + self.enclosure = Enclosure.from_dict(self.enclosure) + + def to_element(self) -> ElementT: + """Returns item element for xml.""" + item = create_element('item', children=[ + create_element('title', CDATA(self.title)), + create_element('description', CDATA(self.description)), + ]) + + if self.url: + item.append(create_element('link', self.url)) + + item.append(create_element( + 'guid', + attrib={ + 'isPermaLink': str(bool(not self.guid and self.url)).lower() + }, + text=(self.guid or self.url or CDATA(self.title)) + )) + + if self.author: + item.append(create_element( + '{http://purl.org/dc/elements/1.1/}creator', + CDATA(self.author) + )) + + for category in self.categories: + item.append(create_element('category', CDATA(category))) + + if self.enclosure: + item.append(self.enclosure.to_element()) + + if self.pub_date: + item.append(create_element('pubDate', self.pub_date)) + + return item + + class GenRSS: """Generates RSS feed of channel. @@ -69,11 +158,12 @@ class GenRSS: self.docs_url: Optional[str] = kwargs.pop('docs_url', None) self.categories: List[str] = kwargs.pop('categories', []) - self.items: List[Element] = [] + self.items: List[Item] = kwargs.pop('items', []) self.generator: str = kwargs.pop('generator', RSS_DEFAULT_GENERATOR) self.root_version: str = '2.0' self.root_nsmap: Dict[str, str] = { - 'atom': 'http://www.w3.org/2005/Atom' + 'atom': 'http://www.w3.org/2005/Atom', + 'dc': 'http://purl.org/dc/elements/1.1/' } def item(self, title: str, **kwargs): @@ -100,58 +190,10 @@ class GenRSS: will also use it to determine if the content should be presented as unread """ - description: str = kwargs.pop('description', '') - url: Optional[str] = kwargs.pop('url', None) - guid: Optional[str] = kwargs.pop('guid', None) - author: Optional[str] = kwargs.pop('author', None) - categories: List[str] = kwargs.pop('categories', []) - enclosure: Optional[Enclosure] = kwargs.pop('enclosure', None) - pub_date: Optional[datetime] = kwargs.pop('pub_date', None) + self.items.append(Item(title, **kwargs)) - item = create_element('item', children=[ - create_element('title', CDATA(title)), - create_element('description', CDATA(description)), - ]) - - if url: - item.append(create_element('link', url)) - - item.append(create_element( - 'guid', - attrib={'isPermaLink': str(bool(not guid and url)).lower()}, - text=(guid or url or CDATA(title)) - )) - - if author or self.author: - if 'dc' not in self.root_nsmap: - self.root_nsmap['dc'] = 'http://purl.org/dc/elements/1.1/' - - item.append(create_element( - '{http://purl.org/dc/elements/1.1/}creator', - CDATA(author or self.author) - )) - - for category in categories: - item.append(create_element('category', CDATA(category))) - - if enclosure: - item.append(create_element( - 'enclosure', - url=enclosure.url, - length=str(enclosure.size or 0), - type=enclosure.type or mimetypes.guess_type(enclosure.url)[0] - )) - - if pub_date: - item.append(create_element('pubDate', pub_date)) - - self.items.append(item) - - def xml(self, pretty: bool = False) -> str: - """Returns the XML as a string. - - :param pretty: Pretty print xml - """ + def to_element(self) -> ElementT: + """Returns root element for xml.""" root = Element('rss', nsmap=self.root_nsmap, version=self.root_version) channel = create_element('channel', children=[ create_element('title', CDATA(self.title)), @@ -186,9 +228,18 @@ class GenRSS: channel.append(create_element('docs', self.docs_url)) for item in self.items: - channel.append(item) + if isinstance(item, Item): + channel.append(item.to_element()) root.append(channel) + return root + + def xml(self, pretty: bool = False) -> str: + """Returns the XML as a string. + + :param pretty: Pretty print xml + """ + root = self.to_element() return '\n' \ + ('\n' if pretty else '') \