Repository: mindsdb/mindsdb

Issue:
[New Integration]: Integration with CKAN
### Is there an existing integration?

- [X] I have searched the existing integrations.

### Use Case

CKAN is an open-source DMS (data management system) for powering data hubs and data portals. It not only stores the data, but makes it discoverable and presentable. Having integration with the ML for its DataStore would be additon for better data presentations, visualizations and makes it smarter 

### Motivation

Having this integration implemented will bring the ML capabilities into the CKAN ecosystem without thee needs of use to external tools or libraries. 

### Implementation

_No response_

### Anything else?

_No response_


Discussion:

Resolving patch:
diff --git a/mindsdb/integrations/handlers/ckan_handler/__about__.py b/mindsdb/integrations/handlers/ckan_handler/__about__.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/ckan_handler/__about__.py
@@ -0,0 +1,9 @@
+__title__ = 'MindsDB CKAN handler'
+__package_name__ = 'mindsdb_ckan_handler'
+__version__ = '0.0.1'
+__description__ = "MindsDB handler for CKAN"
+__author__ = 'Konstantin Sivakov'
+__github__ = 'https://github.com/mindsdb/mindsdb'
+__pypi__ = 'https://pypi.org/project/mindsdb/'
+__license__ = 'GPL-3.0'
+__copyright__ = 'Copyright 2022- mindsdb'
diff --git a/mindsdb/integrations/handlers/ckan_handler/__init__.py b/mindsdb/integrations/handlers/ckan_handler/__init__.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/ckan_handler/__init__.py
@@ -0,0 +1,19 @@
+from mindsdb.integrations.libs.const import HANDLER_TYPE
+
+from mindsdb.integrations.handlers.ckan_handler.__about__ import __version__ as version, __description__ as description
+try:
+    from mindsdb.integrations.handlers.ckan_handler import CkanHandler as Handler
+    import_error = None
+except Exception as e:
+    Handler = None
+    import_error = e
+
+title = 'CKAN'
+name = 'ckan'
+type = HANDLER_TYPE.DATA
+icon_path = 'icon.svg'
+
+__all__ = [
+    'Handler', 'version', 'name', 'type', 'title',
+    'description', 'import_error', 'icon_path'
+]
diff --git a/mindsdb/integrations/handlers/ckan_handler/ckan_handler.py b/mindsdb/integrations/handlers/ckan_handler/ckan_handler.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/ckan_handler/ckan_handler.py
@@ -0,0 +1,99 @@
+from ckanapi import RemoteCKAN as rc
+import pandas as pd
+
+from mindsdb.integrations.libs.base_handler import DatabaseHandler
+from mindsdb.integrations.libs.response import HandlerStatusResponse, HandlerResponse, RESPONSE_TYPE
+from mindsdb_sql.parser.ast.base import ASTNode
+from mindsdb_sql.render.sqlalchemy_render import SqlalchemyRender
+from mindsdb.utilities.log import log
+
+
+class CkanHandler(DatabaseHandler):
+    """
+    This handler handles connection and consumption of the CKAN API.
+    """
+    name = "ckan"
+
+    def __init__(self, name=None, **kwargs):
+        super().__init__(name)
+        self.type = 'ckan'
+        self.name = name
+        self.connection_args = kwargs.get('connection_data')
+        self.ckan = None
+        self.dialect = 'postgresql'
+        self.renderer = SqlalchemyRender('postgres')
+        self.connection = None
+        self.is_connected = False
+
+    def connect(self) -> HandlerStatusResponse:
+        """
+        Handles the connection to a CKAN remote portal instance.
+        """
+        url = self.connection_args.get('url')
+        try:
+            ckan = rc(url)
+            self.is_connected = True
+            self.ckan = ckan
+        except Exception as e:
+            return HandlerStatusResponse(False, f'Failed to connect to CKAN: {e}')
+        self.connection = ckan
+        return HandlerStatusResponse(True)
+
+    def disconnect(self):
+        self.is_connected = False
+
+    def check_connection(self) -> HandlerStatusResponse:
+        response = HandlerStatusResponse(False)
+        try:
+            self.connect()
+            result = self.ckan.action.status_show()
+            if 'datastore' not in result.get('extensions'):
+                """
+                If the CKAN instance does not have the datastore extension,
+                we can't use it.
+                """
+                response.message = 'CKAN datastore is not enabled'
+                response.status = False
+                self.is_connected = False
+                return response
+
+        except Exception as e:
+            log.error(f'Error connecting to CKAN: {e}!')
+            self.is_connected = False
+            response.error_message = e
+
+        if response.success is False and self.is_connected is True:
+            self.is_connected = False
+
+        response.success = True
+        return response
+
+    def query(self, query: ASTNode) -> HandlerResponse:
+        if not self.ckan:
+            self.connect()
+        query_str = self.renderer.get_string(query, with_failback=True)
+        return self.native_query(query_str)
+
+    def native_query(self, query_str: str) -> HandlerResponse:
+        """
+        Execute a query on the CKAN instance.
+        :param query_str: The query to execute.
+        :return: The response of the query.
+        """
+        if not self.ckan:
+            self.connect()
+        result = self.ckan.action.datastore_search_sql(sql=query_str)
+        if len(result.get('records')) > 0:
+            df = pd.DataFrame(result['records'])
+            response = HandlerResponse(RESPONSE_TYPE.TABLE, df)
+        else:
+            response = HandlerResponse(RESPONSE_TYPE.TABLE, None)
+        return response
+
+    def get_tables(self) -> HandlerResponse:
+        if not self.ckan:
+            self.connect()
+        result = self.ckan.action.datastore_search(resource_id='_table_metadata')
+        df = pd.DataFrame(result['records'])
+        response = HandlerResponse(RESPONSE_TYPE.TABLE, df)
+        return response


Test patch:
diff --git a/mindsdb/integrations/handlers/ckan_handler/tests/__init__.py b/mindsdb/integrations/handlers/ckan_handler/tests/__init__.py
new file mode 100644
diff --git a/mindsdb/integrations/handlers/ckan_handler/tests/test_ckan_handler.py b/mindsdb/integrations/handlers/ckan_handler/tests/test_ckan_handler.py
new file mode 100644
--- /dev/null
+++ b/mindsdb/integrations/handlers/ckan_handler/tests/test_ckan_handler.py
@@ -0,0 +1,28 @@
+import unittest
+
+from mindsdb.integrations.handlers.ckan_handler.ckan_handler import CkanHandler
+from mindsdb.api.mysql.mysql_proxy.libs.constants.response_type import RESPONSE_TYPE
+
+
+class CkanHandlerTest(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        cls.kwargs = {
+            "connection_data": {
+                "url": "http://demo.ckan.org/"}
+        }
+        cls.handler = CkanHandler('test_ckan_handler', **cls.kwargs)
+
+    def test_connect(self):
+        self.handler.connect()
+
+    def test_query(self):
+        self.handler.query("SELECT * from 'b53c9e72-6b59-4cda-8c0c-7d6a51dad12a'")
+
+    def test_disconnect(self):
+        self.handler.disconnect()
+
+    def test_get_tables(self):
+        tables = self.handler.get_tables()
+        assert tables['type'] is not RESPONSE_TYPE.ERROR