dataproduct-cli 0.0.1__tar.gz
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_cli-0.0.1/CHANGELOG.md +16 -0
- dataproduct_cli-0.0.1/LICENSE +21 -0
- dataproduct_cli-0.0.1/MANIFEST.in +3 -0
- dataproduct_cli-0.0.1/PKG-INFO +126 -0
- dataproduct_cli-0.0.1/README.md +84 -0
- dataproduct_cli-0.0.1/dataproduct/__init__.py +5 -0
- dataproduct_cli-0.0.1/dataproduct/cli.py +81 -0
- dataproduct_cli-0.0.1/dataproduct/command_init.py +33 -0
- dataproduct_cli-0.0.1/dataproduct/command_lint.py +58 -0
- dataproduct_cli-0.0.1/dataproduct/command_publish.py +47 -0
- dataproduct_cli-0.0.1/dataproduct/config/__init__.py +67 -0
- dataproduct_cli-0.0.1/dataproduct/data_product.py +93 -0
- dataproduct_cli-0.0.1/dataproduct/init/__init__.py +0 -0
- dataproduct_cli-0.0.1/dataproduct/init/init_template.py +26 -0
- dataproduct_cli-0.0.1/dataproduct/integration/__init__.py +0 -0
- dataproduct_cli-0.0.1/dataproduct/integration/entropy_data.py +87 -0
- dataproduct_cli-0.0.1/dataproduct/lint/__init__.py +0 -0
- dataproduct_cli-0.0.1/dataproduct/lint/files.py +38 -0
- dataproduct_cli-0.0.1/dataproduct/lint/resolve.py +11 -0
- dataproduct_cli-0.0.1/dataproduct/lint/schema.py +44 -0
- dataproduct_cli-0.0.1/dataproduct/lint/validate.py +61 -0
- dataproduct_cli-0.0.1/dataproduct/model/__init__.py +4 -0
- dataproduct_cli-0.0.1/dataproduct/model/exceptions.py +23 -0
- dataproduct_cli-0.0.1/dataproduct/model/run.py +69 -0
- dataproduct_cli-0.0.1/dataproduct/output/__init__.py +0 -0
- dataproduct_cli-0.0.1/dataproduct/output/output_format.py +6 -0
- dataproduct_cli-0.0.1/dataproduct/output/result_writer.py +77 -0
- dataproduct_cli-0.0.1/dataproduct/py.typed +0 -0
- dataproduct_cli-0.0.1/dataproduct/schemas/odps-1.0.0.init.yaml +37 -0
- dataproduct_cli-0.0.1/dataproduct/schemas/odps-1.0.0.schema.json +517 -0
- dataproduct_cli-0.0.1/dataproduct_cli.egg-info/PKG-INFO +126 -0
- dataproduct_cli-0.0.1/dataproduct_cli.egg-info/SOURCES.txt +39 -0
- dataproduct_cli-0.0.1/dataproduct_cli.egg-info/dependency_links.txt +1 -0
- dataproduct_cli-0.0.1/dataproduct_cli.egg-info/entry_points.txt +2 -0
- dataproduct_cli-0.0.1/dataproduct_cli.egg-info/requires.txt +15 -0
- dataproduct_cli-0.0.1/dataproduct_cli.egg-info/top_level.txt +1 -0
- dataproduct_cli-0.0.1/pyproject.toml +94 -0
- dataproduct_cli-0.0.1/setup.cfg +4 -0
- dataproduct_cli-0.0.1/tests/test_init.py +57 -0
- dataproduct_cli-0.0.1/tests/test_lint.py +98 -0
- dataproduct_cli-0.0.1/tests/test_publish.py +85 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Each entry is one line:
|
|
4
|
+
what changed (user-facing).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.0.1]
|
|
9
|
+
|
|
10
|
+
- Test release to validate the PyPI publishing pipeline
|
|
11
|
+
|
|
12
|
+
## [0.1.0]
|
|
13
|
+
|
|
14
|
+
- `init` command: create a valid `dataproduct.odps.yaml` from a bundled ODPS v1.0.0 template
|
|
15
|
+
- `lint` command: validate a data product against the ODPS v1.0.0 JSON Schema (json/junit output)
|
|
16
|
+
- `publish` command: publish a data product to Entropy Data (`PUT /api/dataproducts/{id}`)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Data Contract
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dataproduct-cli
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: The dataproduct CLI is an open source command-line tool for working with data products. It uses data product YAML files following the Open Data Product Standard (ODPS) to lint the definition against the standard and publish it to Entropy Data. The tool is written in Python. It can be used as a standalone CLI tool, in a CI/CD pipeline, or directly as a Python library.
|
|
5
|
+
Author-email: Michael Kutz <michael.kutz@entropy-data.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/datacontract/dataproduct-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/datacontract/dataproduct-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/datacontract/dataproduct-cli/issues
|
|
10
|
+
Keywords: data-product,data-products,odps,open-data-product-standard,data-governance,data-mesh,data-engineering
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
24
|
+
Requires-Python: <3.15,>=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: typer<0.28,>=0.18.0
|
|
28
|
+
Requires-Dist: click<9.0.0,>=8.1.0
|
|
29
|
+
Requires-Dist: pydantic<2.14.0,>=2.8.2
|
|
30
|
+
Requires-Dist: pyyaml~=6.0.1
|
|
31
|
+
Requires-Dist: requests<2.35,>=2.31
|
|
32
|
+
Requires-Dist: jsonschema<5.0.0,>=4.23.0
|
|
33
|
+
Requires-Dist: rich<16.0,>=13.7
|
|
34
|
+
Requires-Dist: python-dotenv<2.0.0,>=1.0.0
|
|
35
|
+
Requires-Dist: typing-extensions>=4.7.0
|
|
36
|
+
Provides-Extra: dev
|
|
37
|
+
Requires-Dist: pytest; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
39
|
+
Requires-Dist: ruff==0.16.1; extra == "dev"
|
|
40
|
+
Requires-Dist: pre-commit<4.7.0,>=3.7.1; extra == "dev"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# Data Product CLI
|
|
44
|
+
|
|
45
|
+
The `dataproduct` CLI is an open-source command-line tool for working with
|
|
46
|
+
**data products** defined with the
|
|
47
|
+
[Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.0.0/).
|
|
48
|
+
|
|
49
|
+
It is the data-product sibling of
|
|
50
|
+
[`datacontract-cli`](https://github.com/datacontract/datacontract-cli) (which
|
|
51
|
+
targets the Open Data **Contract** Standard) and mirrors its technology and
|
|
52
|
+
release mechanism. The tool is written in Python and can be used standalone, in
|
|
53
|
+
CI/CD, or as a Python library.
|
|
54
|
+
|
|
55
|
+
> The behavior of each command is specified in [`specs/`](specs/).
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uv tool install --python python3.11 dataproduct-cli
|
|
61
|
+
# or
|
|
62
|
+
pip install dataproduct-cli
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
### `init` — create a data product
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
dataproduct init # writes ./dataproduct.odps.yaml
|
|
71
|
+
dataproduct init my.odps.yaml # custom path
|
|
72
|
+
dataproduct init --overwrite # replace an existing file
|
|
73
|
+
dataproduct init --template <url|path> # seed from a template or existing product
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `lint` — validate against the ODPS standard
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
dataproduct lint # lints ./dataproduct.odps.yaml
|
|
80
|
+
dataproduct lint my.odps.yaml
|
|
81
|
+
dataproduct lint --all-errors # report every schema violation
|
|
82
|
+
dataproduct lint --output-format junit --output TEST-dataproduct.xml
|
|
83
|
+
dataproduct lint --json-schema ./odps.schema.json # validate against a custom schema
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Validation is schema-only in 0.1: the data product is checked against the
|
|
87
|
+
bundled ODPS v1.0.0 JSON Schema. Exit code is `0` when valid, `1` otherwise.
|
|
88
|
+
|
|
89
|
+
### `publish` — publish to Entropy Data
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
export ENTROPY_DATA_API_KEY=... # from Entropy Data → Settings → API Keys
|
|
93
|
+
dataproduct publish my.odps.yaml
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`publish` sends `PUT {host}/api/dataproducts/{id}` with an `x-api-key` header.
|
|
97
|
+
The host defaults to `https://api.entropy-data.com` and can be overridden with
|
|
98
|
+
`ENTROPY_DATA_HOST`. The referenced `team` must already exist in your org.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Purpose | Variable (with fallbacks) |
|
|
103
|
+
|---|---|
|
|
104
|
+
| API host | `ENTROPY_DATA_HOST` → `DATAMESH_MANAGER_HOST` → `DATACONTRACT_MANAGER_HOST` → `https://api.entropy-data.com` |
|
|
105
|
+
| API key | `ENTROPY_DATA_API_KEY` → `DATAMESH_MANAGER_API_KEY` → `DATACONTRACT_MANAGER_API_KEY` |
|
|
106
|
+
|
|
107
|
+
`.env` files are loaded automatically.
|
|
108
|
+
|
|
109
|
+
## Use as a library
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from dataproduct.data_product import DataProduct
|
|
113
|
+
|
|
114
|
+
run = DataProduct(data_product_file="dataproduct.odps.yaml").lint()
|
|
115
|
+
assert run.result == "passed"
|
|
116
|
+
|
|
117
|
+
DataProduct(data_product_file="dataproduct.odps.yaml").publish()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
See [AGENTS.md](AGENTS.md).
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Data Product CLI
|
|
2
|
+
|
|
3
|
+
The `dataproduct` CLI is an open-source command-line tool for working with
|
|
4
|
+
**data products** defined with the
|
|
5
|
+
[Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.0.0/).
|
|
6
|
+
|
|
7
|
+
It is the data-product sibling of
|
|
8
|
+
[`datacontract-cli`](https://github.com/datacontract/datacontract-cli) (which
|
|
9
|
+
targets the Open Data **Contract** Standard) and mirrors its technology and
|
|
10
|
+
release mechanism. The tool is written in Python and can be used standalone, in
|
|
11
|
+
CI/CD, or as a Python library.
|
|
12
|
+
|
|
13
|
+
> The behavior of each command is specified in [`specs/`](specs/).
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install --python python3.11 dataproduct-cli
|
|
19
|
+
# or
|
|
20
|
+
pip install dataproduct-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
### `init` — create a data product
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
dataproduct init # writes ./dataproduct.odps.yaml
|
|
29
|
+
dataproduct init my.odps.yaml # custom path
|
|
30
|
+
dataproduct init --overwrite # replace an existing file
|
|
31
|
+
dataproduct init --template <url|path> # seed from a template or existing product
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `lint` — validate against the ODPS standard
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
dataproduct lint # lints ./dataproduct.odps.yaml
|
|
38
|
+
dataproduct lint my.odps.yaml
|
|
39
|
+
dataproduct lint --all-errors # report every schema violation
|
|
40
|
+
dataproduct lint --output-format junit --output TEST-dataproduct.xml
|
|
41
|
+
dataproduct lint --json-schema ./odps.schema.json # validate against a custom schema
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Validation is schema-only in 0.1: the data product is checked against the
|
|
45
|
+
bundled ODPS v1.0.0 JSON Schema. Exit code is `0` when valid, `1` otherwise.
|
|
46
|
+
|
|
47
|
+
### `publish` — publish to Entropy Data
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
export ENTROPY_DATA_API_KEY=... # from Entropy Data → Settings → API Keys
|
|
51
|
+
dataproduct publish my.odps.yaml
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`publish` sends `PUT {host}/api/dataproducts/{id}` with an `x-api-key` header.
|
|
55
|
+
The host defaults to `https://api.entropy-data.com` and can be overridden with
|
|
56
|
+
`ENTROPY_DATA_HOST`. The referenced `team` must already exist in your org.
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
| Purpose | Variable (with fallbacks) |
|
|
61
|
+
|---|---|
|
|
62
|
+
| API host | `ENTROPY_DATA_HOST` → `DATAMESH_MANAGER_HOST` → `DATACONTRACT_MANAGER_HOST` → `https://api.entropy-data.com` |
|
|
63
|
+
| API key | `ENTROPY_DATA_API_KEY` → `DATAMESH_MANAGER_API_KEY` → `DATACONTRACT_MANAGER_API_KEY` |
|
|
64
|
+
|
|
65
|
+
`.env` files are loaded automatically.
|
|
66
|
+
|
|
67
|
+
## Use as a library
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from dataproduct.data_product import DataProduct
|
|
71
|
+
|
|
72
|
+
run = DataProduct(data_product_file="dataproduct.odps.yaml").lint()
|
|
73
|
+
assert run.result == "passed"
|
|
74
|
+
|
|
75
|
+
DataProduct(data_product_file="dataproduct.odps.yaml").publish()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
See [AGENTS.md](AGENTS.md).
|
|
81
|
+
|
|
82
|
+
## License
|
|
83
|
+
|
|
84
|
+
MIT
|
|
@@ -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
|