Repository: eclipse-tractusx/tractusx-sdk
Pull request #77: feat: create dataspace connector adapters
Author: login: MDSBarbosa
Created: 2025-04-04T13:52:35Z | Merged: 2025-04-11T09:55:35Z

## Description

This PR adds the `Adapter` class, alongside a `AdapterFactory`, and both a base DMA adapter and a 0.9.0 version specific DMA adapter implementation.

This contribution is based on discussion https://github.com/eclipse-tractusx/tractusx-sdk/discussions/49, and in the context of issue https://github.com/eclipse-tractusx/tractusx-sdk/issues/25

## Pre-review checks

Please ensure to do as many of the following checks as possible, before asking for committer review:

- [x] DEPENDENCIES are up-to-date. [Dash license tool](https://github.com/eclipse/dash-licenses). Committers can open IP issues for restricted libs.
- [x] [Copyright and license header](https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-02) are present on all affected files



Diff:
diff --git a/DEPENDENCIES_TRACTUS-X_SDK b/DEPENDENCIES_TRACTUS-X_SDK
index a24c7ead..5d1353a6 100644
--- a/DEPENDENCIES_TRACTUS-X_SDK
+++ b/DEPENDENCIES_TRACTUS-X_SDK
@@ -39,6 +39,7 @@ pypi/pypi/-/pytest/8.1.1, MIT, approved, #19365
 pypi/pypi/-/python-dotenv/1.0.1, BSD-2-Clause AND BSD-3-Clause, approved, clearlydefined
 pypi/pypi/-/python-keycloak/4.0.1, MIT, approved, clearlydefined
 pypi/pypi/-/python-multipart/0.0.20, Apache-2.0, approved, clearlydefined
+pypi/pypi/-/requests-mock/1.12.1, Apache-2.0, approved, clearlydefined
 pypi/pypi/-/requests-toolbelt/1.0.0, Apache-2.0, approved, #15024
 pypi/pypi/-/requests/2.32.3, Apache-2.0 AND MIT AND Apache-2.0, approved, #14884
 pypi/pypi/-/rfc3986/1.5.0, Apache-2.0, approved, #19390
diff --git a/pyproject.toml b/pyproject.toml
index 076f64cc..c4032925 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,6 +52,7 @@ dependencies = [
     "python-multipart (==0.0.20)",
     "pyyaml (==6.0.2)",
     "requests (==2.32.3)",
+    "requests-mock (==1.12.1)",
     "requests-toolbelt (==1.0.0)",
     "rfc3986 (==1.5.0)",
     "rich (==13.9.4)",
diff --git a/src/tractusx_sdk/dataspace/adapters/__init__.py b/src/tractusx_sdk/dataspace/adapters/__init__.py
new file mode 100644
index 00000000..c2aa521f
--- /dev/null
+++ b/src/tractusx_sdk/dataspace/adapters/__init__.py
@@ -0,0 +1,21 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
diff --git a/src/tractusx_sdk/dataspace/adapters/adapter.py b/src/tractusx_sdk/dataspace/adapters/adapter.py
new file mode 100644
index 00000000..2c9a8bc6
--- /dev/null
+++ b/src/tractusx_sdk/dataspace/adapters/adapter.py
@@ -0,0 +1,178 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import requests
+
+from ..tools import HttpTools
+
+
+class Adapter:
+    """
+    Base adapter class
+    """
+
+    base_url: str
+    session = None
+
+    def __init__(
+            self,
+            base_url: str,
+            headers: dict = None
+    ):
+        """
+        Create a new adapter instance
+
+        :param base_url: The URL of the application to be requested
+        :param headers: The headers (i.e.: API Key) of the application to be requested
+        """
+
+        self.base_url = base_url
+        self.session = requests.Session()
+
+        if headers:
+            self.session.headers.update(headers)
+
+    @classmethod
+    def builder(cls):
+        """
+        This method is intended to return a builder for adapters inheriting this class.
+        This means that such an adapter should implement its own `_Builder` inner class.
+        By default, the `Adapter._Builder` class is used.
+
+        :return: a builder for the model inheriting this class
+        """
+        return cls._Builder(cls)
+
+    class _Builder:
+        """
+        Default _Builder class for an Adapter.
+        """
+
+        def __init__(self, cls):
+            self.cls = cls
+            self._data = {}
+
+        def base_url(self, base_url: str):
+            self._data["base_url"] = base_url
+            return self
+
+        def headers(self, headers: dict):
+            self._data["headers"] = headers
+            return self
+
+        def data(self, data: dict):
+            """
+            This method is intended to set all the data of the adapters inheriting this class.
+
+            It can be used to set all the data of the adapter in a single call, without the need to declare
+            each builder method separately. This is useful for cases where an adapter may deviate from its base
+            adapter implementation, and the base adapter builder methods are not sufficient to set all the necessary data.
+            """
+
+            self._data.update(data)
+            return self
+
+        def build(self):
+            """
+            :return: an instance of the class inheriting the base adapter
+            """
+            return self.cls(**self._data)
+
+    def close(self):
+        """
+        Close the requests session
+        """
+
+        self.session.close()
+
+    def get(self, url: str, **kwargs):
+        """
+        Perform a GET request
+
+        :param url: Partial URL to append to the base adapter URL
+        :param kwargs: Keyword arguments to include in the request
+
+        :return: The response of the request
+        """
+
+        return self.request("get", url, **kwargs)
+
+    def post(self, url: str, **kwargs):
+        """
+        Perform a POST request
+
+        :param url: Partial URL to append to the base adapter URL
+        :param kwargs: Keyword arguments to include in the request
+
+        :return: The response of the request
+        """
+
+        return self.request("post", url, **kwargs)
+
+    def put(self, url: str, **kwargs):
+        """
+        Perform a PUT request
+
+        :param url: Partial URL to append to the base adapter URL
+        :param kwargs: Keyword arguments to include in the request
+
+        :return: The response of the request
+        """
+
+        return self.request("put", url, **kwargs)
+
+    def delete(self, url: str, **kwargs):
+        """
+        Perform a DELETE request
+
+        :param url: Partial URL to append to the base adapter URL
+        :param kwargs: Keyword arguments to include in the request
+
+        :return: The response of the request
+        """
+
+        return self.request("delete", url, **kwargs)
+
+    def request(self, method: str, path: str = "", **kwargs):
+        """
+        Main method for performing requests
+
+        :param method: HTTP method to use with requests
+        :param path: Path to append to the base adapter URL
+        :param kwargs: Keyword arguments to include in the request
+
+        :return: The response of the request
+        """
+
+        url = HttpTools.concat_into_url(self.base_url, path)
+
+        response = self.session.request(
+            method=method,
+            url=url,
+            **kwargs
+        )
+
+        return HttpTools.json_response(
+            data=response.json(),
+            status_code=response.status_code,
+            headers=dict(response.headers)
+        )
diff --git a/src/tractusx_sdk/dataspace/adapters/connector/__init__.py b/src/tractusx_sdk/dataspace/adapters/connector/__init__.py
new file mode 100644
index 00000000..c2aa521f
--- /dev/null
+++ b/src/tractusx_sdk/dataspace/adapters/connector/__init__.py
@@ -0,0 +1,21 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
diff --git a/src/tractusx_sdk/dataspace/adapters/connector/adapter_factory.py b/src/tractusx_sdk/dataspace/adapters/connector/adapter_factory.py
new file mode 100644
index 00000000..7a28d33a
--- /dev/null
+++ b/src/tractusx_sdk/dataspace/adapters/connector/adapter_factory.py
@@ -0,0 +1,150 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from enum import Enum
+from importlib import import_module
+from os import listdir, path
+
+
+class AdapterType(Enum):
+    """
+    Enum for different adapter types. Each adapter type corresponds to a specific implementation,
+    and must correspond exactly to the prefix of the adapter class it is associated with.
+    """
+
+    DMA_ADAPTER = "Dma"
+    DATAPLANE_ADAPTER = "Dataplane"
+    # TODO: Add any other existing adapter types
+
+
+class AdapterFactory:
+    """
+    Factory class to manage the creation of Adapter instances
+    """
+    # Dynamically load supported versions from the directory structure
+    _adapters_base_path = path.dirname(__file__)
+    SUPPORTED_VERSIONS = []
+    for module in listdir(_adapters_base_path):
+        module_path = path.join(_adapters_base_path, module)
+        if path.isdir(module_path) and module.startswith("v"):
+            SUPPORTED_VERSIONS.append(module)
+
+    @staticmethod
+    def _get_adapter_builder(
+            adapter_type: AdapterType,
+            connector_version: str,
+    ):
+        """
+        Create an adapter, based on the specified adapter type and version.
+
+        Different adapter types and versions may have different implementations and parameters, which should be the
+        responsibility of the specific adapter class to handle. This factory method dynamically imports the correct
+        adapter class, and returns it, with whatever parameters necessary for its initialization.
+
+        :param adapter_type: The type of adapter to create, as per the AdapterType enum
+        :param connector_version: The version of the Connector (e.g., "v0_9_0")
+        :return: An instance of the specified Adapter subclass
+        """
+
+        # Check if the requested version is supported for the given adapter type
+        if connector_version not in AdapterFactory.SUPPORTED_VERSIONS:
+            raise ValueError(f"Unsupported version {connector_version}")
+
+        # Compute the adapter module path dynamically, depending on the connector version
+        connector_module = ".".join(__name__.split(".")[0:-1])
+        module_name = f"{connector_module}.{connector_version}"
+
+        # Compute the adapter class name based on the adapter type
+        adapter_class_name = f"{adapter_type.value}Adapter"
+
+        try:
+            # Dynamically import the adapter class
+            module = import_module(module_name)
+            adapter_class = getattr(module, adapter_class_name)
+            return adapter_class.builder()
+        except AttributeError as attr_exception:
+            raise AttributeError(
+                f"Failed to import adapter class {adapter_class_name} for module {module_name}"
+            ) from attr_exception
+        except (ModuleNotFoundError, ImportError) as import_exception:
+            raise ImportError(
+                f"Failed to import module {module_name}. Ensure that the required packages are installed and the PYTHONPATH is set correctly."
+            ) from import_exception
+
+    @staticmethod
+    def get_dma_adapter(
+            connector_version: str,
+            base_url: str,
+            dma_path: str,
+            headers: dict = None,
+            **kwargs
+    ):
+        """
+        Create a (DMA) adapter instance, based a specific version.
+
+        :param connector_version: The version of the Connector DMA (e.g., "v0_9_0")
+        :param base_url: The URL of the Connector DMA to be requested
+        :param dma_path: The path of the Connector Data Management API to be requested
+        :param headers: The headers (i.e.: API Key) of the Connector to be requested
+        :return: An instance of the specified Adapter subclass
+        """
+
+        builder = AdapterFactory._get_adapter_builder(
+            adapter_type=AdapterType.DMA_ADAPTER,
+            connector_version=connector_version,
+        )
+
+        builder.base_url(base_url)
+        builder.headers(headers)
+        builder.dma_path(dma_path)
+
+        # Include any additional parameters
+        builder.data(kwargs)
+        return builder.build()
+
+    @staticmethod
+    def get_dataplane_adapter(
+            connector_version: str,
+            base_url: str,
+            headers: dict = None,
+            **kwargs
+    ):
+        """
+        Create a dataplane adapter instance, based a specific version.
+
+        :param connector_version: The version of the Connector dataplane (e.g., "v0_9_0")
+        :param base_url: The URL of the Connector dataplane to be requested
+        :param headers: The headers (i.e.: Edc-Bpn) of the Connector dataplane to be requested
+        :return: An instance of the specified Adapter subclass
+        """
+
+        builder = AdapterFactory._get_adapter_builder(
+            adapter_type=AdapterType.DATAPLANE_ADAPTER,
+            connector_version=connector_version,
+        )
+
+        builder.base_url(base_url)
+        builder.headers(headers)
+
+        # Include any additional parameters
+        builder.data(kwargs)
+        return builder.build()
diff --git a/src/tractusx_sdk/dataspace/adapters/connector/base_dma_adapter.py b/src/tractusx_sdk/dataspace/adapters/connector/base_dma_adapter.py
new file mode 100644
index 00000000..cb70b585
--- /dev/null
+++ b/src/tractusx_sdk/dataspace/adapters/connector/base_dma_adapter.py
@@ -0,0 +1,39 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from ..adapter import Adapter
+from ...tools import HttpTools
+
+
+class BaseDmaAdapter(Adapter):
+    dma_path: str = ""
+
+    def __init__(self, base_url: str, dma_path: str, headers: dict = None):
+        self.dma_path = dma_path
+
+        dma_url = HttpTools.concat_into_url(base_url, dma_path)
+        super().__init__(dma_url, headers)
+
+    class _Builder(Adapter._Builder):
+        def dma_path(self, dma_path: str):
+            self._data["dma_path"] = dma_path
+            return self
diff --git a/src/tractusx_sdk/dataspace/adapters/connector/v0_9_0/__init__.py b/src/tractusx_sdk/dataspace/adapters/connector/v0_9_0/__init__.py
new file mode 100644
index 00000000..f03b813c
--- /dev/null
+++ b/src/tractusx_sdk/dataspace/adapters/connector/v0_9_0/__init__.py
@@ -0,0 +1,25 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from .dma_adapter import DmaAdapter
+
+__all__ = ['DmaAdapter']
diff --git a/src/tractusx_sdk/dataspace/adapters/connector/v0_9_0/dma_adapter.py b/src/tractusx_sdk/dataspace/adapters/connector/v0_9_0/dma_adapter.py
new file mode 100644
index 00000000..48edf61e
--- /dev/null
+++ b/src/tractusx_sdk/dataspace/adapters/connector/v0_9_0/dma_adapter.py
@@ -0,0 +1,27 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from ..base_dma_adapter import BaseDmaAdapter
+
+
+class DmaAdapter(BaseDmaAdapter):
+    pass
diff --git a/src/tractusx_sdk/dataspace/tools/http_tools.py b/src/tractusx_sdk/dataspace/tools/http_tools.py
index 1e46625a..d70733e7 100644
--- a/src/tractusx_sdk/dataspace/tools/http_tools.py
+++ b/src/tractusx_sdk/dataspace/tools/http_tools.py
@@ -102,10 +102,25 @@ def do_delete(url, session=None, verify=True, headers=None, timeout=None, params
 
     # prepare response
     @staticmethod
-    def response(data, status=200, content_type='application/json'):
-        response = JSONResponse(data, status)
-        response.headers["Content-Type"] = content_type
+    def json_response(data, status_code: int = 200, headers: dict = None):
+        response = JSONResponse(
+            content=data,
+            status_code=status_code,
+            headers=headers
+        )
+        response.headers["Content-Type"] = 'application/json'
         return response
+
+    @staticmethod
+    def concat_into_url(*args):
+        """
+        Joins given arguments into an url. Trailing and leading slashes are stripped for each argument.
+
+        :param args: The parts of a URL to be concatenated into one
+        :return: Complete URL
+        """
+
+        return "/".join(map(lambda x: str(x).strip("/"), args))
     
     @staticmethod
     def get_host(url):
@@ -137,7 +152,7 @@ def file_response(buffer: BytesIO, filename: str, status=200, content_type='appl
     # Generates a error response with message
     @staticmethod
     def get_error_response(status=500,message="It was not possible to process/execute this request!"):
-        return HttpTools.response({
+        return HttpTools.json_response({
             "message": message,
             "status": status 
         }, status)
@@ -148,7 +163,7 @@ async def get_body(request):
     
     @staticmethod
     def get_not_authorized():
-        return HttpTools.response({
+        return HttpTools.json_response({
             "message": "Not Authorized",
             "status": 401
         }, 401)
diff --git a/tests/dataspace/adapters/__init__.py b/tests/dataspace/adapters/__init__.py
new file mode 100644
index 00000000..c2aa521f
--- /dev/null
+++ b/tests/dataspace/adapters/__init__.py
@@ -0,0 +1,21 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
diff --git a/tests/dataspace/adapters/connector/__init__.py b/tests/dataspace/adapters/connector/__init__.py
new file mode 100644
index 00000000..c2aa521f
--- /dev/null
+++ b/tests/dataspace/adapters/connector/__init__.py
@@ -0,0 +1,21 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
diff --git a/tests/dataspace/adapters/connector/test_adapter_factory.py b/tests/dataspace/adapters/connector/test_adapter_factory.py
new file mode 100644
index 00000000..4b48af5d
--- /dev/null
+++ b/tests/dataspace/adapters/connector/test_adapter_factory.py
@@ -0,0 +1,79 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import unittest
+from enum import Enum
+from unittest.mock import patch
+from src.tractusx_sdk.dataspace.adapters.connector.adapter_factory import AdapterFactory, AdapterType
+
+
+class TestAdapterFactory(unittest.TestCase):
+
+    def setUp(self):
+        self.base_url = "https://example.com"
+        self.headers = {"Authorization": "Bearer token"}
+        self.dma_path = "/dma"
+
+    def test_get_dma_adapter_success(self):
+        adapter = AdapterFactory.get_dma_adapter(
+            connector_version="v0_9_0",
+            base_url=self.base_url,
+            dma_path=self.dma_path,
+            headers=self.headers
+        )
+        self.assertIsNotNone(adapter)
+        self.assertEqual(adapter.base_url, f"{self.base_url}{self.dma_path}")
+        self.assertIsNotNone(adapter.session)
+
+    def test_get_adapter_unsupported_version(self):
+        with self.assertRaises(ValueError):
+            AdapterFactory.get_dma_adapter(
+                connector_version="NonExistentVersion",
+                base_url=self.base_url,
+                dma_path=self.dma_path,
+                headers=self.headers
+            )
+
+    def test_get_adapter_unsupported_type(self):
+        with self.assertRaises(AttributeError):
+            adapter_type = Enum('AdapterType', { 'foo': 'bar' })
+            AdapterFactory._get_adapter_builder(
+                adapter_type=adapter_type.foo,
+                connector_version="v0_9_0"
+            )
+
+    def test_get_adapter_import_error(self):
+        with patch.object(AdapterFactory, "SUPPORTED_VERSIONS", new=["v0_0_0"]):
+            with self.assertRaises(ImportError):
+                AdapterFactory._get_adapter_builder(
+                    adapter_type=AdapterType.DMA_ADAPTER,
+                    connector_version="v0_0_0"
+                )
+
+    def test_get_dataplane_adapter_failure(self):
+        # TODO: There is no dataplane adapter yet. Adjust this test when it is implemented.
+        with self.assertRaises(AttributeError):
+            AdapterFactory.get_dataplane_adapter(
+                connector_version="v0_9_0",
+                base_url=self.base_url,
+                headers=self.headers
+            )
diff --git a/tests/dataspace/adapters/connector/test_base_dma_adapter.py b/tests/dataspace/adapters/connector/test_base_dma_adapter.py
new file mode 100644
index 00000000..7d367594
--- /dev/null
+++ b/tests/dataspace/adapters/connector/test_base_dma_adapter.py
@@ -0,0 +1,56 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import unittest
+from src.tractusx_sdk.dataspace.adapters.connector.base_dma_adapter import BaseDmaAdapter
+
+
+class TestBaseDmaAdapter(unittest.TestCase):
+    def setUp(self):
+        self.base_url = "https://example.com"
+        self.dma_path = "/dma"
+        self.headers = {"Authorization": "Bearer token"}
+
+    def test_initializer(self):
+        adapter = BaseDmaAdapter(base_url=self.base_url, dma_path=self.dma_path, headers=self.headers)
+
+        self.assertEqual(self.dma_path, adapter.dma_path)
+        self.assertEqual(f"{self.base_url}{self.dma_path}", adapter.base_url)
+        self.assertEqual(self.headers["Authorization"], adapter.session.headers["Authorization"])
+
+    def test_builder(self):
+        builder = BaseDmaAdapter.builder()
+        builder.base_url(self.base_url)
+        builder.headers(self.headers)
+        builder.dma_path(self.dma_path)
+        adapter = builder.build()
+
+        self.assertIsInstance(adapter, BaseDmaAdapter)
+        self.assertEqual(adapter.dma_path, self.dma_path)
+        self.assertEqual(adapter.base_url, f"{self.base_url}{self.dma_path}")
+        self.assertEqual(adapter.session.headers["Authorization"], self.headers["Authorization"])
+
+    def test_builder_fails_without_dma_path(self):
+        builder = BaseDmaAdapter.builder()
+        builder.base_url(self.base_url)
+        with self.assertRaises(TypeError):
+            builder.build()
diff --git a/tests/dataspace/adapters/test_adapter.py b/tests/dataspace/adapters/test_adapter.py
new file mode 100644
index 00000000..fd650c6e
--- /dev/null
+++ b/tests/dataspace/adapters/test_adapter.py
@@ -0,0 +1,64 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import unittest
+import requests_mock
+from json import loads as jloads
+
+from src.tractusx_sdk.dataspace.adapters.adapter import Adapter
+
+
+class TestAdapter(unittest.TestCase):
+    def setUp(self):
+        self.base_url = "https://example.com"
+        self.headers = {"Authorization": "Bearer token"}
+        self.adapter = Adapter(base_url=self.base_url)
+
+    def test_builder_creates_adapter_instance(self):
+        builder = Adapter.builder()
+        builder.base_url(self.base_url)
+        builder.headers(self.headers)
+        adapter = builder.build()
+
+        self.assertIsInstance(adapter, Adapter)
+        self.assertEqual(adapter.base_url, self.base_url)
+        self.assertEqual(adapter.session.headers["Authorization"], self.headers["Authorization"])
+
+    def test_builder_fails_without_base_url(self):
+        builder = Adapter.builder()
+        with self.assertRaises(TypeError):
+            builder.build()
+
+    @requests_mock.Mocker()
+    def test_request_method(self, mock_request):
+        mock_url = f"{self.base_url}/test-endpoint"
+        mock_response_data = {"key": "value"}
+        mock_request.get(mock_url, json=mock_response_data, status_code=200)
+
+        response = self.adapter.request("get", "test-endpoint")
+
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(mock_response_data, jloads(response.body.decode("utf-8")))
+        self.assertEqual("application/json", response.headers["Content-Type"])
+
+    def tearDown(self):
+        self.adapter.close()
diff --git a/tests/dataspace/tools/test_http_tools.py b/tests/dataspace/tools/test_http_tools.py
index f8273449..0b83102f 100644
--- a/tests/dataspace/tools/test_http_tools.py
+++ b/tests/dataspace/tools/test_http_tools.py
@@ -108,10 +108,34 @@ def test_do_delete_failure(self, mock_delete):
 
     def test_response_json(self):
         """Ensure JSON response is properly structured."""
-        response = HttpTools.response({"message": "OK"}, status=200)
+        response = HttpTools.json_response({"message": "OK"}, status_code=200)
         self.assertIsInstance(response, JSONResponse)
         self.assertEqual(response.status_code, 200)
 
+    def test_concat_into_url(self):
+        base_url = "https://example.com"
+        path = "api/v1/resource"
+        expected_url = "https://example.com/api/v1/resource"
+
+        result = HttpTools.concat_into_url(base_url, path)
+        self.assertEqual(expected_url, result)
+
+    def test_concat_into_url_with_slashes(self):
+        base_url = "https://example.com/"
+        path = "/api/v1/resource/"
+        expected_url = "https://example.com/api/v1/resource"
+
+        result = HttpTools.concat_into_url(base_url, path)
+        self.assertEqual(expected_url, result)
+
+    def test_concat_into_url_with_empty_path(self):
+        base_url = "https://example.com"
+        path = ""
+        expected_url = "https://example.com/"
+
+        result = HttpTools.concat_into_url(base_url, path)
+        self.assertEqual(expected_url, result)
+
     def test_empty_response(self):
         """Verify empty response creation."""
         response = HttpTools.empty_response(status=204)


Reviews:
user: login: CDiezRodriguez
body: This looks amazing, Mário. Just a couple of small comments.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T08:20:43Z
user: login: MDSBarbosa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:14:45Z
user: login: MDSBarbosa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:32:14Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:33:15Z
user: login: CDiezRodriguez
body: For Eclipse DASH IP Check, you only need to replace DEPENDENCIES_TRACTUS-X_SDK with the text shown [here](https://github.com/eclipse-tractusx/tractusx-sdk/actions/runs/14400123778/job/40383825053?pr=77#step:9:36).
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T09:36:14Z

Review comments:
diff_hunk: @@ -0,0 +1,88 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import unittest
+import requests_mock
path: tests/dataspace/adapters/test_adapter.py
user: login: CDiezRodriguez
body: Could you add this dependency to the pyproject.toml file at the root of the project?
created_at: 2025-04-11T07:42:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,199 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import requests
+
+from fastapi.responses import JSONResponse
+
+
+class Adapter:
+    """
+    Base adapter class
+    """
+
+    base_url: str
+    session = None
+
+    def __init__(
+            self,
+            base_url: str,
+            headers: dict = None
+    ):
+        """
+        Create a new adapter instance
+
+        :param base_url: The URL of the application to be requested
+        :param headers: The headers (i.e.: API Key) of the application to be requested
+        """
+
+        self.base_url = base_url
+        self.session = requests.Session()
+
+        if headers:
+            self.session.headers.update(headers)
+
+    @classmethod
+    def builder(cls):
+        """
+        This method is intended to return a builder for adapters inheriting this class.
+        This means that such an adapter should implement its own `_Builder` inner class.
+        By default, the `Adapter._Builder` class is used.
+
+        :return: a builder for the model inheriting this class
+        """
+        return cls._Builder(cls)
+
+    class _Builder:
+        """
+        Default _Builder class for an Adapter.
+        """
+
+        def __init__(self, cls):
+            self.cls = cls
+            self._data = {}
+
+        def base_url(self, base_url: str):
+            self._data["base_url"] = base_url
+            return self
+
+        def headers(self, headers: dict):
+            self._data["headers"] = headers
+            return self
+
+        def data(self, data: dict):
+            """
+            This method is intended to set all the data of the adapters inheriting this class.
+
+            It can be used to set all the data of the adapter in a single call, without the need to declare
+            each builder method separately. This is useful for cases where an adapter may deviate from its base
+            adapter implementation, and the base adapter builder methods are not sufficient to set all the necessary data.
+            """
+
+            self._data.update(data)
+            return self
+
+        def build(self):
+            """
+            :return: an instance of the class inheriting the base adapter
+            """
+            return self.cls(**self._data)
+
+    @staticmethod
+    def concat_into_url(*args):
+        """
+        Joins given arguments into an url. Trailing and leading slashes are stripped for each argument.
+
+        :param args: The parts of a URL to be concatenated into one
+        :return: Complete URL
+        """
+
+        return "/".join(map(lambda x: str(x).strip("/"), args))
+
+    @staticmethod
+    def json_response(data, status_code: int = 200, headers: dict = None):
+        response = JSONResponse(
+            content=data,
+            status_code=status_code,
+            headers=headers
+        )
+        response.headers["Content-Type"] = 'application/json'
+        return response
path: src/tractusx_sdk/dataspace/adapters/adapter.py
user: login: CDiezRodriguez
body: Could we move these methods to the tools folder?
created_at: 2025-04-11T07:55:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,88 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import unittest
+import requests_mock
path: tests/dataspace/adapters/test_adapter.py
user: login: MDSBarbosa
body: Implemented at [5969b69](https://github.com/eclipse-tractusx/tractusx-sdk/pull/77/commits/5969b696eebfd4da4f7dadfa0a5ecba7ba1ca5c7)
created_at: 2025-04-11T09:14:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,199 @@
+#################################################################################
+# Eclipse Tractus-X - Software Development KIT
+#
+# Copyright (c) 2025 Contributors to the Eclipse Foundation
+#
+# See the NOTICE file(s) distributed with this work for additional
+# information regarding copyright ownership.
+#
+# This program and the accompanying materials are made available under the
+# terms of the Apache License, Version 2.0 which is available at
+# https://www.apache.org/licenses/LICENSE-2.0.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+# either express or implied. See the
+# License for the specific language govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+import requests
+
+from fastapi.responses import JSONResponse
+
+
+class Adapter:
+    """
+    Base adapter class
+    """
+
+    base_url: str
+    session = None
+
+    def __init__(
+            self,
+            base_url: str,
+            headers: dict = None
+    ):
+        """
+        Create a new adapter instance
+
+        :param base_url: The URL of the application to be requested
+        :param headers: The headers (i.e.: API Key) of the application to be requested
+        """
+
+        self.base_url = base_url
+        self.session = requests.Session()
+
+        if headers:
+            self.session.headers.update(headers)
+
+    @classmethod
+    def builder(cls):
+        """
+        This method is intended to return a builder for adapters inheriting this class.
+        This means that such an adapter should implement its own `_Builder` inner class.
+        By default, the `Adapter._Builder` class is used.
+
+        :return: a builder for the model inheriting this class
+        """
+        return cls._Builder(cls)
+
+    class _Builder:
+        """
+        Default _Builder class for an Adapter.
+        """
+
+        def __init__(self, cls):
+            self.cls = cls
+            self._data = {}
+
+        def base_url(self, base_url: str):
+            self._data["base_url"] = base_url
+            return self
+
+        def headers(self, headers: dict):
+            self._data["headers"] = headers
+            return self
+
+        def data(self, data: dict):
+            """
+            This method is intended to set all the data of the adapters inheriting this class.
+
+            It can be used to set all the data of the adapter in a single call, without the need to declare
+            each builder method separately. This is useful for cases where an adapter may deviate from its base
+            adapter implementation, and the base adapter builder methods are not sufficient to set all the necessary data.
+            """
+
+            self._data.update(data)
+            return self
+
+        def build(self):
+            """
+            :return: an instance of the class inheriting the base adapter
+            """
+            return self.cls(**self._data)
+
+    @staticmethod
+    def concat_into_url(*args):
+        """
+        Joins given arguments into an url. Trailing and leading slashes are stripped for each argument.
+
+        :param args: The parts of a URL to be concatenated into one
+        :return: Complete URL
+        """
+
+        return "/".join(map(lambda x: str(x).strip("/"), args))
+
+    @staticmethod
+    def json_response(data, status_code: int = 200, headers: dict = None):
+        response = JSONResponse(
+            content=data,
+            status_code=status_code,
+            headers=headers
+        )
+        response.headers["Content-Type"] = 'application/json'
+        return response
path: src/tractusx_sdk/dataspace/adapters/adapter.py
user: login: MDSBarbosa
body: Implemented at [48f10fc](https://github.com/eclipse-tractusx/tractusx-sdk/pull/77/commits/48f10fc1ba49d0bae4f7e69fa3f500e9ad6ad20d)
created_at: 2025-04-11T09:32:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -108,10 +108,34 @@
 
     def test_response_json(self):
         """Ensure JSON response is properly structured."""
-        response = HttpTools.response({"message": "OK"}, status=200)
+        response = HttpTools.json_response({"message": "OK"}, status=200)
path: tests/dataspace/tools/test_http_tools.py
user: login: github-advanced-security[bot]
body: ## Wrong name for an argument in a call

Keyword argument 'status' is not a supported parameter name of [method HttpTools.json_response](1).

[Show more details](https://github.com/eclipse-tractusx/tractusx-sdk/security/code-scanning/167)
created_at: 2025-04-11T09:33:15Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-11T09:49:34Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=eclipse-tractusx_tractusx-sdk&pullRequest=77) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=eclipse-tractusx_tractusx-sdk&pullRequest=77&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=eclipse-tractusx_tractusx-sdk&pullRequest=77&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=eclipse-tractusx_tractusx-sdk&pullRequest=77&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=eclipse-tractusx_tractusx-sdk&pullRequest=77&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=eclipse-tractusx_tractusx-sdk&pullRequest=77&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=eclipse-tractusx_tractusx-sdk&pullRequest=77)


author_association: NONE