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

refactor: improve parsing of SHACL nodes #3

Merged
merged 14 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 18 additions & 19 deletions .github/workflows/testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,29 @@ env:
VENV_PATH: .venv

jobs:
# TODO: refactor this using poetry
# Verifies pep8, pyflakes and circular complexity
# flake8:
# name: Lint Python Code (Flake8) (python ${{ matrix.python-version }})
# runs-on: ubuntu-latest
# strategy:
# matrix:
# python-version: ["3.11"]
# steps:
# # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
# - uses: actions/checkout@v4
# - name: Set up Python v${{ matrix.python-version }}
# uses: actions/setup-python@v5
# with:
# python-version: ${{ matrix.python-version }}
# - name: Install flake8
# run: pip install flake8
# - name: Run checks
# run: flake8 -v .
flake8:
name: Lint Python Code (Flake8) (python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11"]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- name: Set up Python v${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install flake8
run: pip install flake8
- name: Run checks
run: flake8 -v rocrate_validator tests

test:
name: "Tests"
runs-on: ubuntu-latest
# needs: [flake8]
needs: [flake8]
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
3 changes: 2 additions & 1 deletion rocrate_validator/cli/commands/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def handle_error(e: Exception, console: Console) -> None:
if logger.isEnabledFor(logging.DEBUG):
console.print_exception()
console.print(textwrap.indent("This error may be due to a bug.\n"
"Please report it to the issue tracker along with the following stack trace:\n", ' ' * 9))
"Please report it to the issue tracker "
"along with the following stack trace:\n", ' ' * 9))
console.print_exception()

console.print(f"\n\n[bold][[red]ERROR[/red]] {error_message}[/bold]\n", style="white")
Expand Down
3 changes: 2 additions & 1 deletion rocrate_validator/cli/commands/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,8 @@ def __verbose_describe_profile__(profile):
# Uncomment the following lines to show the overridden checks
# if check.overridden_by:
# severity_color = get_severity_color(check.overridden_by.severity)
# override = f"[overridden by: [bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\
# override = "[overridden by: " \
# f"[bold][magenta]{check.overridden_by.requirement.profile.identifier}[/magenta] "\
# f"[{severity_color}]{check.overridden_by.relative_identifier}[/{severity_color}][/bold]]"
if check.override:
severity_color = get_severity_color(check.override.severity)
Expand Down
57 changes: 41 additions & 16 deletions rocrate_validator/cli/commands/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def get_single_char(console: Optional[Console] = None, end: str = "\n",
console.print(char, end=end if choices and char in choices else "")
if choices and char not in choices:
if console:
console.print(f" [bold red]INVALID CHOICE[/bold red]", end=end)
console.print(" [bold red]INVALID CHOICE[/bold red]", end=end)
return char


Expand Down Expand Up @@ -275,11 +275,24 @@ def validate(ctx,
logger.info("Auto-detection of the profiles to use for validation is disabled")

# Prompt the user to select the profile to use for validation if the interactive mode is enabled
# and no profile is autodetected or multiple profiles are detected
if interactive and (not candidate_profiles or len(candidate_profiles) == 0 or len(candidate_profiles) == len(available_profiles)):
# and no profile is auto-detected or multiple profiles are detected
if interactive and (
not candidate_profiles or
len(candidate_profiles) == 0 or
len(candidate_profiles) == len(available_profiles)
):
# Define the list of choices
console.print(Padding(Rule("[bold yellow]WARNING: [/bold yellow]"
"[bold]Unable to automatically detect the profile to use for validation[/bold]\n", align="center", style="bold yellow"), (2, 2, 0, 2)))
console.print(
Padding(
Rule(
"[bold yellow]WARNING: [/bold yellow]"
"[bold]Unable to automatically detect the profile to use for validation[/bold]\n",
align="center",
style="bold yellow"
),
(2, 2, 0, 2)
)
)
selected_options = multiple_choice(console, available_profiles)
profile_identifier = [available_profiles[int(
selected_option)].identifier for selected_option in selected_options]
Expand Down Expand Up @@ -331,7 +344,10 @@ def validate(ctx,
verbose_choice = "n"
if interactive and not verbose and enable_pager:
verbose_choice = get_single_char(console, choices=['y', 'n'],
message="[bold] > Do you want to see the validation details? ([magenta]y/n[/magenta]): [/bold]")
message=(
"[bold] > Do you want to see the validation details? "
"([magenta]y/n[/magenta]): [/bold]"
))
if verbose_choice == "y" or verbose:
report_layout.show_validation_details(pager, enable_pager=enable_pager)

Expand Down Expand Up @@ -378,7 +394,6 @@ def multiple_choice(console: Console,
]
console.print("\n")
selected = prompt(question, style={"questionmark": "#ff9d00 bold",
"questionmark": "#e5c07b",
"question": "bold",
"checkbox": "magenta",
"answer": "magenta"},
Expand Down Expand Up @@ -508,8 +523,11 @@ def __init_layout__(self):
base_info_layout = Layout(
Align(
f"\n[bold cyan]RO-Crate:[/bold cyan] [bold]{URI(settings['data_path']).uri}[/bold]"
f"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] {settings['profile_identifier']}[/bold magenta] { '[italic](autodetected)[/italic]' if settings['profile_autodetected'] else ''}"
f"\n[bold cyan]Validation Severity:[/bold cyan] [bold {severity_color}]{settings['requirement_severity']}[/bold {severity_color}]",
"\n[bold cyan]Target Profile:[/bold cyan][bold magenta] "
f"{settings['profile_identifier']}[/bold magenta] "
f"{ '[italic](autodetected)[/italic]' if settings['profile_autodetected'] else ''}"
f"\n[bold cyan]Validation Severity:[/bold cyan] "
f"[bold {severity_color}]{settings['requirement_severity']}[/bold {severity_color}]",
style="white", align="left"),
name="Base Info", size=5)
#
Expand Down Expand Up @@ -557,12 +575,12 @@ def __init_layout__(self):

# Create the main layout
self.checks_stats_layout = Layout(
Panel(report_container_layout, title=f"[bold]- Validation Report -[/bold]",
Panel(report_container_layout, title="[bold]- Validation Report -[/bold]",
border_style="cyan", title_align="center", padding=(1, 2)))

# Create the overall result layout
self.overall_result = Layout(
Padding(Rule(f"\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1)), size=3)
Padding(Rule("\n[italic][cyan]Validating ROCrate...[/cyan][/italic]"), (1, 1)), size=3)

group_layout = Layout()
group_layout.add_split(self.checks_stats_layout)
Expand Down Expand Up @@ -641,11 +659,13 @@ def set_overall_result(self, result: ValidationResult):
self.result = result
if result.passed():
self.overall_result.update(
Padding(Rule(f"[bold][[green]OK[/green]] RO-Crate is a [green]valid[/green] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n\n",
Padding(Rule("[bold][[green]OK[/green]] RO-Crate is a [green]valid[/green] "
f"[magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n\n",
style="bold green"), (1, 1)))
else:
self.overall_result.update(
Padding(Rule(f"[bold][[red]FAILED[/red]] RO-Crate is [red]not[/red] a [red]valid[/red] [magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n",
Padding(Rule("[bold][[red]FAILED[/red]] RO-Crate is [red]not[/red] a [red]valid[/red] "
f"[magenta]{result.context.target_profile.identifier}[/magenta] !!![/bold]\n",
style="bold red"), (1, 1)))

def show_validation_details(self, pager: Pager, enable_pager: bool = True):
Expand Down Expand Up @@ -674,7 +694,8 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True):
f"profile: [magenta bold]{requirement.profile.name }[/magenta bold]]", align="right")
)
console.print(
f" [bold][cyan][{requirement.order_number}] [u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]",
f" [bold][cyan][{requirement.order_number}] "
"[u]{Markdown(requirement.name).markup}[/u][/cyan][/bold]",
style="white",
)
console.print(Padding(Markdown(requirement.description), (1, 7)))
Expand All @@ -685,7 +706,10 @@ def show_validation_details(self, pager: Pager, enable_pager: bool = True):
key=lambda x: (-x.severity.value, x)):
issue_color = get_severity_color(check.level.severity)
console.print(
Padding(f"[bold][{issue_color}][{check.relative_identifier.center(16)}][/{issue_color}] [magenta]{check.name}[/magenta][/bold]:", (0, 7)), style="white bold")
Padding(
f"[bold][{issue_color}][{check.relative_identifier.center(16)}][/{issue_color}] "
f"[magenta]{check.name}[/magenta][/bold]:", (0, 7)),
style="white bold")
console.print(Padding(Markdown(check.description), (0, 27)))
console.print(Padding("[u] Detected issues [/u]", (0, 8)), style="white bold")
for issue in sorted(result.get_issues_by_check(check),
Expand Down Expand Up @@ -743,7 +767,8 @@ def __compute_profile_stats__(validation_settings: dict):
check_count_by_severity[severity] = 0
if severity_validation <= severity:
num_checks = len(
[_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name)) if not _.overridden])
[_ for _ in requirement.get_checks_by_level(LevelCollection.get(severity.name))
if not _.overridden])
check_count_by_severity[severity] += num_checks
total_checks += num_checks

Expand Down
2 changes: 1 addition & 1 deletion rocrate_validator/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def cli(ctx: click.Context, debug: bool, version: bool, disable_color: bool, no_
except Exception as e:
console.print(
f"\n\n[bold][[red]FAILED[/red]] Unexpected error: {e} !!![/bold]\n", style="white")
console.print("""This error may be due to a bug.
console.print("""This error may be due to a bug.
Please report it to the issue tracker
along with the following stack trace:
""")
Expand Down
2 changes: 1 addition & 1 deletion rocrate_validator/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def __str__(self) -> str:
return msg

def __repr__(self):
return f"ProfileSpecificationNotFound()"
return "ProfileSpecificationNotFound()"


class ProfileSpecificationError(ROCValidatorError):
Expand Down
34 changes: 22 additions & 12 deletions rocrate_validator/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,10 @@ def get(name: str) -> RequirementLevel:
class Profile:

# store the map of profiles: profile URI -> Profile instance
__profiles_map: MultiIndexMap = MultiIndexMap("uri",
indexes=[MapIndex("name"), MapIndex("token", unique=False), MapIndex("identifier", unique=True)])
__profiles_map: MultiIndexMap = \
MultiIndexMap("uri", indexes=[
MapIndex("name"), MapIndex("token", unique=False), MapIndex("identifier", unique=True)
])

def __init__(self,
profiles_base_path: Path,
Expand Down Expand Up @@ -191,13 +193,17 @@ def __init__(self,
self._token, self._version = self.__init_token_version__()
# add the profile to the profiles map
self.__profiles_map.add(
self._profile_node.toPython(), self, token=self.token, name=self.name, identifier=self.identifier) # add the profile to the profiles map
self._profile_node.toPython(),
self, token=self.token,
name=self.name, identifier=self.identifier
) # add the profile to the profiles map
else:
raise ProfileSpecificationError(
message=f"Profile specification file {spec_file} must contain exactly one profile")

def __get_specification_property__(self, property: str, namespace: Namespace,
pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]:
def __get_specification_property__(
self, property: str, namespace: Namespace,
pop_first: bool = True, as_Python_object: bool = True) -> Union[str, list[Union[str, URIRef]]]:
assert self._profile_specification_graph is not None, "Profile specification graph not loaded"
values = list(self._profile_specification_graph.objects(self._profile_node, namespace[property]))
if values and as_Python_object:
Expand Down Expand Up @@ -306,15 +312,15 @@ def __get_nested_profiles__(cls, source: str) -> list[str]:
queue = [source]
while len(queue) > 0:
p = queue.pop()
if not p in visited:
if p not in visited:
visited.append(p)
profile = cls.__profiles_map.get_by_key(p)
inherited_profiles = profile.is_profile_of
if inherited_profiles:
for p in sorted(inherited_profiles, reverse=True):
if not p in visited:
if p not in visited:
queue.append(p)
if not p in result:
if p not in result:
result.insert(0, p)
return result

Expand Down Expand Up @@ -1234,7 +1240,8 @@ def parse(cls, settings: Union[dict, ValidationSettings]) -> ValidationSettings:


class ValidationEvent(Event):
def __init__(self, event_type: EventType, validation_result: Optional[ValidationResult] = None, message: Optional[str] = None):
def __init__(self, event_type: EventType,
validation_result: Optional[ValidationResult] = None, message: Optional[str] = None):
super().__init__(event_type, message)
self._validation_result = validation_result

Expand Down Expand Up @@ -1275,7 +1282,9 @@ def validation_result(self) -> Optional[bool]:


class RequirementCheckValidationEvent(Event):
def __init__(self, event_type: EventType, requirement_check: RequirementCheck, validation_result: Optional[bool] = None, message: Optional[str] = None):
def __init__(self, event_type: EventType,
requirement_check: RequirementCheck,
validation_result: Optional[bool] = None, message: Optional[str] = None):
assert event_type in (EventType.REQUIREMENT_CHECK_VALIDATION_START, EventType.REQUIREMENT_CHECK_VALIDATION_END)
super().__init__(event_type, message)
self._requirement_check = requirement_check
Expand Down Expand Up @@ -1545,8 +1554,9 @@ def __load_profiles__(self) -> list[Profile]:
# raised when the profile is not found
if logger.isEnabledFor(logging.DEBUG):
logger.exception(e)
raise ProfileNotFound(self.profile_identifier,
message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") from e
raise ProfileNotFound(
self.profile_identifier,
message=f"Profile '{self.profile_identifier}' not found in '{self.profiles_path}'") from e

# Set the profiles to validate against as the target profile and its inherited profiles
profiles = profile.inherited_profiles + [profile]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@
@requirement(name="Web-based Data Entity: RECOMMENDED resource availability")
class WebDataEntityRecommendedChecker(PyFunctionCheck):
"""
Web-based Data Entity instances SHOULD be available at the URIs specified in the `@id` property of the Web-based Data Entity.
Web-based Data Entity instances SHOULD be available
at the URIs specified in the `@id` property of the Web-based Data Entity.
"""

@check(name="Web-based Data Entity: resource availability")
def check_availability(self, context: ValidationContext) -> bool:
"""
Check if the Web-based Data Entity is directly downloadable
Check if the Web-based Data Entity is directly downloadable
by a simple retrieval (e.g. HTTP GET) permitting redirection and HTTP/HTTPS URIs
"""
result = True
Expand Down Expand Up @@ -62,7 +63,8 @@ def check_content_size(self, context: ValidationContext) -> bool:
content_size = entity.get_property("contentSize")
if content_size and int(content_size) != context.ro_crate.get_external_file_size(entity.id):
context.result.add_check_issue(
f'The property contentSize={content_size} of the Web-based Data Entity {entity.id} does not match the actual size of '
f'The property contentSize={content_size} of the Web-based Data Entity '
f'{entity.id} does not match the actual size of '
f'the downloadable content, i.e., {entity.content_size} (bytes)', self,
focusNode=entity.id, resultPath='contentSize', value=content_size)
result = False
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def check_workflow_diagram(self, context: ValidationContext) -> bool:
image = main_workflow.get_property("image")
diagram_relpath = image.id if image else None
if not diagram_relpath:
context.result.add_error(f"main workflow does not have an 'image' property", self)
context.result.add_error("main workflow does not have an 'image' property", self)
return False
if not image.is_available():
context.result.add_error(f"Workflow diagram '{image.id}' not found in crate", self)
Expand Down
Loading