mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-11 09:13:57 -04:00
Merge from trunk
This commit is contained in:
commit
f3b72988b3
@ -123,7 +123,7 @@ function fetch_library_books(start, num, timeout, sort, order, search) {
|
||||
|
||||
current_library_request = $.ajax({
|
||||
type: "GET",
|
||||
url: "library",
|
||||
url: "xml",
|
||||
data: data,
|
||||
cache: false,
|
||||
timeout: timeout, //milliseconds
|
||||
|
BIN
resources/images/devices/ipad.png
Normal file
BIN
resources/images/devices/ipad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
BIN
resources/images/news/american_thinker.png
Normal file
BIN
resources/images/news/american_thinker.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 705 B |
43
resources/recipes/american_thinker.recipe
Normal file
43
resources/recipes/american_thinker.recipe
Normal file
@ -0,0 +1,43 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Walt Anthony <workshop.northpole at gmail.com>'
|
||||
'''
|
||||
www.americanthinker.com
|
||||
'''
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AmericanThinker(BasicNewsRecipe):
|
||||
title = u'American Thinker'
|
||||
description = "American Thinker is a daily internet publication devoted to the thoughtful exploration of issues of importance to Americans."
|
||||
__author__ = 'Walt Anthony'
|
||||
publisher = 'Thomas Lifson'
|
||||
category = 'news, politics, USA'
|
||||
oldest_article = 7 #days
|
||||
max_articles_per_feed = 50
|
||||
summary_length = 150
|
||||
language = 'en'
|
||||
|
||||
remove_javascript = True
|
||||
no_stylesheets = True
|
||||
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
, 'linearize_tables' : True
|
||||
}
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['table', 'iframe', 'embed', 'object'])
|
||||
]
|
||||
|
||||
remove_tags_after = dict(name='div', attrs={'class':'article_body'})
|
||||
|
||||
|
||||
feeds = [(u'http://feeds.feedburner.com/americanthinker'),
|
||||
(u'http://feeds.feedburner.com/AmericanThinkerBlog')
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
return 'http://www.americanthinker.com/printpage/?url=' + url
|
@ -50,6 +50,7 @@ class Newsweek(BasicNewsRecipe):
|
||||
'articlecontent','photoBox', 'article columnist first']}, ]
|
||||
recursions = 1
|
||||
match_regexps = [r'http://www.newsweek.com/id/\S+/page/\d+']
|
||||
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
|
||||
|
||||
def find_title(self, section):
|
||||
d = {'scope':'Scope', 'thetake':'The Take', 'features':'Features',
|
||||
|
@ -451,6 +451,20 @@ def prepare_string_for_xml(raw, attribute=False):
|
||||
def isbytestring(obj):
|
||||
return isinstance(obj, (str, bytes))
|
||||
|
||||
def human_readable(size):
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
divisor, suffix = 1, "B"
|
||||
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
|
||||
if size < 1024**(i+1):
|
||||
divisor, suffix = 1024**(i), candidate
|
||||
break
|
||||
size = str(float(size)/divisor)
|
||||
if size.find(".") > -1:
|
||||
size = size[:size.find(".")+2]
|
||||
if size.endswith('.0'):
|
||||
size = size[:-2]
|
||||
return size + " " + suffix
|
||||
|
||||
if isosx:
|
||||
import glob, shutil
|
||||
fdir = os.path.expanduser('~/.fonts')
|
||||
|
@ -449,7 +449,7 @@ from calibre.devices.eslick.driver import ESLICK
|
||||
from calibre.devices.nuut2.driver import NUUT2
|
||||
from calibre.devices.iriver.driver import IRIVER_STORY
|
||||
from calibre.devices.binatone.driver import README
|
||||
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA
|
||||
from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
||||
from calibre.devices.edge.driver import EDGE
|
||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS
|
||||
from calibre.devices.sne.driver import SNE
|
||||
@ -528,6 +528,7 @@ plugins += [
|
||||
EB600,
|
||||
README,
|
||||
N516,
|
||||
THEBOOK,
|
||||
EB511,
|
||||
ELONEX,
|
||||
TECLAST_K3,
|
||||
|
@ -24,7 +24,7 @@ class N516(USBMS):
|
||||
|
||||
VENDOR_ID = [0x0525]
|
||||
PRODUCT_ID = [0xa4a5]
|
||||
BCD = [0x323, 0x326, 0x399]
|
||||
BCD = [0x323, 0x326]
|
||||
|
||||
VENDOR_NAME = 'INGENIC'
|
||||
WINDOWS_MAIN_MEM = '_FILE-STOR_GADGE'
|
||||
@ -34,6 +34,16 @@ class N516(USBMS):
|
||||
EBOOK_DIR_MAIN = 'e_book'
|
||||
SUPPORTS_SUB_DIRS = True
|
||||
|
||||
class THEBOOK(N516):
|
||||
name = 'The Book driver'
|
||||
gui_name = 'The Book'
|
||||
description = _('Communicate with The Book reader.')
|
||||
author = 'Kovid Goyal'
|
||||
|
||||
BCD = [0x399]
|
||||
MAIN_MEMORY_VOLUME_LABEL = 'The Book Main Memory'
|
||||
EBOOK_DIR_MAIN = 'My books'
|
||||
|
||||
class ALEX(N516):
|
||||
|
||||
name = 'Alex driver'
|
||||
|
@ -17,7 +17,7 @@ class PALMPRE(USBMS):
|
||||
supported_platforms = ['windows', 'osx', 'linux']
|
||||
|
||||
# Ordered list of supported formats
|
||||
FORMATS = ['mobi', 'prc', 'pdb', 'txt']
|
||||
FORMATS = ['epub', 'mobi', 'prc', 'pdb', 'txt']
|
||||
|
||||
VENDOR_ID = [0x0830]
|
||||
PRODUCT_ID = [0x8004, 0x8002, 0x0101]
|
||||
|
@ -89,8 +89,11 @@ CALIBRE_METADATA_FIELDS = frozenset([
|
||||
)
|
||||
|
||||
CALIBRE_RESERVED_LABELS = frozenset([
|
||||
# reserved for saved searches
|
||||
'search',
|
||||
'search', # reserved for saved searches
|
||||
'date',
|
||||
'all',
|
||||
'ondevice',
|
||||
'inlibrary',
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -1,538 +0,0 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
'''Read/Write metadata from Open Packaging Format (.opf) files.'''
|
||||
|
||||
import re, os
|
||||
import uuid
|
||||
from urllib import unquote, quote
|
||||
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup, BeautifulSoup
|
||||
from calibre.ebooks.lrf import entity_to_unicode
|
||||
from calibre.ebooks.metadata import Resource, ResourceCollection
|
||||
from calibre.ebooks.metadata.toc import TOC
|
||||
|
||||
class OPFSoup(BeautifulStoneSoup):
|
||||
|
||||
def __init__(self, raw):
|
||||
BeautifulStoneSoup.__init__(self, raw,
|
||||
convertEntities=BeautifulSoup.HTML_ENTITIES,
|
||||
selfClosingTags=['item', 'itemref', 'reference'])
|
||||
|
||||
class ManifestItem(Resource):
|
||||
|
||||
@staticmethod
|
||||
def from_opf_manifest_item(item, basedir):
|
||||
if item.has_key('href'):
|
||||
href = item['href']
|
||||
if unquote(href) == href:
|
||||
try:
|
||||
href = quote(href)
|
||||
except KeyError:
|
||||
pass
|
||||
res = ManifestItem(href, basedir=basedir, is_path=False)
|
||||
mt = item.get('media-type', '').strip()
|
||||
if mt:
|
||||
res.mime_type = mt
|
||||
return res
|
||||
|
||||
@dynamic_property
|
||||
def media_type(self):
|
||||
def fget(self):
|
||||
return self.mime_type
|
||||
def fset(self, val):
|
||||
self.mime_type = val
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
|
||||
def __unicode__(self):
|
||||
return u'<item id="%s" href="%s" media-type="%s" />'%(self.id, self.href(), self.media_type)
|
||||
|
||||
def __str__(self):
|
||||
return unicode(self).encode('utf-8')
|
||||
|
||||
def __repr__(self):
|
||||
return unicode(self)
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
if index == 0:
|
||||
return self.href()
|
||||
if index == 1:
|
||||
return self.media_type
|
||||
raise IndexError('%d out of bounds.'%index)
|
||||
|
||||
|
||||
class Manifest(ResourceCollection):
|
||||
|
||||
@staticmethod
|
||||
def from_opf_manifest_element(manifest, dir):
|
||||
m = Manifest()
|
||||
for item in manifest.findAll(re.compile('item')):
|
||||
try:
|
||||
m.append(ManifestItem.from_opf_manifest_item(item, dir))
|
||||
id = item.get('id', '')
|
||||
if not id:
|
||||
id = 'id%d'%m.next_id
|
||||
m[-1].id = id
|
||||
m.next_id += 1
|
||||
except ValueError:
|
||||
continue
|
||||
return m
|
||||
|
||||
@staticmethod
|
||||
def from_paths(entries):
|
||||
'''
|
||||
`entries`: List of (path, mime-type) If mime-type is None it is autodetected
|
||||
'''
|
||||
m = Manifest()
|
||||
for path, mt in entries:
|
||||
mi = ManifestItem(path, is_path=True)
|
||||
if mt:
|
||||
mi.mime_type = mt
|
||||
mi.id = 'id%d'%m.next_id
|
||||
m.next_id += 1
|
||||
m.append(mi)
|
||||
return m
|
||||
|
||||
def __init__(self):
|
||||
ResourceCollection.__init__(self)
|
||||
self.next_id = 1
|
||||
|
||||
|
||||
def item(self, id):
|
||||
for i in self:
|
||||
if i.id == id:
|
||||
return i
|
||||
|
||||
def id_for_path(self, path):
|
||||
path = os.path.normpath(os.path.abspath(path))
|
||||
for i in self:
|
||||
if i.path and os.path.normpath(i.path) == path:
|
||||
return i.id
|
||||
|
||||
def path_for_id(self, id):
|
||||
for i in self:
|
||||
if i.id == id:
|
||||
return i.path
|
||||
|
||||
class Spine(ResourceCollection):
|
||||
|
||||
class Item(Resource):
|
||||
|
||||
def __init__(self, idfunc, *args, **kwargs):
|
||||
Resource.__init__(self, *args, **kwargs)
|
||||
self.is_linear = True
|
||||
self.id = idfunc(self.path)
|
||||
|
||||
@staticmethod
|
||||
def from_opf_spine_element(spine, manifest):
|
||||
s = Spine(manifest)
|
||||
for itemref in spine.findAll(re.compile('itemref')):
|
||||
if itemref.has_key('idref'):
|
||||
r = Spine.Item(s.manifest.id_for_path,
|
||||
s.manifest.path_for_id(itemref['idref']), is_path=True)
|
||||
r.is_linear = itemref.get('linear', 'yes') == 'yes'
|
||||
s.append(r)
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def from_paths(paths, manifest):
|
||||
s = Spine(manifest)
|
||||
for path in paths:
|
||||
try:
|
||||
s.append(Spine.Item(s.manifest.id_for_path, path, is_path=True))
|
||||
except:
|
||||
continue
|
||||
return s
|
||||
|
||||
|
||||
|
||||
def __init__(self, manifest):
|
||||
ResourceCollection.__init__(self)
|
||||
self.manifest = manifest
|
||||
|
||||
|
||||
def linear_items(self):
|
||||
for r in self:
|
||||
if r.is_linear:
|
||||
yield r.path
|
||||
|
||||
def nonlinear_items(self):
|
||||
for r in self:
|
||||
if not r.is_linear:
|
||||
yield r.path
|
||||
|
||||
def items(self):
|
||||
for i in self:
|
||||
yield i.path
|
||||
|
||||
|
||||
class Guide(ResourceCollection):
|
||||
|
||||
class Reference(Resource):
|
||||
|
||||
@staticmethod
|
||||
def from_opf_resource_item(ref, basedir):
|
||||
title, href, type = ref.get('title', ''), ref['href'], ref['type']
|
||||
res = Guide.Reference(href, basedir, is_path=False)
|
||||
res.title = title
|
||||
res.type = type
|
||||
return res
|
||||
|
||||
def __repr__(self):
|
||||
ans = '<reference type="%s" href="%s" '%(self.type, self.href())
|
||||
if self.title:
|
||||
ans += 'title="%s" '%self.title
|
||||
return ans + '/>'
|
||||
|
||||
|
||||
@staticmethod
|
||||
def from_opf_guide(guide_elem, base_dir=os.getcwdu()):
|
||||
coll = Guide()
|
||||
for ref in guide_elem.findAll('reference'):
|
||||
try:
|
||||
ref = Guide.Reference.from_opf_resource_item(ref, base_dir)
|
||||
coll.append(ref)
|
||||
except:
|
||||
continue
|
||||
return coll
|
||||
|
||||
def set_cover(self, path):
|
||||
map(self.remove, [i for i in self if 'cover' in i.type.lower()])
|
||||
for type in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
|
||||
self.append(Guide.Reference(path, is_path=True))
|
||||
self[-1].type = type
|
||||
self[-1].title = ''
|
||||
|
||||
|
||||
class standard_field(object):
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, typ=None):
|
||||
return getattr(obj, 'get_'+self.name)()
|
||||
|
||||
|
||||
class OPF(MetaInformation):
|
||||
|
||||
MIMETYPE = 'application/oebps-package+xml'
|
||||
ENTITY_PATTERN = re.compile(r'&(\S+?);')
|
||||
|
||||
uid = standard_field('uid')
|
||||
application_id = standard_field('application_id')
|
||||
title = standard_field('title')
|
||||
authors = standard_field('authors')
|
||||
language = standard_field('language')
|
||||
title_sort = standard_field('title_sort')
|
||||
author_sort = standard_field('author_sort')
|
||||
comments = standard_field('comments')
|
||||
category = standard_field('category')
|
||||
publisher = standard_field('publisher')
|
||||
isbn = standard_field('isbn')
|
||||
cover = standard_field('cover')
|
||||
series = standard_field('series')
|
||||
series_index = standard_field('series_index')
|
||||
rating = standard_field('rating')
|
||||
tags = standard_field('tags')
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError('Abstract base class')
|
||||
|
||||
@dynamic_property
|
||||
def package(self):
|
||||
def fget(self):
|
||||
return self.soup.find(re.compile('package'))
|
||||
return property(fget=fget)
|
||||
|
||||
@dynamic_property
|
||||
def metadata(self):
|
||||
def fget(self):
|
||||
return self.package.find(re.compile('metadata'))
|
||||
return property(fget=fget)
|
||||
|
||||
|
||||
def get_title(self):
|
||||
title = self.metadata.find('dc:title')
|
||||
if title and title.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, title.string).strip()
|
||||
return self.default_title.strip()
|
||||
|
||||
def get_authors(self):
|
||||
creators = self.metadata.findAll('dc:creator')
|
||||
for elem in creators:
|
||||
role = elem.get('role')
|
||||
if not role:
|
||||
role = elem.get('opf:role')
|
||||
if not role:
|
||||
role = 'aut'
|
||||
if role == 'aut' and elem.string:
|
||||
raw = self.ENTITY_PATTERN.sub(entity_to_unicode, elem.string)
|
||||
return string_to_authors(raw)
|
||||
return []
|
||||
|
||||
def get_author_sort(self):
|
||||
creators = self.metadata.findAll('dc:creator')
|
||||
for elem in creators:
|
||||
role = elem.get('role')
|
||||
if not role:
|
||||
role = elem.get('opf:role')
|
||||
if role == 'aut':
|
||||
fa = elem.get('file-as')
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, fa).strip() if fa else None
|
||||
return None
|
||||
|
||||
def get_title_sort(self):
|
||||
title = self.package.find('dc:title')
|
||||
if title:
|
||||
if title.has_key('file-as'):
|
||||
return title['file-as'].strip()
|
||||
return None
|
||||
|
||||
def get_comments(self):
|
||||
comments = self.soup.find('dc:description')
|
||||
if comments and comments.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, comments.string).strip()
|
||||
return None
|
||||
|
||||
def get_uid(self):
|
||||
package = self.package
|
||||
if package.has_key('unique-identifier'):
|
||||
return package['unique-identifier']
|
||||
|
||||
def get_category(self):
|
||||
category = self.soup.find('dc:type')
|
||||
if category and category.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, category.string).strip()
|
||||
return None
|
||||
|
||||
def get_publisher(self):
|
||||
publisher = self.soup.find('dc:publisher')
|
||||
if publisher and publisher.string:
|
||||
return self.ENTITY_PATTERN.sub(entity_to_unicode, publisher.string).strip()
|
||||
return None
|
||||
|
||||
def get_isbn(self):
|
||||
for item in self.metadata.findAll('dc:identifier'):
|
||||
scheme = item.get('scheme')
|
||||
if not scheme:
|
||||
scheme = item.get('opf:scheme')
|
||||
if scheme is not None and scheme.lower() == 'isbn' and item.string:
|
||||
return str(item.string).strip()
|
||||
return None
|
||||
|
||||
def get_language(self):
|
||||
item = self.metadata.find('dc:language')
|
||||
if not item:
|
||||
return _('Unknown')
|
||||
return ''.join(item.findAll(text=True)).strip()
|
||||
|
||||
def get_application_id(self):
|
||||
for item in self.metadata.findAll('dc:identifier'):
|
||||
scheme = item.get('scheme', None)
|
||||
if scheme is None:
|
||||
scheme = item.get('opf:scheme', None)
|
||||
if scheme in ['libprs500', 'calibre']:
|
||||
return str(item.string).strip()
|
||||
return None
|
||||
|
||||
def get_cover(self):
|
||||
guide = getattr(self, 'guide', [])
|
||||
if not guide:
|
||||
guide = []
|
||||
references = [ref for ref in guide if 'cover' in ref.type.lower()]
|
||||
for candidate in ('cover', 'other.ms-coverimage-standard', 'other.ms-coverimage'):
|
||||
matches = [r for r in references if r.type.lower() == candidate and r.path]
|
||||
if matches:
|
||||
return matches[0].path
|
||||
|
||||
def possible_cover_prefixes(self):
|
||||
isbn, ans = [], []
|
||||
for item in self.metadata.findAll('dc:identifier'):
|
||||
scheme = item.get('scheme')
|
||||
if not scheme:
|
||||
scheme = item.get('opf:scheme')
|
||||
isbn.append((scheme, item.string))
|
||||
for item in isbn:
|
||||
ans.append(item[1].replace('-', ''))
|
||||
return ans
|
||||
|
||||
def get_series(self):
|
||||
s = self.metadata.find('series')
|
||||
if s is not None:
|
||||
return str(s.string).strip()
|
||||
return None
|
||||
|
||||
def get_series_index(self):
|
||||
s = self.metadata.find('series-index')
|
||||
if s and s.string:
|
||||
try:
|
||||
return float(str(s.string).strip())
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_rating(self):
|
||||
s = self.metadata.find('rating')
|
||||
if s and s.string:
|
||||
try:
|
||||
return int(str(s.string).strip())
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
def get_tags(self):
|
||||
ans = []
|
||||
subs = self.soup.findAll('dc:subject')
|
||||
for sub in subs:
|
||||
val = sub.string
|
||||
if val:
|
||||
ans.append(val)
|
||||
return [unicode(a).strip() for a in ans]
|
||||
|
||||
|
||||
class OPFReader(OPF):
|
||||
|
||||
def __init__(self, stream, dir=os.getcwdu()):
|
||||
manage = False
|
||||
if not hasattr(stream, 'read'):
|
||||
manage = True
|
||||
dir = os.path.dirname(stream)
|
||||
stream = open(stream, 'rb')
|
||||
self.default_title = stream.name if hasattr(stream, 'name') else 'Unknown'
|
||||
if hasattr(stream, 'seek'):
|
||||
stream.seek(0)
|
||||
self.soup = OPFSoup(stream.read())
|
||||
if manage:
|
||||
stream.close()
|
||||
self.manifest = Manifest()
|
||||
m = self.soup.find(re.compile('manifest'))
|
||||
if m is not None:
|
||||
self.manifest = Manifest.from_opf_manifest_element(m, dir)
|
||||
self.spine = None
|
||||
spine = self.soup.find(re.compile('spine'))
|
||||
if spine is not None:
|
||||
self.spine = Spine.from_opf_spine_element(spine, self.manifest)
|
||||
|
||||
self.toc = TOC(base_path=dir)
|
||||
self.toc.read_from_opf(self)
|
||||
guide = self.soup.find(re.compile('guide'))
|
||||
if guide is not None:
|
||||
self.guide = Guide.from_opf_guide(guide, dir)
|
||||
self.base_dir = dir
|
||||
self.cover_data = (None, None)
|
||||
|
||||
|
||||
class OPFCreator(MetaInformation):
|
||||
|
||||
def __init__(self, base_path, *args, **kwargs):
|
||||
'''
|
||||
Initialize.
|
||||
@param base_path: An absolute path to the directory in which this OPF file
|
||||
will eventually be. This is used by the L{create_manifest} method
|
||||
to convert paths to files into relative paths.
|
||||
'''
|
||||
MetaInformation.__init__(self, *args, **kwargs)
|
||||
self.base_path = os.path.abspath(base_path)
|
||||
if self.application_id is None:
|
||||
self.application_id = str(uuid.uuid4())
|
||||
if not isinstance(self.toc, TOC):
|
||||
self.toc = None
|
||||
if not self.authors:
|
||||
self.authors = [_('Unknown')]
|
||||
if self.guide is None:
|
||||
self.guide = Guide()
|
||||
if self.cover:
|
||||
self.guide.set_cover(self.cover)
|
||||
|
||||
|
||||
def create_manifest(self, entries):
|
||||
'''
|
||||
Create <manifest>
|
||||
|
||||
`entries`: List of (path, mime-type) If mime-type is None it is autodetected
|
||||
'''
|
||||
entries = map(lambda x: x if os.path.isabs(x[0]) else
|
||||
(os.path.abspath(os.path.join(self.base_path, x[0])), x[1]),
|
||||
entries)
|
||||
self.manifest = Manifest.from_paths(entries)
|
||||
self.manifest.set_basedir(self.base_path)
|
||||
|
||||
def create_manifest_from_files_in(self, files_and_dirs):
|
||||
entries = []
|
||||
|
||||
def dodir(dir):
|
||||
for spec in os.walk(dir):
|
||||
root, files = spec[0], spec[-1]
|
||||
for name in files:
|
||||
path = os.path.join(root, name)
|
||||
if os.path.isfile(path):
|
||||
entries.append((path, None))
|
||||
|
||||
for i in files_and_dirs:
|
||||
if os.path.isdir(i):
|
||||
dodir(i)
|
||||
else:
|
||||
entries.append((i, None))
|
||||
|
||||
self.create_manifest(entries)
|
||||
|
||||
def create_spine(self, entries):
|
||||
'''
|
||||
Create the <spine> element. Must first call :method:`create_manifest`.
|
||||
|
||||
`entries`: List of paths
|
||||
'''
|
||||
entries = map(lambda x: x if os.path.isabs(x) else
|
||||
os.path.abspath(os.path.join(self.base_path, x)), entries)
|
||||
self.spine = Spine.from_paths(entries, self.manifest)
|
||||
|
||||
def set_toc(self, toc):
|
||||
'''
|
||||
Set the toc. You must call :method:`create_spine` before calling this
|
||||
method.
|
||||
|
||||
:param toc: A :class:`TOC` object
|
||||
'''
|
||||
self.toc = toc
|
||||
|
||||
def create_guide(self, guide_element):
|
||||
self.guide = Guide.from_opf_guide(guide_element, self.base_path)
|
||||
self.guide.set_basedir(self.base_path)
|
||||
|
||||
def render(self, opf_stream, ncx_stream=None, ncx_manifest_entry=None):
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
opf_template = open(P('templates/opf.xml'), 'rb').read()
|
||||
template = MarkupTemplate(opf_template)
|
||||
if self.manifest:
|
||||
self.manifest.set_basedir(self.base_path)
|
||||
if ncx_manifest_entry is not None:
|
||||
if not os.path.isabs(ncx_manifest_entry):
|
||||
ncx_manifest_entry = os.path.join(self.base_path, ncx_manifest_entry)
|
||||
remove = [i for i in self.manifest if i.id == 'ncx']
|
||||
for item in remove:
|
||||
self.manifest.remove(item)
|
||||
self.manifest.append(ManifestItem(ncx_manifest_entry, self.base_path))
|
||||
self.manifest[-1].id = 'ncx'
|
||||
self.manifest[-1].mime_type = 'application/x-dtbncx+xml'
|
||||
if not self.guide:
|
||||
self.guide = Guide()
|
||||
if self.cover:
|
||||
cover = self.cover
|
||||
if not os.path.isabs(cover):
|
||||
cover = os.path.abspath(os.path.join(self.base_path, cover))
|
||||
self.guide.set_cover(cover)
|
||||
self.guide.set_basedir(self.base_path)
|
||||
|
||||
opf = template.generate(__appname__=__appname__, mi=self, __version__=__version__).render('xml')
|
||||
if not opf.startswith('<?xml '):
|
||||
opf = '<?xml version="1.0" encoding="UTF-8"?>\n'+opf
|
||||
opf_stream.write(opf)
|
||||
opf_stream.flush()
|
||||
toc = getattr(self, 'toc', None)
|
||||
if toc is not None and ncx_stream is not None:
|
||||
toc.render(ncx_stream, self.application_id)
|
||||
ncx_stream.flush()
|
||||
|
@ -227,19 +227,6 @@ def info_dialog(parent, title, msg, det_msg='', show=False):
|
||||
return d
|
||||
|
||||
|
||||
def human_readable(size):
|
||||
""" Convert a size in bytes into a human readable form """
|
||||
divisor, suffix = 1, "B"
|
||||
for i, candidate in enumerate(('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB')):
|
||||
if size < 1024**(i+1):
|
||||
divisor, suffix = 1024**(i), candidate
|
||||
break
|
||||
size = str(float(size)/divisor)
|
||||
if size.find(".") > -1:
|
||||
size = size[:size.find(".")+2]
|
||||
if size.endswith('.0'):
|
||||
size = size[:-2]
|
||||
return size + " " + suffix
|
||||
|
||||
class Dispatcher(QObject):
|
||||
'''Convenience class to ensure that a function call always happens in the
|
||||
|
@ -650,7 +650,7 @@
|
||||
<normaloff>:/images/merge_books.svg</normaloff>:/images/merge_books.svg</iconset>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Merge books</string>
|
||||
<string>Merge book records</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>M</string>
|
||||
|
@ -13,11 +13,10 @@ from PyQt4.Qt import QListView, QIcon, QFont, QLabel, QListWidget, \
|
||||
QAbstractButton, QPainter, QLineEdit, QComboBox, \
|
||||
QMenu, QStringListModel, QCompleter, QStringList
|
||||
|
||||
from calibre.gui2 import human_readable, NONE, \
|
||||
error_dialog, pixmap_to_data, dynamic
|
||||
from calibre.gui2 import NONE, error_dialog, pixmap_to_data, dynamic
|
||||
|
||||
from calibre.gui2.filename_pattern_ui import Ui_Form
|
||||
from calibre import fit_image
|
||||
from calibre import fit_image, human_readable
|
||||
from calibre.utils.fonts import fontconfig
|
||||
from calibre.ebooks import BOOK_EXTENSIONS
|
||||
from calibre.ebooks.metadata.meta import metadata_from_filename
|
||||
|
@ -100,6 +100,13 @@ class Booq(Device):
|
||||
output_format = 'EPUB'
|
||||
id = 'booq'
|
||||
|
||||
class TheBook(Device):
|
||||
name = 'The Book'
|
||||
manufacturer = 'Augen'
|
||||
output_profile = 'prs505'
|
||||
output_format = 'EPUB'
|
||||
id = 'thebook'
|
||||
|
||||
class Avant(Booq):
|
||||
name = 'Booq Avant'
|
||||
|
||||
|
@ -9,99 +9,18 @@ Command line interface to the calibre database.
|
||||
|
||||
import sys, os, cStringIO
|
||||
from textwrap import TextWrapper
|
||||
from urllib import quote
|
||||
|
||||
from calibre import terminal_controller, preferred_encoding, prints
|
||||
from calibre.utils.config import OptionParser, prefs
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.library.database2 import LibraryDatabase2
|
||||
from calibre.ebooks.metadata.opf2 import OPFCreator, OPF
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
from calibre.utils.date import isoformat
|
||||
|
||||
FIELDS = set(['title', 'authors', 'author_sort', 'publisher', 'rating',
|
||||
'timestamp', 'size', 'tags', 'comments', 'series', 'series_index',
|
||||
'formats', 'isbn', 'uuid', 'pubdate', 'cover'])
|
||||
|
||||
XML_TEMPLATE = '''\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<calibredb xmlns:py="http://genshi.edgewall.org/">
|
||||
<py:for each="record in data">
|
||||
<record>
|
||||
<id>${record['id']}</id>
|
||||
<uuid>${record['uuid']}</uuid>
|
||||
<title>${record['title']}</title>
|
||||
<authors sort="${record['author_sort']}">
|
||||
<py:for each="author in record['authors']">
|
||||
<author>$author</author>
|
||||
</py:for>
|
||||
</authors>
|
||||
<publisher>${record['publisher']}</publisher>
|
||||
<rating>${record['rating']}</rating>
|
||||
<date>${record['timestamp'].isoformat()}</date>
|
||||
<pubdate>${record['pubdate'].isoformat()}</pubdate>
|
||||
<size>${record['size']}</size>
|
||||
<tags py:if="record['tags']">
|
||||
<py:for each="tag in record['tags']">
|
||||
<tag>$tag</tag>
|
||||
</py:for>
|
||||
</tags>
|
||||
<comments>${record['comments']}</comments>
|
||||
<series py:if="record['series']" index="${record['series_index']}">${record['series']}</series>
|
||||
<isbn>${record['isbn']}</isbn>
|
||||
<cover py:if="record['cover']">${record['cover'].replace(os.sep, '/')}</cover>
|
||||
<formats py:if="record['formats']">
|
||||
<py:for each="path in record['formats']">
|
||||
<format>${path.replace(os.sep, '/')}</format>
|
||||
</py:for>
|
||||
</formats>
|
||||
</record>
|
||||
</py:for>
|
||||
</calibredb>
|
||||
'''
|
||||
|
||||
STANZA_TEMPLATE='''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
|
||||
<title>calibre Library</title>
|
||||
<author>
|
||||
<name>calibre</name>
|
||||
<uri>http://calibre-ebook.com</uri>
|
||||
</author>
|
||||
<id>$id</id>
|
||||
<updated>${updated.isoformat()}</updated>
|
||||
<subtitle>
|
||||
${subtitle}
|
||||
</subtitle>
|
||||
<py:for each="record in data">
|
||||
<entry>
|
||||
<title>${record['title']}</title>
|
||||
<id>urn:calibre:${record['uuid']}</id>
|
||||
<author><name>${record['author_sort']}</name></author>
|
||||
<updated>${record['timestamp'].isoformat()}</updated>
|
||||
<link type="application/epub+zip" href="${quote(record['fmt_epub'].replace(sep, '/'))}"/>
|
||||
<link py:if="record['cover']" rel="x-stanza-cover-image" type="image/png" href="${quote(record['cover'].replace(sep, '/'))}"/>
|
||||
<link py:if="record['cover']" rel="x-stanza-cover-image-thumbnail" type="image/png" href="${quote(record['cover'].replace(sep, '/'))}"/>
|
||||
<content type="xhtml">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml">
|
||||
<py:for each="f in ('authors', 'publisher', 'rating', 'tags', 'series', 'isbn')">
|
||||
<py:if test="record[f]">
|
||||
${f.capitalize()}:${unicode(', '.join(record[f]) if f=='tags' else record[f])}
|
||||
<py:if test="f =='series'"># ${str(record['series_index'])}</py:if>
|
||||
<br/>
|
||||
</py:if>
|
||||
</py:for>
|
||||
<py:if test="record['comments']">
|
||||
<br/>
|
||||
${record['comments']}
|
||||
</py:if>
|
||||
</div>
|
||||
</content>
|
||||
</entry>
|
||||
</py:for>
|
||||
</feed>
|
||||
'''
|
||||
|
||||
def send_message(msg=''):
|
||||
prints('Notifying calibre of the change')
|
||||
from calibre.utils.ipc import RC
|
||||
@ -130,81 +49,67 @@ def get_db(dbpath, options):
|
||||
return LibraryDatabase2(dbpath)
|
||||
|
||||
def do_list(db, fields, afields, sort_by, ascending, search_text, line_width, separator,
|
||||
prefix, output_format, subtitle='Books in the calibre database'):
|
||||
prefix, subtitle='Books in the calibre database'):
|
||||
if sort_by:
|
||||
db.sort(sort_by, ascending)
|
||||
if search_text:
|
||||
db.search(search_text)
|
||||
authors_to_string = output_format in ['stanza', 'text']
|
||||
data = db.get_data_as_dict(prefix, authors_as_string=authors_to_string)
|
||||
data = db.get_data_as_dict(prefix, authors_as_string=True)
|
||||
fields = ['id'] + fields
|
||||
title_fields = fields
|
||||
fields = [db.custom_column_label_map[x[1:]]['num'] if x[0]=='*'
|
||||
else x for x in fields]
|
||||
if output_format == 'text':
|
||||
for f in data:
|
||||
fmts = [x for x in f['formats'] if x is not None]
|
||||
f['formats'] = u'[%s]'%u','.join(fmts)
|
||||
widths = list(map(lambda x : 0, fields))
|
||||
for record in data:
|
||||
for f in record.keys():
|
||||
if hasattr(record[f], 'isoformat'):
|
||||
record[f] = isoformat(record[f], as_utc=False)
|
||||
else:
|
||||
record[f] = unicode(record[f])
|
||||
record[f] = record[f].replace('\n', ' ')
|
||||
for i in data:
|
||||
for j, field in enumerate(fields):
|
||||
widths[j] = max(widths[j], len(unicode(i[field])))
|
||||
|
||||
screen_width = terminal_controller.COLS if line_width < 0 else line_width
|
||||
if not screen_width:
|
||||
screen_width = 80
|
||||
field_width = screen_width//len(fields)
|
||||
base_widths = map(lambda x: min(x+1, field_width), widths)
|
||||
for f in data:
|
||||
fmts = [x for x in f['formats'] if x is not None]
|
||||
f['formats'] = u'[%s]'%u','.join(fmts)
|
||||
widths = list(map(lambda x : 0, fields))
|
||||
for record in data:
|
||||
for f in record.keys():
|
||||
if hasattr(record[f], 'isoformat'):
|
||||
record[f] = isoformat(record[f], as_utc=False)
|
||||
else:
|
||||
record[f] = unicode(record[f])
|
||||
record[f] = record[f].replace('\n', ' ')
|
||||
for i in data:
|
||||
for j, field in enumerate(fields):
|
||||
widths[j] = max(widths[j], len(unicode(i[field])))
|
||||
|
||||
while sum(base_widths) < screen_width:
|
||||
adjusted = False
|
||||
for i in range(len(widths)):
|
||||
if base_widths[i] < widths[i]:
|
||||
base_widths[i] += min(screen_width-sum(base_widths), widths[i]-base_widths[i])
|
||||
adjusted = True
|
||||
break
|
||||
if not adjusted:
|
||||
screen_width = terminal_controller.COLS if line_width < 0 else line_width
|
||||
if not screen_width:
|
||||
screen_width = 80
|
||||
field_width = screen_width//len(fields)
|
||||
base_widths = map(lambda x: min(x+1, field_width), widths)
|
||||
|
||||
while sum(base_widths) < screen_width:
|
||||
adjusted = False
|
||||
for i in range(len(widths)):
|
||||
if base_widths[i] < widths[i]:
|
||||
base_widths[i] += min(screen_width-sum(base_widths), widths[i]-base_widths[i])
|
||||
adjusted = True
|
||||
break
|
||||
if not adjusted:
|
||||
break
|
||||
|
||||
widths = list(base_widths)
|
||||
titles = map(lambda x, y: '%-*s%s'%(x-len(separator), y, separator),
|
||||
widths, title_fields)
|
||||
print terminal_controller.GREEN + ''.join(titles)+terminal_controller.NORMAL
|
||||
widths = list(base_widths)
|
||||
titles = map(lambda x, y: '%-*s%s'%(x-len(separator), y, separator),
|
||||
widths, title_fields)
|
||||
print terminal_controller.GREEN + ''.join(titles)+terminal_controller.NORMAL
|
||||
|
||||
wrappers = map(lambda x: TextWrapper(x-1), widths)
|
||||
o = cStringIO.StringIO()
|
||||
wrappers = map(lambda x: TextWrapper(x-1), widths)
|
||||
o = cStringIO.StringIO()
|
||||
|
||||
for record in data:
|
||||
text = [wrappers[i].wrap(unicode(record[field]).encode('utf-8')) for i, field in enumerate(fields)]
|
||||
lines = max(map(len, text))
|
||||
for l in range(lines):
|
||||
for i, field in enumerate(text):
|
||||
ft = text[i][l] if l < len(text[i]) else ''
|
||||
filler = '%*s'%(widths[i]-len(ft)-1, '')
|
||||
o.write(ft)
|
||||
o.write(filler+separator)
|
||||
print >>o
|
||||
return o.getvalue()
|
||||
elif output_format == 'xml':
|
||||
template = MarkupTemplate(XML_TEMPLATE)
|
||||
return template.generate(data=data, os=os).render('xml')
|
||||
elif output_format == 'stanza':
|
||||
data = [i for i in data if i.has_key('fmt_epub')]
|
||||
for x in data:
|
||||
if isinstance(x['fmt_epub'], unicode):
|
||||
x['fmt_epub'] = x['fmt_epub'].encode('utf-8')
|
||||
if isinstance(x['cover'], unicode):
|
||||
x['cover'] = x['cover'].encode('utf-8')
|
||||
template = MarkupTemplate(STANZA_TEMPLATE)
|
||||
return template.generate(id="urn:calibre:main", data=data, subtitle=subtitle,
|
||||
sep=os.sep, quote=quote, updated=db.last_modified()).render('xml')
|
||||
for record in data:
|
||||
text = [wrappers[i].wrap(unicode(record[field]).encode('utf-8')) for i, field in enumerate(fields)]
|
||||
lines = max(map(len, text))
|
||||
for l in range(lines):
|
||||
for i, field in enumerate(text):
|
||||
ft = text[i][l] if l < len(text[i]) else ''
|
||||
filler = '%*s'%(widths[i]-len(ft)-1, '')
|
||||
o.write(ft)
|
||||
o.write(filler+separator)
|
||||
print >>o
|
||||
return o.getvalue()
|
||||
|
||||
def list_option_parser(db=None):
|
||||
fields = set(FIELDS)
|
||||
@ -236,9 +141,6 @@ List the books available in the calibre database.
|
||||
help=_('The maximum width of a single line in the output. Defaults to detecting screen size.'))
|
||||
parser.add_option('--separator', default=' ', help=_('The string used to separate fields. Default is a space.'))
|
||||
parser.add_option('--prefix', default=None, help=_('The prefix for all file paths. Default is the absolute path to the library folder.'))
|
||||
of = ['text', 'xml', 'stanza']
|
||||
parser.add_option('--output-format', choices=of, default='text',
|
||||
help=_('The format in which to output the data. Available choices: %s. Defaults is text.')%of)
|
||||
return parser
|
||||
|
||||
|
||||
@ -272,7 +174,7 @@ def command_list(args, dbpath):
|
||||
return 1
|
||||
|
||||
print do_list(db, fields, afields, opts.sort_by, opts.ascending, opts.search, opts.line_width, opts.separator,
|
||||
opts.prefix, opts.output_format)
|
||||
opts.prefix)
|
||||
return 0
|
||||
|
||||
|
||||
|
@ -14,14 +14,46 @@ import cherrypy
|
||||
from calibre.constants import __appname__, __version__
|
||||
from calibre.utils.date import fromtimestamp
|
||||
from calibre.library.server import listen_on, log_access_file, log_error_file
|
||||
from calibre.library.server.utils import expose
|
||||
from calibre.utils.mdns import publish as publish_zeroconf, \
|
||||
stop_server as stop_zeroconf, get_external_ip
|
||||
from calibre.library.server.content import ContentServer
|
||||
from calibre.library.server.mobile import MobileServer
|
||||
from calibre.library.server.xml import XMLServer
|
||||
from calibre.library.server.opds import OPDSServer
|
||||
from calibre.library.server.cache import Cache
|
||||
|
||||
class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer):
|
||||
|
||||
class DispatchController(object): # {{{
|
||||
|
||||
def __init__(self):
|
||||
self.dispatcher = cherrypy.dispatch.RoutesDispatcher()
|
||||
self.funcs = []
|
||||
self.seen = set([])
|
||||
|
||||
def __call__(self, name, route, func, **kwargs):
|
||||
if name in self.seen:
|
||||
raise NameError('Route name: '+ repr(name) + ' already used')
|
||||
self.seen.add(name)
|
||||
kwargs['action'] = 'f_%d'%len(self.funcs)
|
||||
self.dispatcher.connect(name, route, self, **kwargs)
|
||||
self.funcs.append(expose(func))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if not attr.startswith('f_'):
|
||||
raise AttributeError(attr + ' not found')
|
||||
num = attr.rpartition('_')[-1]
|
||||
try:
|
||||
num = int(num)
|
||||
except:
|
||||
raise AttributeError(attr + ' not found')
|
||||
if num < 0 or num >= len(self.funcs):
|
||||
raise AttributeError(attr + ' not found')
|
||||
return self.funcs[num]
|
||||
|
||||
# }}}
|
||||
|
||||
class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache):
|
||||
|
||||
server_name = __appname__ + '/' + __version__
|
||||
|
||||
@ -88,8 +120,16 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer):
|
||||
|
||||
def start(self):
|
||||
self.is_running = False
|
||||
d = DispatchController()
|
||||
for x in self.__class__.__bases__:
|
||||
if hasattr(x, 'add_routes'):
|
||||
x.add_routes(self, d)
|
||||
root_conf = self.config.get('/', {})
|
||||
root_conf['request.dispatch'] = d.dispatcher
|
||||
self.config['/'] = root_conf
|
||||
|
||||
self.setup_loggers()
|
||||
cherrypy.tree.mount(self, '', config=self.config)
|
||||
cherrypy.tree.mount(root=None, config=self.config)
|
||||
try:
|
||||
try:
|
||||
cherrypy.engine.start()
|
||||
|
18
src/calibre/library/server/cache.py
Normal file
18
src/calibre/library/server/cache.py
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre.utils.date import utcnow
|
||||
|
||||
class Cache(object):
|
||||
|
||||
@property
|
||||
def categories_cache(self):
|
||||
old = getattr(self, '_category_cache', None)
|
||||
if old is None or old[0] <= self.db.last_modified():
|
||||
categories = self.db.get_categories()
|
||||
self._category_cache = (utcnow(), categories)
|
||||
return self._category_cache[1]
|
@ -16,7 +16,7 @@ except ImportError:
|
||||
|
||||
from calibre import fit_image, guess_type
|
||||
from calibre.utils.date import fromtimestamp
|
||||
from calibre.library.server.utils import expose
|
||||
|
||||
|
||||
class ContentServer(object):
|
||||
|
||||
@ -25,6 +25,13 @@ class ContentServer(object):
|
||||
a few utility methods.
|
||||
'''
|
||||
|
||||
def add_routes(self, connect):
|
||||
connect('root', '/', self.index)
|
||||
connect('get', '/get/{what}/{id}', self.get,
|
||||
conditions=dict(method=["GET", "HEAD"]))
|
||||
connect('static', '/static/{name}', self.static,
|
||||
conditions=dict(method=["GET", "HEAD"]))
|
||||
|
||||
# Utility methods {{{
|
||||
def last_modified(self, updated):
|
||||
'''
|
||||
@ -68,8 +75,7 @@ class ContentServer(object):
|
||||
# }}}
|
||||
|
||||
|
||||
@expose
|
||||
def get(self, what, id, *args, **kwargs):
|
||||
def get(self, what, id):
|
||||
'Serves files, covers, thumbnails from the calibre database'
|
||||
try:
|
||||
id = int(id)
|
||||
@ -87,7 +93,6 @@ class ContentServer(object):
|
||||
return self.get_cover(id)
|
||||
return self.get_format(id, what)
|
||||
|
||||
@expose
|
||||
def static(self, name):
|
||||
'Serves static content'
|
||||
name = name.lower()
|
||||
@ -108,7 +113,6 @@ class ContentServer(object):
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(lm)
|
||||
return open(path, 'rb').read()
|
||||
|
||||
@expose
|
||||
def index(self, **kwargs):
|
||||
'The / URL'
|
||||
ua = cherrypy.request.headers.get('User-Agent', '').strip()
|
||||
|
@ -5,34 +5,143 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re, copy
|
||||
import re
|
||||
import __builtin__
|
||||
|
||||
import cherrypy
|
||||
from lxml import html
|
||||
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, LINK, DIV, IMG, BODY, \
|
||||
OPTION, SELECT, INPUT, FORM, SPAN, TABLE, TR, TD, A, HR
|
||||
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
from calibre.library.server.utils import strftime, expose
|
||||
from calibre.library.server.utils import strftime
|
||||
from calibre.ebooks.metadata import fmt_sidx
|
||||
from calibre.constants import __appname__
|
||||
from calibre import human_readable
|
||||
|
||||
# Templates {{{
|
||||
MOBILE_BOOK = '''\
|
||||
<tr xmlns:py="http://genshi.edgewall.org/">
|
||||
<td class="thumbnail">
|
||||
<img type="image/jpeg" src="/get/thumb/${r[FM['id']]}" border="0"/>
|
||||
</td>
|
||||
<td>
|
||||
<py:for each="format in r[FM['formats']].split(',')">
|
||||
<span class="button"><a href="/get/${format}/${authors}-${r[FM['title']]}_${r[FM['id']]}.${format}">${format.lower()}</a></span>
|
||||
</py:for>
|
||||
${r[FM['title']]}${(' ['+r[FM['series']]+'-'+r[FM['series_index']]+']') if r[FM['series']] else ''} by ${authors} - ${r[FM['size']]/1024}k - ${r[FM['publisher']] if r[FM['publisher']] else ''} ${pubdate} ${'['+r[FM['tags']]+']' if r[FM['tags']] else ''}
|
||||
</td>
|
||||
</tr>
|
||||
'''
|
||||
def CLASS(*args, **kwargs): # class is a reserved word in Python
|
||||
kwargs['class'] = ' '.join(args)
|
||||
return kwargs
|
||||
|
||||
MOBILE = MarkupTemplate('''\
|
||||
<html xmlns:py="http://genshi.edgewall.org/">
|
||||
<head>
|
||||
<style>
|
||||
|
||||
def build_search_box(num, search, sort, order): # {{{
|
||||
div = DIV(id='search_box')
|
||||
form = FORM('Show ', method='get', action='mobile')
|
||||
div.append(form)
|
||||
|
||||
num_select = SELECT(name='num')
|
||||
for option in (5, 10, 25, 100):
|
||||
kwargs = {'value':str(option)}
|
||||
if option == num:
|
||||
kwargs['SELECTED'] = 'SELECTED'
|
||||
num_select.append(OPTION(str(option), **kwargs))
|
||||
num_select.tail = ' books matching '
|
||||
form.append(num_select)
|
||||
|
||||
searchf = INPUT(name='search', id='s', value=search if search else '')
|
||||
searchf.tail = ' sorted by '
|
||||
form.append(searchf)
|
||||
|
||||
sort_select = SELECT(name='sort')
|
||||
for option in ('date','author','title','rating','size','tags','series'):
|
||||
kwargs = {'value':option}
|
||||
if option == sort:
|
||||
kwargs['SELECTED'] = 'SELECTED'
|
||||
sort_select.append(OPTION(option, **kwargs))
|
||||
form.append(sort_select)
|
||||
|
||||
order_select = SELECT(name='order')
|
||||
for option in ('ascending','descending'):
|
||||
kwargs = {'value':option}
|
||||
if option == order:
|
||||
kwargs['SELECTED'] = 'SELECTED'
|
||||
order_select.append(OPTION(option, **kwargs))
|
||||
form.append(order_select)
|
||||
|
||||
form.append(INPUT(id='go', type='submit', value='Search'))
|
||||
|
||||
return div
|
||||
# }}}
|
||||
|
||||
def build_navigation(start, num, total, url_base): # {{{
|
||||
end = min((start+num-1), total)
|
||||
tagline = SPAN('Books %d to %d of %d'%(start, end, total),
|
||||
style='display: block; text-align: center;')
|
||||
left_buttons = TD(CLASS('button', style='text-align:left'))
|
||||
right_buttons = TD(CLASS('button', style='text-align:right'))
|
||||
|
||||
if start > 1:
|
||||
for t,s in [('First', 1), ('Previous', max(start-(num+1),1))]:
|
||||
left_buttons.append(A(t, href='%s;start=%d'%(url_base, s)))
|
||||
|
||||
if total > start + num:
|
||||
for t,s in [('Next', start+num), ('Last', total-num+1)]:
|
||||
right_buttons.append(A(t, href='%s;start=%d'%(url_base, s)))
|
||||
|
||||
buttons = TABLE(
|
||||
TR(left_buttons, right_buttons),
|
||||
CLASS('buttons'))
|
||||
return DIV(tagline, buttons, CLASS('navigation'))
|
||||
|
||||
# }}}
|
||||
|
||||
def build_index(books, num, search, sort, order, start, total, url_base):
|
||||
logo = DIV(IMG(src='/static/calibre.png', alt=__appname__), id='logo')
|
||||
|
||||
search_box = build_search_box(num, search, sort, order)
|
||||
navigation = build_navigation(start, num, total, url_base)
|
||||
bookt = TABLE(id='listing')
|
||||
|
||||
body = BODY(
|
||||
logo,
|
||||
search_box,
|
||||
navigation,
|
||||
HR(CLASS('spacer')),
|
||||
bookt
|
||||
)
|
||||
|
||||
# Book list {{{
|
||||
for book in books:
|
||||
thumbnail = TD(
|
||||
IMG(type='image/jpeg', border='0', src='/get/thumb/%s' %
|
||||
book['id']),
|
||||
CLASS('thumbnail'))
|
||||
|
||||
data = TD()
|
||||
last = None
|
||||
for fmt in book['formats'].split(','):
|
||||
s = SPAN(
|
||||
A(
|
||||
fmt.lower(),
|
||||
href='/get/%s/%s-%s_%d.%s' % (fmt, book['authors'],
|
||||
book['title'], book['id'], fmt)
|
||||
),
|
||||
CLASS('button'))
|
||||
s.tail = u'\u202f' #
|
||||
last = s
|
||||
data.append(s)
|
||||
|
||||
series = u'[%s - %s]'%(book['series'], book['series_index']) \
|
||||
if book['series'] else ''
|
||||
tags = u'[%s]'%book['tags'] if book['tags'] else ''
|
||||
|
||||
text = u'\u202f%s %s by %s - %s - %s %s' % (book['title'], series,
|
||||
book['authors'], book['size'], book['timestamp'], tags)
|
||||
|
||||
if last is None:
|
||||
data.text = text
|
||||
else:
|
||||
last.tail += text
|
||||
|
||||
bookt.append(TR(thumbnail, data))
|
||||
# }}}
|
||||
|
||||
return HTML(
|
||||
HEAD(
|
||||
TITLE(__appname__ + ' Library'),
|
||||
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
|
||||
type='image/x-icon'),
|
||||
STYLE( # {{{
|
||||
'''
|
||||
.navigation table.buttons {
|
||||
width: 100%;
|
||||
}
|
||||
@ -109,71 +218,20 @@ div.navigation {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
</style>
|
||||
<link rel="icon" href="http://calibre-ebook.com/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="logo">
|
||||
<img src="/static/calibre.png" alt="Calibre" />
|
||||
</div>
|
||||
<div id="search_box">
|
||||
<form method="get" action="/mobile">
|
||||
Show <select name="num">
|
||||
<py:for each="option in [5,10,25,100]">
|
||||
<option py:if="option == num" value="${option}" SELECTED="SELECTED">${option}</option>
|
||||
<option py:if="option != num" value="${option}">${option}</option>
|
||||
</py:for>
|
||||
</select>
|
||||
books matching <input name="search" id="s" value="${search}" /> sorted by
|
||||
''', type='text/css') # }}}
|
||||
), # End head
|
||||
body
|
||||
) # End html
|
||||
|
||||
<select name="sort">
|
||||
<py:for each="option in ['date','author','title','rating','size','tags','series']">
|
||||
<option py:if="option == sort" value="${option}" SELECTED="SELECTED">${option}</option>
|
||||
<option py:if="option != sort" value="${option}">${option}</option>
|
||||
</py:for>
|
||||
</select>
|
||||
<select name="order">
|
||||
<py:for each="option in ['ascending','descending']">
|
||||
<option py:if="option == order" value="${option}" SELECTED="SELECTED">${option}</option>
|
||||
<option py:if="option != order" value="${option}">${option}</option>
|
||||
</py:for>
|
||||
</select>
|
||||
<input id="go" type="submit" value="Search"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="navigation">
|
||||
<span style="display: block; text-align: center;">Books ${start} to ${ min((start+num-1) , total) } of ${total}</span>
|
||||
<table class="buttons">
|
||||
<tr>
|
||||
<td class="button" style="text-align:left;">
|
||||
<a py:if="start > 1" href="${url_base};start=1">First</a>
|
||||
<a py:if="start > 1" href="${url_base};start=${max(start-(num+1),1)}">Previous</a>
|
||||
</td>
|
||||
<td class="button" style="text-align: right;">
|
||||
<a py:if=" total > (start + num) " href="${url_base};start=${start+num}">Next</a>
|
||||
<a py:if=" total > (start + num) " href="${url_base};start=${total-num+1}">Last</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<hr class="spacer" />
|
||||
<table id="listing">
|
||||
<py:for each="book in books">
|
||||
${Markup(book)}
|
||||
</py:for>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
|
||||
# }}}
|
||||
|
||||
class MobileServer(object):
|
||||
'A view optimized for browsers in mobile devices'
|
||||
|
||||
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian|HD2)')
|
||||
|
||||
@expose
|
||||
def add_routes(self, connect):
|
||||
connect('mobile', '/mobile', self.mobile)
|
||||
|
||||
def mobile(self, start='1', num='25', sort='date', search='',
|
||||
_=None, order='descending'):
|
||||
'''
|
||||
@ -193,26 +251,31 @@ class MobileServer(object):
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
|
||||
ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
|
||||
ids = sorted(ids)
|
||||
FM = self.db.FIELD_MAP
|
||||
items = copy.deepcopy([r for r in iter(self.db) if r[FM['id']] in ids])
|
||||
items = [r for r in iter(self.db) if r[FM['id']] in ids]
|
||||
if sort is not None:
|
||||
self.sort(items, sort, (order.lower().strip() == 'ascending'))
|
||||
|
||||
book, books = MarkupTemplate(MOBILE_BOOK), []
|
||||
books = []
|
||||
for record in items[(start-1):(start-1)+num]:
|
||||
if record[FM['formats']] is None:
|
||||
record[FM['formats']] = ''
|
||||
if record[FM['size']] is None:
|
||||
record[FM['size']] = 0
|
||||
book = {'formats':record[FM['formats']], 'size':record[FM['size']]}
|
||||
if not book['formats']:
|
||||
book['formats'] = ''
|
||||
if not book['size']:
|
||||
book['size'] = 0
|
||||
book['size'] = human_readable(book['size'])
|
||||
|
||||
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
|
||||
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
|
||||
record[FM['series_index']] = \
|
||||
fmt_sidx(float(record[FM['series_index']]))
|
||||
ts, pd = strftime('%Y/%m/%d %H:%M:%S', record[FM['timestamp']]), \
|
||||
strftime('%Y/%m/%d %H:%M:%S', record[FM['pubdate']])
|
||||
books.append(book.generate(r=record, authors=authors, timestamp=ts,
|
||||
pubdate=pd, FM=FM).render('xml').decode('utf-8'))
|
||||
book['authors'] = authors
|
||||
book['series_index'] = fmt_sidx(float(record[FM['series_index']]))
|
||||
book['series'] = record[FM['series']]
|
||||
book['tags'] = record[FM['tags']]
|
||||
book['title'] = record[FM['title']]
|
||||
for x in ('timestamp', 'pubdate'):
|
||||
book[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
|
||||
book['id'] = record[FM['id']]
|
||||
books.append(book)
|
||||
updated = self.db.last_modified()
|
||||
|
||||
cherrypy.response.headers['Content-Type'] = 'text/html; charset=utf-8'
|
||||
@ -221,8 +284,8 @@ class MobileServer(object):
|
||||
|
||||
url_base = "/mobile?search=" + search+";order="+order+";sort="+sort+";num="+str(num)
|
||||
|
||||
return MOBILE.generate(books=books, start=start, updated=updated,
|
||||
search=search, sort=sort, order=order, num=num, FM=FM,
|
||||
total=len(ids), url_base=url_base).render('html')
|
||||
|
||||
return html.tostring(build_index(books, num, search, sort, order,
|
||||
start, len(ids), url_base),
|
||||
encoding='utf-8', include_meta_content_type=True,
|
||||
pretty_print=True)
|
||||
|
||||
|
@ -5,15 +5,102 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
import re, hashlib
|
||||
from itertools import repeat
|
||||
from functools import partial
|
||||
|
||||
import cherrypy
|
||||
from lxml import etree
|
||||
from lxml.builder import ElementMaker
|
||||
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
from calibre.library.server.utils import strftime, expose
|
||||
from calibre.ebooks.metadata import fmt_sidx, title_sort
|
||||
from calibre import guess_type, prepare_string_for_xml
|
||||
from calibre.constants import __appname__
|
||||
|
||||
# Vocabulary for building OPDS feeds {{{
|
||||
E = ElementMaker(namespace='http://www.w3.org/2005/Atom',
|
||||
nsmap={
|
||||
None : 'http://www.w3.org/2005/Atom',
|
||||
'dc' : 'http://purl.org/dc/terms/',
|
||||
'opds' : 'http://opds-spec.org/2010/catalog',
|
||||
})
|
||||
|
||||
|
||||
FEED = E.feed
|
||||
TITLE = E.title
|
||||
ID = E.id
|
||||
|
||||
def UPDATED(dt, *args, **kwargs):
|
||||
return E.updated(dt.strftime('%Y-%m-%dT%H:%M:%S+00:00'), *args, **kwargs)
|
||||
|
||||
LINK = partial(E.link, type='application/atom+xml')
|
||||
NAVLINK = partial(E.link,
|
||||
type='application/atom+xml;type=feed;profile=opds-catalog')
|
||||
|
||||
def SEARCH(base_href, *args, **kwargs):
|
||||
kwargs['rel'] = 'search'
|
||||
kwargs['title'] = 'Search'
|
||||
kwargs['href'] = base_href+'/?search={searchTerms}'
|
||||
return LINK(*args, **kwargs)
|
||||
|
||||
def AUTHOR(name, uri=None):
|
||||
args = [E.name(name)]
|
||||
if uri is not None:
|
||||
args.append(E.uri(uri))
|
||||
return E.author(*args)
|
||||
|
||||
SUBTITLE = E.subtitle
|
||||
|
||||
def NAVCATALOG_ENTRY(base_href, updated, title, description, query_data):
|
||||
data = [u'%s=%s'%(key, val) for key, val in query_data.items()]
|
||||
data = '&'.join(data)
|
||||
href = base_href+'/?'+data
|
||||
id_ = 'calibre-subcatalog:'+str(hashlib.sha1(href).hexdigest())
|
||||
return E.entry(
|
||||
TITLE(title),
|
||||
ID(id_),
|
||||
UPDATED(updated),
|
||||
E.content(description, type='text'),
|
||||
NAVLINK(href=href)
|
||||
)
|
||||
|
||||
# }}}
|
||||
|
||||
class Feed(object):
|
||||
|
||||
def __str__(self):
|
||||
return etree.tostring(self.root, pretty_print=True, encoding='utf-8',
|
||||
xml_declaration=True)
|
||||
|
||||
class TopLevel(Feed):
|
||||
|
||||
def __init__(self,
|
||||
updated, # datetime object in UTC
|
||||
categories,
|
||||
id_ = 'urn:calibre:main',
|
||||
base_href = '/stanza'
|
||||
):
|
||||
self.base_href = base_href
|
||||
subc = partial(NAVCATALOG_ENTRY, base_href, updated)
|
||||
|
||||
subcatalogs = [subc('By '+title,
|
||||
'Books sorted by '+desc, {'sortby':q}) for title, desc, q in
|
||||
categories]
|
||||
|
||||
self.root = \
|
||||
FEED(
|
||||
TITLE(__appname__ + ' ' + _('Library')),
|
||||
ID(id_),
|
||||
UPDATED(updated),
|
||||
SEARCH(base_href),
|
||||
AUTHOR(__appname__, uri='http://calibre-ebook.com'),
|
||||
SUBTITLE(_('Books in your library')),
|
||||
*subcatalogs
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Templates {{{
|
||||
|
||||
@ -42,6 +129,7 @@ STANZA_SUBCATALOG_ENTRY=MarkupTemplate('''\
|
||||
</entry>
|
||||
''')
|
||||
|
||||
# Feed of books
|
||||
STANZA = MarkupTemplate('''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
|
||||
@ -63,62 +151,20 @@ STANZA = MarkupTemplate('''\
|
||||
</feed>
|
||||
''')
|
||||
|
||||
STANZA_MAIN = MarkupTemplate('''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:py="http://genshi.edgewall.org/">
|
||||
<title>calibre Library</title>
|
||||
<id>$id</id>
|
||||
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
||||
<link rel="search" title="Search" type="application/atom+xml" href="/stanza/?search={searchTerms}"/>
|
||||
<author>
|
||||
<name>calibre</name>
|
||||
<uri>http://calibre-ebook.com</uri>
|
||||
</author>
|
||||
<subtitle>
|
||||
${subtitle}
|
||||
</subtitle>
|
||||
<entry>
|
||||
<title>By Author</title>
|
||||
<id>urn:uuid:fc000fa0-8c23-11de-a31d-0002a5d5c51b</id>
|
||||
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
||||
<link type="application/atom+xml" href="/stanza/?sortby=byauthor" />
|
||||
<content type="text">Books sorted by Author</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>By Title</title>
|
||||
<id>urn:uuid:1df4fe40-8c24-11de-b4c6-0002a5d5c51b</id>
|
||||
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
||||
<link type="application/atom+xml" href="/stanza/?sortby=bytitle" />
|
||||
<content type="text">Books sorted by Title</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>By Newest</title>
|
||||
<id>urn:uuid:3c6d4940-8c24-11de-a4d7-0002a5d5c51b</id>
|
||||
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
||||
<link type="application/atom+xml" href="/stanza/?sortby=bynewest" />
|
||||
<content type="text">Books sorted by Date</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>By Tag</title>
|
||||
<id>urn:uuid:824921e8-db8a-4e61-7d38-f1ce41502853</id>
|
||||
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
||||
<link type="application/atom+xml" href="/stanza/?sortby=bytag" />
|
||||
<content type="text">Books sorted by Tags</content>
|
||||
</entry>
|
||||
<entry>
|
||||
<title>By Series</title>
|
||||
<id>urn:uuid:512a5e50-a88f-f6b8-82aa-8f129c719f61</id>
|
||||
<updated>${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}</updated>
|
||||
<link type="application/atom+xml" href="/stanza/?sortby=byseries" />
|
||||
<content type="text">Books sorted by Series</content>
|
||||
</entry>
|
||||
</feed>
|
||||
''')
|
||||
|
||||
# }}}
|
||||
|
||||
class OPDSServer(object):
|
||||
|
||||
def build_top_level(self, updated, base_href='/stanza'):
|
||||
categories = self.categories_cache
|
||||
categories = [(x.capitalize(), x.capitalize(), x) for x in
|
||||
categories.keys()]
|
||||
categories.append(('Title', 'Title', '|title|'))
|
||||
categories.append(('Newest', 'Newest', '|newest|'))
|
||||
|
||||
return TopLevel(updated, categories, base_href=base_href)
|
||||
|
||||
def get_matches(self, location, query):
|
||||
base = self.db.data.get_matches(location, query)
|
||||
epub = self.db.data.get_matches('format', '=epub')
|
||||
@ -173,10 +219,6 @@ class OPDSServer(object):
|
||||
return STANZA.generate(subtitle=subtitle, data=entries, FM=self.db.FIELD_MAP,
|
||||
updated=updated, id='urn:calibre:main', next_link=next_link).render('xml')
|
||||
|
||||
def stanza_main(self, updated):
|
||||
return STANZA_MAIN.generate(subtitle='', data=[], FM=self.db.FIELD_MAP,
|
||||
updated=updated, id='urn:calibre:main').render('xml')
|
||||
|
||||
@expose
|
||||
def stanza(self, search=None, sortby=None, authorid=None, tagid=None,
|
||||
seriesid=None, offset=0):
|
||||
@ -186,9 +228,11 @@ class OPDSServer(object):
|
||||
offset = int(offset)
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
cherrypy.response.headers['Content-Type'] = 'text/xml'
|
||||
# Main feed
|
||||
|
||||
# Top Level feed
|
||||
if not sortby and not search and not authorid and not tagid and not seriesid:
|
||||
return self.stanza_main(updated)
|
||||
return str(self.build_top_level(updated))
|
||||
|
||||
if sortby in ('byseries', 'byauthor', 'bytag'):
|
||||
return self.stanza_sortby_subcategory(updated, sortby, offset)
|
||||
|
||||
@ -296,5 +340,8 @@ class OPDSServer(object):
|
||||
next_link=next_link, updated=updated, id='urn:calibre:main').render('xml')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
from datetime import datetime
|
||||
f = TopLevel(datetime.utcnow())
|
||||
print f
|
||||
|
||||
|
@ -5,20 +5,34 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from calibre import strftime as _strftime
|
||||
import time
|
||||
|
||||
import cherrypy
|
||||
|
||||
from calibre import strftime as _strftime, prints
|
||||
from calibre.utils.date import now as nowf
|
||||
|
||||
|
||||
def expose(func):
|
||||
import cherrypy
|
||||
|
||||
def do(self, *args, **kwargs):
|
||||
def do(*args, **kwargs):
|
||||
self = func.im_self
|
||||
if self.opts.develop:
|
||||
start = time.time()
|
||||
|
||||
dict.update(cherrypy.response.headers, {'Server':self.server_name})
|
||||
if not self.embedded:
|
||||
self.db.check_if_modified()
|
||||
return func(self, *args, **kwargs)
|
||||
ans = func(*args, **kwargs)
|
||||
if self.opts.develop:
|
||||
prints('Function', func.__name__, 'called with args:', args, kwargs)
|
||||
prints('\tTime:', func.__name__, time.time()-start)
|
||||
return ans
|
||||
|
||||
do.__name__ = func.__name__
|
||||
|
||||
return do
|
||||
|
||||
return cherrypy.expose(do)
|
||||
|
||||
def strftime(fmt='%Y/%m/%d %H:%M:%S', dt=None):
|
||||
if not hasattr(dt, 'timetuple'):
|
||||
|
@ -5,52 +5,26 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import copy, __builtin__
|
||||
import __builtin__
|
||||
|
||||
import cherrypy
|
||||
from lxml.builder import ElementMaker
|
||||
from lxml import etree
|
||||
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
from calibre.library.server.utils import strftime, expose
|
||||
from calibre.library.server.utils import strftime
|
||||
from calibre.ebooks.metadata import fmt_sidx
|
||||
from calibre.constants import preferred_encoding
|
||||
from calibre import isbytestring
|
||||
|
||||
# Templates {{{
|
||||
BOOK = '''\
|
||||
<book xmlns:py="http://genshi.edgewall.org/"
|
||||
id="${r[FM['id']]}"
|
||||
title="${r[FM['title']]}"
|
||||
sort="${r[FM['sort']]}"
|
||||
author_sort="${r[FM['author_sort']]}"
|
||||
authors="${authors}"
|
||||
rating="${r[FM['rating']]}"
|
||||
timestamp="${timestamp}"
|
||||
pubdate="${pubdate}"
|
||||
size="${r[FM['size']]}"
|
||||
isbn="${r[FM['isbn']] if r[FM['isbn']] else ''}"
|
||||
formats="${r[FM['formats']] if r[FM['formats']] else ''}"
|
||||
series = "${r[FM['series']] if r[FM['series']] else ''}"
|
||||
series_index="${r[FM['series_index']]}"
|
||||
tags="${r[FM['tags']] if r[FM['tags']] else ''}"
|
||||
publisher="${r[FM['publisher']] if r[FM['publisher']] else ''}">${r[FM['comments']] if r[FM['comments']] else ''}
|
||||
</book>
|
||||
'''
|
||||
|
||||
|
||||
LIBRARY = MarkupTemplate('''\
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<library xmlns:py="http://genshi.edgewall.org/" start="$start" num="${len(books)}" total="$total" updated="${updated.strftime('%Y-%m-%dT%H:%M:%S+00:00')}">
|
||||
<py:for each="book in books">
|
||||
${Markup(book)}
|
||||
</py:for>
|
||||
</library>
|
||||
''')
|
||||
|
||||
# }}}
|
||||
E = ElementMaker()
|
||||
|
||||
class XMLServer(object):
|
||||
'Serves XML and the Ajax based HTML frontend'
|
||||
|
||||
@expose
|
||||
def library(self, start='0', num='50', sort=None, search=None,
|
||||
def add_routes(self, connect):
|
||||
connect('xml', '/xml', self.xml)
|
||||
|
||||
def xml(self, start='0', num='50', sort=None, search=None,
|
||||
_=None, order='ascending'):
|
||||
'''
|
||||
Serves metadata from the calibre database as XML.
|
||||
@ -68,30 +42,63 @@ class XMLServer(object):
|
||||
num = int(num)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(400, 'num: %s is not an integer'%num)
|
||||
|
||||
order = order.lower().strip() == 'ascending'
|
||||
|
||||
ids = self.db.data.parse(search) if search and search.strip() else self.db.data.universal_set()
|
||||
ids = sorted(ids)
|
||||
|
||||
FM = self.db.FIELD_MAP
|
||||
items = copy.deepcopy([r for r in iter(self.db) if r[FM['id']] in ids])
|
||||
|
||||
items = [r for r in iter(self.db) if r[FM['id']] in ids]
|
||||
if sort is not None:
|
||||
self.sort(items, sort, order)
|
||||
|
||||
book, books = MarkupTemplate(BOOK), []
|
||||
|
||||
books = []
|
||||
|
||||
def serialize(x):
|
||||
if isinstance(x, unicode):
|
||||
return x
|
||||
if isbytestring(x):
|
||||
return x.decode(preferred_encoding, 'replace')
|
||||
return unicode(x)
|
||||
|
||||
for record in items[start:start+num]:
|
||||
kwargs = {}
|
||||
aus = record[FM['authors']] if record[FM['authors']] else __builtin__._('Unknown')
|
||||
authors = '|'.join([i.replace('|', ',') for i in aus.split(',')])
|
||||
record[FM['series_index']] = \
|
||||
kwargs['authors'] = authors
|
||||
|
||||
kwargs['series_index'] = \
|
||||
fmt_sidx(float(record[FM['series_index']]))
|
||||
ts, pd = strftime('%Y/%m/%d %H:%M:%S', record[FM['timestamp']]), \
|
||||
strftime('%Y/%m/%d %H:%M:%S', record[FM['pubdate']])
|
||||
books.append(book.generate(r=record, authors=authors, timestamp=ts,
|
||||
pubdate=pd, FM=FM).render('xml').decode('utf-8'))
|
||||
|
||||
for x in ('timestamp', 'pubdate'):
|
||||
kwargs[x] = strftime('%Y/%m/%d %H:%M:%S', record[FM[x]])
|
||||
|
||||
for x in ('id', 'title', 'sort', 'author_sort', 'rating', 'size'):
|
||||
kwargs[x] = serialize(record[FM[x]])
|
||||
|
||||
for x in ('isbn', 'formats', 'series', 'tags', 'publisher',
|
||||
'comments'):
|
||||
y = record[FM[x]]
|
||||
kwargs[x] = serialize(y) if y else ''
|
||||
|
||||
c = kwargs.pop('comments')
|
||||
books.append(E.book(c, **kwargs))
|
||||
|
||||
updated = self.db.last_modified()
|
||||
kwargs = dict(
|
||||
start = str(start),
|
||||
updated=updated.strftime('%Y-%m-%dT%H:%M:%S+00:00'),
|
||||
total=str(len(ids)),
|
||||
num=str(len(books)))
|
||||
ans = E.library(*books, **kwargs)
|
||||
|
||||
cherrypy.response.headers['Content-Type'] = 'text/xml'
|
||||
cherrypy.response.headers['Last-Modified'] = self.last_modified(updated)
|
||||
return LIBRARY.generate(books=books, start=start, updated=updated,
|
||||
total=len(ids), FM=FM).render('xml')
|
||||
|
||||
return etree.tostring(ans, encoding='utf-8', pretty_print=True,
|
||||
xml_declaration=True)
|
||||
|
||||
|
||||
|
||||
|
@ -132,7 +132,6 @@ class PostInstall:
|
||||
self.mime_resources = []
|
||||
if islinux:
|
||||
self.setup_completion()
|
||||
self.setup_udev_rules()
|
||||
self.install_man_pages()
|
||||
if islinux:
|
||||
self.setup_desktop_integration()
|
||||
@ -286,40 +285,6 @@ class PostInstall:
|
||||
raise
|
||||
self.task_failed('Setting up completion failed')
|
||||
|
||||
def setup_udev_rules(self):
|
||||
self.info('Trying to setup udev rules...')
|
||||
try:
|
||||
group_file = os.path.join(self.opts.staging_etc, 'group')
|
||||
if not os.path.exists(group_file):
|
||||
group_file = '/etc/group'
|
||||
groups = open(group_file, 'rb').read()
|
||||
group = 'plugdev' if 'plugdev' in groups else 'usb'
|
||||
old_udev = '/etc/udev/rules.d/95-calibre.rules'
|
||||
if not os.path.exists(old_udev):
|
||||
old_udev = os.path.join(self.opts.staging_etc, 'udev/rules.d/95-calibre.rules')
|
||||
if os.path.exists(old_udev):
|
||||
try:
|
||||
os.remove(old_udev)
|
||||
except:
|
||||
self.warn('Old udev rules found, please delete manually:',
|
||||
old_udev)
|
||||
if self.opts.staging_root == '/usr':
|
||||
base = '/lib'
|
||||
else:
|
||||
base = os.path.join(self.opts.staging_root, 'lib')
|
||||
base = os.path.join(base, 'udev', 'rules.d')
|
||||
if not os.path.exists(base):
|
||||
os.makedirs(base)
|
||||
with open(os.path.join(base, '95-calibre.rules'), 'wb') as udev:
|
||||
self.manifest.append(udev.name)
|
||||
udev.write('''# Sony Reader PRS-500\n'''
|
||||
'''SUBSYSTEMS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="%s"\n'''%(group,)
|
||||
)
|
||||
except:
|
||||
if self.opts.fatal_errors:
|
||||
raise
|
||||
self.task_failed('Setting up udev rules failed')
|
||||
|
||||
def install_man_pages(self):
|
||||
try:
|
||||
from calibre.utils.help2man import create_man_page
|
||||
|
@ -1,100 +1,114 @@
|
||||
from UserDict import DictMixin
|
||||
#!/usr/bin/env python
|
||||
|
||||
class OrderedDict(dict, DictMixin):
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__end
|
||||
except AttributeError:
|
||||
self.clear()
|
||||
self.update(*args, **kwds)
|
||||
'''
|
||||
A ordered dictionary. Use the builtin type on python >= 2.7
|
||||
'''
|
||||
|
||||
def clear(self):
|
||||
self.__end = end = []
|
||||
end += [None, end, end] # sentinel node for doubly linked list
|
||||
self.__map = {} # key --> [key, prev, next]
|
||||
dict.clear(self)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
try:
|
||||
from collections import OrderedDict
|
||||
OrderedDict
|
||||
except ImportError:
|
||||
from UserDict import DictMixin
|
||||
|
||||
class OrderedDict(dict, DictMixin):
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
if len(args) > 1:
|
||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||
try:
|
||||
self.__end
|
||||
except AttributeError:
|
||||
self.clear()
|
||||
self.update(*args, **kwds)
|
||||
|
||||
def clear(self):
|
||||
self.__end = end = []
|
||||
end += [None, end, end] # sentinel node for doubly linked list
|
||||
self.__map = {} # key --> [key, prev, next]
|
||||
dict.clear(self)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if key not in self:
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||
dict.__setitem__(self, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
key, prev, next = self.__map.pop(key)
|
||||
prev[2] = next
|
||||
next[1] = prev
|
||||
|
||||
def __iter__(self):
|
||||
end = self.__end
|
||||
curr = end[2]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[2]
|
||||
|
||||
def __reversed__(self):
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||
dict.__setitem__(self, key, value)
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[1]
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
key, prev, next = self.__map.pop(key)
|
||||
prev[2] = next
|
||||
next[1] = prev
|
||||
def popitem(self, last=True):
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
if last:
|
||||
key = reversed(self).next()
|
||||
else:
|
||||
key = iter(self).next()
|
||||
value = self.pop(key)
|
||||
return key, value
|
||||
|
||||
def __iter__(self):
|
||||
end = self.__end
|
||||
curr = end[2]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[2]
|
||||
def __reduce__(self):
|
||||
items = [[k, self[k]] for k in self]
|
||||
tmp = self.__map, self.__end
|
||||
del self.__map, self.__end
|
||||
inst_dict = vars(self).copy()
|
||||
self.__map, self.__end = tmp
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
|
||||
def __reversed__(self):
|
||||
end = self.__end
|
||||
curr = end[1]
|
||||
while curr is not end:
|
||||
yield curr[0]
|
||||
curr = curr[1]
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def popitem(self, last=True):
|
||||
if not self:
|
||||
raise KeyError('dictionary is empty')
|
||||
if last:
|
||||
key = reversed(self).next()
|
||||
else:
|
||||
key = iter(self).next()
|
||||
value = self.pop(key)
|
||||
return key, value
|
||||
setdefault = DictMixin.setdefault
|
||||
update = DictMixin.update
|
||||
pop = DictMixin.pop
|
||||
values = DictMixin.values
|
||||
items = DictMixin.items
|
||||
iterkeys = DictMixin.iterkeys
|
||||
itervalues = DictMixin.itervalues
|
||||
iteritems = DictMixin.iteritems
|
||||
|
||||
def __reduce__(self):
|
||||
items = [[k, self[k]] for k in self]
|
||||
tmp = self.__map, self.__end
|
||||
del self.__map, self.__end
|
||||
inst_dict = vars(self).copy()
|
||||
self.__map, self.__end = tmp
|
||||
if inst_dict:
|
||||
return (self.__class__, (items,), inst_dict)
|
||||
return self.__class__, (items,)
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
def copy(self):
|
||||
return self.__class__(self)
|
||||
|
||||
setdefault = DictMixin.setdefault
|
||||
update = DictMixin.update
|
||||
pop = DictMixin.pop
|
||||
values = DictMixin.values
|
||||
items = DictMixin.items
|
||||
iterkeys = DictMixin.iterkeys
|
||||
itervalues = DictMixin.itervalues
|
||||
iteritems = DictMixin.iteritems
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __repr__(self):
|
||||
if not self:
|
||||
return '%s()' % (self.__class__.__name__,)
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, OrderedDict):
|
||||
return len(self)==len(other) and self.items() == other.items()
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self)
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, iterable, value=None):
|
||||
d = cls()
|
||||
for key in iterable:
|
||||
d[key] = value
|
||||
return d
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, OrderedDict):
|
||||
return len(self)==len(other) and self.items() == other.items()
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
@ -2,207 +2,193 @@
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
from calibre.utils.genshi.template import MarkupTemplate
|
||||
from calibre import preferred_encoding, strftime
|
||||
|
||||
from lxml import html, etree
|
||||
from lxml.html.builder import HTML, HEAD, TITLE, STYLE, DIV, BODY, \
|
||||
STRONG, BR, H1, SPAN, A, HR, UL, LI, H2, IMG, P as PT
|
||||
|
||||
class Template(MarkupTemplate):
|
||||
from calibre import preferred_encoding, strftime, isbytestring
|
||||
|
||||
def CLASS(*args, **kwargs): # class is a reserved word in Python
|
||||
kwargs['class'] = ' '.join(args)
|
||||
return kwargs
|
||||
|
||||
class Template(object):
|
||||
|
||||
IS_HTML = True
|
||||
|
||||
def generate(self, *args, **kwargs):
|
||||
if not kwargs.has_key('style'):
|
||||
kwargs['style'] = ''
|
||||
for key in kwargs.keys():
|
||||
if isinstance(kwargs[key], basestring) and not isinstance(kwargs[key], unicode):
|
||||
kwargs[key] = unicode(kwargs[key], 'utf-8', 'replace')
|
||||
for arg in args:
|
||||
if isinstance(arg, basestring) and not isinstance(arg, unicode):
|
||||
arg = unicode(arg, 'utf-8', 'replace')
|
||||
if isbytestring(kwargs[key]):
|
||||
kwargs[key] = kwargs[key].decode('utf-8', 'replace')
|
||||
if kwargs[key] is None:
|
||||
kwargs[key] = u''
|
||||
args = list(args)
|
||||
for i in range(len(args)):
|
||||
if isbytestring(args[i]):
|
||||
args[i] = args[i].decode('utf-8', 'replace')
|
||||
if args[i] is None:
|
||||
args[i] = u''
|
||||
|
||||
return MarkupTemplate.generate(self, *args, **kwargs)
|
||||
self._generate(*args, **kwargs)
|
||||
|
||||
return self
|
||||
|
||||
def render(self, *args, **kwargs):
|
||||
if self.IS_HTML:
|
||||
return html.tostring(self.root, encoding='utf-8',
|
||||
include_meta_content_type=True, pretty_print=True)
|
||||
return etree.tostring(self.root, encoding='utf-8', xml_declaration=True,
|
||||
pretty_print=True)
|
||||
|
||||
class NavBarTemplate(Template):
|
||||
|
||||
def __init__(self):
|
||||
Template.__init__(self, u'''\
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xml:lang="en"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
|
||||
>
|
||||
<head>
|
||||
<style py:if="extra_css" type="text/css">
|
||||
${extra_css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="calibre_navbar calibre_rescale_70" style="text-align:${'center' if center else 'left'};">
|
||||
<hr py:if="bottom" />
|
||||
<p py:if="bottom" style="text-align:left">
|
||||
This article was downloaded by <b>${__appname__}</b> from <a href="${url}">${url}</a>
|
||||
</p>
|
||||
<br py:if="bottom" /><br py:if="bottom" />
|
||||
<py:if test="art != num - 1 and not bottom">
|
||||
| <a href="${prefix}../article_${str(art+1)}/index.html">Next</a>
|
||||
</py:if>
|
||||
<py:if test="art == num - 1 and not bottom">
|
||||
| <a href="${prefix}../../feed_${str(feed+1)}/index.html">Next</a>
|
||||
</py:if>
|
||||
| <a href="${prefix}../index.html#article_${str(art)}">Section menu</a>
|
||||
<py:if test="two_levels">
|
||||
| <a href="${prefix}../../index.html#feed_${str(feed)}">Main menu</a>
|
||||
</py:if>
|
||||
<py:if test="art != 0 and not bottom">
|
||||
| <a href="${prefix}../article_${str(art-1)}/index.html">Previous</a>
|
||||
</py:if>
|
||||
|
|
||||
<hr py:if="not bottom" />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
|
||||
def generate(self, bottom, feed, art, number_of_articles_in_feed,
|
||||
def _generate(self, bottom, feed, art, number_of_articles_in_feed,
|
||||
two_levels, url, __appname__, prefix='', center=True,
|
||||
extra_css=None):
|
||||
extra_css=None, style=None):
|
||||
head = HEAD(TITLE('navbar'))
|
||||
if style:
|
||||
head.append(STYLE(style, type='text/css'))
|
||||
if extra_css:
|
||||
head.append(STYLE(extra_css, type='text/css'))
|
||||
|
||||
if prefix and not prefix.endswith('/'):
|
||||
prefix += '/'
|
||||
return Template.generate(self, bottom=bottom, art=art, feed=feed,
|
||||
num=number_of_articles_in_feed,
|
||||
two_levels=two_levels, url=url,
|
||||
__appname__=__appname__, prefix=prefix,
|
||||
center=center, extra_css=extra_css)
|
||||
align = 'center' if center else 'left'
|
||||
navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_70',
|
||||
style='text-align:'+align))
|
||||
if bottom:
|
||||
navbar.append(HR())
|
||||
text = 'This article was downloaded by '
|
||||
p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left')
|
||||
p[0].tail = ' from '
|
||||
navbar.append(BR())
|
||||
navbar.append(BR())
|
||||
else:
|
||||
next = 'feed_%d'%(feed+1) if art == number_of_articles_in_feed - 1 \
|
||||
else 'article_%d'%(art+1)
|
||||
up = '../..' if art == number_of_articles_in_feed - 1 else '..'
|
||||
href = '%s%s/%s/index.html'%(prefix, up, next)
|
||||
navbar.text = '| '
|
||||
navbar.append(A('Next', href=href))
|
||||
href = '%s../index.html#article_%d'%(prefix, art)
|
||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||
navbar.append(A('Section Menu', href=href))
|
||||
href = '%s../../index.html#feed_%d'%(prefix, feed)
|
||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||
navbar.append(A('Main Menu', href=href))
|
||||
if art > 0 and not bottom:
|
||||
href = '%s../article_%d/index.html'%(prefix, art-1)
|
||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||
navbar.append(A('Previous', href=href))
|
||||
navbar.iterchildren(reversed=True).next().tail = ' | '
|
||||
if not bottom:
|
||||
navbar.append(HR())
|
||||
|
||||
self.root = HTML(head, BODY(navbar))
|
||||
|
||||
|
||||
|
||||
|
||||
class IndexTemplate(Template):
|
||||
|
||||
def __init__(self):
|
||||
Template.__init__(self, u'''\
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xml:lang="en"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
|
||||
>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>${title}</title>
|
||||
<style py:if="style" type="text/css">
|
||||
${style}
|
||||
</style>
|
||||
<style py:if="extra_css" type="text/css">
|
||||
${extra_css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="calibre_rescale_100">
|
||||
<h1 class="calibre_recipe_title calibre_rescale_180">${title}</h1>
|
||||
<p style="text-align:right">${date}</p>
|
||||
<ul class="calibre_feed_list">
|
||||
<py:for each="i, feed in enumerate(feeds)">
|
||||
<li py:if="feed" id="feed_${str(i)}">
|
||||
<a class="feed calibre_rescale_120" href="${'feed_%d/index.html'%i}">${feed.title}</a>
|
||||
</li>
|
||||
</py:for>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
|
||||
def generate(self, title, datefmt, feeds, extra_css=None):
|
||||
def _generate(self, title, datefmt, feeds, extra_css=None, style=None):
|
||||
if isinstance(datefmt, unicode):
|
||||
datefmt = datefmt.encode(preferred_encoding)
|
||||
date = strftime(datefmt)
|
||||
return Template.generate(self, title=title, date=date, feeds=feeds,
|
||||
extra_css=extra_css)
|
||||
|
||||
head = HEAD(TITLE(title))
|
||||
if style:
|
||||
head.append(STYLE(style, type='text/css'))
|
||||
if extra_css:
|
||||
head.append(STYLE(extra_css, type='text/css'))
|
||||
ul = UL(CLASS('calibre_feed_list'))
|
||||
for i, feed in enumerate(feeds):
|
||||
if feed:
|
||||
li = LI(A(feed.title, CLASS('feed', 'calibre_rescale_120',
|
||||
href='feed_%d/index.html'%i)), id='feed_%d'%i)
|
||||
ul.append(li)
|
||||
div = DIV(
|
||||
H1(title, CLASS('calibre_recipe_title', 'calibre_rescale_180')),
|
||||
PT(date, style='text-align:right'),
|
||||
ul,
|
||||
CLASS('calibre_rescale_100'))
|
||||
self.root = HTML(head, BODY(div))
|
||||
|
||||
class FeedTemplate(Template):
|
||||
|
||||
def __init__(self):
|
||||
Template.__init__(self, u'''\
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xml:lang="en"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
def _generate(self, feed, cutoff, extra_css=None, style=None):
|
||||
head = HEAD(TITLE(feed.title))
|
||||
if style:
|
||||
head.append(STYLE(style, type='text/css'))
|
||||
if extra_css:
|
||||
head.append(STYLE(extra_css, type='text/css'))
|
||||
body = BODY(style='page-break-before:always')
|
||||
div = DIV(
|
||||
H2(feed.title,
|
||||
CLASS('calibre_feed_title', 'calibre_rescale_160')),
|
||||
CLASS('calibre_rescale_100')
|
||||
)
|
||||
body.append(div)
|
||||
if getattr(feed, 'image', None):
|
||||
div.append(DIV(IMG(
|
||||
alt = feed.image_alt if feed.image_alt else '',
|
||||
src = feed.image_url
|
||||
),
|
||||
CLASS('calibre_feed_image')))
|
||||
if getattr(feed, 'description', None):
|
||||
d = DIV(feed.description, CLASS('calibre_feed_description',
|
||||
'calibre_rescale_80'))
|
||||
d.append(BR())
|
||||
div.append(d)
|
||||
ul = UL(CLASS('calibre_article_list'))
|
||||
for i, article in enumerate(feed.articles):
|
||||
if not getattr(article, 'downloaded', False):
|
||||
continue
|
||||
li = LI(
|
||||
A(article.title, CLASS('article calibre_rescale_120',
|
||||
href=article.url)),
|
||||
SPAN(article.formatted_date, CLASS('article_date')),
|
||||
CLASS('calibre_rescale_100', id='article_%d'%i,
|
||||
style='padding-bottom:0.5em')
|
||||
)
|
||||
if article.summary:
|
||||
li.append(DIV(cutoff(article.text_summary),
|
||||
CLASS('article_description', 'calibre_rescale_70')))
|
||||
ul.append(li)
|
||||
div.append(ul)
|
||||
navbar = DIV('| ', CLASS('calibre_navbar', 'calibre_rescale_70'))
|
||||
link = A('Up one level', href="../index.html")
|
||||
link.tail = ' |'
|
||||
navbar.append(link)
|
||||
div.append(navbar)
|
||||
|
||||
>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>${feed.title}</title>
|
||||
<style py:if="style" type="text/css">
|
||||
${style}
|
||||
</style>
|
||||
<style py:if="extra_css" type="text/css">
|
||||
${extra_css}
|
||||
</style>
|
||||
</head>
|
||||
<body style="page-break-before:always">
|
||||
<div class="calibre_rescale_100">
|
||||
<h2 class="calibre_feed_title calibre_rescale_160">${feed.title}</h2>
|
||||
<py:if test="getattr(feed, 'image', None)">
|
||||
<div class="calibre_feed_image">
|
||||
<img alt="${feed.image_alt}" src="${feed.image_url}" />
|
||||
</div>
|
||||
</py:if>
|
||||
<div class="calibre_feed_description calibre_rescale_80" py:if="getattr(feed, 'description', None)">
|
||||
${feed.description}<br />
|
||||
</div>
|
||||
<ul class="calibre_article_list">
|
||||
<py:for each="i, article in enumerate(feed.articles)">
|
||||
<li id="${'article_%d'%i}" py:if="getattr(article, 'downloaded',
|
||||
False)" style="padding-bottom:0.5em" class="calibre_rescale_100">
|
||||
<a class="article calibre_rescale_120" href="${article.url}">${article.title}</a>
|
||||
<span class="article_date">${article.formatted_date}</span>
|
||||
<div class="article_description calibre_rescale_70" py:if="article.summary">
|
||||
${Markup(cutoff(article.text_summary))}
|
||||
</div>
|
||||
</li>
|
||||
</py:for>
|
||||
</ul>
|
||||
<div class="calibre_navbar calibre_rescale_70">
|
||||
| <a href="../index.html">Up one level</a> |
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
self.root = HTML(head, body)
|
||||
|
||||
def generate(self, feed, cutoff, extra_css=None):
|
||||
return Template.generate(self, feed=feed, cutoff=cutoff,
|
||||
extra_css=extra_css)
|
||||
|
||||
class EmbeddedContent(Template):
|
||||
|
||||
def __init__(self):
|
||||
Template.__init__(self, u'''\
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xml:lang="en"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
xmlns:py="http://genshi.edgewall.org/"
|
||||
def _generate(self, article, style=None, extra_css=None):
|
||||
content = article.content if article.content else ''
|
||||
summary = article.summary if article.summary else ''
|
||||
text = content if len(content) > len(summary) else summary
|
||||
head = HEAD(TITLE(article.title))
|
||||
if style:
|
||||
head.append(STYLE(style, type='text/css'))
|
||||
if extra_css:
|
||||
head.append(STYLE(extra_css, type='text/css'))
|
||||
|
||||
>
|
||||
<head>
|
||||
<title>${article.title}</title>
|
||||
</head>
|
||||
if isbytestring(text):
|
||||
text = text.decode('utf-8', 'replace')
|
||||
elements = html.fragments_fromstring(text)
|
||||
self.root = HTML(head,
|
||||
BODY(H2(article.title), DIV()))
|
||||
div = self.root.find('body').find('div')
|
||||
if elements and isinstance(elements[0], unicode):
|
||||
div.text = elements[0]
|
||||
elements = list(elements)[1:]
|
||||
for elem in elements:
|
||||
elem.getparent().remove(elem)
|
||||
div.append(elem)
|
||||
|
||||
<body>
|
||||
<h2>${article.title}</h2>
|
||||
<div>
|
||||
${Markup(article.content if len(article.content if article.content else '') > len(article.summary if article.summary else '') else article.summary)}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
''')
|
||||
|
||||
def generate(self, article):
|
||||
return Template.generate(self, article=article)
|
||||
|
142
src/routes/__init__.py
Normal file
142
src/routes/__init__.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""Provides common classes and functions most users will want access to."""
|
||||
import threading, sys
|
||||
|
||||
class _RequestConfig(object):
|
||||
"""
|
||||
RequestConfig thread-local singleton
|
||||
|
||||
The Routes RequestConfig object is a thread-local singleton that should
|
||||
be initialized by the web framework that is utilizing Routes.
|
||||
"""
|
||||
__shared_state = threading.local()
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.__shared_state, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
If the name is environ, load the wsgi envion with load_wsgi_environ
|
||||
and set the environ
|
||||
"""
|
||||
if name == 'environ':
|
||||
self.load_wsgi_environ(value)
|
||||
return self.__shared_state.__setattr__(name, value)
|
||||
return self.__shared_state.__setattr__(name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
delattr(self.__shared_state, name)
|
||||
|
||||
def load_wsgi_environ(self, environ):
|
||||
"""
|
||||
Load the protocol/server info from the environ and store it.
|
||||
Also, match the incoming URL if there's already a mapper, and
|
||||
store the resulting match dict in mapper_dict.
|
||||
"""
|
||||
if 'HTTPS' in environ or environ.get('wsgi.url_scheme') == 'https' \
|
||||
or environ.get('HTTP_X_FORWARDED_PROTO') == 'https':
|
||||
self.__shared_state.protocol = 'https'
|
||||
else:
|
||||
self.__shared_state.protocol = 'http'
|
||||
try:
|
||||
self.mapper.environ = environ
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Wrap in try/except as common case is that there is a mapper
|
||||
# attached to self
|
||||
try:
|
||||
if 'PATH_INFO' in environ:
|
||||
mapper = self.mapper
|
||||
path = environ['PATH_INFO']
|
||||
result = mapper.routematch(path)
|
||||
if result is not None:
|
||||
self.__shared_state.mapper_dict = result[0]
|
||||
self.__shared_state.route = result[1]
|
||||
else:
|
||||
self.__shared_state.mapper_dict = None
|
||||
self.__shared_state.route = None
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if 'HTTP_X_FORWARDED_HOST' in environ:
|
||||
self.__shared_state.host = environ['HTTP_X_FORWARDED_HOST']
|
||||
elif 'HTTP_HOST' in environ:
|
||||
self.__shared_state.host = environ['HTTP_HOST']
|
||||
else:
|
||||
self.__shared_state.host = environ['SERVER_NAME']
|
||||
if environ['wsgi.url_scheme'] == 'https':
|
||||
if environ['SERVER_PORT'] != '443':
|
||||
self.__shared_state.host += ':' + environ['SERVER_PORT']
|
||||
else:
|
||||
if environ['SERVER_PORT'] != '80':
|
||||
self.__shared_state.host += ':' + environ['SERVER_PORT']
|
||||
|
||||
def request_config(original=False):
|
||||
"""
|
||||
Returns the Routes RequestConfig object.
|
||||
|
||||
To get the Routes RequestConfig:
|
||||
|
||||
>>> from routes import *
|
||||
>>> config = request_config()
|
||||
|
||||
The following attributes must be set on the config object every request:
|
||||
|
||||
mapper
|
||||
mapper should be a Mapper instance thats ready for use
|
||||
host
|
||||
host is the hostname of the webapp
|
||||
protocol
|
||||
protocol is the protocol of the current request
|
||||
mapper_dict
|
||||
mapper_dict should be the dict returned by mapper.match()
|
||||
redirect
|
||||
redirect should be a function that issues a redirect,
|
||||
and takes a url as the sole argument
|
||||
prefix (optional)
|
||||
Set if the application is moved under a URL prefix. Prefix
|
||||
will be stripped before matching, and prepended on generation
|
||||
environ (optional)
|
||||
Set to the WSGI environ for automatic prefix support if the
|
||||
webapp is underneath a 'SCRIPT_NAME'
|
||||
|
||||
Setting the environ will use information in environ to try and
|
||||
populate the host/protocol/mapper_dict options if you've already
|
||||
set a mapper.
|
||||
|
||||
**Using your own requst local**
|
||||
|
||||
If you have your own request local object that you'd like to use instead
|
||||
of the default thread local provided by Routes, you can configure Routes
|
||||
to use it::
|
||||
|
||||
from routes import request_config()
|
||||
config = request_config()
|
||||
if hasattr(config, 'using_request_local'):
|
||||
config.request_local = YourLocalCallable
|
||||
config = request_config()
|
||||
|
||||
Once you have configured request_config, its advisable you retrieve it
|
||||
again to get the object you wanted. The variable you assign to
|
||||
request_local is assumed to be a callable that will get the local config
|
||||
object you wish.
|
||||
|
||||
This example tests for the presence of the 'using_request_local' attribute
|
||||
which will be present if you haven't assigned it yet. This way you can
|
||||
avoid repeat assignments of the request specific callable.
|
||||
|
||||
Should you want the original object, perhaps to change the callable its
|
||||
using or stop this behavior, call request_config(original=True).
|
||||
"""
|
||||
obj = _RequestConfig()
|
||||
try:
|
||||
if obj.request_local and original is False:
|
||||
return getattr(obj, 'request_local')()
|
||||
except AttributeError:
|
||||
obj.request_local = False
|
||||
obj.using_request_local = False
|
||||
return _RequestConfig()
|
||||
|
||||
from routes.mapper import Mapper
|
||||
from routes.util import redirect_to, url_for, URLGenerator
|
||||
__all__=['Mapper', 'url_for', 'URLGenerator', 'redirect_to', 'request_config']
|
4
src/routes/base.py
Normal file
4
src/routes/base.py
Normal file
@ -0,0 +1,4 @@
|
||||
"""Route and Mapper core classes"""
|
||||
from routes import request_config
|
||||
from routes.mapper import Mapper
|
||||
from routes.route import Route
|
70
src/routes/lru.py
Normal file
70
src/routes/lru.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""LRU caching class and decorator"""
|
||||
import threading
|
||||
|
||||
_marker = object()
|
||||
|
||||
class LRUCache(object):
|
||||
def __init__(self, size):
|
||||
""" Implements a psueudo-LRU algorithm (CLOCK) """
|
||||
if size < 1:
|
||||
raise ValueError('size must be >1')
|
||||
self.clock = []
|
||||
for i in xrange(0, size):
|
||||
self.clock.append({'key':_marker, 'ref':False})
|
||||
self.size = size
|
||||
self.maxpos = size - 1
|
||||
self.hand = 0
|
||||
self.data = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self.data
|
||||
|
||||
def __getitem__(self, key, default=None):
|
||||
try:
|
||||
datum = self.data[key]
|
||||
except KeyError:
|
||||
return default
|
||||
pos, val = datum
|
||||
self.clock[pos]['ref'] = True
|
||||
hand = pos + 1
|
||||
if hand > self.maxpos:
|
||||
hand = 0
|
||||
self.hand = hand
|
||||
return val
|
||||
|
||||
def __setitem__(self, key, val, _marker=_marker):
|
||||
hand = self.hand
|
||||
maxpos = self.maxpos
|
||||
clock = self.clock
|
||||
data = self.data
|
||||
lock = self.lock
|
||||
|
||||
end = hand - 1
|
||||
if end < 0:
|
||||
end = maxpos
|
||||
|
||||
while 1:
|
||||
current = clock[hand]
|
||||
ref = current['ref']
|
||||
if ref is True:
|
||||
current['ref'] = False
|
||||
hand = hand + 1
|
||||
if hand > maxpos:
|
||||
hand = 0
|
||||
elif ref is False or hand == end:
|
||||
lock.acquire()
|
||||
try:
|
||||
oldkey = current['key']
|
||||
if oldkey in data:
|
||||
del data[oldkey]
|
||||
current['key'] = key
|
||||
current['ref'] = True
|
||||
data[key] = (hand, val)
|
||||
hand += 1
|
||||
if hand > maxpos:
|
||||
hand = 0
|
||||
self.hand = hand
|
||||
finally:
|
||||
lock.release()
|
||||
break
|
1161
src/routes/mapper.py
Normal file
1161
src/routes/mapper.py
Normal file
File diff suppressed because it is too large
Load Diff
146
src/routes/middleware.py
Normal file
146
src/routes/middleware.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""Routes WSGI Middleware"""
|
||||
import re
|
||||
import logging
|
||||
|
||||
from webob import Request
|
||||
|
||||
from routes.base import request_config
|
||||
from routes.util import URLGenerator, url_for
|
||||
|
||||
log = logging.getLogger('routes.middleware')
|
||||
|
||||
class RoutesMiddleware(object):
|
||||
"""Routing middleware that handles resolving the PATH_INFO in
|
||||
addition to optionally recognizing method overriding."""
|
||||
def __init__(self, wsgi_app, mapper, use_method_override=True,
|
||||
path_info=True, singleton=True):
|
||||
"""Create a Route middleware object
|
||||
|
||||
Using the use_method_override keyword will require Paste to be
|
||||
installed, and your application should use Paste's WSGIRequest
|
||||
object as it will properly handle POST issues with wsgi.input
|
||||
should Routes check it.
|
||||
|
||||
If path_info is True, then should a route var contain
|
||||
path_info, the SCRIPT_NAME and PATH_INFO will be altered
|
||||
accordingly. This should be used with routes like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
map.connect('blog/*path_info', controller='blog', path_info='')
|
||||
|
||||
"""
|
||||
self.app = wsgi_app
|
||||
self.mapper = mapper
|
||||
self.singleton = singleton
|
||||
self.use_method_override = use_method_override
|
||||
self.path_info = path_info
|
||||
log_debug = self.log_debug = logging.DEBUG >= log.getEffectiveLevel()
|
||||
if self.log_debug:
|
||||
log.debug("Initialized with method overriding = %s, and path "
|
||||
"info altering = %s", use_method_override, path_info)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Resolves the URL in PATH_INFO, and uses wsgi.routing_args
|
||||
to pass on URL resolver results."""
|
||||
old_method = None
|
||||
if self.use_method_override:
|
||||
req = None
|
||||
|
||||
# In some odd cases, there's no query string
|
||||
try:
|
||||
qs = environ['QUERY_STRING']
|
||||
except KeyError:
|
||||
qs = ''
|
||||
if '_method' in qs:
|
||||
req = Request(environ)
|
||||
req.errors = 'ignore'
|
||||
if '_method' in req.GET:
|
||||
old_method = environ['REQUEST_METHOD']
|
||||
environ['REQUEST_METHOD'] = req.GET['_method'].upper()
|
||||
if self.log_debug:
|
||||
log.debug("_method found in QUERY_STRING, altering request"
|
||||
" method to %s", environ['REQUEST_METHOD'])
|
||||
elif environ['REQUEST_METHOD'] == 'POST' and is_form_post(environ):
|
||||
if req is None:
|
||||
req = Request(environ)
|
||||
req.errors = 'ignore'
|
||||
if '_method' in req.POST:
|
||||
old_method = environ['REQUEST_METHOD']
|
||||
environ['REQUEST_METHOD'] = req.POST['_method'].upper()
|
||||
if self.log_debug:
|
||||
log.debug("_method found in POST data, altering request "
|
||||
"method to %s", environ['REQUEST_METHOD'])
|
||||
|
||||
# Run the actual route matching
|
||||
# -- Assignment of environ to config triggers route matching
|
||||
if self.singleton:
|
||||
config = request_config()
|
||||
config.mapper = self.mapper
|
||||
config.environ = environ
|
||||
match = config.mapper_dict
|
||||
route = config.route
|
||||
else:
|
||||
results = self.mapper.routematch(environ=environ)
|
||||
if results:
|
||||
match, route = results[0], results[1]
|
||||
else:
|
||||
match = route = None
|
||||
|
||||
if old_method:
|
||||
environ['REQUEST_METHOD'] = old_method
|
||||
|
||||
if not match:
|
||||
match = {}
|
||||
if self.log_debug:
|
||||
urlinfo = "%s %s" % (environ['REQUEST_METHOD'], environ['PATH_INFO'])
|
||||
log.debug("No route matched for %s", urlinfo)
|
||||
elif self.log_debug:
|
||||
urlinfo = "%s %s" % (environ['REQUEST_METHOD'], environ['PATH_INFO'])
|
||||
log.debug("Matched %s", urlinfo)
|
||||
log.debug("Route path: '%s', defaults: %s", route.routepath,
|
||||
route.defaults)
|
||||
log.debug("Match dict: %s", match)
|
||||
|
||||
url = URLGenerator(self.mapper, environ)
|
||||
environ['wsgiorg.routing_args'] = ((url), match)
|
||||
environ['routes.route'] = route
|
||||
environ['routes.url'] = url
|
||||
|
||||
if route and route.redirect:
|
||||
route_name = '_redirect_%s' % id(route)
|
||||
location = url(route_name, **match)
|
||||
log.debug("Using redirect route, redirect to '%s' with status"
|
||||
"code: %s", location, route.redirect_status)
|
||||
start_response(route.redirect_status,
|
||||
[('Content-Type', 'text/plain; charset=utf8'),
|
||||
('Location', location)])
|
||||
return []
|
||||
|
||||
# If the route included a path_info attribute and it should be used to
|
||||
# alter the environ, we'll pull it out
|
||||
if self.path_info and 'path_info' in match:
|
||||
oldpath = environ['PATH_INFO']
|
||||
newpath = match.get('path_info') or ''
|
||||
environ['PATH_INFO'] = newpath
|
||||
if not environ['PATH_INFO'].startswith('/'):
|
||||
environ['PATH_INFO'] = '/' + environ['PATH_INFO']
|
||||
environ['SCRIPT_NAME'] += re.sub(r'^(.*?)/' + re.escape(newpath) + '$',
|
||||
r'\1', oldpath)
|
||||
|
||||
response = self.app(environ, start_response)
|
||||
|
||||
# Wrapped in try as in rare cases the attribute will be gone already
|
||||
try:
|
||||
del self.mapper.environ
|
||||
except AttributeError:
|
||||
pass
|
||||
return response
|
||||
|
||||
def is_form_post(environ):
|
||||
"""Determine whether the request is a POSTed html form"""
|
||||
content_type = environ.get('CONTENT_TYPE', '').lower()
|
||||
if ';' in content_type:
|
||||
content_type = content_type.split(';', 1)[0]
|
||||
return content_type in ('application/x-www-form-urlencoded',
|
||||
'multipart/form-data')
|
742
src/routes/route.py
Normal file
742
src/routes/route.py
Normal file
@ -0,0 +1,742 @@
|
||||
import re
|
||||
import sys
|
||||
import urllib
|
||||
|
||||
if sys.version < '2.4':
|
||||
from sets import ImmutableSet as frozenset
|
||||
|
||||
from routes.util import _url_quote as url_quote, _str_encode
|
||||
|
||||
|
||||
class Route(object):
|
||||
"""The Route object holds a route recognition and generation
|
||||
routine.
|
||||
|
||||
See Route.__init__ docs for usage.
|
||||
|
||||
"""
|
||||
# reserved keys that don't count
|
||||
reserved_keys = ['requirements']
|
||||
|
||||
# special chars to indicate a natural split in the URL
|
||||
done_chars = ('/', ',', ';', '.', '#')
|
||||
|
||||
def __init__(self, name, routepath, **kargs):
|
||||
"""Initialize a route, with a given routepath for
|
||||
matching/generation
|
||||
|
||||
The set of keyword args will be used as defaults.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> from routes.base import Route
|
||||
>>> newroute = Route(None, ':controller/:action/:id')
|
||||
>>> sorted(newroute.defaults.items())
|
||||
[('action', 'index'), ('id', None)]
|
||||
>>> newroute = Route(None, 'date/:year/:month/:day',
|
||||
... controller="blog", action="view")
|
||||
>>> newroute = Route(None, 'archives/:page', controller="blog",
|
||||
... action="by_page", requirements = { 'page':'\d{1,2}' })
|
||||
>>> newroute.reqs
|
||||
{'page': '\\\d{1,2}'}
|
||||
|
||||
.. Note::
|
||||
Route is generally not called directly, a Mapper instance
|
||||
connect method should be used to add routes.
|
||||
|
||||
"""
|
||||
self.routepath = routepath
|
||||
self.sub_domains = False
|
||||
self.prior = None
|
||||
self.redirect = False
|
||||
self.name = name
|
||||
self._kargs = kargs
|
||||
self.minimization = kargs.pop('_minimize', False)
|
||||
self.encoding = kargs.pop('_encoding', 'utf-8')
|
||||
self.reqs = kargs.get('requirements', {})
|
||||
self.decode_errors = 'replace'
|
||||
|
||||
# Don't bother forming stuff we don't need if its a static route
|
||||
self.static = kargs.pop('_static', False)
|
||||
self.filter = kargs.pop('_filter', None)
|
||||
self.absolute = kargs.pop('_absolute', False)
|
||||
|
||||
# Pull out the member/collection name if present, this applies only to
|
||||
# map.resource
|
||||
self.member_name = kargs.pop('_member_name', None)
|
||||
self.collection_name = kargs.pop('_collection_name', None)
|
||||
self.parent_resource = kargs.pop('_parent_resource', None)
|
||||
|
||||
# Pull out route conditions
|
||||
self.conditions = kargs.pop('conditions', None)
|
||||
|
||||
# Determine if explicit behavior should be used
|
||||
self.explicit = kargs.pop('_explicit', False)
|
||||
|
||||
# Since static need to be generated exactly, treat them as
|
||||
# non-minimized
|
||||
if self.static:
|
||||
self.external = '://' in self.routepath
|
||||
self.minimization = False
|
||||
|
||||
# Strip preceding '/' if present, and not minimizing
|
||||
if routepath.startswith('/') and self.minimization:
|
||||
self.routepath = routepath[1:]
|
||||
self._setup_route()
|
||||
|
||||
def _setup_route(self):
|
||||
# Build our routelist, and the keys used in the route
|
||||
self.routelist = routelist = self._pathkeys(self.routepath)
|
||||
routekeys = frozenset([key['name'] for key in routelist
|
||||
if isinstance(key, dict)])
|
||||
self.dotkeys = frozenset([key['name'] for key in routelist
|
||||
if isinstance(key, dict) and
|
||||
key['type'] == '.'])
|
||||
|
||||
if not self.minimization:
|
||||
self.make_full_route()
|
||||
|
||||
# Build a req list with all the regexp requirements for our args
|
||||
self.req_regs = {}
|
||||
for key, val in self.reqs.iteritems():
|
||||
self.req_regs[key] = re.compile('^' + val + '$')
|
||||
# Update our defaults and set new default keys if needed. defaults
|
||||
# needs to be saved
|
||||
(self.defaults, defaultkeys) = self._defaults(routekeys,
|
||||
self.reserved_keys,
|
||||
self._kargs.copy())
|
||||
# Save the maximum keys we could utilize
|
||||
self.maxkeys = defaultkeys | routekeys
|
||||
|
||||
# Populate our minimum keys, and save a copy of our backward keys for
|
||||
# quicker generation later
|
||||
(self.minkeys, self.routebackwards) = self._minkeys(routelist[:])
|
||||
|
||||
# Populate our hardcoded keys, these are ones that are set and don't
|
||||
# exist in the route
|
||||
self.hardcoded = frozenset([key for key in self.maxkeys \
|
||||
if key not in routekeys and self.defaults[key] is not None])
|
||||
|
||||
# Cache our default keys
|
||||
self._default_keys = frozenset(self.defaults.keys())
|
||||
|
||||
def make_full_route(self):
|
||||
"""Make a full routelist string for use with non-minimized
|
||||
generation"""
|
||||
regpath = ''
|
||||
for part in self.routelist:
|
||||
if isinstance(part, dict):
|
||||
regpath += '%(' + part['name'] + ')s'
|
||||
else:
|
||||
regpath += part
|
||||
self.regpath = regpath
|
||||
|
||||
def make_unicode(self, s):
|
||||
"""Transform the given argument into a unicode string."""
|
||||
if isinstance(s, unicode):
|
||||
return s
|
||||
elif isinstance(s, str):
|
||||
return s.decode(self.encoding)
|
||||
elif callable(s):
|
||||
return s
|
||||
else:
|
||||
return unicode(s)
|
||||
|
||||
def _pathkeys(self, routepath):
|
||||
"""Utility function to walk the route, and pull out the valid
|
||||
dynamic/wildcard keys."""
|
||||
collecting = False
|
||||
current = ''
|
||||
done_on = ''
|
||||
var_type = ''
|
||||
just_started = False
|
||||
routelist = []
|
||||
for char in routepath:
|
||||
if char in [':', '*', '{'] and not collecting and not self.static \
|
||||
or char in ['{'] and not collecting:
|
||||
just_started = True
|
||||
collecting = True
|
||||
var_type = char
|
||||
if char == '{':
|
||||
done_on = '}'
|
||||
just_started = False
|
||||
if len(current) > 0:
|
||||
routelist.append(current)
|
||||
current = ''
|
||||
elif collecting and just_started:
|
||||
just_started = False
|
||||
if char == '(':
|
||||
done_on = ')'
|
||||
else:
|
||||
current = char
|
||||
done_on = self.done_chars + ('-',)
|
||||
elif collecting and char not in done_on:
|
||||
current += char
|
||||
elif collecting:
|
||||
collecting = False
|
||||
if var_type == '{':
|
||||
if current[0] == '.':
|
||||
var_type = '.'
|
||||
current = current[1:]
|
||||
else:
|
||||
var_type = ':'
|
||||
opts = current.split(':')
|
||||
if len(opts) > 1:
|
||||
current = opts[0]
|
||||
self.reqs[current] = opts[1]
|
||||
routelist.append(dict(type=var_type, name=current))
|
||||
if char in self.done_chars:
|
||||
routelist.append(char)
|
||||
done_on = var_type = current = ''
|
||||
else:
|
||||
current += char
|
||||
if collecting:
|
||||
routelist.append(dict(type=var_type, name=current))
|
||||
elif current:
|
||||
routelist.append(current)
|
||||
return routelist
|
||||
|
||||
def _minkeys(self, routelist):
|
||||
"""Utility function to walk the route backwards
|
||||
|
||||
Will also determine the minimum keys we can handle to generate
|
||||
a working route.
|
||||
|
||||
routelist is a list of the '/' split route path
|
||||
defaults is a dict of all the defaults provided for the route
|
||||
|
||||
"""
|
||||
minkeys = []
|
||||
backcheck = routelist[:]
|
||||
|
||||
# If we don't honor minimization, we need all the keys in the
|
||||
# route path
|
||||
if not self.minimization:
|
||||
for part in backcheck:
|
||||
if isinstance(part, dict):
|
||||
minkeys.append(part['name'])
|
||||
return (frozenset(minkeys), backcheck)
|
||||
|
||||
gaps = False
|
||||
backcheck.reverse()
|
||||
for part in backcheck:
|
||||
if not isinstance(part, dict) and part not in self.done_chars:
|
||||
gaps = True
|
||||
continue
|
||||
elif not isinstance(part, dict):
|
||||
continue
|
||||
key = part['name']
|
||||
if self.defaults.has_key(key) and not gaps:
|
||||
continue
|
||||
minkeys.append(key)
|
||||
gaps = True
|
||||
return (frozenset(minkeys), backcheck)
|
||||
|
||||
def _defaults(self, routekeys, reserved_keys, kargs):
|
||||
"""Creates default set with values stringified
|
||||
|
||||
Put together our list of defaults, stringify non-None values
|
||||
and add in our action/id default if they use it and didn't
|
||||
specify it.
|
||||
|
||||
defaultkeys is a list of the currently assumed default keys
|
||||
routekeys is a list of the keys found in the route path
|
||||
reserved_keys is a list of keys that are not
|
||||
|
||||
"""
|
||||
defaults = {}
|
||||
# Add in a controller/action default if they don't exist
|
||||
if 'controller' not in routekeys and 'controller' not in kargs \
|
||||
and not self.explicit:
|
||||
kargs['controller'] = 'content'
|
||||
if 'action' not in routekeys and 'action' not in kargs \
|
||||
and not self.explicit:
|
||||
kargs['action'] = 'index'
|
||||
defaultkeys = frozenset([key for key in kargs.keys() \
|
||||
if key not in reserved_keys])
|
||||
for key in defaultkeys:
|
||||
if kargs[key] is not None:
|
||||
defaults[key] = self.make_unicode(kargs[key])
|
||||
else:
|
||||
defaults[key] = None
|
||||
if 'action' in routekeys and not defaults.has_key('action') \
|
||||
and not self.explicit:
|
||||
defaults['action'] = 'index'
|
||||
if 'id' in routekeys and not defaults.has_key('id') \
|
||||
and not self.explicit:
|
||||
defaults['id'] = None
|
||||
newdefaultkeys = frozenset([key for key in defaults.keys() \
|
||||
if key not in reserved_keys])
|
||||
|
||||
return (defaults, newdefaultkeys)
|
||||
|
||||
def makeregexp(self, clist, include_names=True):
|
||||
"""Create a regular expression for matching purposes
|
||||
|
||||
Note: This MUST be called before match can function properly.
|
||||
|
||||
clist should be a list of valid controller strings that can be
|
||||
matched, for this reason makeregexp should be called by the web
|
||||
framework after it knows all available controllers that can be
|
||||
utilized.
|
||||
|
||||
include_names indicates whether this should be a match regexp
|
||||
assigned to itself using regexp grouping names, or if names
|
||||
should be excluded for use in a single larger regexp to
|
||||
determine if any routes match
|
||||
|
||||
"""
|
||||
if self.minimization:
|
||||
reg = self.buildnextreg(self.routelist, clist, include_names)[0]
|
||||
if not reg:
|
||||
reg = '/'
|
||||
reg = reg + '/?' + '$'
|
||||
|
||||
if not reg.startswith('/'):
|
||||
reg = '/' + reg
|
||||
else:
|
||||
reg = self.buildfullreg(clist, include_names)
|
||||
|
||||
reg = '^' + reg
|
||||
|
||||
if not include_names:
|
||||
return reg
|
||||
|
||||
self.regexp = reg
|
||||
self.regmatch = re.compile(reg)
|
||||
|
||||
def buildfullreg(self, clist, include_names=True):
|
||||
"""Build the regexp by iterating through the routelist and
|
||||
replacing dicts with the appropriate regexp match"""
|
||||
regparts = []
|
||||
for part in self.routelist:
|
||||
if isinstance(part, dict):
|
||||
var = part['name']
|
||||
if var == 'controller':
|
||||
partmatch = '|'.join(map(re.escape, clist))
|
||||
elif part['type'] == ':':
|
||||
partmatch = self.reqs.get(var) or '[^/]+?'
|
||||
elif part['type'] == '.':
|
||||
partmatch = self.reqs.get(var) or '[^/.]+?'
|
||||
else:
|
||||
partmatch = self.reqs.get(var) or '.+?'
|
||||
if include_names:
|
||||
regpart = '(?P<%s>%s)' % (var, partmatch)
|
||||
else:
|
||||
regpart = '(?:%s)' % partmatch
|
||||
if part['type'] == '.':
|
||||
regparts.append('(?:\.%s)??' % regpart)
|
||||
else:
|
||||
regparts.append(regpart)
|
||||
else:
|
||||
regparts.append(re.escape(part))
|
||||
regexp = ''.join(regparts) + '$'
|
||||
return regexp
|
||||
|
||||
def buildnextreg(self, path, clist, include_names=True):
|
||||
"""Recursively build our regexp given a path, and a controller
|
||||
list.
|
||||
|
||||
Returns the regular expression string, and two booleans that
|
||||
can be ignored as they're only used internally by buildnextreg.
|
||||
|
||||
"""
|
||||
if path:
|
||||
part = path[0]
|
||||
else:
|
||||
part = ''
|
||||
reg = ''
|
||||
|
||||
# noreqs will remember whether the remainder has either a string
|
||||
# match, or a non-defaulted regexp match on a key, allblank remembers
|
||||
# if the rest could possible be completely empty
|
||||
(rest, noreqs, allblank) = ('', True, True)
|
||||
if len(path[1:]) > 0:
|
||||
self.prior = part
|
||||
(rest, noreqs, allblank) = self.buildnextreg(path[1:], clist, include_names)
|
||||
|
||||
if isinstance(part, dict) and part['type'] in (':', '.'):
|
||||
var = part['name']
|
||||
typ = part['type']
|
||||
partreg = ''
|
||||
|
||||
# First we plug in the proper part matcher
|
||||
if self.reqs.has_key(var):
|
||||
if include_names:
|
||||
partreg = '(?P<%s>%s)' % (var, self.reqs[var])
|
||||
else:
|
||||
partreg = '(?:%s)' % self.reqs[var]
|
||||
if typ == '.':
|
||||
partreg = '(?:\.%s)??' % partreg
|
||||
elif var == 'controller':
|
||||
if include_names:
|
||||
partreg = '(?P<%s>%s)' % (var, '|'.join(map(re.escape, clist)))
|
||||
else:
|
||||
partreg = '(?:%s)' % '|'.join(map(re.escape, clist))
|
||||
elif self.prior in ['/', '#']:
|
||||
if include_names:
|
||||
partreg = '(?P<' + var + '>[^' + self.prior + ']+?)'
|
||||
else:
|
||||
partreg = '(?:[^' + self.prior + ']+?)'
|
||||
else:
|
||||
if not rest:
|
||||
if typ == '.':
|
||||
exclude_chars = '/.'
|
||||
else:
|
||||
exclude_chars = '/'
|
||||
if include_names:
|
||||
partreg = '(?P<%s>[^%s]+?)' % (var, exclude_chars)
|
||||
else:
|
||||
partreg = '(?:[^%s]+?)' % exclude_chars
|
||||
if typ == '.':
|
||||
partreg = '(?:\.%s)??' % partreg
|
||||
else:
|
||||
end = ''.join(self.done_chars)
|
||||
rem = rest
|
||||
if rem[0] == '\\' and len(rem) > 1:
|
||||
rem = rem[1]
|
||||
elif rem.startswith('(\\') and len(rem) > 2:
|
||||
rem = rem[2]
|
||||
else:
|
||||
rem = end
|
||||
rem = frozenset(rem) | frozenset(['/'])
|
||||
if include_names:
|
||||
partreg = '(?P<%s>[^%s]+?)' % (var, ''.join(rem))
|
||||
else:
|
||||
partreg = '(?:[^%s]+?)' % ''.join(rem)
|
||||
|
||||
if self.reqs.has_key(var):
|
||||
noreqs = False
|
||||
if not self.defaults.has_key(var):
|
||||
allblank = False
|
||||
noreqs = False
|
||||
|
||||
# Now we determine if its optional, or required. This changes
|
||||
# depending on what is in the rest of the match. If noreqs is
|
||||
# true, then its possible the entire thing is optional as there's
|
||||
# no reqs or string matches.
|
||||
if noreqs:
|
||||
# The rest is optional, but now we have an optional with a
|
||||
# regexp. Wrap to ensure that if we match anything, we match
|
||||
# our regexp first. It's still possible we could be completely
|
||||
# blank as we have a default
|
||||
if self.reqs.has_key(var) and self.defaults.has_key(var):
|
||||
reg = '(' + partreg + rest + ')?'
|
||||
|
||||
# Or we have a regexp match with no default, so now being
|
||||
# completely blank form here on out isn't possible
|
||||
elif self.reqs.has_key(var):
|
||||
allblank = False
|
||||
reg = partreg + rest
|
||||
|
||||
# If the character before this is a special char, it has to be
|
||||
# followed by this
|
||||
elif self.defaults.has_key(var) and \
|
||||
self.prior in (',', ';', '.'):
|
||||
reg = partreg + rest
|
||||
|
||||
# Or we have a default with no regexp, don't touch the allblank
|
||||
elif self.defaults.has_key(var):
|
||||
reg = partreg + '?' + rest
|
||||
|
||||
# Or we have a key with no default, and no reqs. Not possible
|
||||
# to be all blank from here
|
||||
else:
|
||||
allblank = False
|
||||
reg = partreg + rest
|
||||
# In this case, we have something dangling that might need to be
|
||||
# matched
|
||||
else:
|
||||
# If they can all be blank, and we have a default here, we know
|
||||
# its safe to make everything from here optional. Since
|
||||
# something else in the chain does have req's though, we have
|
||||
# to make the partreg here required to continue matching
|
||||
if allblank and self.defaults.has_key(var):
|
||||
reg = '(' + partreg + rest + ')?'
|
||||
|
||||
# Same as before, but they can't all be blank, so we have to
|
||||
# require it all to ensure our matches line up right
|
||||
else:
|
||||
reg = partreg + rest
|
||||
elif isinstance(part, dict) and part['type'] == '*':
|
||||
var = part['name']
|
||||
if noreqs:
|
||||
if include_names:
|
||||
reg = '(?P<%s>.*)' % var + rest
|
||||
else:
|
||||
reg = '(?:.*)' + rest
|
||||
if not self.defaults.has_key(var):
|
||||
allblank = False
|
||||
noreqs = False
|
||||
else:
|
||||
if allblank and self.defaults.has_key(var):
|
||||
if include_names:
|
||||
reg = '(?P<%s>.*)' % var + rest
|
||||
else:
|
||||
reg = '(?:.*)' + rest
|
||||
elif self.defaults.has_key(var):
|
||||
if include_names:
|
||||
reg = '(?P<%s>.*)' % var + rest
|
||||
else:
|
||||
reg = '(?:.*)' + rest
|
||||
else:
|
||||
if include_names:
|
||||
reg = '(?P<%s>.*)' % var + rest
|
||||
else:
|
||||
reg = '(?:.*)' + rest
|
||||
allblank = False
|
||||
noreqs = False
|
||||
elif part and part[-1] in self.done_chars:
|
||||
if allblank:
|
||||
reg = re.escape(part[:-1]) + '(' + re.escape(part[-1]) + rest
|
||||
reg += ')?'
|
||||
else:
|
||||
allblank = False
|
||||
reg = re.escape(part) + rest
|
||||
|
||||
# We have a normal string here, this is a req, and it prevents us from
|
||||
# being all blank
|
||||
else:
|
||||
noreqs = False
|
||||
allblank = False
|
||||
reg = re.escape(part) + rest
|
||||
|
||||
return (reg, noreqs, allblank)
|
||||
|
||||
def match(self, url, environ=None, sub_domains=False,
|
||||
sub_domains_ignore=None, domain_match=''):
|
||||
"""Match a url to our regexp.
|
||||
|
||||
While the regexp might match, this operation isn't
|
||||
guaranteed as there's other factors that can cause a match to
|
||||
fail even though the regexp succeeds (Default that was relied
|
||||
on wasn't given, requirement regexp doesn't pass, etc.).
|
||||
|
||||
Therefore the calling function shouldn't assume this will
|
||||
return a valid dict, the other possible return is False if a
|
||||
match doesn't work out.
|
||||
|
||||
"""
|
||||
# Static routes don't match, they generate only
|
||||
if self.static:
|
||||
return False
|
||||
|
||||
match = self.regmatch.match(url)
|
||||
|
||||
if not match:
|
||||
return False
|
||||
|
||||
sub_domain = None
|
||||
|
||||
if sub_domains and environ and 'HTTP_HOST' in environ:
|
||||
host = environ['HTTP_HOST'].split(':')[0]
|
||||
sub_match = re.compile('^(.+?)\.%s$' % domain_match)
|
||||
subdomain = re.sub(sub_match, r'\1', host)
|
||||
if subdomain not in sub_domains_ignore and host != subdomain:
|
||||
sub_domain = subdomain
|
||||
|
||||
if self.conditions:
|
||||
if 'method' in self.conditions and environ and \
|
||||
environ['REQUEST_METHOD'] not in self.conditions['method']:
|
||||
return False
|
||||
|
||||
# Check sub-domains?
|
||||
use_sd = self.conditions.get('sub_domain')
|
||||
if use_sd and not sub_domain:
|
||||
return False
|
||||
elif not use_sd and 'sub_domain' in self.conditions and sub_domain:
|
||||
return False
|
||||
if isinstance(use_sd, list) and sub_domain not in use_sd:
|
||||
return False
|
||||
|
||||
matchdict = match.groupdict()
|
||||
result = {}
|
||||
extras = self._default_keys - frozenset(matchdict.keys())
|
||||
for key, val in matchdict.iteritems():
|
||||
if key != 'path_info' and self.encoding:
|
||||
# change back into python unicode objects from the URL
|
||||
# representation
|
||||
try:
|
||||
val = val and val.decode(self.encoding, self.decode_errors)
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
|
||||
if not val and key in self.defaults and self.defaults[key]:
|
||||
result[key] = self.defaults[key]
|
||||
else:
|
||||
result[key] = val
|
||||
for key in extras:
|
||||
result[key] = self.defaults[key]
|
||||
|
||||
# Add the sub-domain if there is one
|
||||
if sub_domains:
|
||||
result['sub_domain'] = sub_domain
|
||||
|
||||
# If there's a function, call it with environ and expire if it
|
||||
# returns False
|
||||
if self.conditions and 'function' in self.conditions and \
|
||||
not self.conditions['function'](environ, result):
|
||||
return False
|
||||
|
||||
return result
|
||||
|
||||
def generate_non_minimized(self, kargs):
|
||||
"""Generate a non-minimal version of the URL"""
|
||||
# Iterate through the keys that are defaults, and NOT in the route
|
||||
# path. If its not in kargs, or doesn't match, or is None, this
|
||||
# route won't work
|
||||
for k in self.maxkeys - self.minkeys:
|
||||
if k not in kargs:
|
||||
return False
|
||||
elif self.make_unicode(kargs[k]) != \
|
||||
self.make_unicode(self.defaults[k]):
|
||||
return False
|
||||
|
||||
# Ensure that all the args in the route path are present and not None
|
||||
for arg in self.minkeys:
|
||||
if arg not in kargs or kargs[arg] is None:
|
||||
if arg in self.dotkeys:
|
||||
kargs[arg] = ''
|
||||
else:
|
||||
return False
|
||||
|
||||
# Encode all the argument that the regpath can use
|
||||
for k in kargs:
|
||||
if k in self.maxkeys:
|
||||
if k in self.dotkeys:
|
||||
if kargs[k]:
|
||||
kargs[k] = url_quote('.' + kargs[k], self.encoding)
|
||||
else:
|
||||
kargs[k] = url_quote(kargs[k], self.encoding)
|
||||
|
||||
return self.regpath % kargs
|
||||
|
||||
def generate_minimized(self, kargs):
|
||||
"""Generate a minimized version of the URL"""
|
||||
routelist = self.routebackwards
|
||||
urllist = []
|
||||
gaps = False
|
||||
for part in routelist:
|
||||
if isinstance(part, dict) and part['type'] in (':', '.'):
|
||||
arg = part['name']
|
||||
|
||||
# For efficiency, check these just once
|
||||
has_arg = kargs.has_key(arg)
|
||||
has_default = self.defaults.has_key(arg)
|
||||
|
||||
# Determine if we can leave this part off
|
||||
# First check if the default exists and wasn't provided in the
|
||||
# call (also no gaps)
|
||||
if has_default and not has_arg and not gaps:
|
||||
continue
|
||||
|
||||
# Now check to see if there's a default and it matches the
|
||||
# incoming call arg
|
||||
if (has_default and has_arg) and self.make_unicode(kargs[arg]) == \
|
||||
self.make_unicode(self.defaults[arg]) and not gaps:
|
||||
continue
|
||||
|
||||
# We need to pull the value to append, if the arg is None and
|
||||
# we have a default, use that
|
||||
if has_arg and kargs[arg] is None and has_default and not gaps:
|
||||
continue
|
||||
|
||||
# Otherwise if we do have an arg, use that
|
||||
elif has_arg:
|
||||
val = kargs[arg]
|
||||
|
||||
elif has_default and self.defaults[arg] is not None:
|
||||
val = self.defaults[arg]
|
||||
# Optional format parameter?
|
||||
elif part['type'] == '.':
|
||||
continue
|
||||
# No arg at all? This won't work
|
||||
else:
|
||||
return False
|
||||
|
||||
urllist.append(url_quote(val, self.encoding))
|
||||
if part['type'] == '.':
|
||||
urllist.append('.')
|
||||
|
||||
if has_arg:
|
||||
del kargs[arg]
|
||||
gaps = True
|
||||
elif isinstance(part, dict) and part['type'] == '*':
|
||||
arg = part['name']
|
||||
kar = kargs.get(arg)
|
||||
if kar is not None:
|
||||
urllist.append(url_quote(kar, self.encoding))
|
||||
gaps = True
|
||||
elif part and part[-1] in self.done_chars:
|
||||
if not gaps and part in self.done_chars:
|
||||
continue
|
||||
elif not gaps:
|
||||
urllist.append(part[:-1])
|
||||
gaps = True
|
||||
else:
|
||||
gaps = True
|
||||
urllist.append(part)
|
||||
else:
|
||||
gaps = True
|
||||
urllist.append(part)
|
||||
urllist.reverse()
|
||||
url = ''.join(urllist)
|
||||
return url
|
||||
|
||||
def generate(self, _ignore_req_list=False, _append_slash=False, **kargs):
|
||||
"""Generate a URL from ourself given a set of keyword arguments
|
||||
|
||||
Toss an exception if this
|
||||
set of keywords would cause a gap in the url.
|
||||
|
||||
"""
|
||||
# Verify that our args pass any regexp requirements
|
||||
if not _ignore_req_list:
|
||||
for key in self.reqs.keys():
|
||||
val = kargs.get(key)
|
||||
if val and not self.req_regs[key].match(self.make_unicode(val)):
|
||||
return False
|
||||
|
||||
# Verify that if we have a method arg, its in the method accept list.
|
||||
# Also, method will be changed to _method for route generation
|
||||
meth = kargs.get('method')
|
||||
if meth:
|
||||
if self.conditions and 'method' in self.conditions \
|
||||
and meth.upper() not in self.conditions['method']:
|
||||
return False
|
||||
kargs.pop('method')
|
||||
|
||||
if self.minimization:
|
||||
url = self.generate_minimized(kargs)
|
||||
else:
|
||||
url = self.generate_non_minimized(kargs)
|
||||
|
||||
if url is False:
|
||||
return url
|
||||
|
||||
if not url.startswith('/') and not self.static:
|
||||
url = '/' + url
|
||||
extras = frozenset(kargs.keys()) - self.maxkeys
|
||||
if extras:
|
||||
if _append_slash and not url.endswith('/'):
|
||||
url += '/'
|
||||
fragments = []
|
||||
# don't assume the 'extras' set preserves order: iterate
|
||||
# through the ordered kargs instead
|
||||
for key in kargs:
|
||||
if key not in extras:
|
||||
continue
|
||||
if key == 'action' or key == 'controller':
|
||||
continue
|
||||
val = kargs[key]
|
||||
if isinstance(val, (tuple, list)):
|
||||
for value in val:
|
||||
fragments.append((key, _str_encode(value, self.encoding)))
|
||||
else:
|
||||
fragments.append((key, _str_encode(val, self.encoding)))
|
||||
if fragments:
|
||||
url += '?'
|
||||
url += urllib.urlencode(fragments)
|
||||
elif _append_slash and not url.endswith('/'):
|
||||
url += '/'
|
||||
return url
|
503
src/routes/util.py
Normal file
503
src/routes/util.py
Normal file
@ -0,0 +1,503 @@
|
||||
"""Utility functions for use in templates / controllers
|
||||
|
||||
*PLEASE NOTE*: Many of these functions expect an initialized RequestConfig
|
||||
object. This is expected to have been initialized for EACH REQUEST by the web
|
||||
framework.
|
||||
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import urllib
|
||||
from routes import request_config
|
||||
|
||||
|
||||
class RoutesException(Exception):
|
||||
"""Tossed during Route exceptions"""
|
||||
|
||||
|
||||
class MatchException(RoutesException):
|
||||
"""Tossed during URL matching exceptions"""
|
||||
|
||||
|
||||
class GenerationException(RoutesException):
|
||||
"""Tossed during URL generation exceptions"""
|
||||
|
||||
|
||||
def _screenargs(kargs, mapper, environ, force_explicit=False):
|
||||
"""
|
||||
Private function that takes a dict, and screens it against the current
|
||||
request dict to determine what the dict should look like that is used.
|
||||
This is responsible for the requests "memory" of the current.
|
||||
"""
|
||||
# Coerce any unicode args with the encoding
|
||||
encoding = mapper.encoding
|
||||
for key, val in kargs.iteritems():
|
||||
if isinstance(val, unicode):
|
||||
kargs[key] = val.encode(encoding)
|
||||
|
||||
if mapper.explicit and mapper.sub_domains and not force_explicit:
|
||||
return _subdomain_check(kargs, mapper, environ)
|
||||
elif mapper.explicit and not force_explicit:
|
||||
return kargs
|
||||
|
||||
controller_name = kargs.get('controller')
|
||||
|
||||
if controller_name and controller_name.startswith('/'):
|
||||
# If the controller name starts with '/', ignore route memory
|
||||
kargs['controller'] = kargs['controller'][1:]
|
||||
return kargs
|
||||
elif controller_name and not kargs.has_key('action'):
|
||||
# Fill in an action if we don't have one, but have a controller
|
||||
kargs['action'] = 'index'
|
||||
|
||||
route_args = environ.get('wsgiorg.routing_args')
|
||||
if route_args:
|
||||
memory_kargs = route_args[1].copy()
|
||||
else:
|
||||
memory_kargs = {}
|
||||
|
||||
# Remove keys from memory and kargs if kargs has them as None
|
||||
for key in [key for key in kargs.keys() if kargs[key] is None]:
|
||||
del kargs[key]
|
||||
if memory_kargs.has_key(key):
|
||||
del memory_kargs[key]
|
||||
|
||||
# Merge the new args on top of the memory args
|
||||
memory_kargs.update(kargs)
|
||||
|
||||
# Setup a sub-domain if applicable
|
||||
if mapper.sub_domains:
|
||||
memory_kargs = _subdomain_check(memory_kargs, mapper, environ)
|
||||
return memory_kargs
|
||||
|
||||
|
||||
def _subdomain_check(kargs, mapper, environ):
|
||||
"""Screen the kargs for a subdomain and alter it appropriately depending
|
||||
on the current subdomain or lack therof."""
|
||||
if mapper.sub_domains:
|
||||
subdomain = kargs.pop('sub_domain', None)
|
||||
if isinstance(subdomain, unicode):
|
||||
subdomain = str(subdomain)
|
||||
|
||||
fullhost = environ.get('HTTP_HOST') or environ.get('SERVER_NAME')
|
||||
|
||||
# In case environ defaulted to {}
|
||||
if not fullhost:
|
||||
return kargs
|
||||
|
||||
hostmatch = fullhost.split(':')
|
||||
host = hostmatch[0]
|
||||
port = ''
|
||||
if len(hostmatch) > 1:
|
||||
port += ':' + hostmatch[1]
|
||||
sub_match = re.compile('^.+?\.(%s)$' % mapper.domain_match)
|
||||
domain = re.sub(sub_match, r'\1', host)
|
||||
if subdomain and not host.startswith(subdomain) and \
|
||||
subdomain not in mapper.sub_domains_ignore:
|
||||
kargs['_host'] = subdomain + '.' + domain + port
|
||||
elif (subdomain in mapper.sub_domains_ignore or \
|
||||
subdomain is None) and domain != host:
|
||||
kargs['_host'] = domain + port
|
||||
return kargs
|
||||
else:
|
||||
return kargs
|
||||
|
||||
|
||||
def _url_quote(string, encoding):
|
||||
"""A Unicode handling version of urllib.quote."""
|
||||
if encoding:
|
||||
if isinstance(string, unicode):
|
||||
s = string.encode(encoding)
|
||||
elif isinstance(string, str):
|
||||
# assume the encoding is already correct
|
||||
s = string
|
||||
else:
|
||||
s = unicode(string).encode(encoding)
|
||||
else:
|
||||
s = str(string)
|
||||
return urllib.quote(s, '/')
|
||||
|
||||
|
||||
def _str_encode(string, encoding):
|
||||
if encoding:
|
||||
if isinstance(string, unicode):
|
||||
s = string.encode(encoding)
|
||||
elif isinstance(string, str):
|
||||
# assume the encoding is already correct
|
||||
s = string
|
||||
else:
|
||||
s = unicode(string).encode(encoding)
|
||||
return s
|
||||
|
||||
|
||||
def url_for(*args, **kargs):
|
||||
"""Generates a URL
|
||||
|
||||
All keys given to url_for are sent to the Routes Mapper instance for
|
||||
generation except for::
|
||||
|
||||
anchor specified the anchor name to be appened to the path
|
||||
host overrides the default (current) host if provided
|
||||
protocol overrides the default (current) protocol if provided
|
||||
qualified creates the URL with the host/port information as
|
||||
needed
|
||||
|
||||
The URL is generated based on the rest of the keys. When generating a new
|
||||
URL, values will be used from the current request's parameters (if
|
||||
present). The following rules are used to determine when and how to keep
|
||||
the current requests parameters:
|
||||
|
||||
* If the controller is present and begins with '/', no defaults are used
|
||||
* If the controller is changed, action is set to 'index' unless otherwise
|
||||
specified
|
||||
|
||||
For example, if the current request yielded a dict of
|
||||
{'controller': 'blog', 'action': 'view', 'id': 2}, with the standard
|
||||
':controller/:action/:id' route, you'd get the following results::
|
||||
|
||||
url_for(id=4) => '/blog/view/4',
|
||||
url_for(controller='/admin') => '/admin',
|
||||
url_for(controller='admin') => '/admin/view/2'
|
||||
url_for(action='edit') => '/blog/edit/2',
|
||||
url_for(action='list', id=None) => '/blog/list'
|
||||
|
||||
**Static and Named Routes**
|
||||
|
||||
If there is a string present as the first argument, a lookup is done
|
||||
against the named routes table to see if there's any matching routes. The
|
||||
keyword defaults used with static routes will be sent in as GET query
|
||||
arg's if a route matches.
|
||||
|
||||
If no route by that name is found, the string is assumed to be a raw URL.
|
||||
Should the raw URL begin with ``/`` then appropriate SCRIPT_NAME data will
|
||||
be added if present, otherwise the string will be used as the url with
|
||||
keyword args becoming GET query args.
|
||||
|
||||
"""
|
||||
anchor = kargs.get('anchor')
|
||||
host = kargs.get('host')
|
||||
protocol = kargs.get('protocol')
|
||||
qualified = kargs.pop('qualified', None)
|
||||
|
||||
# Remove special words from kargs, convert placeholders
|
||||
for key in ['anchor', 'host', 'protocol']:
|
||||
if kargs.get(key):
|
||||
del kargs[key]
|
||||
config = request_config()
|
||||
route = None
|
||||
static = False
|
||||
encoding = config.mapper.encoding
|
||||
url = ''
|
||||
if len(args) > 0:
|
||||
route = config.mapper._routenames.get(args[0])
|
||||
|
||||
# No named route found, assume the argument is a relative path
|
||||
if not route:
|
||||
static = True
|
||||
url = args[0]
|
||||
|
||||
if url.startswith('/') and hasattr(config, 'environ') \
|
||||
and config.environ.get('SCRIPT_NAME'):
|
||||
url = config.environ.get('SCRIPT_NAME') + url
|
||||
|
||||
if static:
|
||||
if kargs:
|
||||
url += '?'
|
||||
query_args = []
|
||||
for key, val in kargs.iteritems():
|
||||
if isinstance(val, (list, tuple)):
|
||||
for value in val:
|
||||
query_args.append("%s=%s" % (
|
||||
urllib.quote(unicode(key).encode(encoding)),
|
||||
urllib.quote(unicode(value).encode(encoding))))
|
||||
else:
|
||||
query_args.append("%s=%s" % (
|
||||
urllib.quote(unicode(key).encode(encoding)),
|
||||
urllib.quote(unicode(val).encode(encoding))))
|
||||
url += '&'.join(query_args)
|
||||
environ = getattr(config, 'environ', {})
|
||||
if 'wsgiorg.routing_args' not in environ:
|
||||
environ = environ.copy()
|
||||
mapper_dict = getattr(config, 'mapper_dict', None)
|
||||
if mapper_dict is not None:
|
||||
match_dict = mapper_dict.copy()
|
||||
else:
|
||||
match_dict = {}
|
||||
environ['wsgiorg.routing_args'] = ((), match_dict)
|
||||
|
||||
if not static:
|
||||
route_args = []
|
||||
if route:
|
||||
if config.mapper.hardcode_names:
|
||||
route_args.append(route)
|
||||
newargs = route.defaults.copy()
|
||||
newargs.update(kargs)
|
||||
|
||||
# If this route has a filter, apply it
|
||||
if route.filter:
|
||||
newargs = route.filter(newargs)
|
||||
|
||||
if not route.static:
|
||||
# Handle sub-domains
|
||||
newargs = _subdomain_check(newargs, config.mapper, environ)
|
||||
else:
|
||||
newargs = _screenargs(kargs, config.mapper, environ)
|
||||
anchor = newargs.pop('_anchor', None) or anchor
|
||||
host = newargs.pop('_host', None) or host
|
||||
protocol = newargs.pop('_protocol', None) or protocol
|
||||
url = config.mapper.generate(*route_args, **newargs)
|
||||
if anchor is not None:
|
||||
url += '#' + _url_quote(anchor, encoding)
|
||||
if host or protocol or qualified:
|
||||
if not host and not qualified:
|
||||
# Ensure we don't use a specific port, as changing the protocol
|
||||
# means that we most likely need a new port
|
||||
host = config.host.split(':')[0]
|
||||
elif not host:
|
||||
host = config.host
|
||||
if not protocol:
|
||||
protocol = config.protocol
|
||||
if url is not None:
|
||||
url = protocol + '://' + host + url
|
||||
|
||||
if not isinstance(url, str) and url is not None:
|
||||
raise GenerationException("url_for can only return a string, got "
|
||||
"unicode instead: %s" % url)
|
||||
if url is None:
|
||||
raise GenerationException(
|
||||
"url_for could not generate URL. Called with args: %s %s" % \
|
||||
(args, kargs))
|
||||
return url
|
||||
|
||||
|
||||
class URLGenerator(object):
|
||||
"""The URL Generator generates URL's
|
||||
|
||||
It is automatically instantiated by the RoutesMiddleware and put
|
||||
into the ``wsgiorg.routing_args`` tuple accessible as::
|
||||
|
||||
url = environ['wsgiorg.routing_args'][0][0]
|
||||
|
||||
Or via the ``routes.url`` key::
|
||||
|
||||
url = environ['routes.url']
|
||||
|
||||
The url object may be instantiated outside of a web context for use
|
||||
in testing, however sub_domain support and fully qualified URL's
|
||||
cannot be generated without supplying a dict that must contain the
|
||||
key ``HTTP_HOST``.
|
||||
|
||||
"""
|
||||
def __init__(self, mapper, environ):
|
||||
"""Instantiate the URLGenerator
|
||||
|
||||
``mapper``
|
||||
The mapper object to use when generating routes.
|
||||
``environ``
|
||||
The environment dict used in WSGI, alternately, any dict
|
||||
that contains at least an ``HTTP_HOST`` value.
|
||||
|
||||
"""
|
||||
self.mapper = mapper
|
||||
if 'SCRIPT_NAME' not in environ:
|
||||
environ['SCRIPT_NAME'] = ''
|
||||
self.environ = environ
|
||||
|
||||
def __call__(self, *args, **kargs):
|
||||
"""Generates a URL
|
||||
|
||||
All keys given to url_for are sent to the Routes Mapper instance for
|
||||
generation except for::
|
||||
|
||||
anchor specified the anchor name to be appened to the path
|
||||
host overrides the default (current) host if provided
|
||||
protocol overrides the default (current) protocol if provided
|
||||
qualified creates the URL with the host/port information as
|
||||
needed
|
||||
|
||||
"""
|
||||
anchor = kargs.get('anchor')
|
||||
host = kargs.get('host')
|
||||
protocol = kargs.get('protocol')
|
||||
qualified = kargs.pop('qualified', None)
|
||||
|
||||
# Remove special words from kargs, convert placeholders
|
||||
for key in ['anchor', 'host', 'protocol']:
|
||||
if kargs.get(key):
|
||||
del kargs[key]
|
||||
|
||||
route = None
|
||||
use_current = '_use_current' in kargs and kargs.pop('_use_current')
|
||||
|
||||
static = False
|
||||
encoding = self.mapper.encoding
|
||||
url = ''
|
||||
|
||||
more_args = len(args) > 0
|
||||
if more_args:
|
||||
route = self.mapper._routenames.get(args[0])
|
||||
|
||||
if not route and more_args:
|
||||
static = True
|
||||
url = args[0]
|
||||
if url.startswith('/') and self.environ.get('SCRIPT_NAME'):
|
||||
url = self.environ.get('SCRIPT_NAME') + url
|
||||
|
||||
if static:
|
||||
if kargs:
|
||||
url += '?'
|
||||
query_args = []
|
||||
for key, val in kargs.iteritems():
|
||||
if isinstance(val, (list, tuple)):
|
||||
for value in val:
|
||||
query_args.append("%s=%s" % (
|
||||
urllib.quote(unicode(key).encode(encoding)),
|
||||
urllib.quote(unicode(value).encode(encoding))))
|
||||
else:
|
||||
query_args.append("%s=%s" % (
|
||||
urllib.quote(unicode(key).encode(encoding)),
|
||||
urllib.quote(unicode(val).encode(encoding))))
|
||||
url += '&'.join(query_args)
|
||||
if not static:
|
||||
route_args = []
|
||||
if route:
|
||||
if self.mapper.hardcode_names:
|
||||
route_args.append(route)
|
||||
newargs = route.defaults.copy()
|
||||
newargs.update(kargs)
|
||||
|
||||
# If this route has a filter, apply it
|
||||
if route.filter:
|
||||
newargs = route.filter(newargs)
|
||||
if not route.static or (route.static and not route.external):
|
||||
# Handle sub-domains, retain sub_domain if there is one
|
||||
sub = newargs.get('sub_domain', None)
|
||||
newargs = _subdomain_check(newargs, self.mapper,
|
||||
self.environ)
|
||||
# If the route requires a sub-domain, and we have it, restore
|
||||
# it
|
||||
if 'sub_domain' in route.defaults:
|
||||
newargs['sub_domain'] = sub
|
||||
|
||||
elif use_current:
|
||||
newargs = _screenargs(kargs, self.mapper, self.environ, force_explicit=True)
|
||||
elif 'sub_domain' in kargs:
|
||||
newargs = _subdomain_check(kargs, self.mapper, self.environ)
|
||||
else:
|
||||
newargs = kargs
|
||||
|
||||
anchor = anchor or newargs.pop('_anchor', None)
|
||||
host = host or newargs.pop('_host', None)
|
||||
protocol = protocol or newargs.pop('_protocol', None)
|
||||
url = self.mapper.generate(*route_args, **newargs)
|
||||
if anchor is not None:
|
||||
url += '#' + _url_quote(anchor, encoding)
|
||||
if host or protocol or qualified:
|
||||
if 'routes.cached_hostinfo' not in self.environ:
|
||||
cache_hostinfo(self.environ)
|
||||
hostinfo = self.environ['routes.cached_hostinfo']
|
||||
|
||||
if not host and not qualified:
|
||||
# Ensure we don't use a specific port, as changing the protocol
|
||||
# means that we most likely need a new port
|
||||
host = hostinfo['host'].split(':')[0]
|
||||
elif not host:
|
||||
host = hostinfo['host']
|
||||
if not protocol:
|
||||
protocol = hostinfo['protocol']
|
||||
if url is not None:
|
||||
if host[-1] != '/':
|
||||
host += '/'
|
||||
url = protocol + '://' + host + url.lstrip('/')
|
||||
|
||||
if not isinstance(url, str) and url is not None:
|
||||
raise GenerationException("Can only return a string, got "
|
||||
"unicode instead: %s" % url)
|
||||
if url is None:
|
||||
raise GenerationException(
|
||||
"Could not generate URL. Called with args: %s %s" % \
|
||||
(args, kargs))
|
||||
return url
|
||||
|
||||
def current(self, *args, **kwargs):
|
||||
"""Generate a route that includes params used on the current
|
||||
request
|
||||
|
||||
The arguments for this method are identical to ``__call__``
|
||||
except that arguments set to None will remove existing route
|
||||
matches of the same name from the set of arguments used to
|
||||
construct a URL.
|
||||
"""
|
||||
return self(_use_current=True, *args, **kwargs)
|
||||
|
||||
|
||||
def redirect_to(*args, **kargs):
|
||||
"""Issues a redirect based on the arguments.
|
||||
|
||||
Redirect's *should* occur as a "302 Moved" header, however the web
|
||||
framework may utilize a different method.
|
||||
|
||||
All arguments are passed to url_for to retrieve the appropriate URL, then
|
||||
the resulting URL it sent to the redirect function as the URL.
|
||||
"""
|
||||
target = url_for(*args, **kargs)
|
||||
config = request_config()
|
||||
return config.redirect(target)
|
||||
|
||||
|
||||
def cache_hostinfo(environ):
|
||||
"""Processes the host information and stores a copy
|
||||
|
||||
This work was previously done but wasn't stored in environ, nor is
|
||||
it guaranteed to be setup in the future (Routes 2 and beyond).
|
||||
|
||||
cache_hostinfo processes environ keys that may be present to
|
||||
determine the proper host, protocol, and port information to use
|
||||
when generating routes.
|
||||
|
||||
"""
|
||||
hostinfo = {}
|
||||
if environ.get('HTTPS') or environ.get('wsgi.url_scheme') == 'https' \
|
||||
or environ.get('HTTP_X_FORWARDED_PROTO') == 'https':
|
||||
hostinfo['protocol'] = 'https'
|
||||
else:
|
||||
hostinfo['protocol'] = 'http'
|
||||
if environ.get('HTTP_X_FORWARDED_HOST'):
|
||||
hostinfo['host'] = environ['HTTP_X_FORWARDED_HOST']
|
||||
elif environ.get('HTTP_HOST'):
|
||||
hostinfo['host'] = environ['HTTP_HOST']
|
||||
else:
|
||||
hostinfo['host'] = environ['SERVER_NAME']
|
||||
if environ.get('wsgi.url_scheme') == 'https':
|
||||
if environ['SERVER_PORT'] != '443':
|
||||
hostinfo['host'] += ':' + environ['SERVER_PORT']
|
||||
else:
|
||||
if environ['SERVER_PORT'] != '80':
|
||||
hostinfo['host'] += ':' + environ['SERVER_PORT']
|
||||
environ['routes.cached_hostinfo'] = hostinfo
|
||||
return hostinfo
|
||||
|
||||
|
||||
def controller_scan(directory=None):
|
||||
"""Scan a directory for python files and use them as controllers"""
|
||||
if directory is None:
|
||||
return []
|
||||
|
||||
def find_controllers(dirname, prefix=''):
|
||||
"""Locate controllers in a directory"""
|
||||
controllers = []
|
||||
for fname in os.listdir(dirname):
|
||||
filename = os.path.join(dirname, fname)
|
||||
if os.path.isfile(filename) and \
|
||||
re.match('^[^_]{1,1}.*\.py$', fname):
|
||||
controllers.append(prefix + fname[:-3])
|
||||
elif os.path.isdir(filename):
|
||||
controllers.extend(find_controllers(filename,
|
||||
prefix=prefix+fname+'/'))
|
||||
return controllers
|
||||
def longest_first(fst, lst):
|
||||
"""Compare the length of one string to another, shortest goes first"""
|
||||
return cmp(len(lst), len(fst))
|
||||
controllers = find_controllers(directory)
|
||||
controllers.sort(longest_first)
|
||||
return controllers
|
Loading…
x
Reference in New Issue
Block a user