Repository: opsdroid/opsdroid

Issue:
Move redis database module into core
This issue covers adding the [Redis database module](https://github.com/opsdroid/database-redis) to core.

## Background
A while ago we began moving connectors and databases from external plugins into the core of the project (see #185 for more context). We started with [slack](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/connector/slack/__init__.py) and [websockets](https://github.com/opsdroid/opsdroid/blob/master/opsdroid/connector/websocket/__init__.py) connectors but need to go through all the other existing plugins and move them into the core.

## Steps

 - Make a new submodule directory in [`opsdroid.database`](https://github.com/opsdroid/opsdroid/tree/master/opsdroid/database) and copy the database code over.
 - Update the [`requirements.txt`](https://github.com/opsdroid/opsdroid/blob/master/requirements.txt) with any dependencies from the database if necessary.
 - Write tests for the database. (See the [Slack connector tests](https://github.com/jacobtomlinson/opsdroid/blob/master/tests/test_connector_slack.py) for inspiration).
 - Copy the relevant information from the database `README.md` into a [new documentation page](https://github.com/opsdroid/opsdroid/tree/master/docs/connectors).
 - Add the new page to the [mkdocs.yml](https://github.com/opsdroid/opsdroid/blob/master/mkdocs.yml).
 - Add to the [list of databases](https://github.com/opsdroid/opsdroid/blob/master/docs/configuration-reference.md#database-modules).
 - Add a deprecation notice to the old database module. (See [the slack connector](https://github.com/opsdroid/connector-slack))


Discussion:
I will go ahead and tackle this issue.

Resolving patch:
diff --git a/opsdroid/database/redis/__init__.py b/opsdroid/database/redis/__init__.py
new file mode 100644
--- /dev/null
+++ b/opsdroid/database/redis/__init__.py
@@ -0,0 +1,125 @@
+"""Module for storing data within Redis."""
+from datetime import date, datetime
+import json
+import time
+
+import asyncio_redis
+
+from opsdroid.database import Database
+
+
+class RedisDatabase(Database):
+    """Database class for storing data within a Redis instance."""
+
+    def __init__(self, config, opsdroid=None):
+        """Initialise the sqlite database.
+
+        Set basic properties of the database. Initialise properties like
+        name, connection arguments, database file, table name and config.
+
+        Args:
+            config (dict): The configuration of the database which consists
+                           of `file` and `table` name of the sqlite database
+                           specified in `configuration.yaml` file.
+
+        """
+        super().__init__(config, opsdroid=opsdroid)
+        self.config = config
+        self.client = None
+        self.host = self.config.get("host", "localhost")
+        self.port = self.config.get("port", 6379)
+        self.database = self.config.get("database", 0)
+        self.password = self.config.get("password", None)
+        self.reconnect = self.config.get("reconnect", False)
+
+    async def connect(self):
+        """Connect to the database.
+
+        This method will connect to a Redis database. By default it will
+        connect to Redis on localhost on port 6379
+
+        """
+        self.client = await asyncio_redis.Connection.create(
+            host=self.host,
+            port=self.port,
+            db=self.database,
+            auto_reconnect=self.reconnect,
+            password=self.password,
+        )
+
+    async def put(self, key, data):
+        """Store the data object in Redis against the key.
+
+        Args:
+            key (string): The key to store the data object under.
+            data (object): The data object to store.
+
+        """
+        data = self.convert_object_to_timestamp(data)
+        await self.client.set(key, json.dumps(data))
+
+    async def get(self, key):
+        """Get data from Redis for a given key.
+
+        Args:
+            key (string): The key to lookup in the database.
+
+        Returns:
+            object or None: The data object stored for that key, or None if no
+                            object found for that key.
+
+        """
+        data = await self.client.get(key)
+
+        if data:
+            return self.convert_timestamp_to_object(json.loads(data))
+
+        return None
+
+    async def disconnect(self):
+        """Disconnect from the database."""
+        self.client.close()
+
+    @staticmethod
+    def convert_object_to_timestamp(data):
+        """
+        Serialize dict before storing into Redis.
+
+        Args:
+            dict: Dict to serialize
+
+        Returns:
+            dict: Dict from redis to unserialize
+
+        """
+        for k, value in data.items():
+            if isinstance(value, (datetime, date)):
+                value = '::'.join([
+                    type(value).__name__,
+                    '%d' % time.mktime(value.timetuple())
+                ])
+                data[k] = value
+        return data
+
+    @staticmethod
+    def convert_timestamp_to_object(data):
+        """
+        Unserialize data from Redis.
+
+        Args:
+            dict: Dict from redis to unserialize
+
+        Returns:
+            dict: Dict to serialize
+
+        """
+        for k, value in data.items():
+            value_type = value.split('::', 1)[0]
+            if value_type == 'datetime':
+                timestamp = int(value.split('::', 1)[1])
+                value = datetime.fromtimestamp(timestamp)
+            elif value_type == 'date':
+                timestamp = int(value.split('::', 1)[1])
+                value = date.fromtimestamp(timestamp)
+            data[k] = value
+        return data


Test patch:
diff --git a/tests/test_database_redis.py b/tests/test_database_redis.py
new file mode 100644
--- /dev/null
+++ b/tests/test_database_redis.py
@@ -0,0 +1,117 @@
+import asyncio
+import datetime
+import unittest
+
+import asynctest
+import asynctest.mock as amock
+
+from contextlib import suppress
+from opsdroid.database.redis import RedisDatabase
+from opsdroid.__main__ import configure_lang
+
+
+class MockRedisClient:
+    execute = None
+
+
+class TestRedisDatabase(unittest.TestCase):
+    """Test the opsdroid Redis database class."""
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+        configure_lang({})
+
+    def test_init(self):
+        """Test initialisation of database class.
+
+        This method will test the initialisation of the database
+        class. It will assert if the database class properties are
+        declared and equated to None.
+
+        """
+        database = RedisDatabase({})
+        self.assertEqual(None, database.client)
+        self.assertEqual(0, database.database)
+        self.assertEqual("localhost", database.host)
+        self.assertEqual(6379, database.port)
+        self.assertEqual(None, database.password)
+
+    def test_other(self):
+        unserialized_data = {
+            "example_string": "test",
+            "example_datetime": datetime.datetime.utcfromtimestamp(1538389815),
+            "example_date": datetime.date.fromtimestamp(1538366400),
+        }
+
+        serialized_data = RedisDatabase.convert_object_to_timestamp(unserialized_data)
+
+        self.assertEqual(serialized_data["example_string"], "test")
+        # Typically I would do assertDictEqual on the result, but as datetime are parsed based on the
+        # timezone of the computer it makes the unittest fragile depending on the timezone of the user.
+        self.assertEqual(serialized_data["example_datetime"][0:10], "datetime::")
+        self.assertEqual(serialized_data["example_date"][0:6], "date::")
+
+    def test_convert_timestamp_to_object(self):
+        serialized_data = {
+            "example_date": "date::1538366400",
+            "example_datetime": "datetime::1538389815",
+            "example_string": "test"
+        }
+
+        unserialized_data = RedisDatabase.convert_timestamp_to_object(serialized_data)
+
+        self.assertEqual(unserialized_data["example_string"], "test")
+        # Typically I would do assertDictEqual on the result, but as datetime are parsed based on the
+        # timezone of the computer it makes the unittest fragile depending on the timezone of the user.
+        self.assertIsInstance(unserialized_data["example_datetime"], datetime.datetime)
+        self.assertIsInstance(unserialized_data["example_date"], datetime.date)
+
+
+class TestRedisDatabaseAsync(asynctest.TestCase):
+    """Test the opsdroid Redis Database class."""
+
+    async def test_connect(self):
+        opsdroid = amock.CoroutineMock()
+        database = RedisDatabase({}, opsdroid=opsdroid)
+        import asyncio_redis
+        with amock.patch.object(asyncio_redis.Connection, 'create') as mocked_connection:
+            mocked_connection.side_effect = NotImplementedError
+
+            with suppress(NotImplementedError):
+                await database.connect()
+                self.assertTrue(mocked_connection.called)
+
+    async def test_get(self):
+        db = RedisDatabase({})
+        db.client = MockRedisClient()
+        db.client.get = amock.CoroutineMock(return_value='{"key":"value"}')
+
+        result = await db.get("string")
+
+        self.assertDictEqual(result, dict(key="value"))
+
+    async def test_get_return_None(self):
+        db = RedisDatabase({})
+        db.client = MockRedisClient()
+        db.client.get = amock.CoroutineMock(return_value=None)
+
+        result = await db.get("string")
+
+        self.assertEqual(result, None)
+
+    async def test_put(self):
+        db = RedisDatabase({})
+        db.client = MockRedisClient()
+        db.client.set = amock.CoroutineMock(return_value='{"key":"value"}')
+
+        result = await db.put("string", dict(key="value"))
+
+    async def test_disconnect(self):
+        db = RedisDatabase({})
+        db.client = MockRedisClient()
+        db.client.close = amock.CoroutineMock()
+
+        result = await db.disconnect()
+
+        self.assertTrue(db.client.close.called)
+