Basic biblical tools.
[librarian.git] / src / librarian / __init__.py
1 # This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
3 #
4 import io
5 import os
6 import re
7 import shutil
8 from tempfile import NamedTemporaryFile
9 import urllib
10 from lxml import etree
11 from urllib.request import FancyURLopener
12 from .util import makedirs
13
14 # Compatibility imports.
15 from .meta.types.wluri import WLURI
16
17
18 class UnicodeException(Exception):
19     def __str__(self):
20         """ Dirty workaround for Python Unicode handling problems. """
21         args = self.args[0] if len(self.args) == 1 else self.args
22         try:
23             message = str(args)
24         except UnicodeDecodeError:
25             message = str(args, encoding='utf-8', errors='ignore')
26         return message
27
28
29 class ParseError(UnicodeException):
30     pass
31
32
33 class ValidationError(UnicodeException):
34     pass
35
36
37 class NoDublinCore(ValidationError):
38     """There's no DublinCore section, and it's required."""
39     pass
40
41
42 class NoProvider(UnicodeException):
43     """There's no DocProvider specified, and it's needed."""
44     pass
45
46
47 class XMLNamespace:
48     '''A handy structure to repsent names in an XML namespace.'''
49
50     def __init__(self, uri):
51         self.uri = uri
52
53     def __call__(self, tag):
54         return '{%s}%s' % (self.uri, tag)
55
56     def __contains__(self, tag):
57         return tag.startswith('{' + str(self) + '}')
58
59     def __repr__(self):
60         return 'XMLNamespace(%r)' % self.uri
61
62     def __str__(self):
63         return '%s' % self.uri
64
65
66 class EmptyNamespace(XMLNamespace):
67     def __init__(self):
68         super(EmptyNamespace, self).__init__('')
69
70     def __call__(self, tag):
71         return tag
72
73
74 # some common namespaces we use
75 XMLNS = XMLNamespace('http://www.w3.org/XML/1998/namespace')
76 RDFNS = XMLNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#')
77 DCNS = XMLNamespace('http://purl.org/dc/elements/1.1/')
78 XHTMLNS = XMLNamespace("http://www.w3.org/1999/xhtml")
79 PLMETNS = XMLNamespace("http://dl.psnc.pl/schemas/plmet/")
80 FB2NS = XMLNamespace("http://www.gribuser.ru/xml/fictionbook/2.0")
81 XLINKNS = XMLNamespace("http://www.w3.org/1999/xlink")
82 WLNS = EmptyNamespace()
83
84
85 class DocProvider:
86     """Base class for a repository of XML files.
87
88     Used for generating joined files, like EPUBs.
89     """
90
91     def by_slug(self, slug):
92         """Should return a file-like object with a WL document XML."""
93         raise NotImplementedError
94
95
96 class DirDocProvider(DocProvider):
97     """ Serve docs from a directory of files in form <slug>.xml """
98
99     def __init__(self, dir_):
100         self.dir = dir_
101         self.files = {}
102
103     def by_slug(self, slug):
104         fname = slug + '.xml'
105         return open(os.path.join(self.dir, fname), 'rb')
106
107
108 def get_resource(path):
109     return os.path.join(os.path.dirname(__file__), path)
110
111
112 class OutputFile:
113     """Represents a file returned by one of the converters."""
114
115     _bytes = None
116     _filename = None
117
118     def __del__(self):
119         if self._filename:
120             os.unlink(self._filename)
121
122     def __nonzero__(self):
123         return self._bytes is not None or self._filename is not None
124
125     @classmethod
126     def from_bytes(cls, bytestring):
127         """Converter returns contents of a file as a string."""
128
129         instance = cls()
130         instance._bytes = bytestring
131         return instance
132
133     @classmethod
134     def from_filename(cls, filename):
135         """Converter returns contents of a file as a named file."""
136
137         instance = cls()
138         instance._filename = filename
139         return instance
140
141     def get_bytes(self):
142         """Get file's contents as a bytestring."""
143
144         if self._filename is not None:
145             with open(self._filename, 'rb') as f:
146                 return f.read()
147         else:
148             return self._bytes
149
150     def get_file(self):
151         """Get file as a file-like object."""
152
153         if self._bytes is not None:
154             return io.BytesIO(self._bytes)
155         elif self._filename is not None:
156             return open(self._filename, 'rb')
157
158     def get_filename(self):
159         """Get file as a fs path."""
160
161         if self._filename is not None:
162             return self._filename
163         elif self._bytes is not None:
164             temp = NamedTemporaryFile(prefix='librarian-', delete=False)
165             temp.write(self._bytes)
166             temp.close()
167             self._filename = temp.name
168             return self._filename
169         else:
170             return None
171
172     def save_as(self, path):
173         """Save file to a path. Create directories, if necessary."""
174
175         dirname = os.path.dirname(os.path.abspath(path))
176         makedirs(dirname)
177         shutil.copy(self.get_filename(), path)
178
179
180 class URLOpener(FancyURLopener):
181     version = 'WL Librarian (http://github.com/fnp/librarian)'
182
183
184 urllib._urlopener = URLOpener()