Test Failed
Pull Request — master (#86)
by Daniel
06:22 queued 03:08
created

irc2phpbb.marvin_actions   F

Complexity

Total Complexity 91

Size/Duplication

Total Lines 554
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 91
eloc 335
dl 0
loc 554
rs 2
c 0
b 0
f 0

33 Functions

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

How to fix   Complexity   

Complexity

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