commit
08c1c1d6fc
7 changed files with 144 additions and 20 deletions
|
@ -6,8 +6,9 @@ from lxml.etree import Element, CDATA, tostring
|
||||||
from .utils import create_element, ElementT
|
from .utils import create_element, ElementT
|
||||||
from .item import Item
|
from .item import Item
|
||||||
from .enclosure import Enclosure
|
from .enclosure import Enclosure
|
||||||
|
from .image import Image
|
||||||
|
|
||||||
__all__ = ('GenRSS', 'Item', 'Enclosure')
|
__all__ = ('GenRSS', 'Item', 'Enclosure', 'Image')
|
||||||
|
|
||||||
|
|
||||||
RSS_DEFAULT_GENERATOR = f'Generated by genrss for python'
|
RSS_DEFAULT_GENERATOR = f'Generated by genrss for python'
|
||||||
|
@ -21,12 +22,15 @@ class GenRSS:
|
||||||
:param feed_url: Absolute url to the rss feed
|
:param feed_url: Absolute url to the rss feed
|
||||||
:param description: A short description of feed
|
:param description: A short description of feed
|
||||||
:param image_url: Image absolute url for channel
|
:param image_url: Image absolute url for channel
|
||||||
|
:param image: Image element for channel (it replaces image_url)
|
||||||
:param author: Author of channel
|
:param author: Author of channel
|
||||||
:param pub_date: Datetime in utc when last item was published
|
:param pub_date: Datetime in utc when last item was published
|
||||||
:param copyright: Copyright information for this feed
|
:param copyright: Copyright information for this feed
|
||||||
:param language: The language of the content of this feed.
|
:param language: The language of the content of this feed.
|
||||||
:param editor: Who manages content in this feed
|
:param editor: Who manages content in this feed
|
||||||
:param webmaster: Who manages feed availability and technical support
|
:param webmaster: Who manages feed availability and technical support
|
||||||
|
:param docs_url: Url to rss documentation
|
||||||
|
:param categories: List of category names
|
||||||
:param generator: Feed generator
|
:param generator: Feed generator
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -36,6 +40,7 @@ class GenRSS:
|
||||||
self.feed_url: str = feed_url
|
self.feed_url: str = feed_url
|
||||||
self.description: str = kwargs.pop('description', self.title)
|
self.description: str = kwargs.pop('description', self.title)
|
||||||
self.image_url: Optional[str] = kwargs.pop('image_url', None)
|
self.image_url: Optional[str] = kwargs.pop('image_url', None)
|
||||||
|
self.image: Optional[Image] = kwargs.pop('image', None)
|
||||||
self.author: Optional[str] = kwargs.pop('author', None)
|
self.author: Optional[str] = kwargs.pop('author', None)
|
||||||
self.pub_date: Optional[datetime] = kwargs.pop('pub_date', None)
|
self.pub_date: Optional[datetime] = kwargs.pop('pub_date', None)
|
||||||
self.copyright: Optional[str] = kwargs.pop('copyright', None)
|
self.copyright: Optional[str] = kwargs.pop('copyright', None)
|
||||||
|
@ -44,7 +49,6 @@ class GenRSS:
|
||||||
self.webmaster: Optional[str] = kwargs.pop('webmaster', None)
|
self.webmaster: Optional[str] = kwargs.pop('webmaster', None)
|
||||||
self.docs_url: Optional[str] = kwargs.pop('docs_url', None)
|
self.docs_url: Optional[str] = kwargs.pop('docs_url', None)
|
||||||
self.categories: List[str] = kwargs.pop('categories', [])
|
self.categories: List[str] = kwargs.pop('categories', [])
|
||||||
|
|
||||||
self.items: List[Item] = kwargs.pop('items', [])
|
self.items: List[Item] = kwargs.pop('items', [])
|
||||||
self.generator: str = kwargs.pop('generator', RSS_DEFAULT_GENERATOR)
|
self.generator: str = kwargs.pop('generator', RSS_DEFAULT_GENERATOR)
|
||||||
self.root_version: str = '2.0'
|
self.root_version: str = '2.0'
|
||||||
|
@ -93,12 +97,12 @@ class GenRSS:
|
||||||
create_element('lastBuildDate', datetime.utcnow())
|
create_element('lastBuildDate', datetime.utcnow())
|
||||||
])
|
])
|
||||||
|
|
||||||
if self.image_url:
|
channel_image = self.image
|
||||||
channel.append(create_element('image', children=[
|
if not channel_image and self.image_url:
|
||||||
create_element('url', self.image_url),
|
channel_image = Image(self.image_url, self.site_url, self.title)
|
||||||
create_element('title', CDATA(self.title)),
|
if isinstance(image, Image):
|
||||||
create_element('link', self.site_url)
|
channel.append(channel_image.to_element())
|
||||||
]))
|
|
||||||
for category in self.categories:
|
for category in self.categories:
|
||||||
channel.append(create_element('category', CDATA(category)))
|
channel.append(create_element('category', CDATA(category)))
|
||||||
if self.pub_date:
|
if self.pub_date:
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union, TypeVar, Dict
|
||||||
|
|
||||||
from .utils import ElementT, create_element
|
from .utils import ElementT, create_element
|
||||||
|
|
||||||
__all__ = ('Enclosure',)
|
__all__ = ('Enclosure', 'EnclosureOrDictT')
|
||||||
|
|
||||||
|
|
||||||
class Enclosure:
|
class Enclosure:
|
||||||
|
@ -24,9 +24,10 @@ class Enclosure:
|
||||||
return create_element('enclosure', url=self.url, length=str(self.size),
|
return create_element('enclosure', url=self.url, length=str(self.size),
|
||||||
type=self.type)
|
type=self.type)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def from_dict(data: Dict[str, Union[str, int]]):
|
def from_dict(cls, data: Dict[str, Union[str, int]]):
|
||||||
"""Makes enclosure data from dict."""
|
"""Makes enclosure data from dict."""
|
||||||
return Enclosure(data.get('url'), data.get('size'), data.get('type'))
|
return cls(**data)
|
||||||
|
|
||||||
|
|
||||||
|
EnclosureOrDictT = Union[Enclosure, Dict]
|
||||||
|
|
51
genrss/image.py
Normal file
51
genrss/image.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
from typing import Dict, Union
|
||||||
|
from lxml.etree import CDATA
|
||||||
|
|
||||||
|
from .utils import ElementT, create_element
|
||||||
|
|
||||||
|
__all__ = ('Image',)
|
||||||
|
|
||||||
|
|
||||||
|
class Image:
|
||||||
|
"""The element allows an image to be displayed when aggregators
|
||||||
|
present a feed.
|
||||||
|
|
||||||
|
:param url: Absolute url to the image
|
||||||
|
:param link: Hyperlink to the website
|
||||||
|
:param title: Text to display if the image could not be shown
|
||||||
|
:param description: Specifies the text in the HTML title attribute of the
|
||||||
|
link around the image
|
||||||
|
:param width: The width of the image
|
||||||
|
:param height: The height of the image
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, url: str, link: str, title: str, description: str = None,
|
||||||
|
width: int = None, height: int = None):
|
||||||
|
self.url = url
|
||||||
|
self.link = link
|
||||||
|
self.title = title
|
||||||
|
self.description = description
|
||||||
|
self.height = height
|
||||||
|
self.width = width
|
||||||
|
|
||||||
|
def to_element(self) -> ElementT:
|
||||||
|
"""Returns image element for xml."""
|
||||||
|
image = create_element('image', children=[
|
||||||
|
create_element('url', self.url),
|
||||||
|
create_element('link', self.link),
|
||||||
|
create_element('title', CDATA(self.title))
|
||||||
|
])
|
||||||
|
|
||||||
|
if self.description:
|
||||||
|
image.append(create_element('description', CDATA(self.description)))
|
||||||
|
if self.height:
|
||||||
|
image.append(create_element('height', self.height))
|
||||||
|
if self.width:
|
||||||
|
image.append(create_element('width', self.width))
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Union[str, int]]):
|
||||||
|
"""Makes image data from dict."""
|
||||||
|
return cls(**data)
|
|
@ -1,10 +1,11 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Union, Dict
|
||||||
|
|
||||||
from lxml.etree import CDATA
|
from lxml.etree import CDATA
|
||||||
|
|
||||||
from .utils import ElementT, create_element
|
from .utils import ElementT, create_element
|
||||||
from .enclosure import Enclosure
|
from .enclosure import Enclosure, EnclosureOrDictT
|
||||||
|
from .image import Image
|
||||||
|
|
||||||
__all__ = ('Item',)
|
__all__ = ('Item',)
|
||||||
|
|
||||||
|
@ -25,6 +26,7 @@ class Item:
|
||||||
:param categories: If provided, each array item will be added as a
|
:param categories: If provided, each array item will be added as a
|
||||||
category element
|
category element
|
||||||
:param enclosure: An enclosure object
|
:param enclosure: An enclosure object
|
||||||
|
:param image: An image object
|
||||||
:param pub_date: The date and time of when the item was created.
|
:param pub_date: The date and time of when the item was created.
|
||||||
Feed readers use this to determine the sort order. Some readers
|
Feed readers use this to determine the sort order. Some readers
|
||||||
will also use it to determine if the content should be presented
|
will also use it to determine if the content should be presented
|
||||||
|
@ -38,11 +40,15 @@ class Item:
|
||||||
self.guid: Optional[str] = kwargs.pop('guid', None)
|
self.guid: Optional[str] = kwargs.pop('guid', None)
|
||||||
self.author: Optional[str] = kwargs.pop('author', None)
|
self.author: Optional[str] = kwargs.pop('author', None)
|
||||||
self.categories: List[str] = kwargs.pop('categories', [])
|
self.categories: List[str] = kwargs.pop('categories', [])
|
||||||
self.enclosure: Optional[Enclosure] = kwargs.pop('enclosure', None)
|
self.enclosure: Optional[Enclosure, Dict] = kwargs.pop('enclosure',
|
||||||
|
None)
|
||||||
|
self.image: Optional[Image, Dict] = kwargs.pop('image', None)
|
||||||
self.pub_date: Optional[datetime] = kwargs.pop('pub_date', None)
|
self.pub_date: Optional[datetime] = kwargs.pop('pub_date', None)
|
||||||
|
|
||||||
if isinstance(self.enclosure, dict):
|
if isinstance(self.enclosure, dict):
|
||||||
self.enclosure = Enclosure.from_dict(self.enclosure)
|
self.enclosure = Enclosure.from_dict(self.enclosure)
|
||||||
|
if isinstance(self.image, dict):
|
||||||
|
self.image = Image.from_dict(self.image)
|
||||||
|
|
||||||
def to_element(self) -> ElementT:
|
def to_element(self) -> ElementT:
|
||||||
"""Returns item element for xml."""
|
"""Returns item element for xml."""
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
IMAGE_URL = 'http://s3.smartfridge.me/image.jpg'
|
IMAGE_URL = 'https://s3.smartfridge.me/image.jpg'
|
||||||
|
SITE_URL = 'https://smartfridge.me/'
|
||||||
|
SITE_TITLE = 'Smart Fridge'
|
||||||
|
|
||||||
|
IMAGE_DESCRIPTION = 'a'*100
|
||||||
|
IMAGE_HEIGHT = 100
|
||||||
|
IMAGE_WIDTH = 100
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(params=[
|
@pytest.fixture(params=[
|
||||||
|
@ -21,3 +28,29 @@ def enclosure_tuple(request):
|
||||||
])
|
])
|
||||||
def enclosure_dict(request):
|
def enclosure_dict(request):
|
||||||
return request.param
|
return request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=[
|
||||||
|
pytest.param((None, None, None), id='-/-/-'),
|
||||||
|
pytest.param((IMAGE_DESCRIPTION, None, None), id='+/-/-'),
|
||||||
|
pytest.param((IMAGE_DESCRIPTION, 100, None), id='+/+/-'),
|
||||||
|
pytest.param((IMAGE_DESCRIPTION, 100, 200), id='+/+/+'),
|
||||||
|
])
|
||||||
|
def image_tuple(request):
|
||||||
|
return (IMAGE_URL, SITE_URL, SITE_TITLE) + request.param
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=[
|
||||||
|
pytest.param(dict(), id='-/-/-'),
|
||||||
|
pytest.param(dict(description=IMAGE_DESCRIPTION), id='+/-/-'),
|
||||||
|
pytest.param(dict(description=IMAGE_DESCRIPTION, width=100), id='+/+/-'),
|
||||||
|
pytest.param(dict(description=IMAGE_DESCRIPTION, width=100, height=100),
|
||||||
|
id='+/+/+'),
|
||||||
|
])
|
||||||
|
def image_dict(request):
|
||||||
|
return dict(
|
||||||
|
url=IMAGE_URL,
|
||||||
|
link=SITE_URL,
|
||||||
|
title=SITE_TITLE,
|
||||||
|
**request.param
|
||||||
|
)
|
||||||
|
|
|
@ -2,13 +2,13 @@ import pytest
|
||||||
from genrss import Enclosure
|
from genrss import Enclosure
|
||||||
|
|
||||||
|
|
||||||
def test_init_fails():
|
def test_init_enclosure_fails():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
Enclosure()
|
Enclosure()
|
||||||
assert False
|
assert False
|
||||||
|
|
||||||
|
|
||||||
def test_init(enclosure_tuple):
|
def test_init_enclosure(enclosure_tuple):
|
||||||
url, size, type = enclosure_tuple
|
url, size, type = enclosure_tuple
|
||||||
enclosure = Enclosure(url, size, type)
|
enclosure = Enclosure(url, size, type)
|
||||||
assert enclosure.url == url
|
assert enclosure.url == url
|
||||||
|
@ -16,7 +16,7 @@ def test_init(enclosure_tuple):
|
||||||
assert enclosure.type == (type or 'image/jpeg')
|
assert enclosure.type == (type or 'image/jpeg')
|
||||||
|
|
||||||
|
|
||||||
def test_init_from_dict(enclosure_dict):
|
def test_init_enclosure_from_dict(enclosure_dict):
|
||||||
enclosure = Enclosure.from_dict(enclosure_dict)
|
enclosure = Enclosure.from_dict(enclosure_dict)
|
||||||
assert enclosure.url == enclosure_dict.get('url')
|
assert enclosure.url == enclosure_dict.get('url')
|
||||||
assert enclosure.size == enclosure_dict.get('size', 0)
|
assert enclosure.size == enclosure_dict.get('size', 0)
|
||||||
|
|
29
tests/test_image.py
Normal file
29
tests/test_image.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import pytest
|
||||||
|
from genrss import Image
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_image_fails():
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
Image()
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_image(image_tuple):
|
||||||
|
url, link, title, description, width, height = image_tuple
|
||||||
|
image = Image(url, link, title, description, width, height)
|
||||||
|
assert image.url == url
|
||||||
|
assert image.link == link
|
||||||
|
assert image.title == title
|
||||||
|
assert image.description == description
|
||||||
|
assert image.width == width
|
||||||
|
assert image.height == height
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_image_from_dict(image_dict):
|
||||||
|
image = Image.from_dict(image_dict)
|
||||||
|
assert image.url == image_dict.get('url')
|
||||||
|
assert image.link == image_dict.get('link')
|
||||||
|
assert image.title == image_dict.get('title')
|
||||||
|
assert image.description == image_dict.get('description')
|
||||||
|
assert image.width == image_dict.get('width')
|
||||||
|
assert image.height == image_dict.get('height')
|
Loading…
Reference in a new issue