dataproduct-cli 0.0.1__tar.gz → 0.2.0__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.
Files changed (43) hide show
  1. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/CHANGELOG.md +8 -2
  2. {dataproduct_cli-0.0.1/dataproduct_cli.egg-info → dataproduct_cli-0.2.0}/PKG-INFO +6 -4
  3. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/README.md +5 -3
  4. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/data_product.py +8 -5
  5. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/init/init_template.py +1 -1
  6. dataproduct_cli-0.2.0/dataproduct/lint/schema.py +64 -0
  7. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/lint/validate.py +16 -7
  8. dataproduct_cli-0.0.1/dataproduct/schemas/odps-1.0.0.init.yaml → dataproduct_cli-0.2.0/dataproduct/schemas/odps-1.1.0.init.yaml +2 -1
  9. dataproduct_cli-0.2.0/dataproduct/schemas/odps-1.1.0.schema.json +717 -0
  10. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0/dataproduct_cli.egg-info}/PKG-INFO +6 -4
  11. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct_cli.egg-info/SOURCES.txt +2 -1
  12. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/pyproject.toml +1 -1
  13. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/tests/test_init.py +1 -0
  14. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/tests/test_lint.py +55 -0
  15. dataproduct_cli-0.0.1/dataproduct/lint/schema.py +0 -44
  16. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/LICENSE +0 -0
  17. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/MANIFEST.in +0 -0
  18. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/__init__.py +0 -0
  19. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/cli.py +0 -0
  20. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/command_init.py +0 -0
  21. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/command_lint.py +0 -0
  22. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/command_publish.py +0 -0
  23. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/config/__init__.py +0 -0
  24. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/init/__init__.py +0 -0
  25. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/integration/__init__.py +0 -0
  26. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/integration/entropy_data.py +0 -0
  27. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/lint/__init__.py +0 -0
  28. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/lint/files.py +0 -0
  29. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/lint/resolve.py +0 -0
  30. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/model/__init__.py +0 -0
  31. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/model/exceptions.py +0 -0
  32. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/model/run.py +0 -0
  33. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/output/__init__.py +0 -0
  34. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/output/output_format.py +0 -0
  35. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/output/result_writer.py +0 -0
  36. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/py.typed +0 -0
  37. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct/schemas/odps-1.0.0.schema.json +0 -0
  38. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct_cli.egg-info/dependency_links.txt +0 -0
  39. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct_cli.egg-info/entry_points.txt +0 -0
  40. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct_cli.egg-info/requires.txt +0 -0
  41. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/dataproduct_cli.egg-info/top_level.txt +0 -0
  42. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/setup.cfg +0 -0
  43. {dataproduct_cli-0.0.1 → dataproduct_cli-0.2.0}/tests/test_publish.py +0 -0
@@ -5,12 +5,18 @@ what changed (user-facing).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
- ## [0.0.1]
8
+ ## [0.2.0]
9
9
 
10
- - Test release to validate the PyPI publishing pipeline
10
+ - Support ODPS v1.1.0: `lint` validates against the bundled v1.1.0 JSON Schema (`type`, `context`, `synonyms`, `deprecated`, `customProperties[].vendor`, element `id`s, optional port `version`/`contractId`)
11
+ - `lint` validates against the bundled JSON Schema for the `apiVersion` the document declares (`v1.1.0` → v1.1.0 schema; `v1.0.0`/`v0.9.0` → v1.0.0 schema; unknown → newest), and check names state which schema ran
12
+ - `init` template now uses `apiVersion: v1.1.0`
11
13
 
12
14
  ## [0.1.0]
13
15
 
14
16
  - `init` command: create a valid `dataproduct.odps.yaml` from a bundled ODPS v1.0.0 template
15
17
  - `lint` command: validate a data product against the ODPS v1.0.0 JSON Schema (json/junit output)
16
18
  - `publish` command: publish a data product to Entropy Data (`PUT /api/dataproducts/{id}`)
19
+
20
+ ## [0.0.1]
21
+
22
+ - Test release to validate the PyPI publishing pipeline
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataproduct-cli
3
- Version: 0.0.1
3
+ Version: 0.2.0
4
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
5
  Author-email: Michael Kutz <michael.kutz@entropy-data.com>
6
6
  License-Expression: MIT
@@ -44,7 +44,7 @@ Dynamic: license-file
44
44
 
45
45
  The `dataproduct` CLI is an open-source command-line tool for working with
46
46
  **data products** defined with the
47
- [Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.0.0/).
47
+ [Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.1.0/).
48
48
 
49
49
  It is the data-product sibling of
50
50
  [`datacontract-cli`](https://github.com/datacontract/datacontract-cli) (which
@@ -83,8 +83,10 @@ dataproduct lint --output-format junit --output TEST-dataproduct.xml
83
83
  dataproduct lint --json-schema ./odps.schema.json # validate against a custom schema
84
84
  ```
85
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.
86
+ Validation is schema-only: the data product is checked against the bundled
87
+ ODPS JSON Schema matching its `apiVersion` (`v1.1.0`, `v1.0.0`, or `v0.9.0`;
88
+ unknown versions are validated against the latest). Exit code is `0` when
89
+ valid, `1` otherwise.
88
90
 
89
91
  ### `publish` — publish to Entropy Data
90
92
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `dataproduct` CLI is an open-source command-line tool for working with
4
4
  **data products** defined with the
5
- [Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.0.0/).
5
+ [Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.1.0/).
6
6
 
7
7
  It is the data-product sibling of
8
8
  [`datacontract-cli`](https://github.com/datacontract/datacontract-cli) (which
@@ -41,8 +41,10 @@ dataproduct lint --output-format junit --output TEST-dataproduct.xml
41
41
  dataproduct lint --json-schema ./odps.schema.json # validate against a custom schema
42
42
  ```
43
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.
44
+ Validation is schema-only: the data product is checked against the bundled
45
+ ODPS JSON Schema matching its `apiVersion` (`v1.1.0`, `v1.0.0`, or `v0.9.0`;
46
+ unknown versions are validated against the latest). Exit code is `0` when
47
+ valid, `1` otherwise.
46
48
 
47
49
  ### `publish` — publish to Entropy Data
48
50
 
@@ -11,7 +11,7 @@ from typing import Optional, Union
11
11
  from dataproduct.config import Config
12
12
  from dataproduct.integration.entropy_data import publish_data_product_to_entropy_data
13
13
  from dataproduct.lint.files import read_resource
14
- from dataproduct.lint.schema import fetch_schema
14
+ from dataproduct.lint.schema import fetch_schema, schema_version_for
15
15
  from dataproduct.lint.validate import parse_yaml, validate_against_schema
16
16
  from dataproduct.model.exceptions import DataProductException
17
17
  from dataproduct.model.run import Check, ResultEnum, Run
@@ -46,15 +46,16 @@ class DataProduct:
46
46
  return parse_yaml(content)
47
47
 
48
48
  def lint(self) -> Run:
49
- """Validate the data product against the ODPS JSON Schema (schema-only)."""
49
+ """Validate the data product against the ODPS JSON Schema matching its ``apiVersion`` (schema-only)."""
50
50
  run = Run.create_run()
51
51
  run.log_info("Linting data product")
52
52
  try:
53
53
  data = self._load_dict()
54
54
  run.dataProductId = data.get("id")
55
55
  run.dataProductVersion = data.get("version")
56
- schema = fetch_schema(self._schema_location)
57
- checks = validate_against_schema(data, schema, self._all_errors)
56
+ schema_version = None if self._schema_location else schema_version_for(data.get("apiVersion"))
57
+ schema = fetch_schema(self._schema_location, schema_version)
58
+ checks = validate_against_schema(data, schema, self._all_errors, schema_version)
58
59
  if checks:
59
60
  run.checks.extend(checks)
60
61
  for check in checks:
@@ -64,7 +65,9 @@ class DataProduct:
64
65
  Check(
65
66
  type="lint",
66
67
  result=ResultEnum.passed,
67
- name="Data product is syntactically valid",
68
+ name="Data product is syntactically valid"
69
+ if schema_version is None
70
+ else f"Data product is valid against ODPS v{schema_version}",
68
71
  )
69
72
  )
70
73
  except DataProductException as e:
@@ -3,7 +3,7 @@ import logging
3
3
 
4
4
  import requests
5
5
 
6
- DEFAULT_DATA_PRODUCT_INIT_TEMPLATE = "odps-1.0.0.init.yaml"
6
+ DEFAULT_DATA_PRODUCT_INIT_TEMPLATE = "odps-1.1.0.init.yaml"
7
7
 
8
8
 
9
9
  def get_init_template(location: str = None) -> str:
@@ -0,0 +1,64 @@
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, Optional, Union
7
+
8
+ import requests
9
+
10
+ from dataproduct.model.exceptions import DataProductException
11
+ from dataproduct.model.run import ResultEnum
12
+
13
+ # ODPS v1.1.0 relaxed several required fields (e.g. `status`, port `version`/`contractId`)
14
+ # and added new ones, so older documents must be validated against their own schema.
15
+ # v0.9.0 has no dedicated bundled schema; the v1.0.0 schema accepts it.
16
+ ODPS_SCHEMA_VERSIONS = {
17
+ "v1.1.0": "1.1.0",
18
+ "v1.0.0": "1.0.0",
19
+ "v0.9.0": "1.0.0",
20
+ }
21
+ DEFAULT_ODPS_SCHEMA_VERSION = "1.1.0"
22
+
23
+
24
+ def schema_version_for(api_version: Any = None) -> str:
25
+ """Return the bundled ODPS schema version for a document's ``apiVersion``.
26
+
27
+ Unknown or missing versions fall back to the newest bundled schema, which
28
+ then reports the invalid ``apiVersion`` as a schema violation.
29
+ """
30
+ if isinstance(api_version, str):
31
+ return ODPS_SCHEMA_VERSIONS.get(api_version, DEFAULT_ODPS_SCHEMA_VERSION)
32
+ return DEFAULT_ODPS_SCHEMA_VERSION
33
+
34
+
35
+ def fetch_schema(location: Union[str, Path] = None, schema_version: Optional[str] = None) -> Dict[str, Any]:
36
+ """Fetch the ODPS JSON Schema to validate against.
37
+
38
+ ``None`` uses the bundled schema for ``schema_version`` (newest when
39
+ omitted); otherwise ``location`` is a URL or local path.
40
+ """
41
+ if location is None:
42
+ schema_name = f"odps-{schema_version or DEFAULT_ODPS_SCHEMA_VERSION}.schema.json"
43
+ logging.info("Use default bundled schema " + schema_name)
44
+ schemas = resources.files("dataproduct")
45
+ schema_file = schemas.joinpath("schemas", schema_name)
46
+ with schema_file.open("r") as file:
47
+ return json.load(file)
48
+
49
+ location_str = str(location)
50
+ if location_str.startswith("http://") or location_str.startswith("https://"):
51
+ logging.debug(f"Downloading schema from {location_str}")
52
+ response = requests.get(location_str)
53
+ return response.json()
54
+
55
+ if not os.path.exists(location_str):
56
+ raise DataProductException(
57
+ type="lint",
58
+ name=f"Reading schema from {location_str}",
59
+ reason=f"The file '{location_str}' does not exist.",
60
+ result=ResultEnum.error,
61
+ )
62
+ logging.debug(f"Loading JSON schema locally at {location_str}")
63
+ with open(location_str, "r") as file:
64
+ return json.load(file)
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict, List
1
+ from typing import Any, Dict, List, Optional
2
2
 
3
3
  import yaml
4
4
  from jsonschema.validators import validator_for
@@ -32,11 +32,15 @@ def parse_yaml(content: str) -> Dict[str, Any]:
32
32
  return data
33
33
 
34
34
 
35
- def validate_against_schema(data: Dict[str, Any], schema: Dict[str, Any], all_errors: bool = False) -> List[Check]:
35
+ def validate_against_schema(
36
+ data: Dict[str, Any], schema: Dict[str, Any], all_errors: bool = False, schema_version: Optional[str] = None
37
+ ) -> List[Check]:
36
38
  """Validate ``data`` against the ODPS JSON Schema.
37
39
 
38
40
  Returns a list of ``error`` checks — empty when the document is valid. With
39
- ``all_errors=False`` (default) only the first violation is reported.
41
+ ``all_errors=False`` (default) only the first violation is reported. Check
42
+ names state the bundled ``schema_version`` that ran; ``None`` means a custom
43
+ schema was supplied and no version is named.
40
44
  """
41
45
  validator_cls = validator_for(schema)
42
46
  validator_cls.check_schema(schema)
@@ -46,16 +50,21 @@ def validate_against_schema(data: Dict[str, Any], schema: Dict[str, Any], all_er
46
50
  if not all_errors:
47
51
  errors = errors[:1]
48
52
 
53
+ name = (
54
+ "Check that data product YAML is valid"
55
+ if schema_version is None
56
+ else f"Check that data product is valid against ODPS v{schema_version}"
57
+ )
49
58
  checks: List[Check] = []
50
59
  for error in errors:
51
- path = "/".join(str(p) for p in error.absolute_path) or "(root)"
60
+ path = "/".join(str(p) for p in error.absolute_path)
52
61
  checks.append(
53
62
  Check(
54
63
  type="lint",
55
64
  result=ResultEnum.error,
56
- name=f"Schema validation failed at '{path}'",
57
- reason=error.message,
58
- field=path,
65
+ name=name,
66
+ reason=f"{path}: {error.message}" if path else error.message,
67
+ field=path or "(root)",
59
68
  )
60
69
  )
61
70
  return checks
@@ -1,9 +1,10 @@
1
- apiVersion: v1.0.0
1
+ apiVersion: v1.1.0
2
2
  kind: DataProduct
3
3
  id: my-data-product-id
4
4
  name: My Data Product
5
5
  version: v1.0.0
6
6
  status: draft
7
+ # type: consumerAligned # sourceAligned | aggregate | consumerAligned
7
8
 
8
9
  description:
9
10
  purpose: Purpose of the data product.
@@ -0,0 +1,717 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2019-09/schema",
3
+ "title": "Open Data Product Standard (ODPS)",
4
+ "description": "An open data product standard descriptor to enable defining data products.",
5
+ "type": "object",
6
+ "required": ["apiVersion", "kind", "id"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "apiVersion": {
10
+ "type": "string",
11
+ "default": "v1.1.0",
12
+ "description": "Version of the standard used to build data product. Default value is v1.1.0.",
13
+ "enum": ["v1.1.0", "v1.0.0", "v0.9.0"]
14
+ },
15
+ "kind": {
16
+ "type": "string",
17
+ "default": "DataProduct",
18
+ "description": "The kind of file this is. Valid value is `DataProduct`.",
19
+ "enum": ["DataProduct"]
20
+ },
21
+ "id": {
22
+ "type": "string",
23
+ "description": "A unique identifier used to reduce the risk of dataset name collisions, such as a UUID."
24
+ },
25
+ "name": {
26
+ "type": "string",
27
+ "description": "Name of the data product."
28
+ },
29
+ "deprecated": {
30
+ "type": "boolean",
31
+ "description": "Indicates this data product is deprecated and should not be used in new implementations. Defaults to false.",
32
+ "default": false
33
+ },
34
+ "synonyms": {
35
+ "$ref": "#/$defs/Synonyms"
36
+ },
37
+ "version": {
38
+ "type": "string",
39
+ "description": "Current version of the data product. Not required, but highly recommended."
40
+ },
41
+ "status": {
42
+ "type": "string",
43
+ "description": "Current status of the data product.",
44
+ "examples": ["proposed", "draft", "active", "deprecated", "retired"]
45
+ },
46
+ "domain": {
47
+ "type": "string",
48
+ "description": "Business domain"
49
+ },
50
+ "type": {
51
+ "type": "string",
52
+ "description": "Architectural type of the data product. Common values: `sourceAligned`, `aggregate`, `consumerAligned`. Organizations may define custom types."
53
+ },
54
+ "context": {
55
+ "$ref": "#/$defs/Context"
56
+ },
57
+ "tenant": {
58
+ "type": "string",
59
+ "description": "Organization identifier"
60
+ },
61
+ "authoritativeDefinitions": {
62
+ "type": "array",
63
+ "description": "List of links to sources that provide more details on the data contract.",
64
+ "items": {
65
+ "$ref": "#/$defs/AuthoritativeDefinition"
66
+ }
67
+ },
68
+ "description": {
69
+ "$ref": "#/$defs/Description"
70
+ },
71
+ "customProperties": {
72
+ "type": "array",
73
+ "description": "A list of key/value pairs for custom properties.",
74
+ "items": {
75
+ "$ref": "#/$defs/CustomProperty"
76
+ }
77
+ },
78
+ "tags": {
79
+ "$ref": "#/$defs/Tags"
80
+ },
81
+ "inputPorts": {
82
+ "type": "array",
83
+ "description": "List of objects describing an input port. You need at least one as a data product needs to get data somewhere.",
84
+ "items": {
85
+ "$ref": "#/$defs/InputPort"
86
+ }
87
+ },
88
+ "outputPorts": {
89
+ "type": "array",
90
+ "description": "List of objects describing an output port. You need at least one, as a data product without output is useless.",
91
+ "items": {
92
+ "$ref": "#/$defs/OutputPort"
93
+ }
94
+ },
95
+ "managementPorts": {
96
+ "type": "array",
97
+ "description": "Management ports define access points for managing the data product.",
98
+ "items": {
99
+ "$ref": "#/$defs/ManagementPort"
100
+ }
101
+ },
102
+ "support": {
103
+ "type": "array",
104
+ "description": "Support and communication channels.",
105
+ "items": {
106
+ "$ref": "#/$defs/Support"
107
+ }
108
+ },
109
+ "team": {
110
+ "$ref": "#/$defs/Team"
111
+ },
112
+ "productCreatedTs": {
113
+ "type": "string",
114
+ "format": "date-time",
115
+ "description": "Timestamp in UTC of when the data product was created, using ISO 8601."
116
+ }
117
+ },
118
+ "$defs": {
119
+ "Tags": {
120
+ "type": "array",
121
+ "description": "A list of tags that may be assigned to the elements (object or property); the tags keyword may appear at any level. Tags may be used to better categorize an element. For example, `finance`, `sensitive`, `employee_record`.",
122
+ "examples": ["finance", "sensitive", "employee_record"],
123
+ "items": {
124
+ "type": "string"
125
+ }
126
+ },
127
+ "Description": {
128
+ "type": "object",
129
+ "description": "Object containing the descriptions.",
130
+ "additionalProperties": false,
131
+ "properties": {
132
+ "purpose": {
133
+ "type": "string",
134
+ "description": "Intended purpose for the provided data."
135
+ },
136
+ "limitations": {
137
+ "type": "string",
138
+ "description": "Technical, compliance, and legal limitations for data use."
139
+ },
140
+ "usage": {
141
+ "type": "string",
142
+ "description": "Recommended usage of the data."
143
+ },
144
+ "authoritativeDefinitions": {
145
+ "type": "array",
146
+ "description": "List of links to sources that provide more details on the data contract.",
147
+ "items": {
148
+ "$ref": "#/$defs/AuthoritativeDefinition"
149
+ }
150
+ },
151
+ "customProperties": {
152
+ "type": "array",
153
+ "description": "A list of key/value pairs for custom properties.",
154
+ "items": {
155
+ "$ref": "#/$defs/CustomProperty"
156
+ }
157
+ }
158
+ }
159
+ },
160
+ "CustomProperty": {
161
+ "type": "object",
162
+ "description": "A key/value pair for custom properties.",
163
+ "additionalProperties": false,
164
+ "properties": {
165
+ "id": {
166
+ "type": "string",
167
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
168
+ },
169
+ "property": {
170
+ "type": "string",
171
+ "description": "The name of the key. Names should be in camel case, the same as if they were permanent properties in the contract."
172
+ },
173
+ "value": {
174
+ "description": "The value of the key."
175
+ },
176
+ "description": {
177
+ "type": "string",
178
+ "description": "Optional description."
179
+ },
180
+ "vendor": {
181
+ "type": "string",
182
+ "description": "Identifies the vendor, provider, or external system associated with this custom property. SHOULD be a stable, lowercase identifier matching ^[a-z0-9][a-z0-9-]*$ (e.g. confluent, zeenea, atlan, soda). Tools MUST preserve unknown vendor values.",
183
+ "examples": ["confluent", "zeenea", "atlan", "soda"]
184
+ }
185
+ },
186
+ "required": ["property", "value"]
187
+ },
188
+ "Synonyms": {
189
+ "type": "array",
190
+ "description": "A list of alternative names for the object. See RFC 0041.",
191
+ "items": {
192
+ "$ref": "#/$defs/Synonym"
193
+ }
194
+ },
195
+ "Synonym": {
196
+ "type": "object",
197
+ "description": "An alternative name for a named object, helping catalogs, AI/LLM tools, and natural language interfaces resolve business vocabulary to the underlying object. See RFC 0041.",
198
+ "additionalProperties": false,
199
+ "properties": {
200
+ "id": {
201
+ "type": "string",
202
+ "description": "A unique identifier for the synonym, useful when referencing or deduplicating synonyms across tools. Recommended for elements that will be referenced."
203
+ },
204
+ "synonym": {
205
+ "type": "string",
206
+ "description": "The synonymous term."
207
+ },
208
+ "description": {
209
+ "type": "string",
210
+ "description": "Short human-readable note about when or why this synonym is used."
211
+ },
212
+ "locale": {
213
+ "type": "string",
214
+ "description": "BCP 47 language tag (e.g., `en-US`, `fr-FR`) when the synonym is language-specific."
215
+ },
216
+ "source": {
217
+ "type": "string",
218
+ "description": "Origin of the synonym (e.g., `glossary`, `finance-team`, `legacy-system`)."
219
+ },
220
+ "status": {
221
+ "type": "string",
222
+ "description": "Lifecycle status of the synonym (e.g., `active`, `deprecated`)."
223
+ },
224
+ "customProperties": {
225
+ "type": "array",
226
+ "description": "Custom properties block.",
227
+ "items": {
228
+ "$ref": "#/$defs/CustomProperty"
229
+ }
230
+ }
231
+ },
232
+ "required": ["synonym"]
233
+ },
234
+ "AuthoritativeDefinition": {
235
+ "type": "object",
236
+ "description": "A type/link pair for authoritative definitions.",
237
+ "additionalProperties": false,
238
+ "properties": {
239
+ "id": {
240
+ "type": "string",
241
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
242
+ },
243
+ "type": {
244
+ "type": "string",
245
+ "description": "Type of definition for authority.",
246
+ "examples": ["businessDefinition", "canonicalUrl", "glossary", "implementation", "ontology", "taxonomy", "transformationImplementation", "tutorial", "videoTutorial"]
247
+ },
248
+ "url": {
249
+ "type": "string",
250
+ "format": "uri",
251
+ "description": "URL to the authority."
252
+ },
253
+ "description": {
254
+ "type": "string",
255
+ "description": "Optional description."
256
+ }
257
+ },
258
+ "required": ["type", "url"]
259
+ },
260
+ "InputPort": {
261
+ "type": "object",
262
+ "description": "An input port describing expectations.",
263
+ "additionalProperties": false,
264
+ "properties": {
265
+ "deprecated": {
266
+ "type": "boolean",
267
+ "description": "Indicates this input port is deprecated and should not be used in new implementations. Defaults to false.",
268
+ "default": false
269
+ },
270
+ "id": {
271
+ "type": "string",
272
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
273
+ },
274
+ "name": {
275
+ "type": "string",
276
+ "description": "Name of the input port."
277
+ },
278
+ "version": {
279
+ "type": "string",
280
+ "description": "Version of the input port."
281
+ },
282
+ "contractId": {
283
+ "type": "string",
284
+ "description": "Contract ID for the input port."
285
+ },
286
+ "tags": {
287
+ "$ref": "#/$defs/Tags"
288
+ },
289
+ "customProperties": {
290
+ "type": "array",
291
+ "description": "Custom properties block.",
292
+ "items": {
293
+ "$ref": "#/$defs/CustomProperty"
294
+ }
295
+ },
296
+ "authoritativeDefinitions": {
297
+ "type": "array",
298
+ "description": "Authoritative definitions block.",
299
+ "items": {
300
+ "$ref": "#/$defs/AuthoritativeDefinition"
301
+ }
302
+ }
303
+ },
304
+ "required": ["name"]
305
+ },
306
+ "OutputPort": {
307
+ "type": "object",
308
+ "description": "An output port describing promises.",
309
+ "additionalProperties": false,
310
+ "properties": {
311
+ "deprecated": {
312
+ "type": "boolean",
313
+ "description": "Indicates this output port is deprecated and should not be used in new implementations. Defaults to false.",
314
+ "default": false
315
+ },
316
+ "id": {
317
+ "type": "string",
318
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
319
+ },
320
+ "name": {
321
+ "type": "string",
322
+ "description": "Name of the output port."
323
+ },
324
+ "description": {
325
+ "type": "string",
326
+ "description": "Human readable short description of the output port."
327
+ },
328
+ "type": {
329
+ "type": "string",
330
+ "description": "There can be different types of output ports, each automated and handled differently. Here you can indicate the type."
331
+ },
332
+ "version": {
333
+ "type": "string",
334
+ "description": "For each version, a different instance of the output port is listed. The combination of the name and version is the key."
335
+ },
336
+ "contractId": {
337
+ "type": "string",
338
+ "description": "Contract ID for the output port."
339
+ },
340
+ "sbom": {
341
+ "type": "array",
342
+ "description": "The SBOM can/should be at the version level.",
343
+ "items": {
344
+ "$ref": "#/$defs/SBOM"
345
+ }
346
+ },
347
+ "inputContracts": {
348
+ "type": "array",
349
+ "description": "Dependencies or input contracts.",
350
+ "items": {
351
+ "$ref": "#/$defs/InputContract"
352
+ }
353
+ },
354
+ "synonyms": {
355
+ "$ref": "#/$defs/Synonyms"
356
+ },
357
+ "tags": {
358
+ "$ref": "#/$defs/Tags"
359
+ },
360
+ "customProperties": {
361
+ "type": "array",
362
+ "description": "Custom properties block.",
363
+ "items": {
364
+ "$ref": "#/$defs/CustomProperty"
365
+ }
366
+ },
367
+ "authoritativeDefinitions": {
368
+ "type": "array",
369
+ "description": "Authoritative definitions block.",
370
+ "items": {
371
+ "$ref": "#/$defs/AuthoritativeDefinition"
372
+ }
373
+ },
374
+ "context": {
375
+ "$ref": "#/$defs/Context"
376
+ }
377
+ },
378
+ "required": ["name"]
379
+ },
380
+ "SBOM": {
381
+ "type": "object",
382
+ "description": "Software Bill of Materials.",
383
+ "additionalProperties": false,
384
+ "properties": {
385
+ "id": {
386
+ "type": "string",
387
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
388
+ },
389
+ "type": {
390
+ "type": "string",
391
+ "default": "external",
392
+ "description": "Type of SBOM."
393
+ },
394
+ "url": {
395
+ "type": "string",
396
+ "format": "uri",
397
+ "description": "URL to the SBOM."
398
+ },
399
+ "tags": { "$ref": "#/$defs/Tags" },
400
+ "customProperties": {
401
+ "type": "array",
402
+ "description": "Custom properties block.",
403
+ "items": { "$ref": "#/$defs/CustomProperty" }
404
+ },
405
+ "authoritativeDefinitions": {
406
+ "type": "array",
407
+ "description": "Authoritative definitions block.",
408
+ "items": { "$ref": "#/$defs/AuthoritativeDefinition" }
409
+ }
410
+ }
411
+ },
412
+ "InputContract": {
413
+ "type": "object",
414
+ "description": "Input contract dependency.",
415
+ "additionalProperties": false,
416
+ "properties": {
417
+ "id": {
418
+ "type": "string",
419
+ "description": "Contract ID or contractId."
420
+ },
421
+ "version": {
422
+ "type": "string",
423
+ "description": "Version of the input contract."
424
+ }
425
+ },
426
+ "required": ["id", "version"]
427
+ },
428
+ "ManagementPort": {
429
+ "type": "object",
430
+ "description": "Management port for managing the data product.",
431
+ "additionalProperties": false,
432
+ "properties": {
433
+ "deprecated": {
434
+ "type": "boolean",
435
+ "description": "Indicates this management port is deprecated and should not be used in new implementations. Defaults to false.",
436
+ "default": false
437
+ },
438
+ "id": {
439
+ "type": "string",
440
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
441
+ },
442
+ "name": {
443
+ "type": "string",
444
+ "description": "Endpoint identifier or unique name."
445
+ },
446
+ "content": {
447
+ "type": "string",
448
+ "description": "Content type.",
449
+ "examples": ["discoverability", "observability", "control", "dictionary"]
450
+ },
451
+ "type": {
452
+ "type": "string",
453
+ "default": "rest",
454
+ "description": "Type: can be `rest` or `topic`. Default is `rest`.",
455
+ "examples": ["rest", "topic"]
456
+ },
457
+ "url": {
458
+ "type": "string",
459
+ "format": "uri",
460
+ "description": "URL to access the endpoint."
461
+ },
462
+ "channel": {
463
+ "type": "string",
464
+ "description": "Channel to communicate with the data product."
465
+ },
466
+ "description": {
467
+ "type": "string",
468
+ "description": "Purpose and usage."
469
+ },
470
+ "tags": {
471
+ "$ref": "#/$defs/Tags"
472
+ },
473
+ "customProperties": {
474
+ "type": "array",
475
+ "description": "Custom properties block.",
476
+ "items": {
477
+ "$ref": "#/$defs/CustomProperty"
478
+ }
479
+ },
480
+ "authoritativeDefinitions": {
481
+ "type": "array",
482
+ "description": "Authoritative definitions block.",
483
+ "items": {
484
+ "$ref": "#/$defs/AuthoritativeDefinition"
485
+ }
486
+ }
487
+ },
488
+ "required": ["name", "content"]
489
+ },
490
+ "Support": {
491
+ "type": "object",
492
+ "description": "Support channel.",
493
+ "additionalProperties": false,
494
+ "properties": {
495
+ "id": {
496
+ "type": "string",
497
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
498
+ },
499
+ "channel": {
500
+ "type": "string",
501
+ "description": "Channel name or identifier."
502
+ },
503
+ "url": {
504
+ "type": "string",
505
+ "format": "uri",
506
+ "description": "Access URL using normal URL scheme (https, mailto, etc.)."
507
+ },
508
+ "description": {
509
+ "type": "string",
510
+ "description": "Description of the channel, free text."
511
+ },
512
+ "tool": {
513
+ "type": "string",
514
+ "description": "Name of the tool.",
515
+ "examples": ["email", "slack", "teams", "discord", "ticket", "other"]
516
+ },
517
+ "scope": {
518
+ "type": "string",
519
+ "description": "Scope can be: `interactive`, `announcements`, `issues`.",
520
+ "examples": ["interactive", "announcements", "issues"]
521
+ },
522
+ "invitationUrl": {
523
+ "type": "string",
524
+ "format": "uri",
525
+ "description": "Some tools uses invitation URL for requesting or subscribing. Follows the URL scheme."
526
+ },
527
+ "tags": {
528
+ "$ref": "#/$defs/Tags"
529
+ },
530
+ "customProperties": {
531
+ "type": "array",
532
+ "description": "Custom properties block.",
533
+ "items": {
534
+ "$ref": "#/$defs/CustomProperty"
535
+ }
536
+ },
537
+ "authoritativeDefinitions": {
538
+ "type": "array",
539
+ "description": "Authoritative definitions block.",
540
+ "items": {
541
+ "$ref": "#/$defs/AuthoritativeDefinition"
542
+ }
543
+ }
544
+ },
545
+ "required": ["channel", "url"]
546
+ },
547
+ "TeamMember": {
548
+ "type": "object",
549
+ "description": "Team member information.",
550
+ "additionalProperties": false,
551
+ "properties": {
552
+ "id": {
553
+ "type": "string",
554
+ "description": "A unique identifier for the element used to create stable, refactor-safe references. Recommended for elements that will be referenced."
555
+ },
556
+ "username": {
557
+ "type": "string",
558
+ "description": "The user's username or email."
559
+ },
560
+ "name": {
561
+ "type": "string",
562
+ "description": "The user's name."
563
+ },
564
+ "description": {
565
+ "type": "string",
566
+ "description": "The user's description."
567
+ },
568
+ "role": {
569
+ "type": "string",
570
+ "description": "The user's job role; Examples might be owner, data steward. There is no limit on the role."
571
+ },
572
+ "dateIn": {
573
+ "type": "string",
574
+ "format": "date",
575
+ "description": "The date when the user joined the team."
576
+ },
577
+ "dateOut": {
578
+ "type": "string",
579
+ "format": "date",
580
+ "description": "The date when the user ceased to be part of the team."
581
+ },
582
+ "replacedByUsername": {
583
+ "type": "string",
584
+ "description": "The username of the user who replaced the previous user."
585
+ },
586
+ "tags": {
587
+ "$ref": "#/$defs/Tags"
588
+ },
589
+ "customProperties": {
590
+ "type": "array",
591
+ "description": "Custom properties block.",
592
+ "items": {
593
+ "$ref": "#/$defs/CustomProperty"
594
+ }
595
+ },
596
+ "authoritativeDefinitions": {
597
+ "type": "array",
598
+ "description": "Authoritative definitions block.",
599
+ "items": {
600
+ "$ref": "#/$defs/AuthoritativeDefinition"
601
+ }
602
+ }
603
+ },
604
+ "required": ["username"]
605
+ },
606
+ "Context": {
607
+ "type": "object",
608
+ "description": "AI and semantic context block (RFC-0038). Provides structured guidance for AI agents, LLMs, BI tools, and semantic layer platforms. Optional and additive. Applicable at the data product (top level) and output port levels. Input ports refer to the linked ODCS contract's context instead.",
609
+ "additionalProperties": false,
610
+ "properties": {
611
+ "instructions": {
612
+ "type": "string",
613
+ "description": "Natural language guidance for AI agents and tools on how to use this entity. Equivalent to a system prompt scoped to this level."
614
+ },
615
+ "verifiedStatements": {
616
+ "type": "array",
617
+ "description": "Canonical business questions, each with an optional curated answer. Entries with `answer` should be returned verbatim by AI agents when a query is semantically close; entries without `answer` serve as sample questions for text-to-SQL priming and disambiguation.",
618
+ "items": {
619
+ "type": "object",
620
+ "additionalProperties": false,
621
+ "properties": {
622
+ "id": {
623
+ "type": "string",
624
+ "description": "Stable identifier for the entry, useful when referencing or deduplicating verified statements across tools."
625
+ },
626
+ "question": {
627
+ "type": "string",
628
+ "description": "The canonical question."
629
+ },
630
+ "answer": {
631
+ "type": "string",
632
+ "description": "The expected response or result description. Optional — omit to signal an unanswered sample question."
633
+ },
634
+ "authoritativeDefinitions": {
635
+ "type": "array",
636
+ "items": { "$ref": "#/$defs/AuthoritativeDefinition" }
637
+ },
638
+ "tags": { "$ref": "#/$defs/Tags" },
639
+ "customProperties": {
640
+ "type": "array",
641
+ "items": { "$ref": "#/$defs/CustomProperty" }
642
+ }
643
+ },
644
+ "required": ["question"]
645
+ }
646
+ },
647
+ "constraints": {
648
+ "type": "array",
649
+ "description": "Negative guidance: what AI agents must NOT do with this entity.",
650
+ "items": {
651
+ "type": "object",
652
+ "additionalProperties": false,
653
+ "properties": {
654
+ "id": {
655
+ "type": "string",
656
+ "description": "Stable identifier for the constraint."
657
+ },
658
+ "constraint": {
659
+ "type": "string",
660
+ "description": "The constraint text (negative guidance for AI agents)."
661
+ },
662
+ "authoritativeDefinitions": {
663
+ "type": "array",
664
+ "items": { "$ref": "#/$defs/AuthoritativeDefinition" }
665
+ },
666
+ "tags": { "$ref": "#/$defs/Tags" },
667
+ "customProperties": {
668
+ "type": "array",
669
+ "items": { "$ref": "#/$defs/CustomProperty" }
670
+ }
671
+ },
672
+ "required": ["constraint"]
673
+ }
674
+ }
675
+ }
676
+ },
677
+ "Team": {
678
+ "type": "object",
679
+ "description": "Team information.",
680
+ "additionalProperties": false,
681
+ "properties": {
682
+ "name": {
683
+ "type": "string",
684
+ "description": "Team name."
685
+ },
686
+ "description": {
687
+ "type": "string",
688
+ "description": "Team description."
689
+ },
690
+ "members": {
691
+ "type": "array",
692
+ "description": "List of members.",
693
+ "items": {
694
+ "$ref": "#/$defs/TeamMember"
695
+ }
696
+ },
697
+ "tags": {
698
+ "$ref": "#/$defs/Tags"
699
+ },
700
+ "customProperties": {
701
+ "type": "array",
702
+ "description": "Custom properties block.",
703
+ "items": {
704
+ "$ref": "#/$defs/CustomProperty"
705
+ }
706
+ },
707
+ "authoritativeDefinitions": {
708
+ "type": "array",
709
+ "description": "Authoritative definitions block.",
710
+ "items": {
711
+ "$ref": "#/$defs/AuthoritativeDefinition"
712
+ }
713
+ }
714
+ }
715
+ }
716
+ }
717
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataproduct-cli
3
- Version: 0.0.1
3
+ Version: 0.2.0
4
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
5
  Author-email: Michael Kutz <michael.kutz@entropy-data.com>
6
6
  License-Expression: MIT
@@ -44,7 +44,7 @@ Dynamic: license-file
44
44
 
45
45
  The `dataproduct` CLI is an open-source command-line tool for working with
46
46
  **data products** defined with the
47
- [Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.0.0/).
47
+ [Open Data Product Standard (ODPS)](https://bitol-io.github.io/open-data-product-standard/v1.1.0/).
48
48
 
49
49
  It is the data-product sibling of
50
50
  [`datacontract-cli`](https://github.com/datacontract/datacontract-cli) (which
@@ -83,8 +83,10 @@ dataproduct lint --output-format junit --output TEST-dataproduct.xml
83
83
  dataproduct lint --json-schema ./odps.schema.json # validate against a custom schema
84
84
  ```
85
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.
86
+ Validation is schema-only: the data product is checked against the bundled
87
+ ODPS JSON Schema matching its `apiVersion` (`v1.1.0`, `v1.0.0`, or `v0.9.0`;
88
+ unknown versions are validated against the latest). Exit code is `0` when
89
+ valid, `1` otherwise.
88
90
 
89
91
  ### `publish` — publish to Entropy Data
90
92
 
@@ -26,8 +26,9 @@ dataproduct/model/run.py
26
26
  dataproduct/output/__init__.py
27
27
  dataproduct/output/output_format.py
28
28
  dataproduct/output/result_writer.py
29
- dataproduct/schemas/odps-1.0.0.init.yaml
30
29
  dataproduct/schemas/odps-1.0.0.schema.json
30
+ dataproduct/schemas/odps-1.1.0.init.yaml
31
+ dataproduct/schemas/odps-1.1.0.schema.json
31
32
  dataproduct_cli.egg-info/PKG-INFO
32
33
  dataproduct_cli.egg-info/SOURCES.txt
33
34
  dataproduct_cli.egg-info/dependency_links.txt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataproduct-cli"
3
- version = "0.0.1"
3
+ version = "0.2.0"
4
4
  description = "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
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -12,6 +12,7 @@ def test_init_default_creates_valid_file(tmp_path, monkeypatch):
12
12
  assert result.exit_code == 0, result.output
13
13
  created = tmp_path / "dataproduct.odps.yaml"
14
14
  assert created.exists()
15
+ assert "apiVersion: v1.1.0" in created.read_text()
15
16
 
16
17
  # The generated file must pass lint with zero errors.
17
18
  run = DataProduct(data_product_file=str(created)).lint()
@@ -4,6 +4,7 @@ from typer.testing import CliRunner
4
4
 
5
5
  from dataproduct.cli import app
6
6
  from dataproduct.data_product import DataProduct
7
+ from dataproduct.lint.schema import fetch_schema, schema_version_for
7
8
 
8
9
  runner = CliRunner()
9
10
 
@@ -22,6 +23,60 @@ def test_lint_valid():
22
23
  assert run.dataProductId == "064c4630-8aad-4dc0-ba95-0f69940e6b18"
23
24
 
24
25
 
26
+ def test_lint_valid_v1_1_0():
27
+ run = _lint("valid-dataproduct-v1.1.0.odps.yaml", all_errors=True)
28
+ assert run.result == "passed", run.checks
29
+ assert run.checks[0].name == "Data product is valid against ODPS v1.1.0"
30
+
31
+
32
+ def test_lint_names_the_schema_that_ran():
33
+ run = _lint("valid-dataproduct.odps.yaml")
34
+ assert run.result == "passed", run.checks
35
+ assert run.checks[0].name == "Data product is valid against ODPS v1.0.0"
36
+
37
+
38
+ def test_lint_names_no_version_for_custom_schema():
39
+ run = DataProduct(
40
+ data_product_file=str(FIXTURES / "valid-dataproduct.odps.yaml"),
41
+ schema_location=str(BUNDLED_SCHEMA),
42
+ ).lint()
43
+ assert run.checks[0].name == "Data product is syntactically valid"
44
+
45
+
46
+ def test_lint_v1_1_0_status_is_optional():
47
+ run = _lint("missing-status-v1.1.0.odps.yaml")
48
+ assert run.result == "passed", run.checks
49
+
50
+
51
+ def test_lint_v1_1_0_fields_rejected_under_v1_0_0():
52
+ run = _lint("v1.1.0-fields-in-v1.0.0.odps.yaml")
53
+ assert run.result == "failed"
54
+ assert run.checks[0].name == "Check that data product is valid against ODPS v1.0.0"
55
+ assert "type" in (run.checks[0].reason or "")
56
+
57
+
58
+ def test_lint_unknown_api_version_reported():
59
+ run = _lint("multiple-errors.odps.yaml", all_errors=True)
60
+ assert run.result == "failed"
61
+ assert all(c.name == "Check that data product is valid against ODPS v1.1.0" for c in run.checks)
62
+ assert "apiVersion" in [c.field for c in run.checks]
63
+
64
+
65
+ def test_schema_version_selected_by_api_version():
66
+ assert schema_version_for("v1.1.0") == "1.1.0"
67
+ assert schema_version_for("v1.0.0") == "1.0.0"
68
+ assert schema_version_for("v0.9.0") == "1.0.0"
69
+ assert schema_version_for(None) == "1.1.0"
70
+ assert schema_version_for("v9.9.9") == "1.1.0"
71
+ assert schema_version_for(["not", "a", "string"]) == "1.1.0"
72
+
73
+
74
+ def test_fetch_bundled_schema_per_version():
75
+ assert "status" in fetch_schema(schema_version="1.0.0")["required"]
76
+ assert "status" not in fetch_schema(schema_version="1.1.0")["required"]
77
+ assert fetch_schema()["properties"]["apiVersion"]["default"] == "v1.1.0"
78
+
79
+
25
80
  def test_lint_valid_cli_exit_zero():
26
81
  result = runner.invoke(app, ["lint", str(FIXTURES / "valid-dataproduct.odps.yaml")])
27
82
  assert result.exit_code == 0, result.output
@@ -1,44 +0,0 @@
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)
File without changes