Passed
Push — master ( 4510b5...1b94f8 )
by Mikael
07:17 queued 03:41
created

marvin_actions   F

Complexity

Total Complexity 92

Size/Duplication

Total Lines 550
Duplicated Lines 3.27 %

Test Coverage

Coverage 89.46%

Importance

Changes 0
Metric Value
eloc 331
dl 18
loc 550
ccs 263
cts 294
cp 0.8946
rs 2
c 0
b 0
f 0
wmc 92

33 Functions

Rating   Name   Duplication   Size   Complexity  
B marvinBudord() 0 18 7
A marvinHelp() 0 9 2
A getCommit() 0 13 2
A marvinWeather() 0 20 3
A nextBBQ() 0 18 3
A commitStrip() 0 15 2
A videoOfToday() 0 14 2
A marvinCommit() 0 8 2
A marvinNameday() 0 21 4
B marvinTimeToBBQ() 0 24 6
A marvinStats() 0 9 2
A marvinStream() 0 8 2
A wordsAfterKeyWords() 0 14 4
A thirdFridayIn() 0 12 1
A marvinIrcLog() 0 9 2
A marvinExplainShell() 0 13 2
A marvinLunch() 0 22 4
A marvinPrinciple() 0 14 3
B marvinBirthday() 0 27 6
B getString() 18 18 6
A getJoke() 0 12 2
A marvinSun() 0 24 3
A marvinQuote() 0 9 2
A marvinSource() 0 9 2
A getAllActions() 0 29 1
A marvinJoke() 0 8 2
A marvinVideoOfToday() 0 10 3
A marvinStrip() 0 8 2
A marvinSmile() 0 8 2
A marvinWhoIs() 0 9 2
A marvinUptime() 0 8 2
A marvinGoogle() 0 13 2
A marvinSayHi() 0 16 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like marvin_actions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#! /usr/bin/env python3
2
# -*- coding: utf-8 -*-
3
4
"""
5
Make actions for Marvin, one function for each action.
6
"""
7 1
from urllib.parse import quote_plus
8 1
from urllib.request import urlopen
9 1
import calendar
10 1
import datetime
11 1
import json
12 1
import logging
13 1
import random
14 1
import requests
15
16 1
from bs4 import BeautifulSoup
17
18 1
LOG = logging.getLogger("action")
19
20 1
def getAllActions():
21
    """
22
    Return all actions in an array.
23
    """
24
    return [
25
        marvinExplainShell,
26
        marvinGoogle,
27
        marvinLunch,
28
        marvinVideoOfToday,
29
        marvinWhoIs,
30
        marvinHelp,
31
        marvinSource,
32
        marvinBudord,
33
        marvinQuote,
34
        marvinStats,
35
        marvinIrcLog,
36
        marvinWeather,
37
        marvinSun,
38
        marvinSayHi,
39
        marvinSmile,
40
        marvinStrip,
41
        marvinTimeToBBQ,
42
        marvinBirthday,
43
        marvinNameday,
44
        marvinUptime,
45
        marvinStream,
46
        marvinPrinciple,
47
        marvinJoke,
48
        marvinCommit
49
    ]
50
51
52
# Load all strings from file
53 1
with open("marvin_strings.json", encoding="utf-8") as f:
54 1
    STRINGS = json.load(f)
55
56
57 1 View Code Duplication
def getString(key, key1=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
58
    """
59
    Get a string from the string database.
60
    """
61 1
    data = STRINGS[key]
62 1
    if isinstance(data, list):
63 1
        res = data[random.randint(0, len(data) - 1)]
64 1
    elif isinstance(data, dict):
65 1
        if key1 is None:
66 1
            res = data
67
        else:
68 1
            res = data[key1]
69 1
            if isinstance(res, list):
70 1
                res = res[random.randint(0, len(res) - 1)]
71 1
    elif isinstance(data, str):
72 1
        res = data
73
74 1
    return res
0 ignored issues
show
introduced by
The variable res does not seem to be defined for all execution paths.
Loading history...
75
76
77 1
def marvinSmile(row):
78
    """
79
    Make Marvin smile.
80
    """
81 1
    msg = None
82 1
    if any(r in row for r in ["smile", "le", "skratta", "smilies"]):
83 1
        msg = getString("smile")
84 1
    return msg
85
86
87 1
def wordsAfterKeyWords(words, keyWords):
88
    """
89
    Return all items in the words list after the first occurence
90
    of an item in the keyWords list.
91
    """
92 1
    kwIndex = []
93 1
    for kw in keyWords:
94 1
        if kw in words:
95 1
            kwIndex.append(words.index(kw))
96
97 1
    if not kwIndex:
98 1
        return None
99
100 1
    return words[min(kwIndex)+1:]
101
102
103 1
def marvinGoogle(row):
104
    """
105
    Let Marvin present an url to google.
106
    """
107 1
    query = wordsAfterKeyWords(row, ["google", "googla"])
108 1
    if not query:
109 1
        return None
110
111 1
    searchStr = " ".join(query)
112 1
    url = "https://www.google.se/search?q="
113 1
    url += quote_plus(searchStr)
114 1
    msg = getString("google")
115 1
    return msg.format(url)
116
117
118 1
def marvinExplainShell(row):
119
    """
120
    Let Marvin present an url to the service explain shell to
121
    explain a shell command.
122
    """
123 1
    query = wordsAfterKeyWords(row, ["explain", "förklara"])
124 1
    if not query:
125 1
        return None
126 1
    cmd = " ".join(query)
127 1
    url = "http://explainshell.com/explain?cmd="
128 1
    url += quote_plus(cmd, "/:")
129 1
    msg = getString("explainShell")
130 1
    return msg.format(url)
131
132
133 1
def marvinSource(row):
134
    """
135
    State message about sourcecode.
136
    """
137 1
    msg = None
138 1
    if any(r in row for r in ["källkod", "source"]):
139 1
        msg = getString("source")
140
141 1
    return msg
142
143
144 1
def marvinBudord(row):
145
    """
146
    What are the budord for Marvin?
147
    """
148 1
    msg = None
149 1
    if any(r in row for r in ["budord", "stentavla"]):
150 1
        if any(r in row for r in ["#1", "1"]):
151 1
            msg = getString("budord", "#1")
152 1
        elif any(r in row for r in ["#2", "2"]):
153 1
            msg = getString("budord", "#2")
154 1
        elif any(r in row for r in ["#3", "3"]):
155 1
            msg = getString("budord", "#3")
156 1
        elif any(r in row for r in ["#4", "4"]):
157 1
            msg = getString("budord", "#4")
158 1
        elif any(r in row for r in ["#5", "5"]):
159 1
            msg = getString("budord", "#5")
160
161 1
    return msg
162
163
164 1
def marvinQuote(row):
165
    """
166
    Make a quote.
167
    """
168 1
    msg = None
169 1
    if any(r in row for r in ["quote", "citat", "filosofi", "filosofera"]):
170 1
        msg = getString("hitchhiker")
171
172 1
    return msg
173
174
175 1
def videoOfToday():
176
    """
177
    Check what day it is and provide a url to a suitable video together with a greeting.
178
    """
179 1
    dayNum = datetime.date.weekday(datetime.date.today()) + 1
180 1
    msg = getString("weekdays", str(dayNum))
181 1
    video = getString("video-of-today", str(dayNum))
182
183 1
    if video:
184 1
        msg += " En passande video är " + video
185
    else:
186
        msg += " Jag har ännu ingen passande video för denna dagen."
187
188 1
    return msg
189
190
191 1
def marvinVideoOfToday(row):
192
    """
193
    Show the video of today.
194
    """
195 1
    msg = None
196 1
    if any(r in row for r in ["idag", "dagens"]):
197 1
        if any(r in row for r in ["video", "youtube", "tube"]):
198 1
            msg = videoOfToday()
199
200 1
    return msg
201
202
203 1
def marvinWhoIs(row):
204
    """
205
    Who is Marvin.
206
    """
207 1
    msg = None
208 1
    if all(r in row for r in ["vem", "är"]):
209 1
        msg = getString("whois")
210
211 1
    return msg
212
213
214 1
def marvinHelp(row):
215
    """
216
    Provide a menu.
217
    """
218 1
    msg = None
219 1
    if any(r in row for r in ["hjälp", "help", "menu", "meny"]):
220 1
        msg = getString("menu")
221
222 1
    return msg
223
224
225 1
def marvinStats(row):
226
    """
227
    Provide a link to the stats.
228
    """
229 1
    msg = None
230 1
    if any(r in row for r in ["stats", "statistik", "ircstats"]):
231 1
        msg = getString("ircstats")
232
233 1
    return msg
234
235
236 1
def marvinIrcLog(row):
237
    """
238
    Provide a link to the irclog
239
    """
240 1
    msg = None
241 1
    if any(r in row for r in ["irc", "irclog", "log", "irclogg", "logg", "historik"]):
242 1
        msg = getString("irclog")
243
244 1
    return msg
245
246
247 1
def marvinSayHi(row):
248
    """
249
    Say hi with a nice message.
250
    """
251 1
    msg = None
252 1
    if any(r in row for r in [
253
            "snälla", "hej", "tjena", "morsning", "morrn", "mår", "hallå",
254
            "halloj", "läget", "snäll", "duktig", "träna", "träning",
255
            "utbildning", "tack", "tacka", "tackar", "tacksam"
256
    ]):
257 1
        smile = getString("smile")
258 1
        hello = getString("hello")
259 1
        friendly = getString("friendly")
260 1
        msg = f"{smile} {hello} {friendly}"
261
262 1
    return msg
263
264
265 1
def marvinLunch(row):
266
    """
267
    Help decide where to eat.
268
    """
269 1
    lunchOptions = {
270
        'stan centrum karlskrona kna': 'lunch-karlskrona',
271
        'ängelholm angelholm engelholm': 'lunch-angelholm',
272
        'hässleholm hassleholm': 'lunch-hassleholm',
273
        'malmö malmo malmoe': 'lunch-malmo',
274
        'göteborg goteborg gbg': 'lunch-goteborg'
275
    }
276
277 1
    if any(r in row for r in ["lunch", "mat", "äta", "luncha"]):
278 1
        lunchStr = getString('lunch-message')
279
280 1
        for keys, value in lunchOptions.items():
281 1
            if any(r in row for r in keys.split(" ")):
282 1
                return lunchStr.format(getString(value))
283
284 1
        return lunchStr.format(getString('lunch-bth'))
285
286 1
    return None
287
288
289 1
def marvinSun(row):
290
    """
291
    Check when the sun goes up and down.
292
    """
293 1
    msg = None
294 1
    if any(r in row for r in ["sol", "solen", "solnedgång", "soluppgång", "sun"]):
295 1
        try:
296 1
            url = getString("sun", "url")
297 1
            r = requests.get(url, timeout=5)
298 1
            sundata = r.json()
299
            # Formats the time from the response to HH:mm instead of hh:mm:ss
300 1
            sunrise = sundata["results"]["sunrise"].split()[0][:-3]
301 1
            sunset = sundata["results"]["sunset"].split()[0][:-3]
302
            # The api uses AM/PM notation, this converts the sunset to 12 hour time
303 1
            sunsetHour = int(sunset.split(":")[0]) + 12
304 1
            sunset = str(sunsetHour) + sunset[-3:]
305 1
            msg = getString("sun", "msg").format(sunrise, sunset)
306 1
            return msg
307
308 1
        except Exception as e:
309 1
            LOG.error("Failed to get sun times: %s", e)
310 1
            return getString("sun", "error")
311
312
    return msg
313
314
315 1
def marvinWeather(row):
316
    """
317
    Check what the weather prognosis looks like.
318
    """
319
    msg = None
320
    if any(r in row for r in ["väder", "vädret", "prognos", "prognosen", "smhi"]):
321
        url = getString("smhi", "url")
322
        try:
323
            soup = BeautifulSoup(urlopen(url))
324
            msg = "{}. {}. {}".format(
325
                soup.h1.text,
326
                soup.h4.text,
327
                soup.h4.findNextSibling("p").text
328
            )
329
330
        except Exception as e:
331
            LOG.error("Failed to get weather: %s", e)
332
            msg = getString("smhi", "failed")
333
334
    return msg
335
336
337 1
def marvinStrip(row):
338
    """
339
    Get a comic strip.
340
    """
341 1
    msg = None
342 1
    if any(r in row for r in ["strip", "comic", "nöje", "paus"]):
343 1
        msg = commitStrip(randomize=any(r in row for r in ["rand", "random", "slump", "lucky"]))
344 1
    return msg
345
346
347 1
def commitStrip(randomize=False):
348
    """
349
    Latest or random comic strip from CommitStrip.
350
    """
351 1
    msg = getString("commitstrip", "message")
352
353 1
    if randomize:
354 1
        first = getString("commitstrip", "first")
355 1
        last = getString("commitstrip", "last")
356 1
        rand = random.randint(first, last)
357 1
        url = getString("commitstrip", "urlPage") + str(rand)
358
    else:
359 1
        url = getString("commitstrip", "url")
360
361 1
    return msg.format(url=url)
362
363
364 1
def marvinTimeToBBQ(row):
365
    """
366
    Calcuate the time to next barbecue and print a appropriate msg
367
    """
368 1
    msg = None
369 1
    if any(r in row for r in ["grilla", "grill", "grillcon", "bbq"]):
370 1
        url = getString("barbecue", "url")
371 1
        nextDate = nextBBQ()
372 1
        today = datetime.date.today()
373 1
        daysRemaining = (nextDate - today).days
374
375 1
        if daysRemaining == 0:
376 1
            msg = getString("barbecue", "today")
377 1
        elif daysRemaining == 1:
378 1
            msg = getString("barbecue", "tomorrow")
379 1
        elif 1 < daysRemaining < 14:
380 1
            msg = getString("barbecue", "week") % nextDate
381 1
        elif 14 < daysRemaining < 200:
382 1
            msg = getString("barbecue", "base") % nextDate
383
        else:
384 1
            msg = getString("barbecue", "eternity") % nextDate
385
386 1
        msg = url + ". " + msg
387 1
    return msg
388
389 1
def nextBBQ():
390
    """
391
    Calculate the next grillcon date after today
392
    """
393
394 1
    MAY = 5
395 1
    SEPTEMBER = 9
396
397 1
    after = datetime.date.today()
398 1
    spring = thirdFridayIn(after.year, MAY)
399 1
    if after <= spring:
400 1
        return spring
401
402 1
    autumn = thirdFridayIn(after.year, SEPTEMBER)
403 1
    if after <= autumn:
404 1
        return autumn
405
406 1
    return thirdFridayIn(after.year + 1, MAY)
407
408
409 1
def thirdFridayIn(y, m):
410
    """
411
    Get the third Friday in a given month and year
412
    """
413 1
    THIRD = 2
414 1
    FRIDAY = -1
415
416
    # Start the weeks on saturday to prevent fridays from previous month
417 1
    cal = calendar.Calendar(firstweekday=calendar.SATURDAY)
418
419
    # Return the friday in the third week
420 1
    return cal.monthdatescalendar(y, m)[THIRD][FRIDAY]
421
422
423 1
def marvinBirthday(row):
424
    """
425
    Check birthday info
426
    """
427
    msg = None
428
    if any(r in row for r in ["birthday", "födelsedag"]):
429
        try:
430
            url = getString("birthday", "url")
431
            soup = BeautifulSoup(urlopen(url), "html.parser")
432
            my_list = list()
433
434
            for ana in soup.findAll('a'):
435
                if ana.parent.name == 'strong':
436
                    my_list.append(ana.getText())
437
438
            my_list.pop()
439
            my_strings = ', '.join(my_list)
440
            if not my_strings:
441
                msg = getString("birthday", "nobody")
442
            else:
443
                msg = getString("birthday", "somebody").format(my_strings)
444
445
        except Exception as e:
446
            LOG.error("Failed to get birthday: %s", e)
447
            msg = getString("birthday", "error")
448
449
    return msg
450
451 1
def marvinNameday(row):
452
    """
453
    Check current nameday
454
    """
455 1
    msg = None
456 1
    if any(r in row for r in ["nameday", "namnsdag"]):
457 1
        try:
458 1
            now = datetime.datetime.now()
459 1
            raw_url = "http://api.dryg.net/dagar/v2.1/{year}/{month}/{day}"
460 1
            url = raw_url.format(year=now.year, month=now.month, day=now.day)
461 1
            r = requests.get(url, timeout=5)
462 1
            nameday_data = r.json()
463 1
            names = nameday_data["dagar"][0]["namnsdag"]
464 1
            if names:
465 1
                msg = getString("nameday", "somebody").format(",".join(names))
466
            else:
467 1
                msg = getString("nameday", "nobody")
468 1
        except Exception as e:
469 1
            LOG.error("Failed to get nameday: %s", e)
470 1
            msg = getString("nameday", "error")
471 1
    return msg
472
473 1
def marvinUptime(row):
474
    """
475
    Display info about uptime tournament
476
    """
477 1
    msg = None
478 1
    if "uptime" in row:
479 1
        msg = getString("uptime", "info")
480 1
    return msg
481
482 1
def marvinStream(row):
483
    """
484
    Display info about stream
485
    """
486 1
    msg = None
487 1
    if any(r in row for r in ["stream", "streama", "ström", "strömma"]):
488 1
        msg = getString("stream", "info")
489 1
    return msg
490
491 1
def marvinPrinciple(row):
492
    """
493
    Display one selected software principle, or provide one as random
494
    """
495 1
    msg = None
496 1
    if any(r in row for r in ["principle", "princip", "principer"]):
497 1
        principles = getString("principle")
498 1
        principleKeys = list(principles.keys())
499 1
        matchedKeys = [k for k in row if k in principleKeys]
500 1
        if matchedKeys:
501 1
            msg = principles[matchedKeys.pop()]
502
        else:
503 1
            msg = principles[random.choice(principleKeys)]
504 1
    return msg
505
506 1
def getJoke():
507
    """
508
    Retrieves joke from api.chucknorris.io/jokes/random?category=dev
509
    """
510 1
    try:
511 1
        url = getString("joke", "url")
512 1
        r = requests.get(url, timeout=5)
513 1
        joke_data = r.json()
514 1
        return joke_data["value"]
515 1
    except Exception as e:
516 1
        LOG.error("Failed to get joke: %s", e)
517 1
        return getString("joke", "error")
518
519 1
def marvinJoke(row):
520
    """
521
    Display a random Chuck Norris joke
522
    """
523 1
    msg = None
524 1
    if any(r in row for r in ["joke", "skämt", "chuck norris", "chuck", "norris"]):
525 1
        msg = getJoke()
526 1
    return msg
527
528 1
def getCommit():
529
    """
530
    Retrieves random commit message from whatthecommit.com/index.html
531
    """
532 1
    try:
533 1
        url = getString("commit", "url")
534 1
        r = requests.get(url, timeout=5)
535 1
        res = r.text.strip()
536 1
        msg = f"Använd detta meddelandet: '{res}'"
537 1
        return msg
538 1
    except Exception as e:
539 1
        LOG.error("Failed to get commit message: %s", e)
540 1
        return getString("commit", "error")
541
542 1
def marvinCommit(row):
543
    """
544
    Display a random commit message
545
    """
546 1
    msg = None
547 1
    if any(r in row for r in ["commit", "-m"]):
548 1
        msg = getCommit()
549
    return msg
550