dataproduct-cli 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ """dataproduct-cli — a CLI for the Open Data Product Standard (ODPS).
2
+
3
+ Sister project of datacontract-cli. See the ``specs/`` directory for the
4
+ behavioral specification of each command.
5
+ """
dataproduct/cli.py ADDED
@@ -0,0 +1,81 @@
1
+ import logging
2
+ import sys
3
+ from importlib import metadata
4
+ from pathlib import Path
5
+ from typing import Iterable, Optional
6
+
7
+ import typer
8
+ from click import Context
9
+ from dotenv import find_dotenv, load_dotenv
10
+ from rich.console import Console
11
+ from typer.core import TyperGroup
12
+ from typing_extensions import Annotated
13
+
14
+ from dataproduct.output.output_format import OutputFormat
15
+
16
+ console = Console()
17
+
18
+ debug_option = Annotated[bool, typer.Option(help="Enable debug logging")]
19
+
20
+ # Order in which top-level commands appear in `dataproduct --help`.
21
+ COMMAND_ORDER = ["init", "lint", "publish"]
22
+
23
+
24
+ class OrderedCommands(TyperGroup):
25
+ def list_commands(self, ctx: Context) -> Iterable[str]:
26
+ known = set(COMMAND_ORDER)
27
+ return [c for c in COMMAND_ORDER if c in self.commands] + [c for c in self.commands if c not in known]
28
+
29
+
30
+ app = typer.Typer(
31
+ cls=OrderedCommands,
32
+ no_args_is_help=True,
33
+ add_completion=False,
34
+ pretty_exceptions_show_locals=False,
35
+ help="CLI for data products following the Open Data Product Standard (ODPS).",
36
+ )
37
+
38
+
39
+ def version_callback(value: bool) -> None:
40
+ if value:
41
+ try:
42
+ version = metadata.version("dataproduct-cli")
43
+ except metadata.PackageNotFoundError:
44
+ version = "0.0.0"
45
+ console.print(version)
46
+ raise typer.Exit()
47
+
48
+
49
+ @app.callback()
50
+ def common(
51
+ ctx: typer.Context,
52
+ version: Annotated[
53
+ Optional[bool],
54
+ typer.Option("--version", callback=version_callback, is_eager=True, help="Print the version and exit."),
55
+ ] = None,
56
+ ) -> None:
57
+ """dataproduct CLI."""
58
+ load_dotenv(find_dotenv(usecwd=True))
59
+
60
+
61
+ def enable_debug_logging(debug: bool, otherwise_disable_stderr: bool = False) -> None:
62
+ if debug:
63
+ logging.basicConfig(level=logging.DEBUG, stream=sys.stderr, format="%(asctime)s %(levelname)s %(message)s")
64
+ elif otherwise_disable_stderr:
65
+ logging.disable(logging.CRITICAL)
66
+
67
+
68
+ def resolve_output_format(output_format: Optional[OutputFormat], output: Optional[Path]) -> Optional[OutputFormat]:
69
+ if output_format is not None:
70
+ return output_format
71
+ if output is not None:
72
+ return OutputFormat.junit if str(output).endswith(".xml") else OutputFormat.json
73
+ return None
74
+
75
+
76
+ # Register the commands (each module attaches itself to `app`).
77
+ from dataproduct import command_init, command_lint, command_publish # noqa: E402, F401
78
+
79
+
80
+ def main() -> None:
81
+ app()
@@ -0,0 +1,33 @@
1
+ import os
2
+
3
+ import typer
4
+ from typing_extensions import Annotated
5
+
6
+ from dataproduct.cli import app, console, debug_option, enable_debug_logging
7
+ from dataproduct.init.init_template import get_init_template
8
+
9
+
10
+ @app.command(
11
+ name="init",
12
+ epilog="Example: dataproduct init dataproduct.odps.yaml",
13
+ )
14
+ def init(
15
+ location: Annotated[
16
+ str, typer.Argument(help="The location of the data product file to create.")
17
+ ] = "dataproduct.odps.yaml",
18
+ template: Annotated[str, typer.Option(help="URL or path of a template or data product")] = None,
19
+ overwrite: Annotated[bool, typer.Option(help="Replace the existing data product file")] = False,
20
+ debug: debug_option = None,
21
+ ):
22
+ """
23
+ Create a new data product file.
24
+ """
25
+ enable_debug_logging(debug)
26
+
27
+ if not overwrite and os.path.exists(location):
28
+ console.print("File already exists, use --overwrite to overwrite")
29
+ raise typer.Exit(code=1)
30
+ template_str = get_init_template(template)
31
+ with open(location, "w") as f:
32
+ f.write(template_str)
33
+ console.print("📄 data product written to " + location)
@@ -0,0 +1,58 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+ from typing_extensions import Annotated
5
+
6
+ from dataproduct.cli import app, console, debug_option, enable_debug_logging, resolve_output_format
7
+ from dataproduct.config import cli_config
8
+ from dataproduct.data_product import DataProduct
9
+ from dataproduct.output.output_format import OutputFormat
10
+ from dataproduct.output.result_writer import write_result
11
+
12
+
13
+ @app.command(
14
+ name="lint",
15
+ epilog="Example: dataproduct lint dataproduct.odps.yaml",
16
+ )
17
+ def lint(
18
+ location: Annotated[
19
+ str,
20
+ typer.Argument(help="The location (url or local path) of the data product yaml."),
21
+ ] = "dataproduct.odps.yaml",
22
+ schema: Annotated[
23
+ str,
24
+ typer.Option("--json-schema", help="The location (url or path) of the ODPS JSON Schema"),
25
+ ] = None,
26
+ output: Annotated[
27
+ Path,
28
+ typer.Option(
29
+ help="File path to write the results to (e.g. './TEST-dataproduct.xml'). "
30
+ "If omitted, results are printed to stdout."
31
+ ),
32
+ ] = None,
33
+ output_format: Annotated[
34
+ OutputFormat,
35
+ typer.Option(help="The result format. Accepted values: json, junit."),
36
+ ] = None,
37
+ all_errors: Annotated[
38
+ bool,
39
+ typer.Option(
40
+ "--all-errors",
41
+ help="Report all JSON Schema validation errors instead of stopping after the first one.",
42
+ ),
43
+ ] = False,
44
+ debug: debug_option = None,
45
+ ):
46
+ """
47
+ Validate that the data product is correctly formatted (against the ODPS JSON Schema).
48
+ """
49
+ enable_debug_logging(debug, otherwise_disable_stderr=True)
50
+
51
+ output_format = resolve_output_format(output_format, output)
52
+ run = DataProduct(
53
+ config=cli_config(),
54
+ data_product_file=location,
55
+ schema_location=schema,
56
+ all_errors=all_errors,
57
+ ).lint()
58
+ write_result(run, console, output_format, output)
@@ -0,0 +1,47 @@
1
+ import typer
2
+ from typing_extensions import Annotated
3
+
4
+ from dataproduct.cli import app, console, debug_option, enable_debug_logging
5
+ from dataproduct.config import cli_config
6
+ from dataproduct.integration.entropy_data import (
7
+ DataProductPublishError,
8
+ publish_data_product_to_entropy_data,
9
+ )
10
+ from dataproduct.lint.resolve import resolve_data_product_dict
11
+ from dataproduct.model.exceptions import DataProductException
12
+
13
+
14
+ @app.command(
15
+ name="publish",
16
+ epilog="Example: dataproduct publish dataproduct.odps.yaml",
17
+ )
18
+ def publish(
19
+ location: Annotated[
20
+ str,
21
+ typer.Argument(help="The location (url or local path) of the data product yaml."),
22
+ ] = "dataproduct.odps.yaml",
23
+ schema: Annotated[
24
+ str,
25
+ typer.Option("--json-schema", help="The location (url or path) of the ODPS JSON Schema"),
26
+ ] = None,
27
+ ssl_verification: Annotated[
28
+ bool,
29
+ typer.Option(help="SSL verification when publishing the data product."),
30
+ ] = True,
31
+ debug: debug_option = None,
32
+ ):
33
+ """
34
+ Publish the data product to Entropy Data.
35
+ """
36
+ enable_debug_logging(debug)
37
+
38
+ try:
39
+ data_product_dict = resolve_data_product_dict(location, config=cli_config())
40
+ publish_data_product_to_entropy_data(
41
+ data_product_dict=data_product_dict,
42
+ ssl_verification=ssl_verification,
43
+ config=cli_config(),
44
+ )
45
+ except (DataProductPublishError, DataProductException) as e:
46
+ console.print(f"[red]Failed publishing data product. Error: {e}[/red]")
47
+ raise typer.Exit(code=1)
@@ -0,0 +1,67 @@
1
+ """Programmatic configuration for hosts and API keys.
2
+
3
+ Every value the CLI reads from ``ENTROPY_DATA_*`` (and the legacy
4
+ ``DATAMESH_MANAGER_*`` / ``DATACONTRACT_MANAGER_*``) environment variables can
5
+ also be provided programmatically via :class:`Config` (or a plain dict keyed by
6
+ the env var names). Reads fall back to the process environment.
7
+ """
8
+
9
+ import os
10
+ from typing import Optional, Union
11
+
12
+
13
+ class Config:
14
+ def __init__(self, values: Optional[dict] = None):
15
+ self._values = dict(values or {})
16
+
17
+ def getenv(self, key: str) -> Optional[str]:
18
+ value = self._values.get(key)
19
+ if value is not None:
20
+ return value
21
+ return os.environ.get(key)
22
+
23
+ @classmethod
24
+ def resolve(cls, config: "Optional[Union[Config, dict]]" = None) -> "Config":
25
+ if config is None:
26
+ return cls()
27
+ if isinstance(config, Config):
28
+ return config
29
+ if isinstance(config, dict):
30
+ return cls(config)
31
+ raise TypeError(f"Unsupported config type: {type(config)!r}")
32
+
33
+ # Hosts
34
+ def get_entropy_data_host(self) -> Optional[str]:
35
+ return self.getenv("ENTROPY_DATA_HOST")
36
+
37
+ def get_datamesh_manager_host(self) -> Optional[str]:
38
+ return self.getenv("DATAMESH_MANAGER_HOST")
39
+
40
+ def get_datacontract_manager_host(self) -> Optional[str]:
41
+ return self.getenv("DATACONTRACT_MANAGER_HOST")
42
+
43
+ # API keys
44
+ def get_entropy_data_api_key(self) -> Optional[str]:
45
+ return self.getenv("ENTROPY_DATA_API_KEY")
46
+
47
+ def get_datamesh_manager_api_key(self) -> Optional[str]:
48
+ return self.getenv("DATAMESH_MANAGER_API_KEY")
49
+
50
+ def get_datacontract_manager_api_key(self) -> Optional[str]:
51
+ return self.getenv("DATACONTRACT_MANAGER_API_KEY")
52
+
53
+
54
+ # Simple process-wide "current" config set from the CLI entry point.
55
+ _cli_config = Config()
56
+
57
+
58
+ def set_cli_config(config: Config) -> None:
59
+ global _cli_config
60
+ _cli_config = config
61
+
62
+
63
+ def cli_config() -> Config:
64
+ return _cli_config
65
+
66
+
67
+ __all__ = ["Config", "cli_config", "set_cli_config"]
@@ -0,0 +1,93 @@
1
+ """Core library entry point, parallel to datacontract-cli's ``DataContract``.
2
+
3
+ from dataproduct.data_product import DataProduct
4
+
5
+ run = DataProduct(data_product_file="dataproduct.odps.yaml").lint()
6
+ DataProduct(data_product_file="dataproduct.odps.yaml").publish()
7
+ """
8
+
9
+ from typing import Optional, Union
10
+
11
+ from dataproduct.config import Config
12
+ from dataproduct.integration.entropy_data import publish_data_product_to_entropy_data
13
+ from dataproduct.lint.files import read_resource
14
+ from dataproduct.lint.schema import fetch_schema
15
+ from dataproduct.lint.validate import parse_yaml, validate_against_schema
16
+ from dataproduct.model.exceptions import DataProductException
17
+ from dataproduct.model.run import Check, ResultEnum, Run
18
+
19
+
20
+ class DataProduct:
21
+ def __init__(
22
+ self,
23
+ data_product_file: Optional[str] = None,
24
+ data_product_str: Optional[str] = None,
25
+ schema_location: Optional[str] = None,
26
+ all_errors: bool = False,
27
+ config: "Optional[Union[Config, dict]]" = None,
28
+ ):
29
+ self._data_product_file = data_product_file
30
+ self._data_product_str = data_product_str
31
+ self._schema_location = schema_location
32
+ self._all_errors = all_errors
33
+ self._config = Config.resolve(config)
34
+
35
+ def _load_dict(self) -> dict:
36
+ if self._data_product_file is not None:
37
+ content = read_resource(self._data_product_file, self._config)
38
+ elif self._data_product_str is not None:
39
+ content = self._data_product_str
40
+ else:
41
+ raise DataProductException(
42
+ type="lint",
43
+ name="Load data product",
44
+ reason="No data product provided (file or string).",
45
+ )
46
+ return parse_yaml(content)
47
+
48
+ def lint(self) -> Run:
49
+ """Validate the data product against the ODPS JSON Schema (schema-only)."""
50
+ run = Run.create_run()
51
+ run.log_info("Linting data product")
52
+ try:
53
+ data = self._load_dict()
54
+ run.dataProductId = data.get("id")
55
+ run.dataProductVersion = data.get("version")
56
+ schema = fetch_schema(self._schema_location)
57
+ checks = validate_against_schema(data, schema, self._all_errors)
58
+ if checks:
59
+ run.checks.extend(checks)
60
+ for check in checks:
61
+ run.log_error(str(check.reason))
62
+ else:
63
+ run.checks.append(
64
+ Check(
65
+ type="lint",
66
+ result=ResultEnum.passed,
67
+ name="Data product is syntactically valid",
68
+ )
69
+ )
70
+ except DataProductException as e:
71
+ run.checks.append(Check(type=e.type, result=e.result, name=e.name, reason=e.reason, engine=e.engine))
72
+ run.log_error(str(e))
73
+ except Exception as e:
74
+ run.checks.append(
75
+ Check(
76
+ type="general",
77
+ result=ResultEnum.error,
78
+ name="Check Data Product",
79
+ reason=str(e),
80
+ )
81
+ )
82
+ run.log_error(str(e))
83
+ run.finish()
84
+ return run
85
+
86
+ def publish(self, ssl_verification: bool = True) -> None:
87
+ """Publish the data product to Entropy Data (no client-side lint in 0.1)."""
88
+ data = self._load_dict()
89
+ publish_data_product_to_entropy_data(
90
+ data_product_dict=data,
91
+ ssl_verification=ssl_verification,
92
+ config=self._config,
93
+ )
File without changes
@@ -0,0 +1,26 @@
1
+ import importlib.resources as resources
2
+ import logging
3
+
4
+ import requests
5
+
6
+ DEFAULT_DATA_PRODUCT_INIT_TEMPLATE = "odps-1.0.0.init.yaml"
7
+
8
+
9
+ def get_init_template(location: str = None) -> str:
10
+ """Return the contents of an init template.
11
+
12
+ - ``None`` -> the bundled default template.
13
+ - an ``http(s)://`` URL -> the fetched body.
14
+ - anything else -> read as a local file path.
15
+ """
16
+ if location is None:
17
+ logging.info("Use default bundled template " + DEFAULT_DATA_PRODUCT_INIT_TEMPLATE)
18
+ schemas = resources.files("dataproduct")
19
+ template = schemas.joinpath("schemas", DEFAULT_DATA_PRODUCT_INIT_TEMPLATE)
20
+ with template.open("r") as file:
21
+ return file.read()
22
+ elif location.startswith("http://") or location.startswith("https://"):
23
+ return requests.get(location).text
24
+ else:
25
+ with open(location, "r") as file:
26
+ return file.read()
File without changes
@@ -0,0 +1,87 @@
1
+ """Publish a data product to Entropy Data.
2
+
3
+ Confirmed API contract (see specs/003-publish.md):
4
+ ``PUT {host}/api/dataproducts/{id}`` with ``x-api-key`` +
5
+ ``content-type: application/json`` and a JSON ODPS body; ``200 OK`` on success.
6
+ Structurally identical to datacontract-cli's data-contract publish, with the
7
+ path changed from ``datacontracts`` to ``dataproducts``.
8
+ """
9
+
10
+ from urllib.parse import urlparse
11
+
12
+ import requests
13
+
14
+ from dataproduct.config import Config
15
+
16
+ # Response header carrying the HTML location of the published data product.
17
+ RESPONSE_HEADER_LOCATION_HTML = "location-html"
18
+
19
+
20
+ def publish_data_product_to_entropy_data(
21
+ data_product_dict: dict, ssl_verification: bool, config: Config | None = None
22
+ ) -> None:
23
+ config = Config.resolve(config)
24
+ api_key = _get_api_key(config)
25
+ host = _get_host(config)
26
+ headers = {"Content-Type": "application/json", "x-api-key": api_key}
27
+
28
+ id = data_product_dict.get("id")
29
+ if not id:
30
+ raise DataProductPublishError("The data product has no top-level 'id'; cannot publish.")
31
+
32
+ url = f"{host}/api/dataproducts/{id}"
33
+ response = requests.put(
34
+ url=url,
35
+ json=data_product_dict,
36
+ headers=headers,
37
+ verify=ssl_verification,
38
+ )
39
+ if response.status_code != 200:
40
+ display_host = _extract_hostname(host)
41
+ raise DataProductPublishError(f"Error publishing data product to {display_host}: {response.text}")
42
+
43
+ print("✅ Published data product successfully")
44
+
45
+ location_html = response.headers.get(RESPONSE_HEADER_LOCATION_HTML)
46
+ if location_html:
47
+ print(f"🚀 Open {location_html}")
48
+
49
+
50
+ class DataProductPublishError(Exception):
51
+ """Raised when a publish attempt fails (missing key, bad id, non-200)."""
52
+
53
+
54
+ def _get_api_key(config: Config) -> str:
55
+ """API key with fallback priority:
56
+
57
+ 1. ``ENTROPY_DATA_API_KEY``
58
+ 2. ``DATAMESH_MANAGER_API_KEY``
59
+ 3. ``DATACONTRACT_MANAGER_API_KEY``
60
+ """
61
+ api_key = (
62
+ config.get_entropy_data_api_key()
63
+ or config.get_datamesh_manager_api_key()
64
+ or config.get_datacontract_manager_api_key()
65
+ )
66
+ if api_key is None:
67
+ raise DataProductPublishError(
68
+ "Cannot publish, as neither ENTROPY_DATA_API_KEY, DATAMESH_MANAGER_API_KEY, "
69
+ "nor DATACONTRACT_MANAGER_API_KEY is set"
70
+ )
71
+ return api_key
72
+
73
+
74
+ def _get_host(config: Config) -> str:
75
+ """Host with fallback priority: ENTROPY_DATA_HOST, DATAMESH_MANAGER_HOST,
76
+ DATACONTRACT_MANAGER_HOST, then the default ``https://api.entropy-data.com``."""
77
+ return (
78
+ config.get_entropy_data_host()
79
+ or config.get_datamesh_manager_host()
80
+ or config.get_datacontract_manager_host()
81
+ or "https://api.entropy-data.com"
82
+ )
83
+
84
+
85
+ def _extract_hostname(url: str) -> str:
86
+ parsed = urlparse(url)
87
+ return parsed.netloc.split(":")[0] if parsed.netloc else url
File without changes
@@ -0,0 +1,38 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import requests
5
+
6
+ from dataproduct.config import Config
7
+ from dataproduct.model.exceptions import DataProductException
8
+ from dataproduct.model.run import ResultEnum
9
+
10
+
11
+ def read_resource(location: str, config: Config | None = None) -> str:
12
+ """Read the raw text of a data product from a URL or local path.
13
+
14
+ Raises :class:`DataProductException` (an ``error`` result, no traceback) if a
15
+ local file is missing or a URL cannot be fetched.
16
+ """
17
+ location_str = str(location)
18
+ if location_str.startswith("http://") or location_str.startswith("https://"):
19
+ try:
20
+ response = requests.get(location_str)
21
+ response.raise_for_status()
22
+ except requests.RequestException as e:
23
+ raise DataProductException(
24
+ type="lint",
25
+ name=f"Reading data product from {location_str}",
26
+ reason=f"Failed to fetch '{location_str}': {e}",
27
+ result=ResultEnum.error,
28
+ )
29
+ return response.text
30
+
31
+ if not os.path.exists(location_str):
32
+ raise DataProductException(
33
+ type="lint",
34
+ name=f"Reading data product from {location_str}",
35
+ reason=f"The file '{location_str}' does not exist.",
36
+ result=ResultEnum.error,
37
+ )
38
+ return Path(location_str).read_text()
@@ -0,0 +1,11 @@
1
+ from typing import Any, Dict
2
+
3
+ from dataproduct.config import Config
4
+ from dataproduct.lint.files import read_resource
5
+ from dataproduct.lint.validate import parse_yaml
6
+
7
+
8
+ def resolve_data_product_dict(location: str, config: "Config | None" = None) -> Dict[str, Any]:
9
+ """Read and parse the data product at ``location`` into a plain dict."""
10
+ content = read_resource(location, config)
11
+ return parse_yaml(content)
@@ -0,0 +1,44 @@
1
+ import importlib.resources as resources
2
+ import json
3
+ import logging
4
+ import os
5
+ from pathlib import Path
6
+ from typing import Any, Dict, Union
7
+
8
+ import requests
9
+
10
+ from dataproduct.model.exceptions import DataProductException
11
+ from dataproduct.model.run import ResultEnum
12
+
13
+ DEFAULT_DATA_PRODUCT_SCHEMA = "odps-1.0.0.schema.json"
14
+
15
+
16
+ def fetch_schema(location: Union[str, Path] = None) -> Dict[str, Any]:
17
+ """Fetch the ODPS JSON Schema to validate against.
18
+
19
+ ``None`` uses the bundled ODPS v1.0.0 schema; otherwise ``location`` is a URL
20
+ or local path.
21
+ """
22
+ if location is None:
23
+ logging.info("Use default bundled schema " + DEFAULT_DATA_PRODUCT_SCHEMA)
24
+ schemas = resources.files("dataproduct")
25
+ schema_file = schemas.joinpath("schemas", DEFAULT_DATA_PRODUCT_SCHEMA)
26
+ with schema_file.open("r") as file:
27
+ return json.load(file)
28
+
29
+ location_str = str(location)
30
+ if location_str.startswith("http://") or location_str.startswith("https://"):
31
+ logging.debug(f"Downloading schema from {location_str}")
32
+ response = requests.get(location_str)
33
+ return response.json()
34
+
35
+ if not os.path.exists(location_str):
36
+ raise DataProductException(
37
+ type="lint",
38
+ name=f"Reading schema from {location_str}",
39
+ reason=f"The file '{location_str}' does not exist.",
40
+ result=ResultEnum.error,
41
+ )
42
+ logging.debug(f"Loading JSON schema locally at {location_str}")
43
+ with open(location_str, "r") as file:
44
+ return json.load(file)
@@ -0,0 +1,61 @@
1
+ from typing import Any, Dict, List
2
+
3
+ import yaml
4
+ from jsonschema.validators import validator_for
5
+
6
+ from dataproduct.model.exceptions import DataProductException
7
+ from dataproduct.model.run import Check, ResultEnum
8
+
9
+
10
+ def parse_yaml(content: str) -> Dict[str, Any]:
11
+ """Parse a data product YAML string into a mapping.
12
+
13
+ Raises :class:`DataProductException` (single ``error``, no traceback) on
14
+ malformed YAML or a non-mapping document.
15
+ """
16
+ try:
17
+ data = yaml.safe_load(content)
18
+ except yaml.YAMLError as e:
19
+ raise DataProductException(
20
+ type="lint",
21
+ name="Parsing data product YAML",
22
+ reason=f"The data product is not valid YAML: {e}",
23
+ result=ResultEnum.error,
24
+ )
25
+ if not isinstance(data, dict):
26
+ raise DataProductException(
27
+ type="lint",
28
+ name="Parsing data product YAML",
29
+ reason="The data product must be a YAML mapping (object).",
30
+ result=ResultEnum.error,
31
+ )
32
+ return data
33
+
34
+
35
+ def validate_against_schema(data: Dict[str, Any], schema: Dict[str, Any], all_errors: bool = False) -> List[Check]:
36
+ """Validate ``data`` against the ODPS JSON Schema.
37
+
38
+ Returns a list of ``error`` checks — empty when the document is valid. With
39
+ ``all_errors=False`` (default) only the first violation is reported.
40
+ """
41
+ validator_cls = validator_for(schema)
42
+ validator_cls.check_schema(schema)
43
+ validator = validator_cls(schema)
44
+
45
+ errors = sorted(validator.iter_errors(data), key=lambda e: list(e.absolute_path))
46
+ if not all_errors:
47
+ errors = errors[:1]
48
+
49
+ checks: List[Check] = []
50
+ for error in errors:
51
+ path = "/".join(str(p) for p in error.absolute_path) or "(root)"
52
+ checks.append(
53
+ Check(
54
+ type="lint",
55
+ result=ResultEnum.error,
56
+ name=f"Schema validation failed at '{path}'",
57
+ reason=error.message,
58
+ field=path,
59
+ )
60
+ )
61
+ return checks
@@ -0,0 +1,4 @@
1
+ from dataproduct.model.exceptions import DataProductException
2
+ from dataproduct.model.run import Check, ResultEnum, Run
3
+
4
+ __all__ = ["Check", "DataProductException", "ResultEnum", "Run"]