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.
- dataproduct/__init__.py +5 -0
- dataproduct/cli.py +81 -0
- dataproduct/command_init.py +33 -0
- dataproduct/command_lint.py +58 -0
- dataproduct/command_publish.py +47 -0
- dataproduct/config/__init__.py +67 -0
- dataproduct/data_product.py +93 -0
- dataproduct/init/__init__.py +0 -0
- dataproduct/init/init_template.py +26 -0
- dataproduct/integration/__init__.py +0 -0
- dataproduct/integration/entropy_data.py +87 -0
- dataproduct/lint/__init__.py +0 -0
- dataproduct/lint/files.py +38 -0
- dataproduct/lint/resolve.py +11 -0
- dataproduct/lint/schema.py +44 -0
- dataproduct/lint/validate.py +61 -0
- dataproduct/model/__init__.py +4 -0
- dataproduct/model/exceptions.py +23 -0
- dataproduct/model/run.py +69 -0
- dataproduct/output/__init__.py +0 -0
- dataproduct/output/output_format.py +6 -0
- dataproduct/output/result_writer.py +77 -0
- dataproduct/py.typed +0 -0
- dataproduct/schemas/odps-1.0.0.init.yaml +37 -0
- dataproduct/schemas/odps-1.0.0.schema.json +517 -0
- dataproduct_cli-0.0.1.dist-info/METADATA +126 -0
- dataproduct_cli-0.0.1.dist-info/RECORD +31 -0
- dataproduct_cli-0.0.1.dist-info/WHEEL +5 -0
- dataproduct_cli-0.0.1.dist-info/entry_points.txt +2 -0
- dataproduct_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
- dataproduct_cli-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataproduct.model.run import ResultEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DataProductException(Exception):
|
|
5
|
+
"""A handled error surfaced as a check in the run result (no traceback)."""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
type: str,
|
|
10
|
+
name: str,
|
|
11
|
+
reason: str,
|
|
12
|
+
engine: str = "dataproduct",
|
|
13
|
+
result: ResultEnum = ResultEnum.error,
|
|
14
|
+
):
|
|
15
|
+
self.type = type
|
|
16
|
+
self.name = name
|
|
17
|
+
self.reason = reason
|
|
18
|
+
self.engine = engine
|
|
19
|
+
self.result = result
|
|
20
|
+
super().__init__(reason)
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return f"{self.name}: {self.reason}"
|
dataproduct/model/run.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Result model for a CLI run (lint, publish, …).
|
|
2
|
+
|
|
3
|
+
Mirrors datacontract-cli's ``Run``/``Check`` shape closely so the two tools feel
|
|
4
|
+
like siblings and share output tooling. The ``warning`` result level exists for
|
|
5
|
+
parity and forward-compat; 0.1 emits only ``passed`` / ``error``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ResultEnum(str, Enum):
|
|
17
|
+
passed = "passed"
|
|
18
|
+
warning = "warning"
|
|
19
|
+
error = "error"
|
|
20
|
+
failed = "failed"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Check(BaseModel):
|
|
24
|
+
type: str = "lint"
|
|
25
|
+
result: ResultEnum
|
|
26
|
+
name: str
|
|
27
|
+
reason: Optional[str] = None
|
|
28
|
+
engine: str = "dataproduct"
|
|
29
|
+
field: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Run(BaseModel):
|
|
33
|
+
result: ResultEnum = ResultEnum.passed
|
|
34
|
+
checks: List[Check] = Field(default_factory=list)
|
|
35
|
+
dataProductId: Optional[str] = None
|
|
36
|
+
dataProductVersion: Optional[str] = None
|
|
37
|
+
timestampStart: Optional[datetime] = None
|
|
38
|
+
timestampEnd: Optional[datetime] = None
|
|
39
|
+
logs: List[str] = Field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create_run(cls) -> "Run":
|
|
43
|
+
return cls(timestampStart=datetime.now(timezone.utc))
|
|
44
|
+
|
|
45
|
+
def log_info(self, message: str) -> None:
|
|
46
|
+
logging.info(message)
|
|
47
|
+
self.logs.append(f"INFO {message}")
|
|
48
|
+
|
|
49
|
+
def log_warn(self, message: str) -> None:
|
|
50
|
+
logging.warning(message)
|
|
51
|
+
self.logs.append(f"WARN {message}")
|
|
52
|
+
|
|
53
|
+
def log_error(self, message: str) -> None:
|
|
54
|
+
logging.error(message)
|
|
55
|
+
self.logs.append(f"ERROR {message}")
|
|
56
|
+
|
|
57
|
+
def has_passed(self) -> bool:
|
|
58
|
+
return self.result == ResultEnum.passed
|
|
59
|
+
|
|
60
|
+
def finish(self) -> None:
|
|
61
|
+
"""Compute the overall result and stamp the end time.
|
|
62
|
+
|
|
63
|
+
Any ``error`` check fails the run; ``warning`` checks do not.
|
|
64
|
+
"""
|
|
65
|
+
self.timestampEnd = datetime.now(timezone.utc)
|
|
66
|
+
if any(c.result == ResultEnum.error for c in self.checks):
|
|
67
|
+
self.result = ResultEnum.failed
|
|
68
|
+
else:
|
|
69
|
+
self.result = ResultEnum.passed
|
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Render a :class:`Run` to the console or a machine-readable file.
|
|
2
|
+
|
|
3
|
+
Console output uses ``rich``; ``--output-format json|junit`` writes a file (or
|
|
4
|
+
stdout). A failed run exits with code 1.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from xml.etree.ElementTree import Element, SubElement, tostring
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from dataproduct.model.run import ResultEnum, Run
|
|
14
|
+
from dataproduct.output.output_format import OutputFormat
|
|
15
|
+
|
|
16
|
+
_ICON = {
|
|
17
|
+
ResultEnum.passed: "✅",
|
|
18
|
+
ResultEnum.warning: "⚠️ ",
|
|
19
|
+
ResultEnum.error: "❌",
|
|
20
|
+
ResultEnum.failed: "❌",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write_result(run: Run, console, output_format: Optional[OutputFormat], output: Optional[Path]) -> None:
|
|
25
|
+
if output_format == OutputFormat.json:
|
|
26
|
+
_write_or_print(run.model_dump_json(indent=2), output, console)
|
|
27
|
+
elif output_format == OutputFormat.junit:
|
|
28
|
+
_write_or_print(_to_junit(run), output, console)
|
|
29
|
+
else:
|
|
30
|
+
_print_console(run, console)
|
|
31
|
+
|
|
32
|
+
if not run.has_passed():
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _print_console(run: Run, console) -> None:
|
|
37
|
+
for check in run.checks:
|
|
38
|
+
icon = _ICON.get(check.result, "•")
|
|
39
|
+
line = f"{icon} {check.name}"
|
|
40
|
+
if check.reason:
|
|
41
|
+
line += f": {check.reason}"
|
|
42
|
+
console.print(line)
|
|
43
|
+
if run.has_passed():
|
|
44
|
+
console.print("[green]🟢 Data product is valid.[/green]")
|
|
45
|
+
else:
|
|
46
|
+
console.print("[red]🔴 Data product is invalid.[/red]")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _write_or_print(content: str, output: Optional[Path], console) -> None:
|
|
50
|
+
if output is not None:
|
|
51
|
+
Path(output).write_text(content)
|
|
52
|
+
console.print(f"📝 results written to {output}")
|
|
53
|
+
else:
|
|
54
|
+
print(content)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _to_junit(run: Run) -> str:
|
|
58
|
+
failures = sum(1 for c in run.checks if c.result in (ResultEnum.error, ResultEnum.failed))
|
|
59
|
+
testsuite = Element(
|
|
60
|
+
"testsuite",
|
|
61
|
+
{
|
|
62
|
+
"name": "dataproduct-lint",
|
|
63
|
+
"tests": str(len(run.checks)),
|
|
64
|
+
"failures": str(failures),
|
|
65
|
+
"errors": "0",
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
for check in run.checks:
|
|
69
|
+
testcase = SubElement(
|
|
70
|
+
testsuite,
|
|
71
|
+
"testcase",
|
|
72
|
+
{"classname": check.type, "name": check.name},
|
|
73
|
+
)
|
|
74
|
+
if check.result in (ResultEnum.error, ResultEnum.failed):
|
|
75
|
+
failure = SubElement(testcase, "failure", {"message": check.reason or check.name})
|
|
76
|
+
failure.text = check.reason or check.name
|
|
77
|
+
return '<?xml version="1.0" encoding="UTF-8"?>\n' + tostring(testsuite, encoding="unicode")
|
dataproduct/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
apiVersion: v1.0.0
|
|
2
|
+
kind: DataProduct
|
|
3
|
+
id: my-data-product-id
|
|
4
|
+
name: My Data Product
|
|
5
|
+
version: v1.0.0
|
|
6
|
+
status: draft
|
|
7
|
+
|
|
8
|
+
description:
|
|
9
|
+
purpose: Purpose of the data product.
|
|
10
|
+
usage: Intended usage of the data product.
|
|
11
|
+
limitations: Limitations of the data product.
|
|
12
|
+
|
|
13
|
+
# tags: ['example']
|
|
14
|
+
|
|
15
|
+
# inputPorts:
|
|
16
|
+
# - name: source-data
|
|
17
|
+
# version: 1.0.0
|
|
18
|
+
# contractId: 00000000-0000-0000-0000-000000000000
|
|
19
|
+
|
|
20
|
+
outputPorts:
|
|
21
|
+
- name: my-output-port
|
|
22
|
+
description: The data this product exposes.
|
|
23
|
+
type: tables
|
|
24
|
+
version: 1.0.0
|
|
25
|
+
# contractId: 00000000-0000-0000-0000-000000000000
|
|
26
|
+
|
|
27
|
+
# support:
|
|
28
|
+
# - channel: My Team Slack
|
|
29
|
+
# url: https://example.slack.com/archives/C0000000000
|
|
30
|
+
# tool: slack
|
|
31
|
+
|
|
32
|
+
# team:
|
|
33
|
+
# name: My Team
|
|
34
|
+
# members:
|
|
35
|
+
# - username: jane.doe@example.com
|
|
36
|
+
# name: Jane Doe
|
|
37
|
+
# role: owner
|