-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
271 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from exasol.error._cli import main | ||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
import argparse | ||
import ast | ||
import json | ||
import sys | ||
from dataclasses import asdict, dataclass, is_dataclass | ||
from pathlib import Path | ||
from typing import List, Optional | ||
from enum import IntEnum | ||
|
||
|
||
class ExitCode(IntEnum): | ||
SUCCESS = 0 | ||
FAILURE = -1 | ||
|
||
|
||
@dataclass(frozen=True) | ||
class Placeholder: | ||
""" | ||
Placeholder according to schema specification. | ||
https://schemas.exasol.com/error_code_report-1.0.0.json | ||
""" | ||
placeholder: str | ||
description: Optional[str] | ||
|
||
|
||
@dataclass | ||
class ErrorCodeDetails: | ||
""" | ||
Error code details according to schema specification. | ||
https://schemas.exasol.com/error_code_report-1.0.0.json | ||
""" | ||
identifier: str | ||
message: Optional[str] | ||
messagePlaceholders: Optional[List[Placeholder]] | ||
description: Optional[str] | ||
internalDescription: Optional[str] | ||
potentialCauses: Optional[List[str]] | ||
mitigations: Optional[List[str]] | ||
sourceFile: Optional[str] | ||
sourceLine: Optional[str] | ||
contextHash: Optional[str] | ||
|
||
|
||
class ValidationError(Exception): | ||
"""""" | ||
|
||
|
||
class ErrorCollector(ast.NodeVisitor): | ||
def __init__(self, filename="<Unknown>"): | ||
self._filename = filename | ||
self._errors = list() | ||
|
||
@staticmethod | ||
def validate(node): | ||
pass | ||
# self._node = node | ||
# def validate(): | ||
# def validate_code(code): | ||
# pass | ||
|
||
# def validate_message(message): | ||
# pass | ||
|
||
# def validate_mitigations(mitigations): | ||
# pass | ||
|
||
# def validate_parameters(params): | ||
# for key in params.keys: | ||
# if not isinstance(key, ast.Constant): | ||
# raise ValueError("Only constant values for keys are allowed") | ||
# for value in params.values: | ||
# if isinstance(value, ast.Call): | ||
# if not isinstance(value.args[1], ast.Constant): | ||
# raise ValueError( | ||
# "Only constant values for descriptions are allowed" | ||
# ) | ||
|
||
@staticmethod | ||
def _is_exa_error(node): | ||
if not isinstance(node, ast.Call): | ||
return False | ||
name = getattr(node.func, "id", "") | ||
name = getattr(node.func, "attr", "") if name == "" else name | ||
return name == "ExaError" | ||
|
||
@property | ||
def errors(self): | ||
return self._errors | ||
|
||
def _make_error(self, node): | ||
code, message, mitigations, parameters = node.args | ||
|
||
def normalize(params): | ||
for k, v in zip(params.keys, params.keys): | ||
if isinstance(v, ast.Call): | ||
yield k.value, v[1] | ||
else: | ||
yield k.value, "" | ||
|
||
return ErrorCodeDetails( | ||
identifier=code.value, | ||
message=message.value, | ||
messagePlaceholders=[ | ||
Placeholder(name, description) | ||
for name, description in normalize(parameters) | ||
], | ||
description=None, | ||
internalDescription=None, | ||
potentialCauses=None, | ||
mitigations=[m.value for m in mitigations.elts], | ||
sourceFile=self._filename, | ||
sourceLine=node.lineno, | ||
contextHash=None, | ||
) | ||
|
||
def visit(self, node): | ||
if not self._is_exa_error(node): | ||
return | ||
|
||
try: | ||
self.validate(node) | ||
except ValidationError: | ||
raise | ||
|
||
error = self._make_error(node) | ||
self._errors.append(error) | ||
|
||
def generic_visit(self, node): | ||
raise NotImplementedError() | ||
|
||
|
||
class _JsonEncoder(json.JSONEncoder): | ||
"""Json encoder with dataclass support""" | ||
|
||
def default(self, obj): | ||
if is_dataclass(obj): | ||
return asdict(obj) | ||
return super().default(obj) | ||
|
||
|
||
def parse_command(args) -> ExitCode: | ||
"""Parse errors out one or more python files and report them in the jsonl format.""" | ||
errors = list() | ||
for f in args.python_file: | ||
collector = ErrorCollector(f.name) | ||
root_node = ast.parse(f.read()) | ||
for n in ast.walk(root_node): | ||
try: | ||
collector.visit(n) | ||
except ValidationError: | ||
pass # add/print validation errors | ||
errors.extend(collector.errors) | ||
|
||
for e in errors: | ||
print(json.dumps(e, cls=_JsonEncoder)) | ||
|
||
return ExitCode.SUCCESS | ||
|
||
|
||
def generate_command(args) -> ExitCode: | ||
"""Generate an error code file for the specified workspace | ||
first version use root. | ||
future version just get root and get meta information from pyproject.toml | ||
## ec report root(.) project.toml | ||
## parse version from project.toml | ||
## parse packages from pyproject.toml | ||
""" | ||
return ExitCode.FAILURE | ||
|
||
|
||
def _create_parser(): | ||
parser = argparse.ArgumentParser( | ||
prog="ec", | ||
description="Error Crawler", | ||
formatter_class=argparse.ArgumentDefaultsHelpFormatter, | ||
) | ||
subparsers = parser.add_subparsers() | ||
|
||
parse = subparsers.add_parser("parse") | ||
parse.add_argument( | ||
"python_file", | ||
metavar="python-file", | ||
type=argparse.FileType("r"), | ||
default=[sys.stdin], | ||
nargs="*", | ||
help="TBD", | ||
) | ||
parser.set_defaults(func=parse_command) | ||
|
||
generate = subparsers.add_parser("generate") | ||
generate.add_argument( | ||
"root", | ||
metavar="root", | ||
type=Path, | ||
default=[sys.stdin], | ||
nargs="*", | ||
help="some help", | ||
) | ||
generate.set_defaults(func=generate_command) | ||
|
||
return parser | ||
|
||
|
||
def _report(project_name, project_version, errors): | ||
return { | ||
"$schema": "https://schemas.exasol.com/error_code_report-1.0.0.json", | ||
"projectName": project_name, | ||
"projectVersion": project_version, | ||
"errorCodes": [e for e in errors], | ||
} | ||
|
||
|
||
def main(): | ||
parser = _create_parser() | ||
args = parser.parse_args() | ||
sys.exit(args.func(args)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
Prepare Test | ||
|
||
$ export ROOT=$TESTDIR/../../ | ||
|
||
$ cat > pymodule.py << EOF | ||
> from exasol import error | ||
> from exasol.error import ExaError, Parameter | ||
> # partial path/ns | ||
> error1 = error.ExaError('E-TEST-1', 'this is an error', ['no mitigation available'], Parameter('param', 'value', 'some description')) | ||
> error2 = error.ExaError('E-TEST-2', 'this is an error', ['no mitigation available'], {'param': 'value'}) | ||
> # no path/ns | ||
> error3 = ExaError('E-TEST-3', 'this is an error', ['no mitigation available'], | ||
> Parameter('param', 'value', 'some description')) | ||
> error4 = ExaError('E-TEST-4', 'this is an error', ['no mitigation available'], {'param': 'value'}) | ||
> # dynamic parameter value | ||
> value = input('please provide parameter value') | ||
> error5 = ExaError('E-TEST-5', 'this is an error', ['no mitigation available'], | ||
> Parameter('param', value, 'some description')) | ||
> error6 = ExaError('E-TEST-6', 'this is an error', ['no mitigation available'], {'param': value}) | ||
> error7 = ExaError( | ||
> 'E-TEST-7', 'Not enough space on device {{device}}.', | ||
> [ | ||
> "Delete something from {{device}}.", | ||
> "Create larger partition." | ||
> ], | ||
> Parameter('device', '/dev/sda1', 'name of the device') | ||
> ) | ||
> EOF | ||
|
||
Execute Test scenario | ||
|
||
$ python -m exasol.error pymodule.py | ||
E-TEST-1: this is an error no mitigation available | ||
E-TEST-2: this is an error no mitigation available | ||
E-TEST-3: this is an error no mitigation available | ||
E-TEST-4: this is an error no mitigation available | ||
E-TEST-5: this is an error no mitigation available | ||
E-TEST-6: this is an error no mitigation available | ||
E-TEST-7: Not enough space on device '/dev/sda1'. Known mitigations: | ||
* Delete something from '/dev/sda1'. | ||
* Create larger partition. |