Completed
Push — master ( 386d6a...002088 )
by Jace
01:39
created

Template.get_path()   B

Complexity

Conditions 7

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7.2269

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 10
cts 12
cp 0.8333
rs 7.3333
c 0
b 0
f 0
cc 7
crap 7.2269
1 1
import os
2 1
import hashlib
3 1
import shutil
4 1
from pathlib import Path
5 1
from contextlib import suppress
6 1
import tempfile
7
import logging
8 1
9 1
import time
10 1
import requests
11
from PIL import Image
12 1
13
from .text import Text
14
15 1
16
log = logging.getLogger(__name__)
17
18 1
19
class Template:
20
    """Blank image to generate a meme."""
21 1
22 1
    DEFAULT = 'default'
23
    EXTENSIONS = ('.png', '.jpg')
24 1
25
    SAMPLE_LINES = ["YOUR TEXT", "GOES HERE"]
26 1
27
    VALID_LINK_FLAG = '.valid_link.tmp'
28 1
29 1
    MIN_HEIGHT = 240
30
    MIN_WIDTH = 240
31 1
32
    def __init__(self, key, name=None, lines=None, aliases=None, link=None,
33 1
                 root=None):
34 1
        self.key = key
35 1
        self.name = name or ""
36 1
        self.lines = lines or []
37 1
        self.aliases = aliases or []
38 1
        self.link = link or ""
39
        self.root = root or ""
40 1
41 1
    def __str__(self):
42
        return self.key
43 1
44 1
    def __eq__(self, other):
45
        return self.key == other.key
46 1
47 1
    def __ne__(self, other):
48
        return self.key != other.key
49 1
50 1
    def __lt__(self, other):
51
        return self.name < other.name
52 1
53
    @property
54 1
    def dirpath(self):
55
        return os.path.join(self.root, self.key)
56 1
57
    @property
58 1
    def path(self):
59
        return self.get_path()
60 1
61
    @property
62 1
    def default_text(self):
63
        return Text(self.lines)
64 1
65
    @property
66 1
    def default_path(self):
67
        return self.default_text.path or Text.EMPTY
68 1
69
    @property
70 1
    def sample_text(self):
71
        return self.default_text or Text(self.SAMPLE_LINES)
72 1
73
    @property
74 1
    def sample_path(self):
75
        return self.sample_text.path
76 1
77
    @property
78 1
    def aliases_lowercase(self):
79
        return [self.strip(a, keep_special=True) for a in self.aliases]
80 1
81
    @property
82 1
    def aliases_stripped(self):
83
        return [self.strip(a, keep_special=False) for a in self.aliases]
84 1
85
    @property
86 1
    def styles(self):
87
        return sorted(self._styles())
88 1
89
    def _styles(self):
90 1
        """Yield all template style names."""
91 1
        for filename in os.listdir(self.dirpath):
92 1
            name, ext = os.path.splitext(filename.lower())
93 1
            if ext in self.EXTENSIONS and name != self.DEFAULT:
94
                yield name
95 1
96
    @property
97 1
    def keywords(self):
98 1
        words = set()
99 1
        for fields in [self.key, self.name] + self.aliases + self.lines:
100 1
            for word in fields.lower().replace(Text.SPACE, ' ').split(' '):
101 1
                if word:
102 1
                    words.add(word)
103
        return words
104 1
105 1
    @staticmethod
106 1
    def strip(text, keep_special=False):
107 1
        text = text.lower().strip().replace(' ', '-')
108 1
        if not keep_special:
109 1
            for char in ('-', '_', '!', "'"):
110 1
                text = text.replace(char, '')
111
        return text
112 1
113 1
    def get_path(self, style_or_url=None, *, download=True):
114 1
        path = None
115 1
116 1
        if style_or_url and '://' in style_or_url:
117
            if download:
118 1
                path = download_image(style_or_url)
119 1
                if path is None:
120 1
                    path = self._find_path_for_style(self.DEFAULT)
121 1
122 1
        else:
123 1
            names = [n.lower() for n in [style_or_url, self.DEFAULT] if n]
124
            path = self._find_path_for_style(*names)
125
126
        return path
127 1
128
    def _find_path_for_style(self, *names):
129 1
        for name in names:
130
            for extension in self.EXTENSIONS:
131 1
                path = Path(self.dirpath, name + extension)
132 1
                with suppress(OSError):
133
                    if path.is_file():
134 1
                        return path
135
136 1
    def search(self, query):
137 1
        """Count the number of times a query exists in relevant fields."""
138
        if query is None:
139 1
            return -1
140
141 1
        count = 0
142 1
143
        for field in [self.key, self.name] + self.aliases + self.lines:
144
            count += field.lower().count(query.lower())
145
146
        return count
147
148 1
    def validate(self, validators=None):
149 1
        if validators is None:
150 1
            validators = [
151 1
                self.validate_meta,
152
                self.validate_link,
153 1
                self.validate_size,
154 1
            ]
155
        for validator in validators:
156
            if not validator():
157 1
                return False
158 1
        return True
159 1
160 1
    def validate_meta(self):
161 1
        if not self.lines:
162 1
            self._error("has no default lines of text")
163 1
            return False
164 1
        if not self.name:
165 1
            self._error("has no name")
166
            return False
167
        if not self.name[0].isalnum():
168 1
            self._error("name %r should start with an alphanumeric", self.name)
169 1
            return False
170
        if not self.path:
171
            self._error("has no default image")
172 1
            return False
173 1
        return True
174 1
175 1
    def validate_link(self, delay=3):
176
        if not os.getenv('VALIDATE_LINKS'):
177 1
            log.warning("Link validation skipped ('VALIDATE_LINKS' unset)")
178 1
            return True
179 1
        if self.link:
180
            flag = Path(self.dirpath, self.VALID_LINK_FLAG)
181
            if flag.is_file():
182
                log.info("Link already checked: %s", self.link)
183 1
            else:
184
                log.info("Checking link %s ...", self.link)
185 1
                try:
186 1
                    response = requests.head(self.link, timeout=5)
187 1
                except requests.exceptions.ReadTimeout:
188
                    log.warning("Connection timed out")
189
                    return True  # assume URL is OK; it will be checked again
190
                if response.status_code in [403, 429]:
191
                    self._warn("link is unavailable (%s)", response.status_code)
192 1
                elif response.status_code >= 400:
193
                    self._error("link is invalid (%s)", response.status_code)
194 1
                    return False
195 1
                else:
196 1
                    with open(str(flag), 'w') as f:
197 1
                        f.write(str(int(time.time())))
198 1
                time.sleep(delay)
199
        return True
200 1
201 1
    def validate_size(self):
202
        im = Image.open(self.path)
203 1
        w, h = im.size
204
        if w < self.MIN_WIDTH or h < self.MIN_HEIGHT:
205
            log.error("Image must be at least %ix%i (is %ix%i)",
206 1
                      self.MIN_WIDTH, self.MIN_HEIGHT, w, h)
207 1
            return False
208
        return True
209
210 1
    def _warn(self, fmt, *objects):
211
        log.warning("Template '%s' " + fmt, self, *objects)
212
213 1
    def _error(self, fmt, *objects):
214
        log.error("Template '%s' " + fmt, self, *objects)
215 1
216 1
217
class Placeholder:
218 1
    """Default image for missing templates."""
219
220 1
    FALLBACK_PATH = str(Path(__file__)
221
                        .parents[1]
222 1
                        .joinpath('static', 'images', 'missing.png'))
223 1
224 1
    path = None
225
226
    def __init__(self, key):
227 1
        self.key = key
228 1
229
    @classmethod
230 1
    def get_path(cls, url=None, download=True):
231
        path = None
232
233 1
        if url and download:
234 1
            path = download_image(url)
235 1
236
        if path is None:
237 1
            path = cls.FALLBACK_PATH
238
239
        return path
240 1
241 1
242 1
def download_image(url):
243
    assert url and '://' in url
244 1
245 1
    path = Path(tempfile.gettempdir(),
246 1
                hashlib.md5(url.encode('utf-8')).hexdigest())
247 1
248 1
    if path.is_file():
249 1
        log.debug("Already downloaded: %s", url)
250 1
        return path
251 1
252
    try:
253 1
        response = requests.get(url, stream=True, timeout=5)
254 1
    except ValueError:
255 1
        log.error("Invalid link: %s", url)
256 1
        return None
257 1
    except requests.exceptions.RequestException:
258 1
        log.error("Bad connection: %s", url)
259
        return None
260 1
261 1
    if response.status_code == 200:
262
        log.info("Downloading %s", url)
263
        with open(str(path), 'wb') as outfile:
264
            response.raw.decode_content = True
265
            shutil.copyfileobj(response.raw, outfile)
266
        return path
267
268
    log.error("Unable to download: %s", url)
269
    return None
270