Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enh/schema validation attribute values #830

Merged
merged 13 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 79 additions & 14 deletions osp/core/utils/schema_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from osp.core.namespaces import get_entity
from osp.core.ontology import OntologyAttribute, OntologyRelationship
from osp.core.ontology.datatypes import YML_DATATYPES

logger = logging.getLogger(__name__)

Expand All @@ -18,7 +19,7 @@ class CardinalityError(Exception):
"""A cardinality constraint is violated."""


def validate_tree_against_schema(root_obj, schema_file):
def validate_tree_against_schema(root_obj, schema_file, strict_check=False):
"""Test cardinality constraints on given CUDS tree.

The tree that starts at root_obj.
Expand All @@ -28,6 +29,8 @@ def validate_tree_against_schema(root_obj, schema_file):
root_obj (Cuds): The root CUDS object of the tree
schema_file (str): The path to the schema file that
defines the constraints
strict_check (bool): whether extra cuds not listed in
the schema_file should be tolerated or not

Raise:
Exception: Tells the user which constraint was violated
Expand Down Expand Up @@ -68,9 +71,12 @@ def validate_tree_against_schema(root_obj, schema_file):
try:
relationships = data_model_dict["model"][oclass]
except KeyError:
# TODO ask Yoav: is it ok when there is an object
# in the tree that is not part of the datamodel?
continue
if strict_check:
message = f"An entity for {oclass} was found,"
" but it is not part of the provided schema"
raise ConsistencyError(message)
else:
continue
if relationships is None:
# if there are no relationships defined,
# the only constraint is that the object exists
Expand All @@ -97,7 +103,6 @@ def _load_data_model_from_yaml(data_model_file):


def _check_cuds_object_cardinality(origin_cuds, dest_oclass, rel, constraints):

rel_entity = get_entity(rel)

if type(rel_entity) == OntologyRelationship:
Expand All @@ -117,8 +122,9 @@ def _check_cuds_object_cardinality(origin_cuds, dest_oclass, rel, constraints):

min, max = _interpret_cardinality_value_from_constraints(constraints)
if actual_cardinality < min or actual_cardinality > max:
message = """Found invalid cardinality between {} and {} with relationship {}.
The constraint says it should be between {} and {}, but we found {}.
message = """Found invalid cardinality between {} and {}
with relationship {}. The constraint says it should be
between {} and {}, but we found {}.
The uid of the affected cuds_object is: {}""".format(
str(origin_cuds.oclass),
dest_oclass,
Expand All @@ -130,6 +136,64 @@ def _check_cuds_object_cardinality(origin_cuds, dest_oclass, rel, constraints):
)
raise CardinalityError(message)

_check_attribute_contraints(
origin_cuds, rel_entity, dest_oclass, constraints
)


def _check_attribute_contraints(
origin_cuds, rel_entity, dest_oclass, constraints
):
attribute = origin_cuds.get_attributes().get(rel_entity)
value = constraints.get("value")
if attribute:
if value and attribute != value:
message = """Found invalid attribute value
between {} and {} with relationship {}.
The constraint says it should be valued '{}',
but we found '{}'. The uid of the affected
cuds_object is: {}""".format(
str(origin_cuds.oclass),
dest_oclass,
rel_entity,
value,
attribute,
origin_cuds.uid,
)
raise ConsistencyError(message)

if type(attribute) == str:
attribute = len(attribute)
target = "length"
else:
target = "range"
min, max = _interpret_attribute_from_constraints(constraints, target)
if attribute < min or attribute > max:
message = """Found invalid attribute value {} between {} and {}
relationship {}. The constraint says it should be between {}
and {}, but we found {}. The uid of the affected
cuds_object is: {}""".format(
target,
str(origin_cuds.oclass),
dest_oclass,
rel_entity,
min,
max,
attribute,
origin_cuds.uid,
)
raise CardinalityError(message)


def _interpret_attribute_from_constraints(constraints, range_or_len: str):
min = -float("inf")
if constraints is not None:
value = constraints.get(range_or_len)
min, max = _interpret_cardinality_value_from_constraints(
dict(cardinality=value)
)
return min, max


def _interpret_cardinality_value_from_constraints(constraints):
# default is arbitrary
Expand All @@ -140,11 +204,12 @@ def _interpret_cardinality_value_from_constraints(constraints):
if isinstance(cardinality_value, int):
min = cardinality_value
max = cardinality_value
elif "-" in cardinality_value:
min = int(cardinality_value.split("-")[0])
max = int(cardinality_value.split("-")[1])
elif "+" in cardinality_value:
min = int(cardinality_value.split("+")[0])
elif isinstance(cardinality_value, str):
if "-" in cardinality_value:
min = int(cardinality_value.split("-")[0])
max = int(cardinality_value.split("-")[1])
elif "+" in cardinality_value:
min = int(cardinality_value.split("+")[0])
return min, max


Expand Down Expand Up @@ -181,9 +246,9 @@ def _get_optional_and_mandatory_subtrees(data_model_dict):
min, max = _interpret_cardinality_value_from_constraints(
constraints
)
if min == 0:
if min == 0 and neighbor not in YML_DATATYPES.keys():
optional_subtrees.add(neighbor)
if min > 0:
if min > 0 and neighbor not in YML_DATATYPES.keys():
mandatory_subtrees.add(neighbor)

if optional_subtrees & mandatory_subtrees:
Expand Down
25 changes: 24 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ def test_validate_tree_against_schema(self):
"test_validation_schema_city_with_attribute.yml",
)

c = city.City(name="freiburg")
schema_file_with_attribute_value = os.path.join(
os.path.dirname(__file__),
"test_validation_schema_city_with_attribute_value.yml",
)

c = city.City(name="Freiburg")

# empty city is not valid
self.assertRaises(
Expand Down Expand Up @@ -193,8 +198,26 @@ def test_validate_tree_against_schema(self):
schema_file_with_missing_entity,
)

# now we validate the attributes and their cardinality
validate_tree_against_schema(c, schema_file_with_attribute)

# additionally we check the length and the value of the attribute
validate_tree_against_schema(c, schema_file_with_attribute_value)

# and if there are more objects in tree than in the schema
# it can be specified if the test should be done strictly
c.add(wrong_object, rel=city.hasPart)
# first no strict check - additional cuds is tolerated:
validate_tree_against_schema(c, schema_file_with_attribute_value)
# second with strict check - additional cuds is not tolerated:
self.assertRaises(
ConsistencyError,
validate_tree_against_schema,
c,
schema_file_with_attribute_value,
strict_check=True,
)

def test_branch(self):
"""Test the branch function."""
x = branch(
Expand Down
40 changes: 40 additions & 0 deletions tests/test_validation_schema_city_with_attribute_value.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version: "1.0.0"

oclass: city.City

model:
city.City:
city.name:
STRING:
value: Freiburg
city.hasInhabitant:
city.Citizen:
cardinality: 1-2
city.hasPart:
city.Neighborhood:
cardinality: 1

city.Neighborhood:
city.name:
STRING:
cardinality: 1
city.hasPart:
city.Street:
cardinality: 1+

city.Street:
city.name:
STRING:
length: 1-10
cardinality: 1

city.Citizen:
city.name:
STRING:
value: peter
length: 1+
cardinality: 1
city.age:
INT:
range: 0+
cardinality: 1