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

Support the new result file schema #8

Merged
merged 11 commits into from
Jun 11, 2024
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