Skip to content
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
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ dynamic = ["version"]
dependencies = [
"pyshacl[js]>=0.30.0",
"rdflib>=7.1.1",
"pydantic>=2.9.2",
"pydantic-settings[toml]>=2.6.1",
"toml>=0.10.2",
"jinja2>=3.1.6",
]
requires-python = ">=3.10"
Expand Down
57 changes: 44 additions & 13 deletions src/sc_validate/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
# SPDX-FileContributor: David Pape

import operator
import pathlib
import sys
from argparse import ArgumentParser, ArgumentTypeError
from argparse import ArgumentError, ArgumentParser, ArgumentTypeError
from functools import reduce
from pathlib import Path
from typing import Dict
from urllib.parse import urlparse

from sc_validate import __version__ as version
from sc_validate.config import Policy, Settings
from sc_validate.config import Policy, make_config
from sc_validate.data_model import (
parameterize_graph,
read_rdf_resource,
Expand All @@ -20,15 +20,23 @@
from sc_validate.report import create_report


def path_or_url(path: str) -> pathlib.Path | str:
if (path_obj := pathlib.Path(path)).exists():
def _path_or_url(path: str) -> Path | str:
if (path_obj := Path(path)).exists():
return path_obj
result = urlparse(path)
if result.scheme and result.netloc:
return path
raise ArgumentTypeError(f"Argument '{path}' is neither an existing file nor a URL")


def _path(path: str) -> Path:
path_obj = Path(path)
if path_obj.exists():
return path_obj
raise ArgumentError(f"Argument '{path}' is not an existing file")


# TODO: Move this function somewhere more useful
def policies_to_shacl(policies: Dict[str, Policy]):
shacl_graphs = []
# TODO: We're only using the values. What to do with the keys?
Expand All @@ -39,7 +47,7 @@ def policies_to_shacl(policies: Dict[str, Policy]):
return reduce(operator.add, shacl_graphs)


def main():
def make_argument_parser() -> ArgumentParser:
parser = ArgumentParser(
prog="sc-validate",
description="Validate publication metadata using Software CaRD policies.",
Expand All @@ -50,21 +58,44 @@ def main():
"file containing Codemeta-based software publication metadata "
"(as a file path or URL)"
),
type=path_or_url,
type=_path_or_url,
metavar="METADATA_FILE",
)
parser.add_argument("--debug", help="run in debug mode", action="store_true")
parser.add_argument("--version", action="version", version=f"%(prog)s {version}")
parser.add_argument(
"-c",
"--config",
help="configuration file (the default is config.toml)",
type=_path,
default="config.toml",
metavar="CONFIG_FILE",
)
parser.add_argument(
"-d",
"--debug",
help="run in debug mode",
action="store_true",
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"%(prog)s {version}",
)
return parser


def main():
parser = make_argument_parser()
arguments = parser.parse_args()

try:
settings = Settings()
except ValueError as e:
print("Failed to parse configuration file", str(e), sep="\n\n", file=sys.stderr)
config = make_config(config_file=arguments.config)
except Exception as e:
print(e, file=sys.stderr)
sys.exit(2)

data_graph = read_rdf_resource(arguments.metadata_file)
shapes_graph = policies_to_shacl(settings.policies)
shapes_graph = policies_to_shacl(config.policies)

if arguments.debug:
data_graph.serialize("debug-input-data.ttl", "turtle")
Expand Down
70 changes: 46 additions & 24 deletions src/sc_validate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,58 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileContributor: David Pape

from typing import Any, Dict, Tuple, Type
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict

from pydantic import BaseModel
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
SettingsConfigDict,
TomlConfigSettingsSource,
)
import toml

CONFIG_FILE_NAME = "config.toml"


class Policy(BaseModel):
@dataclass
class Policy:
source: str
parameters: Dict[str, Any] = {}
parameters: Dict[str, Any] = field(default_factory=dict)

@classmethod
def from_dict(cls, policy: dict):
assert isinstance(policy, dict)
source = policy.get("source")
parameters = policy.get("parameters")
assert source is not None
assert isinstance(parameters, (dict, type(None)))
return cls(source=source, parameters=parameters or {})


class Settings(BaseSettings):
@dataclass
class Config:
policies: Dict[str, Policy]

model_config = SettingsConfigDict(toml_file=CONFIG_FILE_NAME)

@classmethod
def settings_customise_sources(
cls,
settings_cls: Type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> Tuple[PydanticBaseSettingsSource, ...]:
return (TomlConfigSettingsSource(settings_cls),)
def from_dict(cls, settings: dict):
assert isinstance(settings, dict)
assert "policies" in settings
policies: dict = settings["policies"]
assert all(isinstance(key, str) for key in policies.keys())
return cls(
policies={
policy_name: Policy.from_dict(policy_settings)
for policy_name, policy_settings in policies.items()
}
)


def make_config(*, config_file: Path = None, config_dict: dict = None) -> Config:
if config_file is None and config_dict is None:
raise ValueError("Neither config_file nor config_dict were given")
if config_file is not None and config_dict is not None:
raise ValueError("Only one of config_file and config_dict may be given")
if config_dict is None:
if not config_file.exists():
raise FileNotFoundError(f"Config file '{config_file}' does not exist")
if not config_file.is_file():
raise TypeError(f"Config file '{config_file}' is not a regular file")
try:
config_dict = toml.load(config_file)
except toml.TomlDecodeError:
raise ValueError(f"Config file '{config_file}' could not be parsed")
return Config.from_dict(config_dict)
4 changes: 2 additions & 2 deletions src/sc_validate/data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ class Policy:
@classmethod
def from_graph(cls, reference: URIRef, graph: Graph):
return cls(
name=graph.value(reference, SH.name, None),
description=graph.value(reference, SH.description, None),
name=get_language_tagged_literal(graph, reference, SH.name),
description=get_language_tagged_literal(graph, reference, SH.description),
)


Expand Down
Loading