diff --git a/CHANGELOG.md b/CHANGELOG.md index f24f8f299b0af2cf04688194e0766fd2c6b5d8fa..c4a81f1cef3b2dfac5a36f1fa566369340047745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### +* Parsing from YAML now allows to give an existing model to which the YAML data model shall be + added. + ### Changed ### * A bit better error handling in the yaml model parser. diff --git a/src/caosadvancedtools/models/data_model.py b/src/caosadvancedtools/models/data_model.py index df2f3ad2244c24049830cb3c2f06d1def5b22e0c..beff7e9d847c6a0854d4e38d7cee900d8a376eab 100644 --- a/src/caosadvancedtools/models/data_model.py +++ b/src/caosadvancedtools/models/data_model.py @@ -60,7 +60,8 @@ class DataModel(dict): different purpose (e.g. someone else's experiment). DataModel inherits from dict. The keys are always the names of the - entities. Thus you cannot have unnamed entities in your model. + entities. Thus you cannot have unnamed or ambiguously named entities in your + model. Example: diff --git a/src/caosadvancedtools/models/parser.py b/src/caosadvancedtools/models/parser.py index 60021a8048af3c2f8d26409f3a278fbe8762f989..b354c42bc555a73e97f69889d4a64b6b50b56c83 100644 --- a/src/caosadvancedtools/models/parser.py +++ b/src/caosadvancedtools/models/parser.py @@ -1,8 +1,8 @@ # This file is a part of the CaosDB Project. # -# Copyright (C) 2022 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2023 IndiScale GmbH <info@indiscale.com> # Copyright (C) 2022 Florian Spreckelsen <f.spreckelsen@indiscale.com> -# Copyright (C) 2022 Daniel Hornung <d.hornung@indiscale.com> +# Copyright (C) 2023 Daniel Hornung <d.hornung@indiscale.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -42,7 +42,7 @@ import re import sys import yaml -from typing import List +from typing import List, Optional from warnings import warn import jsonschema @@ -140,15 +140,29 @@ class JsonSchemaDefinitionError(RuntimeError): super().__init__(msg) -def parse_model_from_yaml(filename, existing_model=None): - """Shortcut if the Parser object is not needed.""" +def parse_model_from_yaml(filename, existing_model: Optional[dict] = None): + """Shortcut if the Parser object is not needed. + +Parameters +---------- + +existing_model : dict, optional + An existing model to which the created model shall be added. + """ parser = Parser() return parser.parse_model_from_yaml(filename, existing_model=existing_model) -def parse_model_from_string(string, existing_model=None): - """Shortcut if the Parser object is not needed.""" +def parse_model_from_string(string, existing_model: Optional[dict] = None): + """Shortcut if the Parser object is not needed. + +Parameters +---------- + +existing_model : dict, optional + An existing model to which the created model shall be added. + """ parser = Parser() return parser.parse_model_from_string(string, existing_model=existing_model) @@ -158,28 +172,37 @@ def parse_model_from_json_schema( filename: str, top_level_recordtype: bool = True, types_for_missing_array_items: dict = {}, - ignore_unspecified_array_items: bool = False + ignore_unspecified_array_items: bool = False, + existing_model: Optional[dict] = None ): """Return a datamodel parsed from a json schema definition. Parameters ---------- + filename : str The path of the json schema file that is to be parsed + top_level_recordtype : bool, optional Whether there is a record type defined at the top level of the schema. Default is true. + types_for_missing_array_items : dict, optional dictionary containing fall-back types for json entries with `type: array` but without `items` specification. Default is an empty dict. + ignore_unspecified_array_items : bool, optional Whether to ignore `type: array` entries the type of which is not specified by their `items` property or given in `types_for_missing_array_items`. An error is raised if they are not ignored. Default is False. + existing_model : dict, optional + An existing model to which the created model shall be added. + Returns ------- + out : Datamodel The datamodel generated from the input schema which then can be used for synchronizing with CaosDB. @@ -207,7 +230,7 @@ class Parser(object): self.model = {} self.treated = [] - def parse_model_from_yaml(self, filename, existing_model=None): + def parse_model_from_yaml(self, filename, existing_model: Optional[dict] = None): """Create and return a data model from the given file. Parameters @@ -215,6 +238,9 @@ class Parser(object): filename : str The path to the YAML file. + existing_model : dict, optional + An existing model to which the created model shall be added. + Returns ------- out : DataModel @@ -225,7 +251,7 @@ class Parser(object): return self._create_model_from_dict(ymlmodel, existing_model=existing_model) - def parse_model_from_string(self, string, existing_model=None): + def parse_model_from_string(self, string, existing_model: Optional[dict] = None): """Create and return a data model from the given YAML string. Parameters @@ -233,6 +259,9 @@ class Parser(object): string : str The YAML string. + existing_model : dict, optional + An existing model to which the created model shall be added. + Returns ------- out : DataModel @@ -242,7 +271,7 @@ class Parser(object): return self._create_model_from_dict(ymlmodel, existing_model=existing_model) - def _create_model_from_dict(self, ymlmodel, existing_model=None): + def _create_model_from_dict(self, ymlmodel, existing_model: Optional[dict] = None): """Create and return a data model out of the YAML dict `ymlmodel`. Parameters @@ -251,7 +280,7 @@ class Parser(object): The dictionary parsed from a YAML file. existing_model : dict, optional - An existing model to which the ymlmodel shall be added. + An existing model to which the created model shall be added. Returns ------- @@ -356,8 +385,7 @@ class Parser(object): if definition is None: return - if (self.model[name] is None - and isinstance(definition, dict) + if (self.model[name] is None and isinstance(definition, dict) # is it a property and "datatype" in definition # but not simply an RT of the model diff --git a/unittests/test_yaml_model_parser.py b/unittests/test_yaml_model_parser.py index b1177a46a90d51feb9aea0fa7c3d3d5d95cc2a7a..a26cdf034eea80df0caf64a73ba0d715f66c31db 100644 --- a/unittests/test_yaml_model_parser.py +++ b/unittests/test_yaml_model_parser.py @@ -1,3 +1,21 @@ +# This file is a part of the CaosDB Project. +# +# Copyright (C) 2023 IndiScale GmbH <info@indiscale.com> +# Copyright (C) 2023 Daniel Hornung <d.hornung@indiscale.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + import unittest from datetime import date from tempfile import NamedTemporaryFile @@ -340,6 +358,35 @@ A: assert "line {}".format(line) in yde.exception.args[0] +def test_existing_model(): + """Parsing more than one model may require to append to existing models.""" + model_str_1 = """ +A: + obligatory_properties: + number: + datatype: INTEGER + """ + model_str_2 = """ +B: + obligatory_properties: + A: + """ + model_1 = parse_model_from_string(model_str_1) + model_2 = parse_model_from_string(model_str_2, existing_model=model_1) + for ent in ["A", "B", "number"]: + assert ent in model_2 + + model_str_redefine = """ +number: + datatype: DOUBLE + description: Hello number! + """ + model_redefine = parse_model_from_string(model_str_redefine, existing_model=model_1) + print(model_redefine) + assert model_redefine["number"].description == "Hello number!" + assert model_redefine["number"].datatype == db.INTEGER # FIXME Shouldn't this be DOUBLE? + + def test_define_role(): model = """ A: