diff --git a/taipy/config/_serializer/_base_serializer.py b/taipy/config/_serializer/_base_serializer.py index 8cdf6d0ef1..45d3502790 100644 --- a/taipy/config/_serializer/_base_serializer.py +++ b/taipy/config/_serializer/_base_serializer.py @@ -81,6 +81,8 @@ def _stringify(cls, as_dict): return [cls._stringify(val) for val in as_dict] if isinstance(as_dict, tuple): return [cls._stringify(val) for val in as_dict] + if isinstance(as_dict, set): + return [cls._stringify(val) for val in as_dict] return as_dict @staticmethod diff --git a/taipy/core/data/_data_manager.py b/taipy/core/data/_data_manager.py index 96791f1c28..e74c4820a9 100644 --- a/taipy/core/data/_data_manager.py +++ b/taipy/core/data/_data_manager.py @@ -24,6 +24,8 @@ from ..cycle.cycle_id import CycleId from ..exceptions.exceptions import InvalidDataNodeType from ..notification import Event, EventEntityType, EventOperation, Notifier, _make_event +from ..reason._reason_factory import _build_not_global_scope_reason, _build_wrong_config_type_reason +from ..reason.reason import Reasons from ..scenario.scenario_id import ScenarioId from ..sequence.sequence_id import SequenceId from ._data_fs_repository import _DataFSRepository @@ -68,6 +70,19 @@ def _bulk_get_or_create( for dn_config, owner_id in dn_configs_and_owner_id } + @classmethod + def _can_create(cls, config: Optional[DataNodeConfig] = None) -> Reasons: + config_id = getattr(config, "id", None) or str(config) + reason = Reasons(config_id) + + if config is not None: + if not isinstance(config, DataNodeConfig): + reason._add_reason(config_id, _build_wrong_config_type_reason(config_id, "DataNodeConfig")) + elif config.scope is not Scope.GLOBAL: + reason._add_reason(config_id, _build_not_global_scope_reason(config_id)) + + return reason + @classmethod def _create_and_set( cls, data_node_config: DataNodeConfig, owner_id: Optional[str], parent_ids: Optional[Set[str]] diff --git a/taipy/core/reason/_reason_factory.py b/taipy/core/reason/_reason_factory.py index 8c18ff1404..c3fb327cd0 100644 --- a/taipy/core/reason/_reason_factory.py +++ b/taipy/core/reason/_reason_factory.py @@ -9,6 +9,8 @@ # 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 Optional + from ..data.data_node import DataNodeId @@ -22,3 +24,14 @@ def _build_data_node_is_not_written(dn_id: DataNodeId) -> str: def _build_not_submittable_entity_reason(entity_id: str) -> str: return f"Entity {entity_id} is not a submittable entity" + + +def _build_wrong_config_type_reason(config_id: str, config_type: Optional[str]) -> str: + if config_type: + return f'Object "{config_id}" must be a valid {config_type}' + + return f'Object "{config_id}" is not a valid config to be created' + + +def _build_not_global_scope_reason(config_id: str) -> str: + return f'Data node config "{config_id}" does not have GLOBAL scope' diff --git a/taipy/core/scenario/_scenario_manager.py b/taipy/core/scenario/_scenario_manager.py index 0b2cee2341..463f130640 100644 --- a/taipy/core/scenario/_scenario_manager.py +++ b/taipy/core/scenario/_scenario_manager.py @@ -46,7 +46,7 @@ from ..job._job_manager_factory import _JobManagerFactory from ..job.job import Job from ..notification import EventEntityType, EventOperation, Notifier, _make_event -from ..reason._reason_factory import _build_not_submittable_entity_reason +from ..reason._reason_factory import _build_not_submittable_entity_reason, _build_wrong_config_type_reason from ..reason.reason import Reasons from ..submission._submission_manager_factory import _SubmissionManagerFactory from ..submission.submission import Submission @@ -114,6 +114,17 @@ def __remove_subscriber(cls, callback, params, scenario: Scenario) -> None: _make_event(scenario, EventOperation.UPDATE, attribute_name="subscribers", attribute_value=params) ) + @classmethod + def _can_create(cls, config: Optional[ScenarioConfig] = None) -> Reasons: + config_id = getattr(config, "id", None) or str(config) + reason = Reasons(config_id) + + if config is not None: + if not isinstance(config, ScenarioConfig): + reason._add_reason(config_id, _build_wrong_config_type_reason(config_id, "ScenarioConfig")) + + return reason + @classmethod def _create( cls, diff --git a/taipy/core/taipy.py b/taipy/core/taipy.py index 0641bb5e68..63cc4b5326 100644 --- a/taipy/core/taipy.py +++ b/taipy/core/taipy.py @@ -873,6 +873,20 @@ def get_cycles() -> List[Cycle]: return _CycleManagerFactory._build_manager()._get_all() +def can_create(config: Optional[Union[ScenarioConfig, DataNodeConfig]] = None) -> Reasons: + """Indicate if a config can be created. The config should be a scenario or data node config. + + If no config is provided, the function indicates if any scenario or data node config can be created. + + Returns: + True if the given config can be created. False otherwise. + """ + if isinstance(config, DataNodeConfig): + return _DataManagerFactory._build_manager()._can_create(config) + + return _ScenarioManagerFactory._build_manager()._can_create(config) + + def create_scenario( config: ScenarioConfig, creation_date: Optional[datetime] = None, diff --git a/tests/core/data/test_data_manager.py b/tests/core/data/test_data_manager.py index f00338fca1..7fb99200da 100644 --- a/tests/core/data/test_data_manager.py +++ b/tests/core/data/test_data_manager.py @@ -45,6 +45,28 @@ def test_create_data_node_and_modify_properties_does_not_modify_config(self): assert dn.properties.get("foo") == "bar" assert dn.properties.get("baz") == "qux" + def test_can_create(self): + dn_config = Config.configure_data_node("dn", 10, scope=Scope.SCENARIO) + global_dn_config = Config.configure_data_node( + id="global_dn", storage_type="in_memory", scope=Scope.GLOBAL, data=10 + ) + + reasons = _DataManager._can_create() + assert bool(reasons) is True + assert reasons._reasons == {} + + reasons = _DataManager._can_create(global_dn_config) + assert bool(reasons) is True + assert reasons._reasons == {} + + reasons = _DataManager._can_create(dn_config) + assert bool(reasons) is False + assert reasons._reasons == {dn_config.id: {'Data node config "dn" does not have GLOBAL scope'}} + + reasons = _DataManager._can_create(1) + assert bool(reasons) is False + assert reasons._reasons == {"1": {'Object "1" must be a valid DataNodeConfig'}} + def test_create_data_node_with_name_provided(self): dn_config = Config.configure_data_node(id="dn", foo="bar", name="acb") dn = _DataManager._create_and_set(dn_config, None, None) diff --git a/tests/core/scenario/test_scenario_manager.py b/tests/core/scenario/test_scenario_manager.py index 17475ae178..022e1762ed 100644 --- a/tests/core/scenario/test_scenario_manager.py +++ b/tests/core/scenario/test_scenario_manager.py @@ -365,6 +365,33 @@ def test_create_and_delete_scenario(): assert len(_ScenarioManager._get_all()) == 0 +def test_can_create(): + dn_config = Config.configure_in_memory_data_node("dn", 10) + task_config = Config.configure_task("task", print, [dn_config]) + scenario_config = Config.configure_scenario("sc", {task_config}, [], Frequency.DAILY) + + reasons = _ScenarioManager._can_create() + assert bool(reasons) is True + assert reasons._reasons == {} + + reasons = _ScenarioManager._can_create(scenario_config) + assert bool(reasons) is True + assert reasons._reasons == {} + _ScenarioManager._create(scenario_config) + + reasons = _ScenarioManager._can_create(task_config) + assert bool(reasons) is False + assert reasons._reasons == {task_config.id: {'Object "task" must be a valid ScenarioConfig'}} + with pytest.raises(AttributeError): + _ScenarioManager._create(task_config) + + reasons = _ScenarioManager._can_create(1) + assert bool(reasons) is False + assert reasons._reasons == {"1": {'Object "1" must be a valid ScenarioConfig'}} + with pytest.raises(AttributeError): + _ScenarioManager._create(1) + + def test_is_deletable(): assert len(_ScenarioManager._get_all()) == 0 scenario_config = Config.configure_scenario("sc", None, None, Frequency.DAILY) diff --git a/tests/core/test_taipy.py b/tests/core/test_taipy.py index 103a1c1184..815ef5a23e 100644 --- a/tests/core/test_taipy.py +++ b/tests/core/test_taipy.py @@ -677,6 +677,18 @@ def test_cycle_exists(self): tp.exists(cycle_id) mck.assert_called_once_with(cycle_id) + def test_can_create(self): + global_dn_config = Config.configure_in_memory_data_node("global_dn", 10, scope=Scope.GLOBAL) + dn_config = Config.configure_in_memory_data_node("dn", 10) + task_config = Config.configure_task("task", print, [dn_config]) + scenario_config = Config.configure_scenario("sc", {task_config}, [], Frequency.DAILY) + + assert tp.can_create() + assert tp.can_create(scenario_config) + assert tp.can_create(global_dn_config) + assert not tp.can_create(dn_config) + assert not tp.can_create("1") + def test_create_global_data_node(self): dn_cfg_global = DataNodeConfig("id", "pickle", Scope.GLOBAL) dn_cfg_scenario = DataNodeConfig("id", "pickle", Scope.SCENARIO)