diff --git a/config.py b/config.py index 657937b..99726cd 100644 --- a/config.py +++ b/config.py @@ -1,3 +1,6 @@ +# pylint: disable=C0103 +""" Configuration classes and global_config. """ + import json from os import path @@ -6,7 +9,7 @@ from frozen import FrozenClass class ConfigDict(FrozenClass): """ ConfigDict is a dict with attributes as it's contents. """ - + def __init__(self, parent=None, **kwargs): """ Takes a dictionary and stores them as attributes. """ # get all items from kwargs and assigned them as attributes @@ -16,45 +19,44 @@ class ConfigDict(FrozenClass): setattr(self, k, ConfigDict(parent=self, **v)) else: setattr(self, k, v) - + self._parent = parent # freeze object self._freeze() - + def __getitem__(self, attr): """ Makes ConfigDict subscriptable. """ return getattr(self, attr) - + def __contains__(self, attr): return True if attr in self.__dict__.keys() else False - + def to_dict(self): + """ Converts ConfigDict to dict. """ return { - k: v + k: v for k, v in self.__dict__.items() if not k.startswith("_")} - + class Configuration(ConfigDict): """ Configuration is a ConfigDict with ability to read json files. """ - + def __init__(self, fp): """ Takes a file-like or filename fp and stores it as attributes. """ close = False # if fp is str convert to file-like - if type(fp) is str: + if isinstance(fp, str): fp = open(fp, "r") close = True - - # call super + + # call super super().__init__(parent=self, **json.load(fp)) - + if close: fp.close() - - + + # global_config is the global configuration for the file global_config = Configuration( path.join(path.dirname(__file__), "config.json")) - - \ No newline at end of file diff --git a/config_test.py b/config_test.py index 7fd8971..bb0bc17 100644 --- a/config_test.py +++ b/config_test.py @@ -1,3 +1,5 @@ +# pylint: disable=C0414,C0111,W0104 + import unittest import os.path as path @@ -6,23 +8,22 @@ from config import Configuration class ConfigurationTestCase(unittest.TestCase): - + def setUp(self): dirname = path.dirname(__file__) self.config_file = path.join(dirname, "config.example.json") self.config = Configuration(self.config_file) - + def test_mutation(self): with self.assertRaises(FrozenException): self.config.discord.token = "hello" - + def test_missing_attr(self): with self.assertRaises(AttributeError): self.config.not_here - + def test_working_attr(self): self.assertTrue(self.config.discord.token, "TOKEN_HERE") - + def test_working_subscript(self): self.assertTrue(self.config["discord"]["token"], "TOKEN_HERE") - \ No newline at end of file diff --git a/datastore.py b/datastore.py index 6ea1e7d..9c03482 100644 --- a/datastore.py +++ b/datastore.py @@ -1,3 +1,5 @@ +# pylint: disable=C0103 +""" All things redis related. """ import redis from config import global_config @@ -20,4 +22,4 @@ def create_datastore(**kwargs): if "redis" in global_config: global_datastore = create_datastore(**global_config.redis.to_dict()) else: - global_datastore = create_datastore(**_default_config) \ No newline at end of file + global_datastore = create_datastore(**_default_config) diff --git a/datastore_test.py b/datastore_test.py index ed289c7..d774dff 100644 --- a/datastore_test.py +++ b/datastore_test.py @@ -1,10 +1,11 @@ +# pylint: disable=C0414,C0111,W0104 import unittest from datastore import global_datastore class DataStoreTestCase(unittest.TestCase): - + def test_set(self): self.assertTrue(global_datastore.set("hello", "world")) self.assertEqual(global_datastore.get("hello"), "world") diff --git a/frozen.py b/frozen.py index 723a129..aba1224 100644 --- a/frozen.py +++ b/frozen.py @@ -1,21 +1,20 @@ +""" FrozenClass and related. """ class FrozenException(Exception): - - def __init__(self, message): - super().__init__(message) - - + """ Is raised when a FrozenClass object is mutated. """ + + class FrozenClass(object): - + """ Prevents mutation after initialisation. + Designed to be inherited from. """ + _frozen = False - + def __setattr__(self, name, value): if self._frozen: raise FrozenException("This object is immutable") - + super().__setattr__(name, value) - + def _freeze(self): self._frozen = True - - \ No newline at end of file diff --git a/handlers/dice.py b/handlers/dice.py index 8070fd3..43b1652 100644 --- a/handlers/dice.py +++ b/handlers/dice.py @@ -1,3 +1,5 @@ +""" DiceService and related. """ + import random import math @@ -5,15 +7,18 @@ from .service import Service class DiceService(Service): + """ Service that provides dice related functions. """ def roll_dice(self, sides=6): + """ Returns the result of a dice roll. """ accepted = (4, 6, 8, 10, 12, 20, 100, math.inf) if not isinstance(sides, (int, float)): raise TypeError("sides must be numeric.") if sides not in accepted: - raise ValueError(("{} is not a valid number of sides. " - + "Valid sides are {}").format(sides, accepted)) - + raise ValueError( + ("{} is not a valid number of sides. " + + "Valid sides are {}").format(sides, accepted)) + # if sides is math.inf, return it if sides == math.inf: return math.inf diff --git a/handlers/dice_test.py b/handlers/dice_test.py index f8b75a9..4e115bc 100644 --- a/handlers/dice_test.py +++ b/handlers/dice_test.py @@ -1,3 +1,5 @@ +# pylint: disable=C0414,C0111,C0103,W0104 + import unittest import math @@ -5,10 +7,10 @@ from .dice import DiceService class DiceServiceTestCase(unittest.TestCase): - + def setUp(self): self.ds = DiceService() - + def test_valid_dx_roll(self): valid = (4, 6, 8, 10, 12, 20, 100) for d in valid: @@ -18,13 +20,13 @@ class DiceServiceTestCase(unittest.TestCase): def test_inf_roll(self): roll = self.ds.roll_dice(math.inf) self.assertEqual(roll, math.inf) - + def test_invalid_dx_roll(self): with self.assertRaises(ValueError): self.ds.roll_dice(9) self.ds.roll_dice(5) self.ds.roll_dice(6.0) - + def test_type_roll(self): with self.assertRaises(TypeError): self.ds.roll_dice("hello world") diff --git a/handlers/joke.py b/handlers/joke.py index 4aca8fe..53a1344 100644 --- a/handlers/joke.py +++ b/handlers/joke.py @@ -1,10 +1,13 @@ +""" JokeService and related. """ + import random from .service import Service class JokeService(Service): - + """ Services that provides jokes and other comedic things. """ + _jokes = ( "Why did the chicken cross the road?\nTo get to the other side.", "Why do keyboards work 24/7?\nBecause they have two shifts.", @@ -17,6 +20,7 @@ class JokeService(Service): "What do you call a cow in an earthquake?\nA milkshake.", "Where do animals go when their tails fall off?\nThe retail store.", ) - + def random_joke(self): + """ Returns a random joke. """ return random.choice(self._jokes) diff --git a/handlers/joke_test.py b/handlers/joke_test.py index df706c5..9ba7490 100644 --- a/handlers/joke_test.py +++ b/handlers/joke_test.py @@ -1,12 +1,14 @@ +# pylint: disable=C0414,C0111,C0103,W0104,W0212 + import unittest from .joke import JokeService class JokeServiceTestCase(unittest.TestCase): - + def setUp(self): self.js = JokeService() - + def test_random_joke(self): self.assertTrue(self.js.random_joke() in self.js._jokes) diff --git a/handlers/location.py b/handlers/location.py index cc376c1..b00da82 100644 --- a/handlers/location.py +++ b/handlers/location.py @@ -1,4 +1,4 @@ -import json +""" Location services provided by LocationService class. """ import requests @@ -7,13 +7,15 @@ from .service import Service, register_service @register_service("location") class LocationService(Service): - + """ Service that provides location related functions. """ + def __init__(self): super().__init__() self._api_endpoint = "https://nominatim.openstreetmap.org/" - + def get_location_coords(self, location: str) -> dict: + """ Queries DarkSky and returns the data as a dictionary. """ query = "{}/search?q={}&format=json".format(self._api_endpoint, location) r = requests.get(query) diff --git a/handlers/location_test.py b/handlers/location_test.py index 71d91aa..6c6cba1 100644 --- a/handlers/location_test.py +++ b/handlers/location_test.py @@ -1,10 +1,12 @@ +# pylint: disable=C0414,C0111,C0103,W0104 + import unittest from .location import LocationService class LocationServiceTestCase(unittest.TestCase): - + def setUp(self): self.ls = LocationService() diff --git a/handlers/pickle.py b/handlers/pickle.py deleted file mode 100644 index 53d7cf7..0000000 --- a/handlers/pickle.py +++ /dev/null @@ -1,20 +0,0 @@ -from io import BytesIO -from tempfile import NamedTemporaryFile - -from PIL import Image -import requests - -from .service import Service, register_service - - -@register_service("pickle") -class PickleService(Service): - - def pickle_user(self, avatar_url: str): - res = requests.get(avatar_url) - print(avatar_url) - image = Image.open(BytesIO(res.content)) - image = image.rotate(180) - temp = NamedTemporaryFile() - image.save(temp, format="jpeg") - return temp \ No newline at end of file diff --git a/handlers/service.py b/handlers/service.py index 24753ea..d085ec1 100644 --- a/handlers/service.py +++ b/handlers/service.py @@ -1,3 +1,6 @@ +# pylint: disable=C0413,C0103 +""" Service base clase and global_services. """ + import sys import os sys.path.insert(0, os.path.abspath("..")) @@ -8,7 +11,7 @@ from config import ConfigDict, global_config global_services = {} def register_service(name): - global services + """ Registers a service globally. """ def wrap(c): if name in global_config.services: # create service using global_config @@ -23,14 +26,14 @@ def register_service(name): return c return wrap - + class Service(object): - + """ Service base class. """ + def __init__(self, config=ConfigDict()): if isinstance(config, dict): config = ConfigDict(**config) elif not isinstance(config, ConfigDict): raise TypeError("config must be dict or ConfigDict") - + self.config = config - \ No newline at end of file diff --git a/handlers/weather.py b/handlers/weather.py index 48c4bcf..7b525f9 100644 --- a/handlers/weather.py +++ b/handlers/weather.py @@ -1,6 +1,7 @@ +""" Weather services """ + from concurrent.futures import ThreadPoolExecutor, wait as future_wait from datetime import datetime, timedelta -from collections import OrderedDict from functools import partial import requests @@ -10,29 +11,32 @@ from .service import Service, register_service @register_service("weather") class WeatherService(Service): - + """ Service that provides data and summaries of weather + at certain times and locations. """ + def __init__(self, config): super().__init__(config) - + self._api_endpoint = "https://api.darksky.net/forecast/{}".format( self.config.token) - + def get_weather_at(self, long, lat, time=None) -> dict: + """ Queries DarkSky and returns weather data as a dictionary. """ epoch = 0 # epoch is the unix timestamp if time is not None: - if type(time) is datetime: - epoch = time.timestamp() - elif type(time) in (int, float): + if isinstance(time, datetime): + epoch = time.timestamp() + elif isinstance(time, (int, float)): epoch = time else: raise TypeError( "time should be datetime.datetime, int, or float") - - if type(long) not in (float, int): + + if not isinstance(long, (float, int)): raise TypeError("long must be numeric") - elif type(lat) not in (float, int): + elif not isinstance(lat, (float, int)): raise TypeError("lat must be numeric") - + # if we have a valid epoch, pass it to the DarkSky API if epoch > 0: res = requests.get("{}/{},{},{:.0f}?units=uk2".format( @@ -41,25 +45,27 @@ class WeatherService(Service): else: res = requests.get("{}/{},{}?units=uk2".format( self._api_endpoint, long, lat)) - + return res.json() - + def get_hourly_summary(self, long, lat, time=None) -> str: + """ Queries DarkSky and returns an hourly summary. """ return self.get_weather_at(long, lat, time)["hourly"]["summary"] - + def get_weekly_summary(self, long, lat, time=datetime.now()) -> str: + """ Queries DarkSky and returns a weekly summary. """ day_pairs = [] # nested function that gets the daily summary for a given time def _get_day(t): w_info = self.get_weather_at(long, lat, t) return (t, w_info["hourly"]["summary"]) - + # nested function that acts as a callback to _get_day future def _update_days(i, f): print(f, i) # insert the result to day_pairs at i day_pairs.insert(i, f.result()) - + futures = [] with ThreadPoolExecutor(max_workers=7) as executor: for i in range(0, 7): @@ -68,9 +74,10 @@ class WeatherService(Service): # pass the index to the callback so it can be ordered f.add_done_callback(partial(_update_days, i)) futures.append(f) - + future_wait(futures) # wait for all futures to complete summary = "" for pair in day_pairs: - summary += "{}: {}\n".format(pair[0].strftime("%A"), pair[1]) + summary += "{}: {}\n".format(pair[0].strftime("%A"), + pair[1]) return summary[:-1] # remove last newline diff --git a/handlers/weather_test.py b/handlers/weather_test.py index 95859eb..4467217 100644 --- a/handlers/weather_test.py +++ b/handlers/weather_test.py @@ -1,3 +1,5 @@ +# pylint: disable=C0414,C0111,C0103,C0413,C0411,W0104 + import sys import os sys.path.insert(0, os.path.abspath("..")) # add parent @@ -10,22 +12,22 @@ from config import Configuration class WeatherServiceTestCase(unittest.TestCase): - + def setUp(self): config = Configuration("config.json") self.ws = WeatherService(config.services.weather) - + def test_get_weather_at(self): self.ws.get_weather_at(0, 0) - + def test_get_weather_at_time(self): self.ws.get_weather_at(0, 0, datetime.now()) self.ws.get_weather_at(0, 0, 1541168113) - + def test_get_weather_at_type_handling(self): with self.assertRaises(TypeError): self.ws.get_weather_at("0", "0") - + def test_get_weather_at_time_type_handling(self): with self.assertRaises(TypeError): self.ws.get_weather_at(0, 0, "hello") diff --git a/run_discord.py b/run_discord.py index 8cf99db..54c7163 100644 --- a/run_discord.py +++ b/run_discord.py @@ -1,13 +1,15 @@ -from os import path +# pylint: disable=C0103 +""" Discord stuff. """ from wrappers import DiscordBot from config import global_config def main(): - b = DiscordBot(global_config.discord) + """ Run Discord bot. """ + b = DiscordBot(global_config.discord) b.start() - - + + if __name__ == "__main__": main() diff --git a/wrappers/bot.py b/wrappers/bot.py index f14f475..fc6057d 100644 --- a/wrappers/bot.py +++ b/wrappers/bot.py @@ -1,3 +1,6 @@ +# pylint: disable=C0413,C0111 +""" ChatBot base class. """ + import sys import os # so we can access ../config.py @@ -5,37 +8,36 @@ sys.path.insert(0, os.path.abspath('..')) from config import ConfigDict from datastore import global_datastore -from handlers import global_services +from handlers import global_services class ChatBot(object): """ ChatBot is an interface for chat bots. """ - + def __init__(self, config=ConfigDict(), services=None, datastore=None): if isinstance(config, dict): config = ConfigDict(**config) elif not isinstance(config, ConfigDict): raise TypeError("config must be dict or ConfigDict") - + self.services = services self.config = config - + if datastore: self.datastore = datastore else: self.datastore = global_datastore - + # get service configs if "services" in self.config and services is None: self.services = { k: v for k, v in global_services.items() if k in self.config.services} - + def start(self): raise NotImplementedError("start is not implemented!") - + def stop(self): raise NotImplementedError("stop is not implemented!") - - \ No newline at end of file + diff --git a/wrappers/discord.py b/wrappers/discord.py index ec4db85..6762bf7 100644 --- a/wrappers/discord.py +++ b/wrappers/discord.py @@ -1,4 +1,6 @@ -import asyncio +# pylint: disable=W0201 +""" Discord bot implementation. """ + import json from datetime import datetime @@ -10,10 +12,11 @@ from .bot import ChatBot class DiscordBot(ChatBot): + """ Discord bot. """ def start(self): self.client = discord.Client() - # nested event handlers because self.client + # nested event handlers because self.client # doesn't exist at class definition scope @self.client.event async def on_ready(): @@ -31,23 +34,24 @@ class DiscordBot(ChatBot): async def _handle_message(self, message): client = self.client - + if message.author.bot: return - + if client.user in message.mentions: # remove the first word message_str = " ".join(message.content.split(" ")[1:]) interpreted = self._interpreter.parse(message_str) - + print(json.dumps(interpreted, indent=2)) - + intent = interpreted["intent"]["name"] entities = interpreted["entities"] if intent == "greet": await client.send_message(message.channel, "Hello World!") elif intent == "get_weather": - await client.send_message(message.channel, + await client.send_message( + message.channel, self.get_weather_summary(message.author, entities)) elif intent == "remember_location": self.remember_user_location(message.author, @@ -60,7 +64,7 @@ class DiscordBot(ChatBot): await client.send_message( message.channel, "You told me you live in {} ({}, {})".format( - l["location_name"], + l["location_name"], l["location_lat"], l["location_long"])) else: @@ -69,18 +73,20 @@ class DiscordBot(ChatBot): "You haven't told me where you live.") def get_weather_summary(self, user, entities: list) -> str: + """ Parses the users request for weather forecast and sends it. """ ws = self.services["weather"] if not entities: # if no entities supplied, return minutely summary w_info = ws.get_weather_at(51.5, 0) return w_info["minutely"]["summary"] - - get_entities = lambda name: [x for x in entities if x["entity"] == name] + + get_entities = lambda name: [ + x for x in entities if x["entity"] == name] periods = get_entities("period") days = get_entities("day") locations = get_entities("location") times = get_entities("time") - + # get user's stored location user_location = self.get_user_location(user) location = [51.5, 0] # default to London @@ -94,8 +100,7 @@ class DiscordBot(ChatBot): float(user_location["location_long"]), float(user_location["location_lat"]), ] - - summary = "" + # test for time periods if periods: # if it's a week, return weekly @@ -106,13 +111,12 @@ class DiscordBot(ChatBot): d = days[0]["value"] if d == "today": return ws.get_hourly_summary(*location, datetime.today()) - + return "I don't understand that request" def remember_user_location(self, user, location): - """ - remember_user_location sets the user's location in the - datastore. + """ remember_user_location sets the user's location in the + datastore. """ key = "user:{}".format(user.id) ls = self.services["location"] @@ -123,7 +127,7 @@ class DiscordBot(ChatBot): "location_lat" : data["latitude"], } return self.datastore.hmset(key, mapping) - + def get_user_location(self, user): """ get_user_location returns the stored user location. """ key = "user:{}".format(user.id)