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