# -*- coding: utf-8 -*-
"""openepda.validators.py
This file contains validators for the data formats defined by
the openEPDA.
Author: Dima Pustakhod
Copyright: 2020, TU/e - PITC and authors
"""
import os
from itertools import chain
from numbers import Real
from typing import Dict
from typing import Iterable
from .expr import OpenEpdaExpressionError, check_parameter_name, openepda_parser
try:
from ruamel.yaml import YAML
yaml = YAML(typ="safe")
safe_load = yaml.load
except ImportError:
from yaml import safe_load
module_fpath = os.path.abspath(os.path.join(__file__, ".."))
CDF_VERSIONS = ["0.1", "0.2", "0.3"]
CDF_VERSION_STATUS = {'stable': '0.3', 'latest': "0.3"}
CDF_SCHEMA_NAME_TEMPLATE = "/schemas/cdf_schema_v{version}.yaml"
UPDK_SBB_VERSIONS = ["0.4", "0.3", "0.2"]
UPDK_SBB_VERSION_STATUS = {'stable': '0.3', "draft": '0.4', 'latest': "0.4"}
UPDK_SBB_SCHEMA_NAME_TEMPLATE = "/schemas/updk_sbb_schema_v{version}.yaml"
def get_version_number(version, all_versions, version_status: Dict[str, str]):
assert set(version_status.values()) - set(all_versions) == set()
if version in all_versions:
result = version
elif version in version_status:
result = version_status[version]
else:
allowed_versions = all_versions + list(version_status)
raise ValueError(
f"Unknown version specified: {version}. "
f"Allowed versions: {allowed_versions}."
)
return result
[docs]def is_cdf_valid(data, version="latest", raise_error=False, full_output=False):
"""Check if the data is a correct CDF of a given version.
JSON Schema is used for validation. See http://json-schema.org
Parameters
----------
data : any
data structure to be validated
version : str
CDF format version, 'X.Y' or 'latest'
raise_error : bool
If True, a jsonschema.ValidationError is raised in case of wrong
format. If False, a boolean result will be returned (with
optional message).
full_output : bool
If False, only resulting boolean value is returned. Otherwise,
additional string with a validation message is returned.
Returns
-------
bool
validation result
str
validation message (optional)
Examples
--------
>>> with open('tests/_test_data/cdf_v0.3_correct.cdf') as s:
... data = safe_load(s)
>>> is_cdf_valid(data, raise_error=False)
True
>>> is_cdf_valid(data, version='0.3', raise_error=False)
True
"""
from jsonschema import ValidationError, validate
version = get_version_number(version, CDF_VERSIONS, CDF_VERSION_STATUS)
cdf_schema_file_name = module_fpath + CDF_SCHEMA_NAME_TEMPLATE.format(
version=version
)
with open(cdf_schema_file_name) as s:
schema = safe_load(s)
# default reply
result, msg = True, "Data is valid CDF v.{version}.".format(version=version)
# Validate against the schema
if raise_error:
validate(instance=data, schema=schema)
else:
try:
validate(instance=data, schema=schema)
except ValidationError as e:
result = False
msg = str(e)
if full_output:
return result, msg
else:
return result
[docs]def is_updk_sbb_valid(
data, version="latest", raise_error=False, full_output=False
):
"""Check if the data structure is a correct uPDK SBB.
JSON Schema is used for validation. See http://json-schema.org .
Validation runs against a specification with a given version.
Parameters
----------
data : Dict
data structure to be validated
version : str
format version, 'X.Y' or 'latest'
raise_error : bool
If True, a jsonschema.ValidationError is raised in case of wrong
format. If False, an boolean result will be returned (with
optional message).
full_output : bool
If False, only resulting boolean value is returned. Otherwise,
additional string with a validation message is returned.
Returns
-------
bool
validation result
str
validation message (optional)
Examples
--------
>>> with open('tests/_test_data/sbb_v0.4_correct.yaml') as s:
... data = safe_load(s)
>>> is_updk_sbb_valid(data, raise_error=True)
True
"""
from jsonschema import ValidationError, validate
version = get_version_number(version, UPDK_SBB_VERSIONS, UPDK_SBB_VERSION_STATUS)
updk_schema_file_name = module_fpath + UPDK_SBB_SCHEMA_NAME_TEMPLATE.format(
version=version
)
with open(updk_schema_file_name) as s:
schema = safe_load(s)
# default reply
result, msg = True, "Data is valid uPDK SBB v.{}.".format(version)
# validate against the schema
if raise_error:
validate(instance=data, schema=schema)
else:
try:
validate(instance=data, schema=schema)
except ValidationError as e:
result = False
msg = str(e)
# validate bb parameter names
if result:
if raise_error:
_check_building_blocks(data["blocks"])
else:
try:
_check_building_blocks(data["blocks"])
except ValueError as e:
result = False
msg = str(e)
if full_output:
return result, msg
else:
return result
def _check_building_blocks(building_blocks):
"""Check building blocks' params and expressions.
Parameters
----------
building_blocks : dict
Returns
-------
bool
True if all BBs are correct
Raises
------
ValueError
if check fails
"""
for bb_name, bb in building_blocks.items():
_check_building_block(bb_name, bb)
def _check_building_block(bb_name, bb):
"""Check building blocks' params and expressions.
Currently checking:
- BB parameter names
- BB width, length expressions
- BB bounding box coordinates
- BB pins width and xya
Parameters
----------
bb_name : str
name of the building block.
bb : dict
Building block item from the uPDK.
Returns
-------
bool
True if all BBs are correct
Raises
------
ValueError
if check fails
"""
# Check parameters
has_params = bool(bb["parameters"])
if has_params:
for name, param in bb["parameters"].items():
_check_parameter(name, param, bb_name)
bb_params = set(bb["parameters"].keys())
else:
bb_params = set()
if "bb_width" in bb:
_check_bb_num_value(bb["bb_width"], bb_name, params=bb_params)
if "bb_length" in bb:
_check_bb_num_value(bb["bb_length"], bb_name, params=bb_params)
# check bbox
_check_bbox(bb["bbox"], bb_name, bb_params)
# check pins
for pin_name, pin in bb["pins"].items():
_check_pin(pin_name, pin, bb_name, bb_params)
for p in ("pin_in", "pin_out"):
if p in bb:
if not bb[p] in bb["pins"].keys():
raise ValueError(
"{} '{}' is not present in the list of pins "
"for in BB {}.".format(p, bb["pin_in"], bb_name)
)
return True
def _check_parameter(name, data, bb_name):
check_parameter_name(name, reserved=openepda_parser.reserved_identifiers)
def _check_bbox(bbox_data, bb_name, bb_params: Iterable[str]):
# check bbox
for bbox_coord_expr in chain(*bbox_data):
_check_bb_num_value(bbox_coord_expr, bb_name, params=bb_params)
def _check_bb_num_value(
v, bb_name, params: Iterable[str] = (), parser=openepda_parser
):
"""Wrapper around _check_num_value() for BB params.
"""
try:
_check_num_value(v, params=params, parser=parser)
except OpenEpdaExpressionError as e:
raise ValueError(
"error in numeric value in BB {}: {}".format(bb_name, e)
)
return True
def _check_pin(pin_name, pin_data, bb_name, bb_params: Iterable[str]):
_check_pin_num_value(pin_data["width"], pin_name, bb_name, params=bb_params)
for i in range(3):
_check_pin_num_value(
pin_data["xya"][i], pin_name, bb_name, params=bb_params
)
def _check_pin_num_value(
v, pin_name, bb_name, params: Iterable[str] = (), parser=openepda_parser
):
"""Wrapper around _check_num_value() for BB pin params.
"""
try:
_check_num_value(v, params=params, parser=parser)
except OpenEpdaExpressionError as e:
raise ValueError(
"error in numeric value in pin {} of BB {}: {}".format(
pin_name, bb_name, e
)
)
return True
def _check_num_value(v, params: Iterable[str] = (), parser=openepda_parser):
"""Check if the value is a proper number or expression
Check is successful if the expression is a number, or if:
- expression can be parsed using parser
- it contains only params which are explicitly listed in params
Parameters
----------
v : num or str
Expression to be checked.
params : iterable of str
allowed variable names.
parser : _BaseParser
parser to be used for the expression parsing
Returns
-------
bool
True if expr is correct
Raises
------
ValueError
if the check is not successful
"""
if isinstance(v, Real):
return True
else:
return parser.check_missing_params(v, params)
def main():
import doctest
os.chdir("{}/..".format(module_fpath))
doctest.testmod(verbose=False)
if __name__ == "__main__":
main()