Repository: opsdroid/opsdroid

Issue:
Move the sqlite database module into core
This issue covers adding the [sqlite database module](https://github.com/opsdroid/database-sqlite) 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:
Hi @jacobtomlinson can i take this issue ?
Hello @vshelke  please go ahead and work on this issue, let us know if you encounter any problems or if you need any help 👍 

Resolving patch:
diff --git a/opsdroid/database/sqlite/__init__.py b/opsdroid/database/sqlite/__init__.py
new file mode 100644
--- /dev/null
+++ b/opsdroid/database/sqlite/__init__.py
@@ -0,0 +1,236 @@
+"""A module for sqlite database."""
+import os
+import logging
+import json
+import datetime
+import aiosqlite
+
+from opsdroid.const import DEFAULT_ROOT_PATH
+from opsdroid.database import Database
+
+_LOGGER = logging.getLogger(__name__)
+
+# pylint: disable=too-few-public-methods
+# As the current module needs only one public method to register json types
+
+
+class DatabaseSqlite(Database):
+    """A sqlite database class.
+
+    SQLite Database class used to persist data in sqlite.
+
+    """
+
+    def __init__(self, config):
+        """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)
+        self.name = "sqlite"
+        self.config = config
+        self.conn_args = {'isolation_level': None}
+        self.db_file = None
+        self.table = None
+        _LOGGER.debug(_("Loaded sqlite database connector"))
+
+    async def connect(self, opsdroid):
+        """Connect to the database.
+
+        This method will connect to the sqlite database. It will create
+        the database file named `sqlite.db` in DEFAULT_ROOT_PATH and set
+        the table name to `opsdroid`. It will create the table if it does
+        not exist in the database.
+
+        Args:
+            opsdroid (OpsDroid): An instance of opsdroid core.
+
+        """
+        self.db_file = self.config.get(
+            "file", os.path.join(DEFAULT_ROOT_PATH, "sqlite.db"))
+        self.table = self.config.get("table", "opsdroid")
+
+        async with aiosqlite.connect(self.db_file, **self.conn_args) as _db:
+            await _db.execute(
+                "CREATE TABLE IF NOT EXISTS {}"
+                "(key text PRIMARY KEY, data text)"
+                .format(self.table)
+            )
+
+        self.client = _db
+        _LOGGER.info(_("Connected to sqlite %s"), self.db_file)
+
+    async def put(self, key, data):
+        """Put data into the database.
+
+        This method will insert or replace an object into the database for
+        a given key. The data object is serialised into JSON data using the
+        JSONEncoder class.
+
+        Args:
+            key (string): The key to store the data object under.
+            data (object): The data object to store.
+
+        """
+        _LOGGER.debug(_("Putting %s into sqlite"), key)
+        json_data = json.dumps(data, cls=JSONEncoder)
+
+        async with aiosqlite.connect(self.db_file, **self.conn_args) as _db:
+            cur = await _db.cursor()
+            await cur.execute(
+                "DELETE FROM {} WHERE key=?".format(self.table), (key,))
+            await cur.execute(
+                "INSERT INTO {} VALUES (?, ?)".format(self.table),
+                (key, json_data))
+
+        self.client = _db
+
+    async def get(self, key):
+        """Get data from the database 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.
+
+        """
+        _LOGGER.debug(_("Getting %s from sqlite"), key)
+        data = None
+
+        async with aiosqlite.connect(self.db_file, **self.conn_args) as _db:
+            cur = await _db.cursor()
+            await cur.execute(
+                "SELECT data FROM {} WHERE key=?".format(self.table),
+                (key,))
+            row = await cur.fetchone()
+            if row:
+                data = json.loads(row[0], object_hook=JSONDecoder())
+
+        self.client = _db
+        return data
+
+
+class JSONEncoder(json.JSONEncoder):
+    """A extended JSONEncoder class.
+
+    This class is customised JSONEncoder class which helps to convert
+    dict to JSON. The datetime objects are converted to dict with fields
+    as keys.
+
+    """
+
+    # pylint: disable=method-hidden
+    # See https://github.com/PyCQA/pylint/issues/414 for reference
+
+    serializers = {}
+
+    def default(self, o):
+        """Convert the given datetime object to dict.
+
+        Args:
+            o (object): The datetime object to be marshalled.
+
+        Returns:
+            dict (object): A dict with datatime object data.
+
+        Example:
+            A dict which is returned after marshalling::
+
+                {
+                    "__class__": "datetime",
+                    "year": 2018,
+                    "month": 10,
+                    "day": 2,
+                    "hour": 0,
+                    "minute": 41,
+                    "second": 17,
+                    "microsecond": 74644
+                }
+
+        """
+        marshaller = self.serializers.get(
+            type(o), super(JSONEncoder, self).default)
+        return marshaller(o)
+
+
+class JSONDecoder():
+    """A JSONDecoder class.
+
+    This class will convert dict containing datetime values
+    to datetime objects.
+
+    """
+
+    decoders = {}
+
+    def __call__(self, dct):
+        """Convert given dict to datetime objects.
+
+        Args:
+            dct (object): A dict containing datetime values and class type.
+
+        Returns:
+            object or dct: The datetime object for given dct, or dct if
+                             respective class decoder is not found.
+
+        Example:
+            A datetime object returned after decoding::
+
+                datetime.datetime(2018, 10, 2, 0, 41, 17, 74644)
+
+        """
+        if dct.get('__class__') in self.decoders:
+            return self.decoders[dct['__class__']](dct)
+        return dct
+
+
+def register_json_type(type_cls, fields, decode_fn):
+    """Register JSON types.
+
+    This method will register the serializers and decoders for the
+    JSONEncoder and JSONDecoder classes respectively.
+
+    Args:
+        type_cls (object): A datetime object.
+        fields (list): List of fields used to store data in dict.
+        decode_fn (object): A lambda function object for decoding.
+
+    """
+    type_name = type_cls.__name__
+    JSONEncoder.serializers[type_cls] = lambda obj: dict(
+        __class__=type_name,
+        **{field: getattr(obj, field) for field in fields}
+    )
+    JSONDecoder.decoders[type_name] = decode_fn
+
+
+register_json_type(
+    datetime.datetime,
+    ['year', 'month', 'day', 'hour', 'minute', 'second', 'microsecond'],
+    lambda dct: datetime.datetime(
+        dct['year'], dct['month'], dct['day'],
+        dct['hour'], dct['minute'], dct['second'], dct['microsecond']
+    )
+)
+
+register_json_type(
+    datetime.date,
+    ['year', 'month', 'day'],
+    lambda dct: datetime.date(dct['year'], dct['month'], dct['day'])
+)
+
+register_json_type(
+    datetime.time,
+    ['hour', 'minute', 'second', 'microsecond'],
+    lambda dct: datetime.time(
+        dct['hour'], dct['minute'], dct['second'], dct['microsecond'])
+)


Test patch:
diff --git a/tests/test_database_sqlite.py b/tests/test_database_sqlite.py
new file mode 100644
--- /dev/null
+++ b/tests/test_database_sqlite.py
@@ -0,0 +1,153 @@
+"""Tests for the DatabaseSqlite class."""
+import asyncio
+import json
+import datetime
+
+import unittest
+import unittest.mock as mock
+import asynctest
+import asynctest.mock as amock
+
+from opsdroid.database.sqlite import DatabaseSqlite, JSONEncoder, JSONDecoder
+from opsdroid.database.sqlite import register_json_type
+
+
+class TestDatabaseSqlite(unittest.TestCase):
+    """A database test class.
+
+    Test the opsdroid sqlite database class.
+
+    """
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+
+    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 = DatabaseSqlite({"file": "sqlite.db"})
+        self.assertEqual(None, database.client)
+        self.assertEqual(None, database.database)
+        self.assertEqual(None, database.db_file)
+        self.assertEqual(None, database.table)
+        self.assertEqual({'isolation_level': None}, database.conn_args)
+
+
+class TestDatabaseSqliteAsync(asynctest.TestCase):
+    """A async database test class.
+
+    Test the async methods of the opsdroid sqlite database class.
+
+    """
+
+    async def test_connect(self):
+        """Test database connection.
+
+        This method will test the database connection of sqlite database.
+        As the database is created `opsdroid` table is created first.
+
+        """
+        database = DatabaseSqlite({"file": "sqlite.db"})
+        opsdroid = amock.CoroutineMock()
+        opsdroid.eventloop = self.loop
+
+        try:
+            await database.connect(opsdroid)
+        except NotImplementedError:
+            raise Exception
+        else:
+            self.assertEqual("opsdroid", database.table)
+            self.assertEqual("Connection", type(database.client).__name__)
+
+    async def test_get_and_put(self):
+        """Test get and put functions of database
+
+        This method will test the get and put functions which help to read
+        and write data from the database. The function `put` a value with
+        key and asserts the same value after the `get` operation is completed.
+
+        """
+        database = DatabaseSqlite({"file": "sqlite.db"})
+        opsdroid = amock.CoroutineMock()
+        opsdroid.eventloop = self.loop
+
+        try:
+            await database.connect(opsdroid)
+            await database.put("hello", {})
+            data = await database.get("hello")
+        except NotImplementedError:
+            raise Exception
+        else:
+            self.assertEqual("opsdroid", database.table)
+            self.assertEqual({}, data)
+            self.assertEqual("Connection", type(database.client).__name__)
+
+
+class TestJSONEncoder(unittest.TestCase):
+    """A JSON Encoder test class.
+
+    Test the custom json encoder class.
+
+    """
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+
+    def test_datetime_to_dict(self):
+        """Test default of json encoder class.
+
+        This method will test the conversion of the datetime
+        object to dict.
+
+        """
+        type_cls = datetime.datetime
+        test_obj = datetime.datetime(2018, 10, 2, 0, 41, 17, 74644)
+        encoder = JSONEncoder()
+        obj = encoder.default(o=test_obj)
+        self.assertEqual({
+            "__class__": type_cls.__name__,
+            "year": 2018,
+            "month": 10,
+            "day": 2,
+            "hour": 0,
+            "minute": 41,
+            "second": 17,
+            "microsecond": 74644
+        }, obj)
+
+
+class TestJSONDecoder(unittest.TestCase):
+    """A JSON Decoder test class.
+
+    Test the custom json decoder class.
+
+    """
+
+    def setUp(self):
+        self.loop = asyncio.new_event_loop()
+
+    def test_dict_to_datetime(self):
+        """Test call of json decoder class.
+
+        This method will test the conversion of the dict to
+        datetime object.
+
+        """
+        test_obj = {
+            "__class__": datetime.datetime.__name__,
+            "year": 2018,
+            "month": 10,
+            "day": 2,
+            "hour": 0,
+            "minute": 41,
+            "second": 17,
+            "microsecond": 74644
+        }
+        decoder = JSONDecoder()
+        obj = decoder(test_obj)
+        self.assertEqual(datetime.datetime(2018, 10, 2, 0, 41, 17, 74644), obj)