import os
from pykindle import readers
[docs]class FieldNotSetError(Exception):
"""
When user get a field of an item that has not been set yet, raise.
"""
def __init__(self, field):
super(FieldNotSetError, self).__init__('{} not set.'.format(field))
[docs]class Item(dict):
"""
Item represents an asset of mobi files.
This is a base class, you should inherit from it.
Mobi file consists of several assets and a manifest.
Each item renders to a asset,
including articles, images and manifest files.
Item class inherited from python dict class.
You can set attributes in two ways:
>>> item = Item()
>>> item['href'] = 'path/to/item'
>>> assert item.href == 'path/to/item'
>>> item.href = 'path/to/item/changed'
>>> assert item['href'] = 'path/to/item/changed'
"""
def __init__(self, *args, **kwargs):
super(Item, self).__init__(*args, **kwargs)
self._default = dict()
def __getitem__(self, key):
assert isinstance(key, str)
if key in self:
return super(Item, self).__getitem__(key)
elif key in self._default:
return self._default[key]
else:
raise FieldNotSetError(key)
def __setitem__(self, key, value):
super(Item, self).__setitem__(key, value)
@property
def href(self):
"""
Mobi file is just like a html website,
and each asset has it's relative url in the mobi file.
Html assets get other html assets and image assets by relative url.
"""
return self['href']
@href.setter
def href(self, value):
assert isinstance(value, str)
self['href'] = value
@property
def media_type(self):
"""
Every mobi asset has a field to indicates what it is, for example,
application/xhtml+xml represents an article,
image/jpg represents a photo in jpg format.
"""
return self['media_type']
@media_type.setter
def media_type(self, value):
assert isinstance(value, str)
assert '/' in value
self['media_type'] = value
@property
def id(self):
"""
Id field should be a string! Not integer!
Just like html elements has a "id" field,
mobi assets also have such field.
The difference is that in mobi assets, "id" field in required.
"""
return self['id']
@id.setter
def id(self, value):
assert isinstance(value, str)
self['id'] = value
@property
def source(self):
"""
Source is the main content of the asset.
It will later send to self.reader.render to get the rendered result.
"""
return self['source']
@source.setter
def source(self, value):
assert value is not None
self['source'] = value
@property
def reader(self):
"""
Smetimes we need to convert some files to mobi supported assets.
That's why we use reader.
Reader should be a instance of pykindle.reader.Reader class.
Item will call self.reader.render(self.source) to generate rendered assets.
"""
return self['reader']
@reader.setter
def reader(self, value):
assert isinstance(value, readers.Reader)
self['reader'] = value
@property
def content(self):
"""
Readonly field.
It just return the value of self.reader.render(self.source).
"""
return self.reader.render(self.source)
[docs] def write(self, directory):
"""
Write the rendered content to a file.
File path is calculated by os.path.join(directory, self.href).
"""
path = os.path.join(directory, *self.href.split('/'))
with open(path, mode='w', encoding='utf-8') as file:
file.write(self.content)
[docs]class ArticleItem(Item):
"""
ArticleItem represents an real html article.
This is the main body of your book.
This class is used for inherit.
Default attributes:
title = Untitled
author = No Author
description = No description
category = default
"""
def __init__(self, *args, **kwargs):
super(ArticleItem, self).__init__(*args, **kwargs)
self.media_type = 'application/xhtml+xml'
self._default.update({
'title': 'Untitled',
'author': 'No Author',
'description': 'No description.',
'category': 'default',
})
@property
def title(self):
"""
The title of article.
"""
return self['title']
@title.setter
def title(self, value):
assert isinstance(value, str)
self['title'] = value
@property
def author(self):
"""
The author of article.
"""
return self['author']
@author.setter
def author(self, value):
assert isinstance(value, str)
self['author'] = value
@property
def description(self):
"""
The description of article.
"""
return self['description']
@description.setter
def description(self, value):
assert isinstance(value, str)
self['description'] = value
@property
def category(self):
"""
Optional.
If you generates a magazine book, this field is used to calculate the magazine category.
Deprecated. We will create a Category class to do this.
:return:
"""
return self['category']
@category.setter
def category(self, value):
assert isinstance(value, str)
self['category'] = value
@property
def content(self):
return self.reader.render(self.title, self.author, self.description, self.source)
[docs]class HtmlArticleItem(ArticleItem):
"""
HtmlArticleItem represents a html article.
Although all the articles in mobi files are html,
we have other ArticleItem like MarkdownArticleItem to convert other formats to html.
"""
def __init__(self, *args, **kwargs):
super(HtmlArticleItem, self).__init__(*args, **kwargs)
self.reader = readers.HtmlReader()
[docs]class MarkdownArticleItem(ArticleItem):
"""
MarkdownArticleItem reads a markdown source.
"""
def __init__(self, *args, **kwargs):
super(MarkdownArticleItem, self).__init__(*args, **kwargs)
self.reader = readers.MarkdownReader()
[docs]class ImageItem(Item):
"""
ImageItem represents a image asset.
It has a positional argument, image_format='jpeg', alternative formats are jpeg, png, gif
"""
def __init__(self, image_format='jpeg', *args, **kwargs):
super(ImageItem, self).__init__(*args, **kwargs)
assert image_format in ('jpeg', 'png', 'gif')
self.image_format = image_format
self.media_type = 'image/' + image_format
self.reader = readers.ImageReader()
[docs]class CoverImageItem(ImageItem):
"""
CoverImageItem represents a cover image.
"""
pass
[docs]class NcxItem(Item):
"""
NcxItem is used to generate ncx file.
Ncx file is the category of a book.
It will be automatically added to the book when book.create is called.
"""
def __init__(self, *args, **kwargs):
super(NcxItem, self).__init__(*args, **kwargs)
self.media_type = 'application/x-dtbncx+xml'
self._default.update({
'id': 'ncx',
'href': 'ncx.toc',
'title': 'Untitled',
'author': 'No Author',
})
@property
def title(self):
return self['title']
@title.setter
def title(self, value):
assert isinstance(value, str)
self['title'] = value
@property
def author(self):
return self['author']
@author.setter
def author(self, value):
assert isinstance(value, str)
self['author'] = value
[docs]class MagazineNcxItem(NcxItem):
"""
MagazineNcxItem represents a magazine toc.
The way to implement magazine is very ugly, and we'll reconstruct these classes.
"""
def __init__(self, *args, **kwargs):
super(MagazineNcxItem, self).__init__(*args, **kwargs)
self.reader = readers.MagazineNcxReader()
@property
def categories(self):
return self['categories']
@categories.setter
def categories(self, value):
assert isinstance(value, list)
for category, articles in value:
if not isinstance(category, str):
raise ValueError('category should be a string.')
for article in articles:
if not isinstance(article, ArticleItem):
raise ValueError('article should be a pykindle.items.Article object')
self['categories'] = value
@property
def content(self):
return self.reader.render(title=self.title, author=self.author, categories=self.categories)
__all__ = ['Item', 'ArticleItem', 'HtmlArticleItem', 'MarkdownArticleItem', 'ImageItem', 'CoverImageItem', 'NcxItem',
'MagazineNcxItem', 'FieldNotSetError']