Completed
Push — master ( 0264a9...69cfd2 )
by Jace
19s
created

Template.validate_size()   A

Complexity

Conditions 3

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
dl 0
loc 8
ccs 7
cts 7
cp 1
crap 3
rs 9.4285
c 0
b 0
f 0
1 1
import os
2 1
import hashlib
3 1
import shutil
4 1
from pathlib import Path
5 1
import tempfile
6 1
import logging
7
8 1
import time
9 1
import requests
10 1
from PIL import Image
0 ignored issues
show
Configuration introduced by
The import PIL could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

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