Repository: mindsdb/mindsdb (language: python)

Issue:
Add IBM Cloud Object Storage Integration
The goal of this issue is to build a new handler to integrate IBM Cloud Object Storage with MindsDB.

## Resources :bulb: 

:checkered_flag: To get started, please refer to the following resources:

* :books:  [Building Application Handler docs](https://docs.mindsdb.com/contribute/app-handlers): This documentation will guide you through the process of building a custom handler for MindsDB. It contains essential information on the handler structure, methods, and best practices.
* :book:  [Object Storage Python SDK](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-python) 
*  :point_right:  [S3 API Handler]https://github.com/mindsdb/mindsdb/tree/main/mindsdb/integrations/handlers/s3_handlerr)

## Next Steps :male_detective: :female_detective: 
 
-   You can familiarize yourself with IBM Cloud Object Storage.
-   Create a new handler that connects MindsDB to IBM Cloud Object Storage.
-   Implement the core features mentioned above.
-   Thoroughly test the integration to ensure proper functionality.
-   Submit a pull request with the new IBM Cloud Object Storage handler.

## :loudspeaker: Additional rewards :trophy:

Each App integration brings :eight:  :zero:  : points for SWAG and entry into the draw for a  :point_down: :
- :computer:  [Razer Blade 16 Laptop](https://www.razer.com/gaming-laptops/Razer-Blade-16/RZ09-0483SEJ3-R3U1)
- :shirt: :bear:  [MindsDB Swag](https://store.covver.io/mindsdb/collections/swag-collection)
:information_source: For more info check out https://mindsdb.com/hacktoberfest/ :point_left: 





Discussion / hints:
I would like to work on this.
For creating an account with IBM cloud, I need an international credit card which I currently don't own. If anybody else wants to give this a try, go ahead.
@ZoranPandovski  Can you assign this to me, as previous assignment has been dropped of. It will be a good experience for me.
### ![New Quest! image](https://quine-assets.s3.amazonaws.com/assets/quest-bot/quest-generic/opening28.svg) New Quest!

*A new Quest has been launched in @mindsdb’s repo.
Merge a PR that solves this issue to loot the Quest and earn your reward.*

---

A badge has been stashed in this issue to reward the solver!

🗡 Comment `@quest-bot embark` to check-in for this Quest and start solving the issue. Other solvers will be notified!

⚔️ When you submit a PR, comment `@quest-bot loot #9958` to link your PR to this Quest.

Questions? Check out [the docs](https://docs.quira.sh/for-developers/quests/solver-quests).
Happy to work on this!

Fix patch:
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/README.md b/mindsdb/integrations/handlers/ibm_cos_handler/README.md
new file mode 100644
index 00000000000..ab330c4b34d
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/README.md
@@ -0,0 +1,98 @@
+---
+title: IBM COS
+sidebarTitle: IBM Cloud Object Storage
+---
+
+This documentation describes the integration of MindsDB with [IBM COS](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-getting-started-cloud-object-storage), an object storage service that offers industry-leading scalability, data availability, security, and performance.
+
+## Prerequisites
+
+Before proceeding, ensure that MindsDB is installed locally via [Docker](/setup/self-hosted/docker) or [Docker Desktop](/setup/self-hosted/docker-desktop).
+
+## Connection
+
+Establish a connection to your IBM COS buckets from MindsDB by executing the following SQL command:
+
+```sql
+CREATE DATABASE ibm_datasource
+WITH ENGINE = 'ibm_cos',
+    PARAMETERS = {
+        'cos_hmac_access_key_id': 'your-access-key-id',
+        'cos_hmac_secret_access_key': 'your-secret-access-key',
+        'cos_endpoint_url': 'https://s3.eu-gb.cloud-object-storage.appdomain.cloud',
+        'bucket': 'your-bucket-name' -- Not required
+    };
+```
+
+<Note>
+Note that sample parameter values are provided here for reference, and you should replace them with your connection parameters.
+</Note>
+
+Required connection parameters include the following:
+
+- `cos_hmac_access_key_id`: The IBM COS access key that identifies the user or IAM role.
+- `cos_hmac_secret_access_key`: The IBM COS secret access key that identifies the user or IAM role.
+- `cos_endpoint_url`: The IBM COS resource ID for your cloud Object Storage.
+
+Optional connection parameters include the following:
+
+- `bucket`: The name of the IBM COS bucket. If not provided, all available buckets can be queried, however, this can affect performance, especially when listing all of the available objects.
+
+## Usage
+
+Retrieve data from a specified object (file) in a IBM COS bucket by providing the integration name and the object key:
+
+```sql
+SELECT *
+FROM ibm_datasource.`my-file.csv`;
+LIMIT 10;
+```
+
+<Tip>
+If a bucket name is provided in the `CREATE DATABASE` command, querying will be limited to that bucket and the bucket name can be ommitted from the object key as shown in the example above. However, if the bucket name is not provided, the object key must include the bucket name, such as `ibm_datasource.`my-bucket/my-folder/my-file.csv`.
+
+Wrap the object key in backticks (\`) to avoid any issues parsing the SQL statements provided. This is especially important when the object key contains spaces, special characters or prefixes, such as `my-folder/my-file.csv`.
+
+At the moment, the supported file formats are CSV, TSV, JSON, and Parquet.
+</Tip>
+
+<Note>
+The above examples utilize `ibm_datasource` as the datasource name, which is defined in the `CREATE DATABASE` command.
+</Note>
+
+The special `files` table can be used to list all objects available in the specified bucket or all buckets if the bucket name is not provided:
+
+```sql
+SELECT *
+FROM ibm_datasource.files LIMIT 10
+```
+
+The content of files can also be retrieved by explicitly requesting the `content` column. This column is empty by default to avoid unnecessary data transfer:
+
+```sql
+SELECT path, content
+FROM ibm_datasource.files LIMIT 10
+```
+
+<Tip>
+This table will return all objects regardless of the file format, however, only the supported file formats mentioned above can be queried.
+</Tip>
+
+## Troubleshooting Guide
+
+<Warning>
+`Database Connection Error`
+
+- **Symptoms**: Failure to connect MindsDB with the Amazon S3 bucket.
+- **Checklist**: 1. Make sure the IBM COS bucket exists. 2. Confirm that provided IBM COS credentials are correct. Try making a direct connection to the IBM COS bucket using the IBM CLI. 3. Ensure a stable network between MindsDB and IBM COS.
+  </Warning>
+
+<Warning>
+`SQL statement cannot be parsed by mindsdb_sql`
+
+- **Symptoms**: SQL queries failing or not recognizing object names containing spaces, special characters or prefixes.
+- **Checklist**: 1. Ensure object names with spaces, special characters or prefixes are enclosed in backticks. 2. Examples:
+  _ Incorrect: SELECT _ FROM integration.travel/travel_data.csv
+  - Incorrect: SELECT _ FROM integration.'travel/travel_data.csv'
+    _ Correct: SELECT \_ FROM integration.\`travel/travel_data.csv\`
+    </Warning>
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/__about__.py b/mindsdb/integrations/handlers/ibm_cos_handler/__about__.py
new file mode 100644
index 00000000000..c0197eaec7e
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/__about__.py
@@ -0,0 +1,9 @@
+__title__ = "MindsDB IBM COS Handler"
+__package_name__ = "mindsdb_ibm_cos_handler"
+__version__ = "0.0.1"
+__description__ = "MindsDB handler for IBM Cloud Object Storage"
+__author__ = "Ton Hoang Nguyen (Bill)"
+__github__ = "'https://github.com/mindsdb/mindsdb'"
+__pypi__ = "'https://github.com/mindsdb/mindsdb'"
+__license__ = "MIT"
+__copyright__ = "Copyright 2024 - mindsdb"
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/__init__.py b/mindsdb/integrations/handlers/ibm_cos_handler/__init__.py
new file mode 100644
index 00000000000..d3dac2794bd
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/__init__.py
@@ -0,0 +1,30 @@
+from mindsdb.integrations.libs.const import HANDLER_TYPE
+
+from .__about__ import __version__ as version, __description__ as description
+from .connection_args import connection_args, connection_args_example
+
+try:
+    from .ibm_cos_handler import IBMCloudObjectStorageHandler as Handler
+
+    import_error = None
+except Exception as e:
+    Handler = None
+    import_error = e
+
+title = "IBM Cloud Object Storage"
+name = "ibm_cos"
+type = HANDLER_TYPE.DATA
+icon_path = "icon.svg"
+
+__all__ = [
+    "Handler",
+    "version",
+    "name",
+    "type",
+    "title",
+    "description",
+    "connection_args",
+    "connection_args_example",
+    "import_error",
+    "icon_path",
+]
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/connection_args.py b/mindsdb/integrations/handlers/ibm_cos_handler/connection_args.py
new file mode 100644
index 00000000000..9fa875fc015
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/connection_args.py
@@ -0,0 +1,38 @@
+from collections import OrderedDict
+from mindsdb.integrations.libs.const import HANDLER_CONNECTION_ARG_TYPE as ARG_TYPE
+
+connection_args = OrderedDict(
+    cos_hmac_access_key_id={
+        "type": ARG_TYPE.PWD,
+        "description": "IBM COS HMAC Access Key ID.",
+        "required": True,
+        "label": "HMAC Access Key ID",
+        "secret": True,
+    },
+    cos_hmac_secret_access_key={
+        "type": ARG_TYPE.PWD,
+        "description": "IBM COS HMAC Secret Access Key.",
+        "required": True,
+        "label": "HMAC Secret Access Key",
+        "secret": True,
+    },
+    cos_endpoint_url={
+        "type": ARG_TYPE.STR,
+        "description": "IBM COS Endpoint URL (e.g., https://s3.eu-gb.cloud-object-storage.appdomain.cloud).",
+        "required": True,
+        "label": "Endpoint URL",
+    },
+    bucket={
+        "type": ARG_TYPE.STR,
+        "description": "IBM COS Bucket Name (Optional).",
+        "required": False,
+        "label": "Bucket Name",
+    },
+)
+
+connection_args_example = OrderedDict(
+    cos_hmac_access_key_id="YOUR_HMAC_ACCESS_KEY_ID",
+    cos_hmac_secret_access_key="YOUR_HMAC_SECRET_ACCESS_KEY",
+    cos_endpoint_url="https://s3.eu-gb.cloud-object-storage.appdomain.cloud",
+    bucket="YOUR_BUCKET_NAME",
+)
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/ibm_cos_handler.py b/mindsdb/integrations/handlers/ibm_cos_handler/ibm_cos_handler.py
new file mode 100644
index 00000000000..df4dfd50623
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/ibm_cos_handler.py
@@ -0,0 +1,338 @@
+from contextlib import contextmanager
+from typing import Text, Dict, Optional, List
+
+import ibm_boto3
+from ibm_botocore.client import ClientError
+import pandas as pd
+import duckdb
+
+from mindsdb_sql.parser.ast.base import ASTNode
+from mindsdb_sql.parser.ast import Select, Identifier, Insert, Star, Constant
+
+from mindsdb.utilities import log
+from mindsdb.integrations.libs.response import (
+    HandlerStatusResponse as StatusResponse,
+    HandlerResponse as Response,
+    RESPONSE_TYPE,
+)
+
+from mindsdb.integrations.libs.api_handler import APIResource, APIHandler
+from mindsdb.integrations.utilities.sql_utils import FilterCondition, FilterOperator
+
+logger = log.getLogger(__name__)
+
+
+class ListFilesTable(APIResource):
+    def list(
+        self,
+        targets: List[str] = None,
+        conditions: List[FilterCondition] = None,
+        limit: int = None,
+        *args,
+        **kwargs,
+    ) -> pd.DataFrame:
+
+        buckets = None
+        for condition in conditions:
+            if condition.column == "bucket":
+                if condition.op == FilterOperator.IN:
+                    buckets = condition.value
+                elif condition.op == FilterOperator.EQUAL:
+                    buckets = [condition.value]
+                condition.applied = True
+
+        data = []
+        for obj in self.handler.get_objects(limit=limit, buckets=buckets):
+            path = obj["Key"]
+            if obj["Filename"].split(".")[1] in self.handler.supported_file_formats:
+                item = {
+                    "path": path,
+                    "bucket": obj["Bucket"],
+                    "name": path[path.rfind("/") + 1:],
+                    "extension": path[path.rfind(".") + 1:],
+                }
+
+                data.append(item)
+
+        return pd.DataFrame(data=data, columns=self.get_columns())
+
+    def get_columns(self) -> List[str]:
+        return ["path", "name", "extension", "bucket", "content"]
+
+
+class FileTable(APIResource):
+    def list(
+        self, targets: List[str] = None, table_name=None, *args, **kwargs
+    ) -> pd.DataFrame:
+        return self.handler.read_as_table(table_name)
+
+    def add(self, data, table_name=None):
+        df = pd.DataFrame(data)
+        return self.handler.add_data_to_table(table_name, df)
+
+
+class IBMCloudObjectStorageHandler(APIHandler):
+
+    name = "ibm_cos"
+    supported_file_formats = ["csv", "tsv", "json", "parquet"]
+
+    def __init__(self, name: Text, connection_data: Optional[Dict] = None, **kwargs):
+        super().__init__(name)
+        self.connection_data = connection_data or {}
+        self.kwargs = kwargs
+
+        self.connection = None
+        self.is_connected = False
+        self.thread_safe = True
+        self._regions = {}
+
+        self.bucket = self.connection_data.get("bucket")
+        self._files_table = ListFilesTable(self)
+
+    def __del__(self):
+        if self.is_connected is True:
+            self.disconnect()
+
+    def connect(self):
+        if self.is_connected is True:
+            return self.connection
+
+        required_params = [
+            "cos_hmac_access_key_id",
+            "cos_hmac_secret_access_key",
+            "cos_endpoint_url",
+        ]
+        if not all(key in self.connection_data for key in required_params):
+            raise ValueError(
+                "Required parameters (cos_hmac_access_key_id, cos_hmac_secret_access_key, cos_endpoint_url) must be provided."
+            )
+
+        self.connection = self._connect_ibm_boto3()
+        self.is_connected = True
+
+        return self.connection
+
+    def _connect_ibm_boto3(self) -> ibm_boto3.client:
+        config = {
+            "aws_access_key_id": self.connection_data["cos_hmac_access_key_id"],
+            "aws_secret_access_key": self.connection_data["cos_hmac_secret_access_key"],
+            "endpoint_url": self.connection_data["cos_endpoint_url"],
+        }
+
+        client = ibm_boto3.client("s3", **config)
+
+        if self.bucket is not None:
+            client.head_bucket(Bucket=self.bucket)
+        else:
+            client.list_buckets()
+
+        return client
+
+    def disconnect(self):
+        if not self.is_connected:
+            return
+        self.connection = None
+        self.is_connected = False
+
+    def check_connection(self) -> StatusResponse:
+        response = StatusResponse(False)
+        need_to_close = self.is_connected is False
+
+        try:
+            self._connect_ibm_boto3()
+            response.success = True
+        except (ClientError, ValueError) as e:
+            logger.error(
+                f"Error connecting to IBM COS with the given credentials, {e}!"
+            )
+            response.error_message = str(e)
+
+        if response.success and need_to_close:
+            self.disconnect()
+
+        elif not response.success and self.is_connected:
+            self.is_connected = False
+
+        return response
+
+    @contextmanager
+    def _connect_duckdb(self):
+        duckdb_conn = duckdb.connect(":memory:")
+        duckdb_conn.execute("INSTALL httpfs")
+        duckdb_conn.execute("LOAD httpfs")
+
+        duckdb_conn.execute(
+            f"SET s3_access_key_id='{self.connection_data['cos_hmac_access_key_id']}'"
+        )
+        duckdb_conn.execute(
+            f"SET s3_secret_access_key='{self.connection_data['cos_hmac_secret_access_key']}'"
+        )
+
+        endpoint_url = self.connection_data["cos_endpoint_url"]
+        if endpoint_url.startswith("https://"):
+            endpoint_url = endpoint_url[len("https://"):]
+        elif endpoint_url.startswith("http://"):
+            endpoint_url = endpoint_url[len("http://"):]
+
+        duckdb_conn.execute(f"SET s3_endpoint='{endpoint_url}'")
+        duckdb_conn.execute("SET s3_url_style='path'")
+        duckdb_conn.execute("SET s3_use_ssl=true")
+
+        try:
+            yield duckdb_conn
+        finally:
+            duckdb_conn.close()
+
+    def _get_bucket(self, key):
+        if self.bucket is not None:
+            return self.bucket, key
+
+        ar = key.split("/")
+        return ar[0], "/".join(ar[1:])
+
+    def read_as_table(self, key) -> pd.DataFrame:
+        bucket, key = self._get_bucket(key)
+
+        with self._connect_duckdb() as connection:
+
+            cursor = connection.execute(f"SELECT * FROM 's3://{bucket}/{key}'")
+
+            return cursor.fetchdf()
+
+    def _read_as_content(self, key) -> None:
+        bucket, key = self._get_bucket(key)
+
+        client = self.connect()
+
+        obj = client.get_object(Bucket=bucket, Key=key)
+        content = obj["Body"].read()
+        return content
+
+    def add_data_to_table(self, key, df) -> None:
+
+        bucket, key = self._get_bucket(key)
+
+        try:
+            client = self.connect()
+            client.head_object(Bucket=bucket, Key=key)
+        except ClientError as e:
+            logger.error(f"Error querying the file {key} in the bucket {bucket}, {e}!")
+            raise e
+
+        with self._connect_duckdb() as connection:
+            connection.execute(
+                f"CREATE TABLE tmp_table AS SELECT * FROM 's3://{bucket}/{key}'"
+            )
+
+            connection.execute("INSERT INTO tmp_table BY NAME SELECT * FROM df")
+
+            connection.execute(f"COPY tmp_table TO 's3://{bucket}/{key}'")
+
+    def query(self, query: ASTNode) -> Response:
+        self.connect()
+        if isinstance(query, Select):
+            table_name = query.from_table.parts[-1]
+
+            if table_name == "files":
+                table = self._files_table
+                df = table.select(query)
+
+                has_content = False
+                for target in query.targets:
+                    if (
+                        isinstance(target, Identifier)
+                        and target.parts[-1].lower() == "content"
+                    ):
+                        has_content = True
+                        break
+                if has_content:
+                    df["content"] = df["path"].apply(self._read_as_content)
+            else:
+                extension = table_name.split(".")[-1]
+                if extension not in self.supported_file_formats:
+                    logger.error(f"The file format {extension} is not supported!")
+                    raise ValueError(f"The file format {extension} is not supported!")
+
+                table = FileTable(self, table_name=table_name)
+                df = table.select(query)
+
+            response = Response(RESPONSE_TYPE.TABLE, data_frame=df)
+        elif isinstance(query, Insert):
+            table_name = query.table.parts[-1]
+            table = FileTable(self, table_name=table_name)
+            table.insert(query)
+            response = Response(RESPONSE_TYPE.OK)
+        else:
+            raise NotImplementedError
+
+        return response
+
+    def get_objects(self, limit=None, buckets=None) -> List[dict]:
+        client = self.connect()
+        if self.bucket is not None:
+            add_bucket_to_name = False
+            scan_buckets = [self.bucket]
+        else:
+            add_bucket_to_name = True
+            resp = client.list_buckets()
+            scan_buckets = [b["Name"] for b in resp["Buckets"]]
+
+        objects = []
+        for bucket in scan_buckets:
+            if buckets is not None and bucket not in buckets:
+                continue
+
+            resp = client.list_objects_v2(Bucket=bucket)
+            if "Contents" not in resp:
+                continue
+
+            for obj in resp["Contents"]:
+                obj["Bucket"] = bucket
+                obj["Filename"] = obj["Key"]
+                if add_bucket_to_name:
+                    obj["Key"] = f'{bucket}/{obj["Key"]}'
+                objects.append(obj)
+            if limit is not None and len(objects) >= limit:
+                break
+
+        return objects
+
+    def get_tables(self) -> Response:
+        supported_names = [
+            f"{obj['Key']}"
+            for obj in self.get_objects()
+            if obj["Key"].split(".")[-1] in self.supported_file_formats
+        ]
+
+        supported_names.insert(0, "files")
+
+        response = Response(
+            RESPONSE_TYPE.TABLE,
+            data_frame=pd.DataFrame(supported_names, columns=["table_name"]),
+        )
+
+        return response
+
+    def get_columns(self, table_name: str) -> Response:
+        query = Select(
+            targets=[Star()],
+            from_table=Identifier(parts=[table_name]),
+            limit=Constant(1),
+        )
+
+        result = self.query(query)
+
+        response = Response(
+            RESPONSE_TYPE.TABLE,
+            data_frame=pd.DataFrame(
+                {
+                    "column_name": result.data_frame.columns,
+                    "data_type": [
+                        str(dtype) if str(dtype) != "object" else "string"
+                        for dtype in result.data_frame.dtypes
+                    ],
+                }
+            ),
+        )
+
+        return response
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/icon.svg b/mindsdb/integrations/handlers/ibm_cos_handler/icon.svg
new file mode 100644
index 00000000000..f8574f62860
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/icon.svg
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Livello_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="1000px" height="401.149px" viewBox="0 0 1000 401.149" enable-background="new 0 0 1000 401.149" xml:space="preserve">
+<g>
+	<g>
+		<polygon id="Rectangle-path" fill="#1F70C1" points="0,373.217 194.433,373.217 194.433,401.05 0,401.05 		"/>
+		<polygon id="Rectangle-path_1_" fill="#1F70C1" points="0,319.83 194.433,319.83 194.433,347.761 0,347.761 		"/>
+		<polygon id="Rectangle-path_2_" fill="#1F70C1" points="55.468,266.541 138.867,266.541 138.867,294.473 55.468,294.473 		"/>
+		<polygon id="Rectangle-path_3_" fill="#1F70C1" points="55.468,213.253 138.867,213.253 138.867,241.185 55.468,241.185 		"/>
+		<polygon id="Rectangle-path_4_" fill="#1F70C1" points="55.468,159.964 138.867,159.964 138.867,187.896 55.468,187.896 		"/>
+		<polygon id="Rectangle-path_5_" fill="#1F70C1" points="55.468,106.577 138.867,106.577 138.867,134.509 55.468,134.509 		"/>
+		<rect id="Rectangle-path_6_" y="53.288" fill="#1F70C1" width="194.433" height="27.932"/>
+		<rect id="Rectangle-path_7_" fill="#1F70C1" width="194.433" height="27.932"/>
+	</g>
+	<g>
+		<path id="Shape_16_" fill="#1F70C1" d="M222.167,347.761h299.029c5.051-8.617,8.815-18.027,11.094-27.932H222.167V347.761z"/>
+		<path id="Shape_17_" fill="#1F70C1" d="M497.92,213.253H277.734v27.932h243.463C514.857,230.487,507.032,221.078,497.92,213.253z"
+			/>
+		<path id="Shape_18_" fill="#1F70C1" d="M277.734,159.964v27.932H497.92c9.311-7.825,17.135-17.235,23.277-27.932H277.734z"/>
+		<path id="Shape_19_" fill="#1F70C1" d="M521.197,53.288H222.167V81.22H532.29C529.715,71.315,525.951,61.906,521.197,53.288z"/>
+		<path id="Shape_20_" fill="#1F70C1" d="M429.279,0H222.167v27.932h278.526C482.072,10.697,456.815,0,429.279,0z"/>
+		<rect id="Rectangle-path_8_" x="277.734" y="106.577" fill="#1F70C1" width="83.3" height="27.932"/>
+		<path id="Shape_21_" fill="#1F70C1" d="M444.433,134.509h87.163c2.476-8.914,3.764-18.324,3.764-27.932h-90.927L444.433,134.509
+			L444.433,134.509z"/>
+		<polygon id="Rectangle-path_9_" fill="#1F70C1" points="277.734,266.541 361.034,266.541 361.034,294.473 277.734,294.473 		"/>
+		<path id="Shape_22_" fill="#1F70C1" d="M444.433,266.541v27.932h90.927c0-9.608-1.288-19.017-3.764-27.932H444.433z"/>
+		<path id="Shape_23_" fill="#1F70C1" d="M222.167,400.852l207.112,0.297c27.734,0,52.793-10.697,71.513-27.932H222.167V400.852z"/>
+	</g>
+	<g>
+		<polygon id="Rectangle-path_10_" fill="#1F70C1" points="555.567,373.217 694.433,373.217 694.433,401.05 555.567,401.05 		"/>
+		<polygon id="Rectangle-path_11_" fill="#1F70C1" points="555.567,319.83 694.433,319.83 694.433,347.761 555.567,347.761 		"/>
+		<polygon id="Rectangle-path_12_" fill="#1F70C1" points="611.034,266.541 694.433,266.541 694.433,294.473 611.034,294.473 		"/>
+		<polygon id="Rectangle-path_13_" fill="#1F70C1" points="611.034,213.253 694.433,213.253 694.433,241.185 611.034,241.185 		"/>
+		<polygon id="Shape_24_" fill="#1F70C1" points="733.063,53.288 555.567,53.288 555.567,81.22 742.67,81.22 		"/>
+		<polygon id="Shape_25_" fill="#1F70C1" points="714.639,0 555.567,0 555.567,27.932 724.247,27.932 		"/>
+		<polygon id="Rectangle-path_14_" fill="#1F70C1" points="861.034,373.217 1000,373.217 1000,401.05 861.034,401.05 		"/>
+		<polygon id="Rectangle-path_15_" fill="#1F70C1" points="861.034,319.83 1000,319.83 1000,347.761 861.034,347.761 		"/>
+		<polygon id="Rectangle-path_16_" fill="#1F70C1" points="861.034,266.541 944.433,266.541 944.433,294.473 861.034,294.473 		"/>
+		<polygon id="Rectangle-path_17_" fill="#1F70C1" points="861.034,213.253 944.433,213.253 944.433,241.185 861.034,241.185 		"/>
+		<polygon id="Shape_26_" fill="#1F70C1" points="861.034,187.896 944.433,187.896 944.433,159.964 861.034,159.964 
+			861.034,159.964 785.559,159.964 777.734,182.548 769.909,159.964 694.433,159.964 694.433,159.964 611.034,159.964 
+			611.034,187.896 694.433,187.896 694.433,162.242 703.249,187.896 852.219,187.896 861.034,162.242 		"/>
+		<polygon id="Shape_27_" fill="#1F70C1" points="944.433,106.577 803.982,106.577 794.374,134.509 944.433,134.509 		"/>
+		<polygon id="Shape_28_" fill="#1F70C1" points="840.927,0 831.319,27.932 1000,27.932 1000,0 		"/>
+		<polygon id="Shape_29_" fill="#1F70C1" points="777.734,400.852 787.341,373.217 768.126,373.217 		"/>
+		<polygon id="Shape_30_" fill="#1F70C1" points="759.311,347.761 796.157,347.761 806.062,319.83 749.505,319.83 		"/>
+		<polygon id="Shape_31_" fill="#1F70C1" points="740.59,294.473 814.877,294.473 824.683,266.541 730.784,266.541 		"/>
+		<polygon id="Shape_32_" fill="#1F70C1" points="721.969,241.185 833.597,241.185 843.106,213.253 712.361,213.253 		"/>
+		<polygon id="Shape_33_" fill="#1F70C1" points="611.034,134.509 761.093,134.509 751.486,106.577 611.034,106.577 		"/>
+		<polygon id="Shape_34_" fill="#1F70C1" points="812.896,81.22 1000,81.22 1000,53.288 822.405,53.288 		"/>
+	</g>
+</g>
+</svg>
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/requirements.txt b/mindsdb/integrations/handlers/ibm_cos_handler/requirements.txt
new file mode 100644
index 00000000000..e64c5eb3812
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/requirements.txt
@@ -0,0 +1,1 @@
+ibm-cos-sdk
\ No newline at end of file


Test patch:
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/tests/__init__.py b/mindsdb/integrations/handlers/ibm_cos_handler/tests/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/mindsdb/integrations/handlers/ibm_cos_handler/tests/test_ibm_cos_handler.py b/mindsdb/integrations/handlers/ibm_cos_handler/tests/test_ibm_cos_handler.py
new file mode 100644
index 00000000000..294a698bdfe
--- /dev/null
+++ b/mindsdb/integrations/handlers/ibm_cos_handler/tests/test_ibm_cos_handler.py
@@ -0,0 +1,23 @@
+import unittest
+from mindsdb.integrations.handlers.ibm_cos_handler.ibm_cos_handler import (
+    IBMCloudObjectStorageHandler,
+)
+
+
+class IBMCloudObjectStorageHandlerTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.kwargs = {
+            "cos_hmac_access_key_id": "your-access-key-id",
+            "cos_hmac_secret_access_key": "your-secret-access-key",
+            "cos_endpoint_url": "https://s3.eu-gb.cloud-object-storage.appdomain.cloud",
+            "bucket": "your-bucket-name",
+        }
+        cls.handler = IBMCloudObjectStorageHandler("test_ibm_cos_handler", cls.kwargs)
+
+    def test_0_check_connection(self):
+        assert self.handler.check_connection()
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/tests/scripts/check_requirements.py b/tests/scripts/check_requirements.py
index 5c8f84d28c5..98d13e3f0c7 100644
--- a/tests/scripts/check_requirements.py
+++ b/tests/scripts/check_requirements.py
@@ -87,6 +87,7 @@ def get_requirements_from_file(path):
     "google-cloud-storage": ["google"],
     "protobuf": ["google"],
     "google-api-python-client": ["googleapiclient"],
+    "ibm-cos-sdk": ["ibm_boto3", "ibm_botocore"],
     "binance-connector": ["binance"],
     "pysqlite3": ["pysqlite3"],
     "atlassian-python-api": ["atlassian"],