Skip to content

Commit

Permalink
Put suggestions into error messages when parsing jaquel.
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
2 people authored and a.krantz committed Oct 28, 2024
1 parent 9edc710 commit cea586b
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 11 deletions.
70 changes: 64 additions & 6 deletions src/odsbox/jaquel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import re
from typing import Tuple, List, Any
from difflib import get_close_matches

from google.protobuf.internal import containers as _containers

Expand Down Expand Up @@ -127,7 +128,60 @@ def __model_get_entity_ex(model: ods.Model, entity_name_or_aid: str | int) -> od
if entity.name.lower() == entity_name_or_aid.lower() or entity.base_name.lower() == entity_name_or_aid.lower():
return entity

raise SyntaxError(f"Entity '{entity_name_or_aid}' is unknown in model.")
raise SyntaxError(
f"Entity '{entity_name_or_aid}' is unknown in model.{__model_get_suggestion_entity(model, entity_name_or_aid)}"
)


def __model_get_suggestion(lower_case_dict: dict, str_val: str) -> str:
suggestions = get_close_matches(
str_val.lower(),
lower_case_dict,
n=1,
cutoff=0.3,
)
if len(suggestions) > 0:
return_value = lower_case_dict[suggestions[0]]
return f" Did you mean '{return_value}'?"
return ""


def __model_get_enum_suggestion(enumeration: ods.Model.Enumeration, str_val: str) -> str:
available = {key.lower(): key for key in enumeration.items}
return __model_get_suggestion(available, str_val)


def __model_get_suggestion_attribute(entity: ods.Model.Entity, attribute_or_relation_name: str) -> str:
available = {}
available.update({relation.base_name.lower(): relation.base_name for key, relation in entity.relations.items()})
available.update({attribute.base_name.lower(): attribute.base_name for key, attribute in entity.attributes.items()})
available.update({relation.name.lower(): relation.name for key, relation in entity.relations.items()})
available.update({attribute.name.lower(): attribute.name for key, attribute in entity.attributes.items()})
return __model_get_suggestion(available, attribute_or_relation_name)


def __model_get_suggestion_relation(entity: ods.Model.Entity, relation_name: str) -> str:
available = {}
available.update({relation.base_name.lower(): relation.base_name for key, relation in entity.relations.items()})
available.update({relation.name.lower(): relation.name for key, relation in entity.relations.items()})
return __model_get_suggestion(available, relation_name)


def __model_get_suggestion_entity(model: ods.Model, entity_name: str) -> str:
available = {}
available.update({entity.base_name.lower(): entity.base_name for key, entity in model.entities.items()})
available.update({entity.name.lower(): entity.name for key, entity in model.entities.items()})
return __model_get_suggestion(available, entity_name)


def __model_get_suggestion_aggregate(aggregate_name: str) -> str:
available = {key.lower(): key for key in _jo_aggregates}
return __model_get_suggestion(available, aggregate_name)


def __model_get_suggestion_operators(operator_name: str) -> str:
available = {key.lower(): key for key in _jo_operators}
return __model_get_suggestion(available, operator_name)


def __model_get_enum_index(model: ods.Model, entity: ods.Model.Entity, attribute_name: str, str_val: str) -> int:
Expand All @@ -137,7 +191,7 @@ def __model_get_enum_index(model: ods.Model, entity: ods.Model.Entity, attribute
if key.lower() == str_val.lower():
return enum.items[key]

raise SyntaxError('Enum entry for "' + str_val + '" does not exist')
raise SyntaxError(f"Enum entry for '{str_val}' does not exist.{__model_get_enum_suggestion(enum, str_val)}")


def _jo_enum_get_numeric_value(
Expand Down Expand Up @@ -191,7 +245,10 @@ def __parse_path_and_add_joins(
# Must be a relation
relation = __model_get_relation(model, attribute_entity, path_part)
if relation is None:
raise SyntaxError(f"'{path_part}' is no relation of entity '{attribute_entity.name}'")
raise SyntaxError(
f"'{path_part}' is no relation of entity '{attribute_entity.name}'.{
__model_get_suggestion_relation(attribute_entity, path_part)}"
)
attribute_name = relation.name

# add join
Expand All @@ -217,7 +274,8 @@ def __parse_path_and_add_joins(
relation = __model_get_relation(model, attribute_entity, path_part)
if relation is None:
raise SyntaxError(
f"'{path_part}' is neither attribute nor relation of entity '{attribute_entity.name}'"
f"'{path_part}' is neither attribute nor relation of entity '{attribute_entity.name}'.{
__model_get_suggestion_attribute(attribute_entity, path_part)}"
)
attribute_name = relation.name
attribute_type = ods.DataTypeEnum.DT_LONGLONG # its an id
Expand Down Expand Up @@ -281,7 +339,7 @@ def __parse_attributes(
elif "$options" == element:
raise SyntaxError("Actually no $options defined for attributes")
else:
raise SyntaxError('Unknown aggregate "' + element + '"')
raise SyntaxError(f"Unknown aggregate '{element}'.{__model_get_suggestion_aggregate(element)}")
else:
if element_attribute["path"]:
element_attribute["path"] += "."
Expand Down Expand Up @@ -612,7 +670,7 @@ def __parse_conditions(
elif "$options" == elem:
continue
else:
raise SyntaxError('Unknown operator "' + elem + '"')
raise SyntaxError(f"Unknown operator '{elem}'.{__model_get_suggestion_operators(elem)}")
else:
if elem_attribute["path"]:
elem_attribute["path"] += "."
Expand Down
105 changes: 100 additions & 5 deletions tests/test_jaquel_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,22 @@ def test_syntax_errors():
with pytest.raises(SyntaxError, match="Does not define a target entity."):
jaquel_to_ods(model, {"$attributes": {"factor": {"$min": 1}}})

with pytest.raises(SyntaxError, match='Unknown aggregate "\\$mi"'):
with pytest.raises(SyntaxError, match="Unknown aggregate '\\$mi'."):
jaquel_to_ods(model, {"AoUnit": {}, "$attributes": {"factor": {"$mi": 1}}})

with pytest.raises(SyntaxError, match='Unknown operator "\\$lik"'):
with pytest.raises(SyntaxError, match="Unknown operator '\\$lik'. Did you mean '\\$like'?'"):
jaquel_to_ods(model, {"AoLocalColumn": {"name": {"$lik": "abc"}}})

with pytest.raises(SyntaxError, match="'name' is no relation of entity 'LocalColumn'"):
with pytest.raises(SyntaxError, match="'name' is no relation of entity 'LocalColumn'."):
jaquel_to_ods(model, {"AoLocalColumn": {"name": {"like": "abc"}}})

with pytest.raises(json.decoder.JSONDecodeError):
jaquel_to_ods(model, "{")

with pytest.raises(SyntaxError, match="'nr_of_rows' is neither attribute nor relation of entity 'SubMatrix'"):
with pytest.raises(
SyntaxError,
match="'nr_of_rows' is neither attribute nor relation of entity 'SubMatrix'. Did you mean 'number_of_rows'?",
):
jaquel_to_ods(
model, {"AoLocalColumn": {}, "$attributes": {"Id": 1, "name": 1, "submatrix": {"nr_of_rows": 1, "name": 1}}}
)
Expand Down Expand Up @@ -205,5 +208,97 @@ def test_is_in():
_, select_statement = jaquel_to_ods(model, {"AoMeasurementQuantity": {"name": {"$in": ["first", "second"]}}})
assert select_statement is not None

with pytest.raises(SyntaxError, match='Enum entry for "does_not_exist" does not exist'):
with pytest.raises(SyntaxError, match="Enum entry for 'does_not_exist' does not exist."):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"datatype": {"$in": ["does_not_exist"]}}})

with pytest.raises(SyntaxError, match="Enum entry for 'DTLONG' does not exist."):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"datatype": {"$in": ["DTLONG"]}}})


def test_suggestions_enum():
model = __get_model("application_model.json")

with pytest.raises(SyntaxError, match="Enum entry for 'DTLONG' does not exist. Did you mean 'DT_LONG'?"):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"datatype": "DTLONG"}})

with pytest.raises(SyntaxError, match="Enum entry for 'dtlong' does not exist. Did you mean 'DT_LONG'?"):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"datatype": "dtlong"}})

with pytest.raises(SyntaxError, match="Enum entry for 'LONG' does not exist. Did you mean 'DT_LONG'?"):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"datatype": "LONG"}})

with pytest.raises(SyntaxError, match="Enum entry for 'INT32' does not exist."):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"datatype": "INT32"}})


def test_suggestions_attribute():
model = __get_model("application_model.json")

with pytest.raises(
SyntaxError,
match="'data_type' is neither attribute nor relation of entity 'MeaQuantity'. Did you mean 'DataType'?",
):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"data_type": "DT_LONG"}})

with pytest.raises(
SyntaxError,
match="'units' is neither attribute nor relation of entity 'MeaQuantity'. Did you mean 'Unit'?",
):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"units": 4711}})


def test_suggestions_relation():
model = __get_model("application_model.json")

with pytest.raises(
SyntaxError,
match="'units' is no relation of entity 'MeaQuantity'. Did you mean 'Unit'?",
):
jaquel_to_ods(model, {"AoMeasurementQuantity": {"units.name": "m"}})


def test_suggestions_entity():
model = __get_model("application_model.json")

with pytest.raises(
SyntaxError,
match="Entity 'AoMeasurmentQuantity' is unknown in model. Did you mean 'AoMeasurementQuantity'?",
):
jaquel_to_ods(model, {"AoMeasurmentQuantity": {"datatype": "DT_LONG"}})

with pytest.raises(
SyntaxError,
match="Entity 'MeasurmentQuantity' is unknown in model. Did you mean 'AoMeasurementQuantity'?",
):
jaquel_to_ods(model, {"MeasurmentQuantity": {"datatype": "DT_LONG"}})

with pytest.raises(
SyntaxError,
match="Entity 'AoMeasurmentQuantity' is unknown in model. Did you mean 'AoMeasurementQuantity'?",
):
jaquel_to_ods(model, {"AoMeasurmentQuantity": {"datatype": "DT_LONG"}})

with pytest.raises(
SyntaxError,
match="Entity 'AoTests' is unknown in model. Did you mean 'AoTest'?",
):
jaquel_to_ods(model, {"AoTests": {"name": "Start"}})


def test_suggestions_aggregate():
model = __get_model("application_model.json")

with pytest.raises(SyntaxError, match="Unknown aggregate '\\$stev'. Did you mean '\\$stddev'?"):
jaquel_to_ods(model, {"AoUnit": {}, "$attributes": {"factor": {"$stev": 1}}})

with pytest.raises(SyntaxError, match="Unknown aggregate '\\$regexp'. Did you mean '\\$max'?"):
jaquel_to_ods(model, {"AoUnit": {}, "$attributes": {"factor": {"$regexp": "a.*"}}})

with pytest.raises(SyntaxError, match="Unknown operator '\\$GTEQ'. Did you mean '\\$gte'?"):
jaquel_to_ods(model, {"AoUnit": {"factor": {"$GTEQ": 2.0}}})

with pytest.raises(SyntaxError, match="Unknown operator '\\$gtE'. Did you mean '\\$gte'?."):
jaquel_to_ods(model, {"AoUnit": {"factor": {"$gtE": 2.0}}})

with pytest.raises(SyntaxError, match="Unknown operator '\\$abc'. Did you mean '\\$between'?"):
jaquel_to_ods(model, {"AoUnit": {"factor": {"$abc": 2.0}}})

0 comments on commit cea586b

Please sign in to comment.