Completed
Push — master ( f202bb...a73a95 )
by Jace
03:27
created

Template.validate_link()   C

Complexity

Conditions 7

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 8.5432

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
dl 0
loc 21
ccs 13
cts 19
cp 0.6842
crap 8.5432
rs 6.4705
c 1
b 0
f 0
1 1
import os
2 1
import re
3 1
import hashlib
4 1
import shutil
5 1
from pathlib import Path
6 1
import tempfile
7 1
import logging
8
9 1
import time
10 1
import requests
11 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...
12
13 1
from .text import Text
14
15
16 1
log = logging.getLogger(__name__)
17
18
19 1
class Template:
20
    """Blank image to generate a meme."""
21
22 1
    DEFAULT = 'default'
23 1
    EXTENSIONS = ('.png', '.jpg')
24
25 1
    SAMPLE_LINES = ["YOUR TEXT", "GOES HERE"]
26
27 1
    VALID_LINK_FLAG = '.valid_link.tmp'
28
29 1
    MIN_HEIGHT = 240
30 1
    MIN_WIDTH = 240
31
32 1
    def __init__(self, key, name=None, lines=None, patterns=None,
33
                 aliases=None, link=None, root=None):
34 1
        self.key = key
35 1
        self.name = name or ""
36 1
        self.lines = lines or []
37 1
        self.regexes = []
38 1
        self.aliases = aliases or []
39 1
        self.link = link or ""
40 1
        self.root = root or ""
41 1
        self.compile_regexes(patterns or [])
42
43 1
    def __str__(self):
44 1
        return self.key
45
46 1
    def __eq__(self, other):
47 1
        return self.key == other.key
48
49 1
    def __ne__(self, other):
50 1
        return self.key != other.key
51
52 1
    def __lt__(self, other):
53 1
        return self.name < other.name
54
55 1
    @property
56
    def dirpath(self):
57 1
        return os.path.join(self.root, self.key)
58
59 1
    @property
60
    def path(self):
61 1
        return self.get_path()
62
63 1
    @property
64
    def default_text(self):
65 1
        return Text(self.lines)
66
67 1
    @property
68
    def default_path(self):
69 1
        return self.default_text.path or Text.EMPTY
70
71 1
    @property
72
    def sample_text(self):
73 1
        return self.default_text or Text(self.SAMPLE_LINES)
74
75 1
    @property
76
    def sample_path(self):
77 1
        return self.sample_text.path
78
79 1
    @property
80
    def aliases_lowercase(self):
81 1
        return [self.strip(a, keep_special=True) for a in self.aliases]
82
83 1
    @property
84
    def aliases_stripped(self):
85 1
        return [self.strip(a, keep_special=False) for a in self.aliases]
86
87 1
    @property
88
    def styles(self):
89 1
        return sorted(self._styles())
90
91 1
    def _styles(self):
92
        """Yield all template style names."""
93 1
        for filename in os.listdir(self.dirpath):
94 1
            name, ext = os.path.splitext(filename.lower())
95 1
            if ext in self.EXTENSIONS and name != self.DEFAULT:
96 1
                yield name
97
98 1
    @staticmethod
99 1
    def strip(text, keep_special=False):
100 1
        text = text.lower().strip().replace(' ', '-')
101 1
        if not keep_special:
102 1
            for char in ('-', '_', '!', "'"):
103 1
                text = text.replace(char, '')
104 1
        return text
105
106 1
    def get_path(self, *styles):
107 1
        for style in styles:
108 1
            path = download_image(style)
109 1
            if path:
110 1
                return path
111
112 1
        for name in (n.lower() for n in (*styles, self.DEFAULT) if n):
113 1
            for extension in self.EXTENSIONS:
114 1
                path = Path(self.dirpath, name + extension)
115 1
                try:
116 1
                    if path.is_file():
117 1
                        return path
118
                except OSError:
119
                    continue
120
121 1
        return None
122
123 1
    def compile_regexes(self, patterns):
124 1
        self.regexes = [re.compile(p, re.IGNORECASE) for p in patterns]
125
126 1
    def match(self, string):
127 1
        if self.regexes:
128 1
            log.debug("Matching patterns in %s", self)
129
130 1
        for regex in self.regexes:
131 1
            log.debug("Comparing %r to %r", string, regex.pattern)
132 1
            result = regex.match(string)
133 1
            if result:
134 1
                ratio = round(min(len(regex.pattern) / len(string),
135
                                  len(string) / len(regex.pattern)), 2)
136 1
                path = Text(result.group(1) + '/' + result.group(2)).path
137 1
                log.debug("Matched: %r", ratio)
138 1
                return ratio, path
139
140 1
        return 0, None
141
142 1
    def validate(self, validators=None):
143 1
        if validators is None:
144
            validators = [
145
                self.validate_meta,
146
                self.validate_link,
147
                self.validate_size,
148
                self.validate_regexes,
149
            ]
150 1
        for validator in validators:
151 1
            if not validator():
152 1
                return False
153 1
        return True
154
155 1
    def validate_meta(self):
156 1
        if not self.lines:
157
            self._error("has no default lines of text")
158
            return False
159 1
        if not self.name:
160 1
            self._error("has no name")
161 1
            return False
162 1
        if not self.name[0].isalnum():
163 1
            self._error("name %r should start with an alphanumeric", self.name)
164 1
            return False
165 1
        if not self.path:
166 1
            self._error("has no default image")
167 1
            return False
168
        return True
169
170 1
    def validate_link(self):
171 1
        if self.link:
172 1
            flag = Path(self.dirpath, self.VALID_LINK_FLAG)
173 1
            if flag.is_file():
174 1
                log.info("Link already checked: %s", self.link)
175
            else:
176 1
                log.info("Checking link %s ...", self.link)
177 1
                try:
178 1
                    response = requests.head(self.link, timeout=5)
179
                except requests.exceptions.ReadTimeout:
0 ignored issues
show
Bug introduced by
The Module requests.exceptions does not seem to have a member named ReadTimeout.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
180
                    log.warning("Connection timed out")
181
                    return True  # assume URL is OK; it will be checked again
182 1
                if response.status_code in [403, 429]:
183
                    self._warn("link is unavailable (%s)", response.status_code)
184 1
                elif response.status_code >= 400:
185 1
                    self._error("link is invalid (%s)", response.status_code)
186 1
                    return False
187
                else:
188
                    with open(str(flag), 'w') as stream:
189
                        stream.write(str(int(time.time())))
190 1
        return True
191
192 1
    def validate_size(self):
193 1
        im = Image.open(self.path)
194 1
        w, h = im.size
195 1
        if w < self.MIN_WIDTH or h < self.MIN_HEIGHT:
196 1
            log.error("Image must be at least %ix%i (is %ix%i)",
197
                      self.MIN_WIDTH, self.MIN_HEIGHT, w, h)
198 1
            return False
199 1
        return True
200
201 1
    def validate_regexes(self):
202 1
        if not self.regexes:
203
            self._warn("has no regexes")
204 1
        for regex in self.regexes:
205 1
            pattern = regex.pattern
206 1
            if ")/?(" not in pattern:
207 1
                self._error("regex missing separator: %r", pattern)
208 1
                return False
209
        return True
210
211 1
    def _warn(self, fmt, *objects):
212
        log.warning("Template '%s' " + fmt, self, *objects)
213
214 1
    def _error(self, fmt, *objects):
215 1
        log.error("Template '%s' " + fmt, self, *objects)
216
217
218 1
class Placeholder:
219
    """Default image for missing templates."""
220
221 1
    path = None
222
223 1
    def __init__(self, key):
224 1
        self.key = key
225
226 1
    @staticmethod
227
    def get_path(*styles):
228 1
        path = None
229
230 1
        for style in styles:
231 1
            path = download_image(style)
232 1
            if path:
233
                break
234
235 1
        if not path:
236 1
            path = os.path.dirname(__file__) + "/../static/images/missing.png"
237
238 1
        return path
239
240
241 1
def download_image(url):
242 1
    if not url or not url.startswith("http"):
243 1
        return None
244
245 1
    path = Path(tempfile.gettempdir(),
246
                hashlib.md5(url.encode('utf-8')).hexdigest())
247
248 1
    if path.is_file():
249 1
        log.debug("Already downloaded: %s", url)
250 1
        return path
251
252 1
    try:
253 1
        response = requests.get(url, stream=True)
254 1
    except requests.exceptions.InvalidURL:
255 1
        log.error("Invalid link: %s", url)
256 1
        return None
257 1
    except requests.exceptions.ConnectionError:
258 1
        log.error("Bad connection: %s", url)
259 1
        return None
260
261 1
    if response.status_code == 200:
262 1
        log.info("Downloading %s", url)
263 1
        with open(str(path), 'wb') as outfile:
264 1
            response.raw.decode_content = True
265 1
            shutil.copyfileobj(response.raw, outfile)
266 1
        return path
267
268 1
    log.error("Unable to download: %s", url)
269
    return None
270