Преглед изворни кода

feat: add rest config and rest config checker (#2432)

* feat: add rest config and rest config checker
---------
Co-authored-by: jean-robin medori <jeanrobin.medori@avaiga.com>
João André пре 3 месеци
родитељ
комит
14d6981878

+ 29 - 0
taipy/common/config/config.pyi

@@ -16,6 +16,7 @@ from taipy.common.config._config import _Config
 from taipy.core.common.frequency import Frequency
 from taipy.core.common.scope import Scope
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
+from taipy.rest.config import RestConfig
 
 from .checker.issue_collector import IssueCollector
 from .common._classproperty import _Classproperty
@@ -254,6 +255,10 @@ class Config:
     def core(cls) -> Dict[str, CoreSection]:
         """"""
 
+    @_Classproperty
+    def rest(cls) -> Dict[str, RestConfig]:
+        """"""
+
     @staticmethod
     def configure_scenario(
         id: str,
@@ -1022,3 +1027,27 @@ class Config:
         Returns:
             The Core configuration.
         """
+
+    @staticmethod
+    def configure_rest(
+        port: Optional[int] = None,
+        host: Optional[str] = None,
+        use_https: Optional[bool] = None,
+        ssl_cert: Optional[str] = None,
+        ssl_key: Optional[str] = None,
+        **properties
+    ) -> "RestConfig":
+        """Configure the Rest service.
+
+        Arguments:
+            port (Optional[int]): The port on which the REST service will be running
+            host (Optional[str]): The host on which the REST service will be running
+            use_https (Optional[bool]): Whether to use HTTPS for the REST service
+            ssl_cert (Optional[str]): The path to the SSL certificate file
+            ssl_key (Optional[str]): The path to the SSL key file
+            **properties (Dict[str, Any]): A keyworded variable length list of additional
+                arguments configure the behavior of the `Rest^` service.
+
+        Returns:
+            The Rest configuration.
+        """

+ 22 - 18
taipy/common/config/stubs/generate_pyi.py

@@ -103,25 +103,25 @@ def _build_entity_config_pyi(base_pyi, filename, entity_map) -> str:
     return base_pyi
 
 
-def _generate_entity_and_property_maps(filename):
+def _generate_entity_and_property_maps(filenames):
     entities_map = {}
     property_map = {}
-    entity_tree = _get_file_ast(filename)
-    functions = [
-        f for f in ast.walk(entity_tree) if isinstance(f, ast.Call) and getattr(f.func, "id", "") == "_inject_section"
-    ]
-
-    for f in functions:
-        entity = ast.unparse(f.args[0])
-        entities_map[entity] = {}
-        property_map[eval(ast.unparse(f.args[1]))] = entity
-        # Remove class name from function map
-        text = ast.unparse(f.args[-1]).replace(f"{entity}.", "")
-        matches = re.findall(r"\((.*?)\)", text)
-
-        for m in matches:
-            v, k = m.replace("'", "").split(",")
-            entities_map[entity][k.strip()] = v
+    for filename in filenames:
+        etty_tree = _get_file_ast(filename)
+        functions = [
+            f for f in ast.walk(etty_tree) if isinstance(f, ast.Call) and getattr(f.func, "id", "") == "_inject_section"
+        ]
+        for f in functions:
+            entity = ast.unparse(f.args[0])
+            entities_map[entity] = {}
+            property_map[eval(ast.unparse(f.args[1]))] = entity
+            # Remove class name from function map
+            text = ast.unparse(f.args[-1]).replace(f"{entity}.", "")
+            matches = re.findall(r"\((.*?)\)", text)
+
+            for m in matches:
+                v, k = m.replace("'", "").split(",")
+                entities_map[entity][k.strip()] = v
     return entities_map, property_map
 
 
@@ -142,8 +142,8 @@ def _build_header(filename) -> str:
 
 if __name__ == "__main__":
     header_file = "taipy/common/config/stubs/pyi_header.py"
-    config_init = Path("taipy/core/config/__init__.py")
     base_config = "taipy/common/config/config.py"
+    config_init = [Path("taipy/core/config/__init__.py"), Path("taipy/rest/config/__init__.py")]
 
     dn_filename = "taipy/core/config/data_node_config.py"
     job_filename = "taipy/core/config/job_config.py"
@@ -151,6 +151,8 @@ if __name__ == "__main__":
     task_filename = "taipy/core/config/task_config.py"
     core_filename = "taipy/core/config/core_section.py"
 
+    rest_filename = "taipy/rest/config/rest_config.py"
+
     entities_map, property_map = _generate_entity_and_property_maps(config_init)
     pyi = _build_header(header_file)
     pyi = _build_base_config_pyi(base_config, pyi)
@@ -161,6 +163,8 @@ if __name__ == "__main__":
     pyi = _build_entity_config_pyi(pyi, job_filename, entities_map["JobConfig"])
     pyi = _build_entity_config_pyi(pyi, core_filename, entities_map["CoreSection"])
 
+    pyi = _build_entity_config_pyi(pyi, rest_filename, entities_map["RestConfig"])
+
     # Remove the final redundant \n
     pyi = pyi[:-1]
 

+ 1 - 0
taipy/common/config/stubs/pyi_header.py

@@ -16,6 +16,7 @@ from taipy.common.config._config import _Config
 from taipy.core.common.frequency import Frequency
 from taipy.core.common.scope import Scope
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
+from taipy.rest.config import RestConfig
 
 from .checker.issue_collector import IssueCollector
 from .common._classproperty import _Classproperty

+ 26 - 0
taipy/rest/config/__init__.py

@@ -0,0 +1,26 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://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 governing permissions and limitations under the License.
+"""Configuration of the rest service."""
+
+from taipy.common.config import _inject_section
+from taipy.common.config.checker._checker import _Checker
+
+from .rest_checker import _RestConfigChecker
+from .rest_config import RestConfig
+
+_inject_section(
+    RestConfig,
+    "rest",
+    RestConfig.default_config(),
+    [("configure_rest", RestConfig._configure_rest)]
+)
+
+_Checker.add_checker(_RestConfigChecker)

+ 72 - 0
taipy/rest/config/rest_checker.py

@@ -0,0 +1,72 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://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 governing permissions and limitations under the License.
+
+from typing import cast
+
+from taipy.common.config._config import _Config
+from taipy.common.config.checker._checkers._config_checker import _ConfigChecker
+from taipy.common.config.checker.issue_collector import IssueCollector
+
+from .rest_config import RestConfig
+
+
+class _RestConfigChecker(_ConfigChecker):
+    def __init__(self, config: _Config, collector: IssueCollector):
+        super().__init__(config, collector)
+
+    def _check(self) -> IssueCollector:
+        if rest_configs := self._config._unique_sections.get(RestConfig.name):
+            rest_config = cast(RestConfig, rest_configs)
+            self._check_port(rest_config)
+            self._check_host(rest_config)
+            self._check_https_settings(rest_config)
+        return self._collector
+
+    def _check_port(self, rest_config: RestConfig):
+        if not isinstance(rest_config.port, int) or not (1 <= rest_config.port <= 65535):
+            self._error(
+                "port",
+                rest_config.port,
+                "The port of the RestConfig must be an integer between 1 and 65535.",
+            )
+
+    def _check_host(self, rest_config: RestConfig):
+        if not isinstance(rest_config.host, str) or not rest_config.host:
+            self._error(
+                "host", rest_config.host, "The host of the RestConfig must be a non-empty string."
+            )
+
+    def _check_https_settings(self, rest_config: RestConfig):
+        if rest_config.use_https:
+            if not rest_config.ssl_cert:
+                self._error(
+                    "ssl_cert",
+                    rest_config.ssl_cert,
+                    "When HTTPS is enabled in the RestConfig ssl_cert must be set.",
+                )
+            elif not isinstance(rest_config.ssl_cert, str):
+                self._error(
+                    "ssl_cert",
+                    rest_config.ssl_cert,
+                    "The ssl_cert of the RestConfig must be valid string.",
+                )
+            if not rest_config.ssl_key:
+                self._error(
+                    "ssl_key",
+                    rest_config.ssl_key,
+                    "When HTTPS is enabled in the RestConfig ssl_key must be set.",
+                )
+            elif not isinstance(rest_config.ssl_key, str):
+                self._error(
+                    "ssl_key",
+                    rest_config.ssl_key,
+                    "The ssl_key of the RestConfig must be valid string.",
+                )

+ 199 - 0
taipy/rest/config/rest_config.py

@@ -0,0 +1,199 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://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 governing permissions and limitations under the License.
+from copy import copy
+from typing import Any, Dict, Optional, Tuple
+
+from taipy.common.config import UniqueSection
+from taipy.common.config._config import _Config
+from taipy.common.config.common._template_handler import _TemplateHandler as _tpl
+from taipy.common.config.config import Config
+
+
+class RestConfig(UniqueSection):
+    """Configuration parameters for running the `Rest^` service"""
+
+    name: str = "REST"
+
+    _PORT_KEY: str = "port"
+    _DEFAULT_PORT: int = 5000
+    _HOST_KEY: str = "host"
+    _DEFAULT_HOST: str = "127.0.0.1"
+    _USE_HTTPS_KEY: str = "use_https"
+    _DEFAULT_USE_HTTPS: bool = False
+    _SSL_CERT_KEY: str = "ssl_cert"
+    _DEFAULT_SSL_CERT: Optional[str] = None
+    _SSL_KEY_KEY: str = "ssl_key"
+    _DEFAULT_SSL_KEY: Optional[str] = None
+
+    def __init__(
+        self,
+        port: Optional[int] = _DEFAULT_PORT,
+        host: Optional[str] = _DEFAULT_HOST,
+        use_https: Optional[bool] = _DEFAULT_USE_HTTPS,
+        ssl_cert: Optional[str] = _DEFAULT_SSL_CERT,
+        ssl_key: Optional[str] = _DEFAULT_SSL_KEY,
+        **properties,
+    ):
+        self._port = port
+        self._host = host
+        self._use_https = use_https
+        self._ssl_cert = ssl_cert
+        self._ssl_key = ssl_key
+        super().__init__(**properties)
+
+    def __copy__(self) -> "RestConfig":
+        return RestConfig(
+            self._port,
+            self._host,
+            self._use_https,
+            self._ssl_cert,
+            self._ssl_key,
+            **copy(self._properties),
+        )
+
+    def _clean(self):
+        self._port = self._DEFAULT_PORT
+        self._host = self._DEFAULT_HOST
+        self._use_https = self._DEFAULT_USE_HTTPS
+        self._ssl_cert = self._DEFAULT_SSL_CERT
+        self._ssl_key = self._DEFAULT_SSL_KEY
+        self._properties.clear()
+
+    def _update(self, config_as_dict: Dict, default_section=None):
+        self._port = config_as_dict.pop(self._PORT_KEY, self.port)
+        self._host = config_as_dict.pop(self._HOST_KEY, self.host)
+        self._use_https = config_as_dict.pop(self._USE_HTTPS_KEY, self.use_https)
+        self._ssl_cert = config_as_dict.pop(self._SSL_CERT_KEY, self.ssl_cert)
+        self._ssl_key = config_as_dict.pop(self._SSL_KEY_KEY, self.ssl_key)
+        self._properties.update(config_as_dict)
+
+    def _to_dict(self):
+        as_dict = {
+            key: value
+            for key, value in {
+                self._PORT_KEY: self._port,
+                self._HOST_KEY: self._host,
+                self._USE_HTTPS_KEY: self._use_https,
+                self._SSL_CERT_KEY: self._ssl_cert,
+                self._SSL_KEY_KEY: self._ssl_key
+            }.items()
+            if value is not None
+        }
+        as_dict.update(self._properties)
+        return as_dict
+
+    @classmethod
+    def _from_dict(cls, as_dict: Dict[str, Any], id=None, config: Optional[_Config] = None):
+        port = as_dict.pop(cls._PORT_KEY, None)
+        host = as_dict.pop(cls._HOST_KEY, None)
+        use_https = as_dict.pop(cls._USE_HTTPS_KEY, None)
+        ssl_cert = as_dict.pop(cls._SSL_CERT_KEY, None)
+        ssl_key = as_dict.pop(cls._SSL_KEY_KEY, None)
+        return RestConfig(port, host, use_https, ssl_cert, ssl_key, **as_dict)
+
+    @classmethod
+    def default_config(cls) -> "RestConfig":
+        """Return a RestConfig with all the default values.
+
+        Returns:
+            The default rest configuration.
+        """
+        return RestConfig(
+            cls._DEFAULT_PORT,
+            cls._DEFAULT_HOST,
+            cls._DEFAULT_USE_HTTPS,
+            cls._DEFAULT_SSL_CERT,
+            cls._DEFAULT_SSL_KEY,
+        )
+
+    @property
+    def port(self) -> int:
+        """The port on which the REST service will be running"""
+        return _tpl._replace_templates(self._port)
+
+    @port.setter
+    def port(self, value: int):
+        self._port = value
+
+    @property
+    def host(self) -> str:
+        """The host on which the REST service will be running"""
+        return _tpl._replace_templates(self._host)
+
+    @host.setter
+    def host(self, value: str):
+        self._host = value
+
+    @property
+    def use_https(self) -> bool:
+        """Whether to use HTTPS for the REST service"""
+        return _tpl._replace_templates(self._use_https)
+
+    @use_https.setter
+    def use_https(self, value: bool):
+        self._use_https = value
+
+    @property
+    def ssl_cert(self) -> Optional[str]:
+        """The path to the SSL certificate file"""
+        return _tpl._replace_templates(self._ssl_cert)
+
+    @ssl_cert.setter
+    def ssl_cert(self, value: Optional[str]):
+        self._ssl_cert = value
+
+    @property
+    def ssl_key(self) -> Optional[str]:
+        """The path to the SSL key file"""
+        return _tpl._replace_templates(self._ssl_key)
+
+    @ssl_key.setter
+    def ssl_key(self, value: Optional[str]):
+        self._ssl_key = value
+
+    @property
+    def ssl_context(self) -> Optional[Tuple[Optional[str], Optional[str]]]:
+        """The ssl_context as a tuple of the certificate and the key files"""
+        return (self.ssl_cert, self.ssl_key) if self.use_https else None
+
+    @staticmethod
+    def _configure_rest(
+        port: Optional[int] = None,
+        host: Optional[str] = None,
+        use_https: Optional[bool] = None,
+        ssl_cert: Optional[str] = None,
+        ssl_key: Optional[str] = None,
+        **properties
+    ) -> "RestConfig":
+        """Configure the Rest service.
+
+        Arguments:
+            port (Optional[int]): The port on which the REST service will be running
+            host (Optional[str]): The host on which the REST service will be running
+            use_https (Optional[bool]): Whether to use HTTPS for the REST service
+            ssl_cert (Optional[str]): The path to the SSL certificate file
+            ssl_key (Optional[str]): The path to the SSL key file
+            **properties (Dict[str, Any]): A keyworded variable length list of additional
+                arguments configure the behavior of the `Rest^` service.
+
+        Returns:
+            The Rest configuration.
+        """
+        section = RestConfig(
+            port=port,
+            host=host,
+            use_https=use_https,
+            ssl_cert=ssl_cert,
+            ssl_key=ssl_key,
+            **properties
+        )
+        Config._register(section)
+        return Config.unique_sections[RestConfig.name]

+ 8 - 0
taipy/rest/rest.py

@@ -44,4 +44,12 @@ class Rest:
         Arguments:
             **kwargs : Options to provide to the application server.
         """
+        rest_config = Config.rest
+        kwargs.update(
+            {
+                "port": rest_config.port,
+                "host": rest_config.host,
+                "ssl_context": rest_config.ssl_context,
+            }
+        )
         self._app.run(**kwargs)

+ 15 - 0
tests/conftest.py

@@ -23,6 +23,7 @@ from taipy.common.config._serializer._toml_serializer import _TomlSerializer
 from taipy.common.config.checker._checker import _Checker
 from taipy.common.config.checker.issue_collector import IssueCollector
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
+from taipy.rest.config import RestConfig
 
 
 def pytest_addoption(parser: pytest.Parser) -> None:
@@ -160,3 +161,17 @@ def inject_core_sections() -> t.Callable:
         )
 
     return _inject_core_sections
+
+@pytest.fixture
+def inject_rest_sections() -> t.Callable:
+    """Fixture to inject core sections into the configuration."""
+
+    def _inject_rest_sections() -> None:
+        _inject_section(
+            RestConfig,
+            "rest",
+            default=RestConfig.default_config(),
+            configuration_methods=[("configure_rest", RestConfig._configure_rest)],
+        )
+
+    return _inject_rest_sections

+ 18 - 15
tests/rest/conftest.py

@@ -20,6 +20,7 @@ import pytest
 from dotenv import load_dotenv
 
 from taipy.common.config import Config
+from taipy.common.config.checker._checker import _Checker
 from taipy.core import Cycle, DataNodeId, Job, JobId, Scenario, Sequence, Task
 from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
 from taipy.core.common.frequency import Frequency
@@ -29,6 +30,7 @@ from taipy.core.data.pickle import PickleDataNode
 from taipy.core.job._job_manager import _JobManager
 from taipy.core.task._task_manager import _TaskManager
 from taipy.rest.app import create_app
+from taipy.rest.config import _RestConfigChecker
 
 from .setup.shared.algorithms import evaluate, forecast
 
@@ -309,11 +311,26 @@ def create_job_list():
         manager._set(c)
     return jobs
 
+@pytest.fixture
+def init_orchestrator():
+    def _init_orchestrator():
+        _OrchestratorFactory._remove_dispatcher()
+
+        if _OrchestratorFactory._orchestrator is None:
+            _OrchestratorFactory._build_orchestrator()
+        _OrchestratorFactory._build_dispatcher(force_restart=True)
+        _OrchestratorFactory._orchestrator.jobs_to_run = Queue()
+        _OrchestratorFactory._orchestrator.blocked_jobs = []
+
+    return _init_orchestrator
 
 @pytest.fixture(scope="function", autouse=True)
-def cleanup_files(reset_configuration_singleton, inject_core_sections):
+def cleanup_files(reset_configuration_singleton, inject_rest_sections, inject_core_sections):
     reset_configuration_singleton()
     inject_core_sections()
+    inject_rest_sections()
+
+    _Checker.add_checker(_RestConfigChecker)
 
     Config.configure_core(repository_type="filesystem")
     if os.path.exists(".data"):
@@ -325,17 +342,3 @@ def cleanup_files(reset_configuration_singleton, inject_core_sections):
     for path in [".data", ".my_data", "user_data", ".taipy"]:
         if os.path.exists(path):
             shutil.rmtree(path, ignore_errors=True)
-
-
-@pytest.fixture
-def init_orchestrator():
-    def _init_orchestrator():
-        _OrchestratorFactory._remove_dispatcher()
-
-        if _OrchestratorFactory._orchestrator is None:
-            _OrchestratorFactory._build_orchestrator()
-        _OrchestratorFactory._build_dispatcher(force_restart=True)
-        _OrchestratorFactory._orchestrator.jobs_to_run = Queue()
-        _OrchestratorFactory._orchestrator.blocked_jobs = []
-
-    return _init_orchestrator

+ 195 - 0
tests/rest/test_rest_config.py

@@ -0,0 +1,195 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://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 governing permissions and limitations under the License.
+import os
+from unittest.mock import patch
+
+import pytest
+
+from taipy.common.config.config import Config
+from taipy.common.config.exceptions import MissingEnvVariableError
+from taipy.rest.config.rest_config import RestConfig
+from tests.core.utils.named_temporary_file import NamedTemporaryFile
+
+
+def test_rest_config_no_values():
+    assert Config.rest.port == 5000
+    assert Config.rest.host == "127.0.0.1"
+    assert Config.rest.use_https is False
+    assert Config.rest.ssl_cert is None
+    assert Config.rest.ssl_key is None
+
+def test_rest_config_default_values():
+    Config.configure_rest()
+    assert Config.rest.port == RestConfig._DEFAULT_PORT
+    assert Config.rest.host == RestConfig._DEFAULT_HOST
+    assert Config.rest.use_https is RestConfig._DEFAULT_USE_HTTPS
+    assert Config.rest.ssl_cert is RestConfig._DEFAULT_SSL_CERT
+    assert Config.rest.ssl_key is RestConfig._DEFAULT_SSL_KEY
+
+def test_rest_config_only_part_of_custom_values():
+    Config.configure_rest(
+        use_https=True,
+        ssl_cert="cert.pem",
+        ssl_key="key.pem"
+    )
+    assert Config.rest.port == RestConfig._DEFAULT_PORT
+    assert Config.rest.host == RestConfig._DEFAULT_HOST
+    assert Config.rest.use_https is True
+    assert Config.rest.ssl_cert == "cert.pem"
+    assert Config.rest.ssl_key == "key.pem"
+
+def test_rest_config_custom_values_and_toml_override():
+    # We override some default values with the Python API
+    Config.configure_rest(
+        port=8080,
+        host="0.0.0.0",
+    )
+    assert Config.rest.port == 8080
+    assert Config.rest.host == "0.0.0.0"
+    assert Config.rest.use_https is RestConfig._DEFAULT_USE_HTTPS
+    assert Config.rest.ssl_cert is RestConfig._DEFAULT_SSL_CERT
+    assert Config.rest.ssl_key is RestConfig._DEFAULT_SSL_KEY
+
+    # now we load a toml file
+    toml_cfg = NamedTemporaryFile(
+        content="""
+[TAIPY]
+
+[REST]
+port = 2
+host = "192.168.0.87"
+use_https = "true:bool"
+ssl_cert = "cert.pem"
+ssl_key = "key.pem"
+"""
+    )
+    Config.load(toml_cfg.filename)
+    assert Config.rest.port == 2
+    assert Config.rest.host == "192.168.0.87"
+    assert Config.rest.use_https is True
+    assert Config.rest.ssl_cert == "cert.pem"
+    assert Config.rest.ssl_key == "key.pem"
+
+
+def test_rest_config_custom_values_and_missing_env_var_override():
+    #we use env variables
+    Config.configure_rest(
+        port="ENV[PORT]:int",
+        host="ENV[HOST]",
+        ssl_cert="ENV[SSL_CERT]",
+        ssl_key="ENV[SSL_KEY]"
+    )
+    Config.rest.use_https = "ENV[USE_HTTPS]"
+    with pytest.raises(MissingEnvVariableError):
+        _ = Config.rest.port
+    with pytest.raises(MissingEnvVariableError):
+        _ = Config.rest.host
+    with pytest.raises(MissingEnvVariableError):
+        _ = Config.rest.use_https
+    with pytest.raises(MissingEnvVariableError):
+        _ = Config.rest.ssl_cert
+    with pytest.raises(MissingEnvVariableError):
+        _ = Config.rest.ssl_key
+
+def test_rest_config_custom_values_and_env_var_override():
+    with patch.dict(os.environ, {
+        "PORT": "3",
+        "HOST": "1.2.3.4",
+        "USE_HTTPS": "true",
+        "SSL_CERT": "cert.pem",
+        "SSL_KEY": "key.pem"
+    }):
+        # we use env variables
+        Config.configure_rest(
+            port="ENV[PORT]:int",
+            host="ENV[HOST]",
+            use_https="ENV[USE_HTTPS]:bool",
+            ssl_cert="ENV[SSL_CERT]",
+            ssl_key="ENV[SSL_KEY]"
+        )
+        assert Config.rest.port == 3
+        assert Config.rest.host == "1.2.3.4"
+        assert Config.rest.use_https is True
+        assert Config.rest.ssl_cert == "cert.pem"
+        assert Config.rest.ssl_key == "key.pem"
+
+
+def test_rest_config_copy():
+    rest_config = Config.configure_rest(
+        port=8080, host="0.0.0.0", use_https=True, ssl_cert="cert.pem", ssl_key="key.pem"
+    )
+    rest_config_copy = rest_config.__copy__()
+
+    assert rest_config_copy.port == 8080
+    assert rest_config_copy.host == "0.0.0.0"
+    assert rest_config_copy.use_https is True
+    assert rest_config_copy.ssl_cert == "cert.pem"
+    assert rest_config_copy.ssl_key == "key.pem"
+
+    # Ensure it's a deep copy
+    rest_config_copy.port = 9090
+    assert rest_config.port == 8080
+
+
+def test_rest_default_config_is_valid():
+    issues = Config.check()
+
+    assert len(issues.errors) == 0
+    assert len(issues.warnings) == 0
+    assert len(issues.infos) == 0
+
+
+def test_rest_config_checker_valid_config():
+    Config.configure_rest(port=8080, host="0.0.0.0", use_https=True, ssl_cert="cert.pem", ssl_key="key.pem")
+    issues = Config.check()
+
+    assert len(issues.errors) == 0
+    assert len(issues.warnings) == 0
+    assert len(issues.infos) == 0
+
+
+def test_rest_config_checker_invalid_port_and_host():
+    Config.configure_rest(port=70000, host="")  # Invalid port and host
+    with pytest.raises(SystemExit):
+        Config.check()
+
+    issues = Config._collector
+    assert len(issues.errors) == 2
+    assert len(issues.warnings) == 0
+    assert len(issues.infos) == 0
+    assert "port" in issues.errors[0].field
+    assert "host" in issues.errors[1].field
+
+
+def test_rest_config_checker_https_missing_cert_and_key():
+    Config.configure_rest(use_https=True)  # Missing ssl_cert and ssl_key
+    with pytest.raises(SystemExit):
+        Config.check()
+
+    issues = Config._collector
+    assert len(issues.errors) == 2
+    assert len(issues.warnings) == 0
+    assert len(issues.infos) == 0
+    assert "ssl_cert" in issues.errors[0].field
+    assert "ssl_key" in issues.errors[1].field
+
+
+def test_rest_config_checker_https_invalid_cert_and_key():
+    Config.configure_rest(use_https=True, ssl_cert=123, ssl_key=456)  # Invalid types for ssl_cert and ssl_key
+    with pytest.raises(SystemExit):
+        Config.check()
+
+    issues = Config._collector
+    assert len(issues.errors) == 2
+    assert len(issues.warnings) == 0
+    assert len(issues.infos) == 0
+    assert "ssl_cert" in issues.errors[0].field
+    assert "ssl_key" in issues.errors[1].field