Procházet zdrojové kódy

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é před 3 měsíci
rodič
revize
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.frequency import Frequency
 from taipy.core.common.scope import Scope
 from taipy.core.common.scope import Scope
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
+from taipy.rest.config import RestConfig
 
 
 from .checker.issue_collector import IssueCollector
 from .checker.issue_collector import IssueCollector
 from .common._classproperty import _Classproperty
 from .common._classproperty import _Classproperty
@@ -254,6 +255,10 @@ class Config:
     def core(cls) -> Dict[str, CoreSection]:
     def core(cls) -> Dict[str, CoreSection]:
         """"""
         """"""
 
 
+    @_Classproperty
+    def rest(cls) -> Dict[str, RestConfig]:
+        """"""
+
     @staticmethod
     @staticmethod
     def configure_scenario(
     def configure_scenario(
         id: str,
         id: str,
@@ -1022,3 +1027,27 @@ class Config:
         Returns:
         Returns:
             The Core configuration.
             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
     return base_pyi
 
 
 
 
-def _generate_entity_and_property_maps(filename):
+def _generate_entity_and_property_maps(filenames):
     entities_map = {}
     entities_map = {}
     property_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
     return entities_map, property_map
 
 
 
 
@@ -142,8 +142,8 @@ def _build_header(filename) -> str:
 
 
 if __name__ == "__main__":
 if __name__ == "__main__":
     header_file = "taipy/common/config/stubs/pyi_header.py"
     header_file = "taipy/common/config/stubs/pyi_header.py"
-    config_init = Path("taipy/core/config/__init__.py")
     base_config = "taipy/common/config/config.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"
     dn_filename = "taipy/core/config/data_node_config.py"
     job_filename = "taipy/core/config/job_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"
     task_filename = "taipy/core/config/task_config.py"
     core_filename = "taipy/core/config/core_section.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)
     entities_map, property_map = _generate_entity_and_property_maps(config_init)
     pyi = _build_header(header_file)
     pyi = _build_header(header_file)
     pyi = _build_base_config_pyi(base_config, pyi)
     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, job_filename, entities_map["JobConfig"])
     pyi = _build_entity_config_pyi(pyi, core_filename, entities_map["CoreSection"])
     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
     # Remove the final redundant \n
     pyi = pyi[:-1]
     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.frequency import Frequency
 from taipy.core.common.scope import Scope
 from taipy.core.common.scope import Scope
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
+from taipy.rest.config import RestConfig
 
 
 from .checker.issue_collector import IssueCollector
 from .checker.issue_collector import IssueCollector
 from .common._classproperty import _Classproperty
 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:
         Arguments:
             **kwargs : Options to provide to the application server.
             **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)
         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._checker import _Checker
 from taipy.common.config.checker.issue_collector import IssueCollector
 from taipy.common.config.checker.issue_collector import IssueCollector
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
 from taipy.core.config import CoreSection, DataNodeConfig, JobConfig, ScenarioConfig, TaskConfig
+from taipy.rest.config import RestConfig
 
 
 
 
 def pytest_addoption(parser: pytest.Parser) -> None:
 def pytest_addoption(parser: pytest.Parser) -> None:
@@ -160,3 +161,17 @@ def inject_core_sections() -> t.Callable:
         )
         )
 
 
     return _inject_core_sections
     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 dotenv import load_dotenv
 
 
 from taipy.common.config import Config
 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 import Cycle, DataNodeId, Job, JobId, Scenario, Sequence, Task
 from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
 from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
 from taipy.core.common.frequency import Frequency
 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.job._job_manager import _JobManager
 from taipy.core.task._task_manager import _TaskManager
 from taipy.core.task._task_manager import _TaskManager
 from taipy.rest.app import create_app
 from taipy.rest.app import create_app
+from taipy.rest.config import _RestConfigChecker
 
 
 from .setup.shared.algorithms import evaluate, forecast
 from .setup.shared.algorithms import evaluate, forecast
 
 
@@ -309,11 +311,26 @@ def create_job_list():
         manager._set(c)
         manager._set(c)
     return jobs
     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)
 @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()
     reset_configuration_singleton()
     inject_core_sections()
     inject_core_sections()
+    inject_rest_sections()
+
+    _Checker.add_checker(_RestConfigChecker)
 
 
     Config.configure_core(repository_type="filesystem")
     Config.configure_core(repository_type="filesystem")
     if os.path.exists(".data"):
     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"]:
     for path in [".data", ".my_data", "user_data", ".taipy"]:
         if os.path.exists(path):
         if os.path.exists(path):
             shutil.rmtree(path, ignore_errors=True)
             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