Skip to content

Commit

Permalink
Support the new result file schema (#8)
Browse files Browse the repository at this point in the history
Signed-off-by: patrickpa <[email protected]>
  • Loading branch information
patrickpa authored Jun 11, 2024
1 parent e671003 commit 75db6fc
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 18 deletions.
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ From Pypi:
pip install qc_baselib
```

From Github repository:

```bash
pip install qc_baselib @ git+https://github.com/asam-ev/qc-baselib-py@main
```

Locally for developing using [Poetry](https://python-poetry.org/):

```bash
Expand Down Expand Up @@ -187,24 +193,32 @@ def main():
summary="Executed evaluation",
)

result.register_issue(
rule_uid = result.register_rule(
checker_bundle_name="TestBundle",
checker_id="TestChecker",
emanating_entity="test.com",
standard="qc",
definition_setting="1.0.0",
rule_full_name="qwerty.qwerty",
)

issue_id = result.register_issue(
checker_bundle_name="TestBundle",
checker_id="TestChecker",
issue_id=0,
description="Issue found at odr",
level=IssueSeverity.INFORMATION,
rule_uid=rule_uid,
)

result.add_file_location(
checker_bundle_name="TestBundle",
checker_id="TestChecker",
issue_id=0,
issue_id=issue_id,
row=1,
column=0,
file_type="odr",
description="Location for issue",
)
# xml location are also supported

result.write_to_file("testResults.xqar")

Expand Down Expand Up @@ -290,6 +304,8 @@ Issue id: 0
Issue level: 3
```

For more use case examples refer to the library [tests](tests/).

## Tests

- Install module on development mode
Expand Down
1 change: 1 addition & 0 deletions qc_baselib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
from .configuration import Configuration as Configuration
from .result import Result as Result
from .models import IssueSeverity as IssueSeverity
from .models import StatusType as StatusType
1 change: 1 addition & 0 deletions qc_baselib/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .common import IssueSeverity as IssueSeverity
from .result import StatusType as StatusType
159 changes: 151 additions & 8 deletions qc_baselib/models/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
# This Source Code Form is subject to the terms of the Mozilla
# Public License, v. 2.0. If a copy of the MPL was not distributed
# with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
from typing import List, Any
import enum

from typing import List, Any, Set, Dict
from pydantic import model_validator
from pydantic_xml import BaseXmlModel, attr
from pydantic_xml import BaseXmlModel, attr, element

from .common import ParamType, IssueSeverity

Expand All @@ -18,10 +20,10 @@ class XMLLocationType(BaseXmlModel, tag="XMLLocation"):
xpath: str = attr(name="xpath")


class RoadLocationType(BaseXmlModel, tag="RoadLocation"):
road_id: int = attr(name="roadId")
t: str = attr(name="t")
s: str = attr(name="s")
class InertialLocationType(BaseXmlModel, tag="InertialLocation"):
x: float
y: float
z: float


class FileLocationType(BaseXmlModel, tag="FileLocation"):
Expand All @@ -33,7 +35,7 @@ class FileLocationType(BaseXmlModel, tag="FileLocation"):
class LocationType(BaseXmlModel, tag="Location"):
file_location: List[FileLocationType] = []
xml_location: List[XMLLocationType] = []
road_location: List[RoadLocationType] = []
road_location: List[InertialLocationType] = []
description: str = attr(name="description")

@model_validator(mode="after")
Expand All @@ -48,19 +50,160 @@ def check_at_least_one_element(self) -> Any:
return self


class RuleType(BaseXmlModel, tag="AddressedRule"):
"""
Type containing the Rule Schema rules and its required checks
More information at:
https://github.com/asam-ev/qc-framework/blob/main/doc/manual/rule_uid_schema.md
"""

# The current implementation makes Rule members required, so no element can
# be left empty for the uid composition.

emanating_entity: str = attr(
name="emanating_entity", default="", pattern=r"^((\w+(\.\w+)+))$", exclude=True
)
standard: str = attr(
name="standard", default="", pattern=r"^(([a-z]+))$", exclude=True
)
definition_setting: str = attr(
name="definition_setting",
default="",
pattern=r"^(([0-9]+(\.[0-9]+)+))$",
exclude=True,
)
rule_full_name: str = attr(
name="rule_full_name",
default="",
pattern=r"^((([a-z][\w_]*)\.)*)([a-z][\w_]*)$",
exclude=True,
)

rule_uid: str = attr(
name="ruleUID",
default="",
pattern=r"^((\w+(\.\w+)+)):(([a-z]+)):(([0-9]+(\.[0-9]+)+)):((([a-z][\w_]*)\.)*)([a-z][\w_]*)$",
)

@model_validator(mode="after")
def load_fields_into_uid(self) -> Any:
"""
Loads fields into rule uid if all required fields are present.
Otherwise it skips initialization.
"""
if (
self.emanating_entity != ""
and self.standard != ""
and self.definition_setting != ""
and self.rule_full_name != ""
):
self.rule_uid = f"{self.emanating_entity}:{self.standard}:{self.definition_setting}:{self.rule_full_name}"

return self

@model_validator(mode="after")
def load_uid_into_fields(self) -> Any:
"""
Loads fields from rule uid if no field is present in the model.
Otherwise it skips initialization.
"""
if (
self.emanating_entity == ""
and self.standard == ""
and self.definition_setting == ""
and self.rule_full_name == ""
):
elements = self.rule_uid.split(":")

if len(elements) < 4:
raise ValueError(
"Not enough elements to parse Rule UID. This should follow pattern described at https://github.com/asam-ev/qc-framework/blob/main/doc/manual/rule_uid_schema.md"
)

self.emanating_entity = elements[0]
self.standard = elements[1]
self.definition_setting = elements[2]
self.rule_full_name = elements[3]

return self

@model_validator(mode="after")
def check_any_empty(self) -> Any:
"""
Validates if any field is empty after initialization. No field should
be leave empty after a successful initialization happens.
"""
if self.rule_uid == "":
raise ValueError("Empty initialization of rule_uid")
if self.emanating_entity == "":
raise ValueError("Empty initialization of emanating_entity")
if self.standard == "":
raise ValueError("Empty initialization of standard")
if self.definition_setting == "":
raise ValueError("Empty initialization of definition_setting")
if self.rule_full_name == "":
raise ValueError("Empty initialization of rule_full_name")

return self


class IssueType(BaseXmlModel, tag="Issue"):
locations: List[LocationType] = []
issue_id: int = attr(name="issueId")
description: str = attr(name="description")
level: IssueSeverity = attr(name="level")
rule_uid: str = attr(
name="ruleUID",
default="",
pattern=r"^((\w+(\.\w+)+)):(([a-z]+)):(([0-9]+(\.[0-9]+)+)):((([a-z][\w_]*)\.)*)([a-z][\w_]*)$",
)


class MetadataType(BaseXmlModel, tag="Metadata"):
key: str = attr(name="key")
value: str = attr(name="value")
description: str = attr(name="description")


class StatusType(str, enum.Enum):
COMPLETED = "completed"
ERROR = "error"
SKIPPED = "skipped"


class CheckerType(BaseXmlModel, tag="Checker"):
class CheckerType(BaseXmlModel, tag="Checker", validate_assignment=True):
addressed_rule: List[RuleType] = []
issues: List[IssueType] = []
metadata: List[MetadataType] = []
status: StatusType = attr(name="status", default="")
checker_id: str = attr(name="checkerId")
description: str = attr(name="description")
summary: str = attr(name="summary")

@model_validator(mode="after")
def check_issue_ruleUID_matches_addressed_rules(self) -> Any:
if len(self.issues):
addressed_rule_uids: Set[int] = set()

for addressed_rule in self.addressed_rule:
addressed_rule_uids.add(addressed_rule.rule_uid)

for issue in self.issues:
if issue.rule_uid not in addressed_rule_uids:
raise ValueError(
f"Issue Rule UID '{issue.rule_uid}' does not match addressed rules UIDs {list(addressed_rule_uids)}"
)
return self

@model_validator(mode="after")
def check_skipped_status_containing_issues(self) -> Any:
if self.status == StatusType.SKIPPED and len(self.issues) > 0:
raise ValueError(
f"{self.checker_id}\nCheckers with skipped status cannot contain issues. Issues found: {len(self.issues)}"
)
return self


class CheckerBundleType(BaseXmlModel, tag="CheckerBundle"):
params: List[ParamType] = []
Expand Down
48 changes: 46 additions & 2 deletions qc_baselib/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ def register_checker_bundle(
self._report_results.checker_bundles.append(bundle)

def register_checker(
self, checker_bundle_name: str, checker_id: str, description: str, summary: str
self,
checker_bundle_name: str,
checker_id: str,
description: str,
summary: str,
) -> None:

checker = result.CheckerType(
Expand All @@ -181,12 +185,40 @@ def register_checker(

bundle.checkers.append(checker)

def register_rule(
self,
checker_bundle_name: str,
checker_id: str,
emanating_entity: str,
standard: str,
definition_setting: str,
rule_full_name: str,
) -> str:
"""
Rule will be registered to checker and the generated rule uid will be
returned.
"""

rule = result.RuleType(
emanating_entity=emanating_entity,
standard=standard,
definition_setting=definition_setting,
rule_full_name=rule_full_name,
)

bundle = self._get_checker_bundle(checker_bundle_name=checker_bundle_name)
checker = self._get_checker(bundle=bundle, checker_id=checker_id)
checker.addressed_rule.append(rule)

return rule.rule_uid

def register_issue(
self,
checker_bundle_name: str,
checker_id: str,
description: str,
level: IssueSeverity,
rule_uid: str,
) -> int:
"""
Issue will be registered to checker and the generated issue id will be
Expand All @@ -195,7 +227,7 @@ def register_issue(
issue_id = self._id_manager.get_next_free_id()

issue = result.IssueType(
issue_id=issue_id, description=description, level=level
issue_id=issue_id, description=description, level=level, rule_uid=rule_uid
)

bundle = self._get_checker_bundle(checker_bundle_name=checker_bundle_name)
Expand All @@ -204,6 +236,10 @@ def register_issue(

checker.issues.append(issue)

# Validation need to be triggered to check if no schema relation was
# violated by the new issue addition.
result.CheckerType.model_validate(checker)

return issue_id

def add_file_location(
Expand Down Expand Up @@ -250,6 +286,14 @@ def add_xml_location(
result.LocationType(xml_location=[xml_location], description=description)
)

def set_checker_status(
self, checker_bundle_name: str, checker_id: str, status: result.StatusType
) -> None:
bundle = self._get_checker_bundle(checker_bundle_name=checker_bundle_name)
checker = self._get_checker(bundle=bundle, checker_id=checker_id)
checker.status = status
result.CheckerType.model_validate(checker)

def get_result_version(self) -> str:
return self._report_results.version

Expand Down
3 changes: 2 additions & 1 deletion tests/data/demo_checker_bundle.xqar
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@

<CheckerBundle build_date="" description="" name="DemoCheckerBundle" summary="Found 1 issue" version="">
<Checker checkerId="exampleChecker" description="This is a description" summary="">
<Issue description="This is an information from the demo usecase" issueId="0" level="3"/>
<AddressedRule ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
<Issue description="This is an information from the demo usecase" issueId="0" level="3" ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
</Checker>
</CheckerBundle>

Expand Down
29 changes: 29 additions & 0 deletions tests/data/demo_checker_bundle_extended.xqar
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<CheckerResults version="1.0.0">

<CheckerBundle build_date="" description="" name="DemoCheckerBundle" summary="Found 3 issues" version="">
<Checker checkerId="exampleChecker" description="This is a description" status="completed" summary="">
<AddressedRule ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
<Issue description="This is an information from the demo usecase" issueId="0" level="3" ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
</Checker>
<Checker checkerId="exampleInertialChecker" description="This is a description of inertial checker" status="completed" summary="">
<AddressedRule ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
<Issue description="This is an information from the demo usecase" issueId="1" level="3" ruleUID="test.com:qc:1.0.0:qwerty.qwerty">
<Locations description="inertial position">
<InertialLocation x="1.000000" y="2.000000" z="3.000000"/>
</Locations>
</Issue>
</Checker>
<Checker checkerId="exampleRuleUIDChecker" description="This is a description of ruleUID checker" status="completed" summary="">
<AddressedRule ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
<Metadata description="Date in which the checker was executed" key="run date" value="2024/06/06"/>
<Metadata description="Name of the project that created the checker" key="reference project" value="project01"/>
</Checker>
<Checker checkerId="exampleIssueRuleChecker" description="This is a description of checker with issue and the involved ruleUID" status="completed" summary="">
<AddressedRule ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
<Issue description="This is an information from the demo usecase" issueId="2" level="1" ruleUID="test.com:qc:1.0.0:qwerty.qwerty"/>
</Checker>
<Checker checkerId="exampleSkippedChecker" description="This is a description of checker with skipped status" status="skipped" summary="Skipped execution"/>
</CheckerBundle>

</CheckerResults>
Loading

0 comments on commit 75db6fc

Please sign in to comment.