unitysvc-core 0.2.34__tar.gz → 0.2.35__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.
- {unitysvc_core-0.2.34/src/unitysvc_core.egg-info → unitysvc_core-0.2.35}/PKG-INFO +6 -2
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/README.md +5 -1
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/pyproject.toml +1 -1
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/parameter_usage.py +34 -3
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/validator.py +25 -1
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35/src/unitysvc_core.egg-info}/PKG-INFO +6 -2
- unitysvc_core-0.2.35/tests/test_parameter_usage.py +185 -0
- unitysvc_core-0.2.34/tests/test_parameter_usage.py +0 -121
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/LICENSE +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/setup.cfg +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/__init__.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/base.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/pricing.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/validators.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/listing_v1.json +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/offering_v1.json +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_effective_price.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_offering_data.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_validator.py +0 -0
- {unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/tests/test_validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unitysvc-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.35
|
|
4
4
|
Summary: Shared data models and validation helpers for UnitySVC packages
|
|
5
5
|
Author-email: Bo Peng <bo.peng@unitysvc.com>
|
|
6
6
|
Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
|
|
@@ -136,7 +136,11 @@ MIT
|
|
|
136
136
|
### Required enrollment parameter usage
|
|
137
137
|
|
|
138
138
|
The shared validator rejects required user parameters that no channel, interface,
|
|
139
|
-
or document consumes.
|
|
139
|
+
or document consumes. Only those consumer fields count — the offering's
|
|
140
|
+
`upstream_access_config`, the listing's `user_access_interfaces`, and document
|
|
141
|
+
content (a `.j2` document's template source, which the seller validator reads from
|
|
142
|
+
disk as the SDK uploads it); metadata such as `display_name` or `description` is
|
|
143
|
+
never rendered with `params`. File validation checks a listing against its paired offering
|
|
140
144
|
when present; backend ingest calls the same `validate_required_parameter_usage`
|
|
141
145
|
helper with the complete listing and offering. Schema declarations alone are not
|
|
142
146
|
consumers. Jinja attribute access, string-key lookups, and `params.get("name")`
|
|
@@ -101,7 +101,11 @@ MIT
|
|
|
101
101
|
### Required enrollment parameter usage
|
|
102
102
|
|
|
103
103
|
The shared validator rejects required user parameters that no channel, interface,
|
|
104
|
-
or document consumes.
|
|
104
|
+
or document consumes. Only those consumer fields count — the offering's
|
|
105
|
+
`upstream_access_config`, the listing's `user_access_interfaces`, and document
|
|
106
|
+
content (a `.j2` document's template source, which the seller validator reads from
|
|
107
|
+
disk as the SDK uploads it); metadata such as `display_name` or `description` is
|
|
108
|
+
never rendered with `params`. File validation checks a listing against its paired offering
|
|
105
109
|
when present; backend ingest calls the same `validate_required_parameter_usage`
|
|
106
110
|
helper with the complete listing and offering. Schema declarations alone are not
|
|
107
111
|
consumers. Jinja attribute access, string-key lookups, and `params.get("name")`
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unitysvc-core"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.35"
|
|
8
8
|
description = "Shared data models and validation helpers for UnitySVC packages"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Bo Peng", email = "bo.peng@unitysvc.com" }]
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
This checks that required parameters have consumers. It does not prove that
|
|
4
4
|
an intended enrollment channel consumes them; interface/document use counts.
|
|
5
|
+
|
|
6
|
+
Only the fields the platform renders with an enrollment's ``params`` are
|
|
7
|
+
consumers: the offering's channels (``upstream_access_config``), the listing's
|
|
8
|
+
interfaces (``user_access_interfaces``) and document content. Metadata such as
|
|
9
|
+
``display_name`` or ``description`` is never rendered with ``params``, so a
|
|
10
|
+
reference there does not count (unitysvc/unitysvc#2649).
|
|
5
11
|
"""
|
|
6
12
|
|
|
7
13
|
from typing import Any
|
|
@@ -66,6 +72,31 @@ def unused_required_params(parameters_schema: dict[str, Any] | None, *sources: A
|
|
|
66
72
|
return [name for name in required if name not in used]
|
|
67
73
|
|
|
68
74
|
|
|
75
|
+
# Keys of a document entry that hold its rendered content. Backend ingest sees
|
|
76
|
+
# a ``.j2`` document's template source inlined as ``file_content``; the seller
|
|
77
|
+
# validator inlines the same file before calling this module.
|
|
78
|
+
_DOCUMENT_CONTENT_KEYS = ("file_content", "content")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _document_contents(documents: Any) -> list[str]:
|
|
82
|
+
entries = documents.values() if isinstance(documents, dict) else documents if isinstance(documents, list) else ()
|
|
83
|
+
out: list[str] = []
|
|
84
|
+
for doc in entries:
|
|
85
|
+
if isinstance(doc, dict):
|
|
86
|
+
out.extend(doc[key] for key in _DOCUMENT_CONTENT_KEYS if isinstance(doc.get(key), str))
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def consumer_sources(listing: dict[str, Any], offering: dict[str, Any]) -> list[Any]:
|
|
91
|
+
"""The parts of a service the platform renders with enrollment ``params``."""
|
|
92
|
+
return [
|
|
93
|
+
offering.get("upstream_access_config"),
|
|
94
|
+
listing.get("user_access_interfaces"),
|
|
95
|
+
_document_contents(listing.get("documents")),
|
|
96
|
+
_document_contents(offering.get("documents")),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
69
100
|
def validate_required_parameter_usage(listing: dict[str, Any], offering: dict[str, Any]) -> list[str]:
|
|
70
101
|
"""Return hard validation errors, shared by seller validation and backend ingest.
|
|
71
102
|
|
|
@@ -75,15 +106,15 @@ def validate_required_parameter_usage(listing: dict[str, Any], offering: dict[st
|
|
|
75
106
|
if listing.get("status") == "deprecated" or offering.get("status") == "deprecated":
|
|
76
107
|
return []
|
|
77
108
|
schema = listing.get("user_parameters_schema") or listing.get("parameters_schema")
|
|
78
|
-
content = {k: v for k, v in listing.items() if k not in ("user_parameters_schema", "parameters_schema")}
|
|
79
109
|
try:
|
|
80
|
-
unused = unused_required_params(schema,
|
|
110
|
+
unused = unused_required_params(schema, *consumer_sources(listing, offering))
|
|
81
111
|
except TemplateSyntaxError as exc:
|
|
82
112
|
return [f"Cannot validate required parameter usage: invalid Jinja template: {exc}"]
|
|
83
113
|
if not unused:
|
|
84
114
|
return []
|
|
85
115
|
return [
|
|
86
|
-
f"Required user parameter(s) {', '.join(unused)} are used by no channel,
|
|
116
|
+
f"Required user parameter(s) {', '.join(unused)} are used by no channel (upstream_access_config), "
|
|
117
|
+
"interface (user_access_interfaces) or document content. "
|
|
87
118
|
"Reference each one as {{ params.NAME }} (for example ${ customer_secrets.{{ params.NAME }} }), "
|
|
88
119
|
"or drop it from the required list."
|
|
89
120
|
]
|
|
@@ -1027,9 +1027,33 @@ class DataValidator:
|
|
|
1027
1027
|
except Exception as exc:
|
|
1028
1028
|
return [f"Cannot check required parameter usage: {exc}"]
|
|
1029
1029
|
if isinstance(offering, dict) and (offering.get("schema") == "offering_v1" or candidate.stem == "offering"):
|
|
1030
|
-
return validate_required_parameter_usage(
|
|
1030
|
+
return validate_required_parameter_usage(
|
|
1031
|
+
self._with_template_documents(listing, file_path.parent),
|
|
1032
|
+
self._with_template_documents(offering, candidate.parent),
|
|
1033
|
+
)
|
|
1031
1034
|
return []
|
|
1032
1035
|
|
|
1036
|
+
@staticmethod
|
|
1037
|
+
def _with_template_documents(data: dict[str, Any], base_dir: Path) -> dict[str, Any]:
|
|
1038
|
+
"""``data`` with each ``.j2`` document's template source inlined as
|
|
1039
|
+
``file_content``, as the seller SDK uploads it — so the seller check
|
|
1040
|
+
sees the same document content backend ingest does."""
|
|
1041
|
+
documents = data.get("documents")
|
|
1042
|
+
if not isinstance(documents, dict):
|
|
1043
|
+
return data
|
|
1044
|
+
inlined: dict[str, Any] = {}
|
|
1045
|
+
for title, doc in documents.items():
|
|
1046
|
+
if isinstance(doc, dict) and "file_content" not in doc:
|
|
1047
|
+
rel = doc.get("file_path")
|
|
1048
|
+
if isinstance(rel, str) and rel.endswith(".j2"):
|
|
1049
|
+
path = Path(rel) if Path(rel).is_absolute() else base_dir / rel
|
|
1050
|
+
try:
|
|
1051
|
+
doc = {**doc, "file_content": path.read_text(encoding="utf-8")}
|
|
1052
|
+
except OSError:
|
|
1053
|
+
pass # a missing file is reported by the file-reference checks
|
|
1054
|
+
inlined[title] = doc
|
|
1055
|
+
return {**data, "documents": inlined}
|
|
1056
|
+
|
|
1033
1057
|
def _resolve_provider_name(self, file_path: Path) -> str | None:
|
|
1034
1058
|
"""Find the provider name (``provider_v1.name``) governing a listing file.
|
|
1035
1059
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: unitysvc-core
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.35
|
|
4
4
|
Summary: Shared data models and validation helpers for UnitySVC packages
|
|
5
5
|
Author-email: Bo Peng <bo.peng@unitysvc.com>
|
|
6
6
|
Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
|
|
@@ -136,7 +136,11 @@ MIT
|
|
|
136
136
|
### Required enrollment parameter usage
|
|
137
137
|
|
|
138
138
|
The shared validator rejects required user parameters that no channel, interface,
|
|
139
|
-
or document consumes.
|
|
139
|
+
or document consumes. Only those consumer fields count — the offering's
|
|
140
|
+
`upstream_access_config`, the listing's `user_access_interfaces`, and document
|
|
141
|
+
content (a `.j2` document's template source, which the seller validator reads from
|
|
142
|
+
disk as the SDK uploads it); metadata such as `display_name` or `description` is
|
|
143
|
+
never rendered with `params`. File validation checks a listing against its paired offering
|
|
140
144
|
when present; backend ingest calls the same `validate_required_parameter_usage`
|
|
141
145
|
helper with the complete listing and offering. Schema declarations alone are not
|
|
142
146
|
consumers. Jinja attribute access, string-key lookups, and `params.get("name")`
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Required enrollment parameters must have consumers across service data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from unitysvc_core.parameter_usage import referenced_param_names, validate_required_parameter_usage
|
|
9
|
+
from unitysvc_core.validator import DataValidator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize("expression", ["params.name", "params['name']", 'params.get("name")'])
|
|
13
|
+
def test_named_reads(expression):
|
|
14
|
+
assert referenced_param_names("{{ " + expression + " }}") == {"name"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize(
|
|
18
|
+
"expression",
|
|
19
|
+
[
|
|
20
|
+
"params",
|
|
21
|
+
"params | tojson",
|
|
22
|
+
"params.items()",
|
|
23
|
+
"params.keys()",
|
|
24
|
+
"params.values()",
|
|
25
|
+
"params.get(key)",
|
|
26
|
+
"params[key]",
|
|
27
|
+
],
|
|
28
|
+
)
|
|
29
|
+
def test_dynamic_and_whole_map_reads(expression):
|
|
30
|
+
assert referenced_param_names("{{ " + expression + " }}") is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_strings_comments_and_non_template_text_are_not_consumers():
|
|
34
|
+
assert referenced_param_names(["params.name", "{# params.name #}", '{{ "params.name" }}']) == set()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def listing(**changes):
|
|
38
|
+
return {"status": "ready", "user_parameters_schema": {"required": ["host", "token"]}, **changes}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def channel(**fields):
|
|
42
|
+
"""An offering whose one channel carries ``fields``."""
|
|
43
|
+
return {"upstream_access_config": {"plus": fields}}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_required_names_in_the_schema_do_not_count_as_use():
|
|
47
|
+
errors = validate_required_parameter_usage(
|
|
48
|
+
listing(), {"upstream_access_config": {"free": {"api_key": "${ customer_secrets.TOKEN }"}}}
|
|
49
|
+
)
|
|
50
|
+
assert len(errors) == 1
|
|
51
|
+
assert "host, token are used by no channel" in errors[0]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.parametrize(
|
|
55
|
+
("listing_changes", "offering"),
|
|
56
|
+
[
|
|
57
|
+
({"display_name": "{{ params.host }} {{ params.token }}"}, {}),
|
|
58
|
+
({"description": "{{ params.host }}", "summary": "{{ params.token }}"}, {}),
|
|
59
|
+
({}, {"description": "{{ params.host }} {{ params.token }}", "name": "{{ params.host }}"}),
|
|
60
|
+
# a document's metadata is not its content
|
|
61
|
+
({"documents": {"Doc": {"description": "{{ params.host }} {{ params.token }}"}}}, {}),
|
|
62
|
+
({"service_options": {"note": "{{ params.host }} {{ params.token }}"}}, {}),
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
def test_metadata_is_not_a_consumer(listing_changes, offering):
|
|
66
|
+
"""Only channels, interfaces and document content are rendered with
|
|
67
|
+
params; a reference anywhere else must not satisfy the rule (#2649)."""
|
|
68
|
+
errors = validate_required_parameter_usage(listing(**listing_changes), offering)
|
|
69
|
+
assert len(errors) == 1
|
|
70
|
+
assert "host, token are used by no channel" in errors[0]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_dynamic_use_in_metadata_does_not_disable_the_rule():
|
|
74
|
+
errors = validate_required_parameter_usage(listing(description="{{ params | tojson }}"), {})
|
|
75
|
+
assert "host, token" in errors[0]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_partial_use_is_rejected():
|
|
79
|
+
errors = validate_required_parameter_usage(listing(), channel(base_url="{{ params.host }}"))
|
|
80
|
+
assert "token are used by no channel" in errors[0]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_channel_interface_and_document_reads_count_without_proving_attribution():
|
|
84
|
+
data = listing(documents={"example": {"file_content": "{{ params.token }}"}})
|
|
85
|
+
assert validate_required_parameter_usage(data, channel(base_url="{{ params.host }}")) == []
|
|
86
|
+
assert (
|
|
87
|
+
validate_required_parameter_usage(
|
|
88
|
+
listing(
|
|
89
|
+
user_access_interfaces={"canonical": {"base_url": "{{ params.host }}"}},
|
|
90
|
+
documents={"Example": {"file_content": "{{ params.token }}"}},
|
|
91
|
+
),
|
|
92
|
+
{},
|
|
93
|
+
)
|
|
94
|
+
== []
|
|
95
|
+
)
|
|
96
|
+
# offering-level document content counts too
|
|
97
|
+
assert (
|
|
98
|
+
validate_required_parameter_usage(
|
|
99
|
+
listing(),
|
|
100
|
+
{**channel(base_url="{{ params.host }}"), "documents": {"Doc": {"file_content": "{{ params.token }}"}}},
|
|
101
|
+
)
|
|
102
|
+
== []
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_required_parameter_can_have_punctuation_in_its_name():
|
|
107
|
+
assert (
|
|
108
|
+
validate_required_parameter_usage(
|
|
109
|
+
listing(user_parameters_schema={"required": ["host-name"]}), channel(host="{{ params['host-name'] }}")
|
|
110
|
+
)
|
|
111
|
+
== []
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_deprecated_content_can_be_retired():
|
|
116
|
+
assert validate_required_parameter_usage(listing(status="deprecated"), {}) == []
|
|
117
|
+
assert validate_required_parameter_usage(listing(), {"status": "deprecated"}) == []
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_no_required_parameters():
|
|
121
|
+
assert validate_required_parameter_usage({}, {}) == []
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.parametrize("suffix", ["json", "toml"])
|
|
125
|
+
def test_file_validator_checks_paired_offering(tmp_path, suffix):
|
|
126
|
+
schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
|
|
127
|
+
validator = DataValidator(tmp_path, schema_dir)
|
|
128
|
+
data = listing()
|
|
129
|
+
(tmp_path / "listing.json").write_text(json.dumps(data))
|
|
130
|
+
path = tmp_path / f"offering.{suffix}"
|
|
131
|
+
path.write_text("{}" if suffix == "json" else "")
|
|
132
|
+
errors = validator.validate_required_parameter_usage(data, tmp_path / "listing.json")
|
|
133
|
+
assert "host, token" in errors[0]
|
|
134
|
+
path.write_text(
|
|
135
|
+
json.dumps(channel(host="{{ params.host }}", token="{{ params.token }}"))
|
|
136
|
+
if suffix == "json"
|
|
137
|
+
else '[upstream_access_config.plus]\nhost = "{{ params.host }}"\ntoken = "{{ params.token }}"\n'
|
|
138
|
+
)
|
|
139
|
+
assert validator.validate_required_parameter_usage(data, tmp_path / "listing.json") == []
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_file_validator_reads_template_documents_like_ingest(tmp_path):
|
|
143
|
+
"""The seller SDK uploads a .j2 document's source as file_content, so the
|
|
144
|
+
file validator must count a param used only there, as ingest does."""
|
|
145
|
+
schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
|
|
146
|
+
validator = DataValidator(tmp_path, schema_dir)
|
|
147
|
+
(tmp_path / "offering.json").write_text(json.dumps(channel(base_url="{{ params.host }}")))
|
|
148
|
+
(tmp_path / "connectivity.sh.j2").write_text('curl "$URL" -H "X-Token: {{ params.token }}"\n')
|
|
149
|
+
data = listing(documents={"Connectivity": {"file_path": "connectivity.sh.j2", "category": "connectivity_test"}})
|
|
150
|
+
(tmp_path / "listing.json").write_text(json.dumps(data))
|
|
151
|
+
assert validator.validate_required_parameter_usage(data, tmp_path / "listing.json") == []
|
|
152
|
+
# a non-template document is uploaded as-is, never rendered: not a consumer
|
|
153
|
+
(tmp_path / "usage.md").write_text("{{ params.token }}")
|
|
154
|
+
data = listing(documents={"Usage": {"file_path": "usage.md"}})
|
|
155
|
+
errors = validator.validate_required_parameter_usage(data, tmp_path / "listing.json")
|
|
156
|
+
assert "token are used by no channel" in errors[0]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_validation_pipeline_invokes_the_shared_rule(tmp_path, monkeypatch):
|
|
160
|
+
schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
|
|
161
|
+
validator = DataValidator(tmp_path, schema_dir)
|
|
162
|
+
path = tmp_path / "listing.json"
|
|
163
|
+
path.write_text(json.dumps({"name": "labs/test"}))
|
|
164
|
+
calls = []
|
|
165
|
+
|
|
166
|
+
def check(data, file_path):
|
|
167
|
+
calls.append(file_path)
|
|
168
|
+
return ["unused-param-sentinel"]
|
|
169
|
+
|
|
170
|
+
monkeypatch.setattr(validator, "validate_required_parameter_usage", check)
|
|
171
|
+
valid, errors = validator.validate_data_file(path, "listing_v1")
|
|
172
|
+
assert not valid
|
|
173
|
+
assert "unused-param-sentinel" in errors
|
|
174
|
+
assert calls == [path]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_invalid_jinja_does_not_bypass_the_rule():
|
|
178
|
+
errors = validate_required_parameter_usage(listing(), channel(base_url="{{ params.host"))
|
|
179
|
+
assert "invalid Jinja template" in errors[0]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_invalid_jinja_in_metadata_is_ignored():
|
|
183
|
+
"""Metadata is never rendered with params, so it is not parsed either."""
|
|
184
|
+
errors = validate_required_parameter_usage(listing(description="Go: {{ .Name }}"), {})
|
|
185
|
+
assert "host, token are used by no channel" in errors[0]
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
"""Required enrollment parameters must have consumers across service data."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from unitysvc_core.parameter_usage import referenced_param_names, validate_required_parameter_usage
|
|
9
|
-
from unitysvc_core.validator import DataValidator
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@pytest.mark.parametrize("expression", ["params.name", "params['name']", 'params.get("name")'])
|
|
13
|
-
def test_named_reads(expression):
|
|
14
|
-
assert referenced_param_names("{{ " + expression + " }}") == {"name"}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.mark.parametrize(
|
|
18
|
-
"expression",
|
|
19
|
-
[
|
|
20
|
-
"params",
|
|
21
|
-
"params | tojson",
|
|
22
|
-
"params.items()",
|
|
23
|
-
"params.keys()",
|
|
24
|
-
"params.values()",
|
|
25
|
-
"params.get(key)",
|
|
26
|
-
"params[key]",
|
|
27
|
-
],
|
|
28
|
-
)
|
|
29
|
-
def test_dynamic_and_whole_map_reads(expression):
|
|
30
|
-
assert referenced_param_names("{{ " + expression + " }}") is None
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def test_strings_comments_and_non_template_text_are_not_consumers():
|
|
34
|
-
assert referenced_param_names(["params.name", "{# params.name #}", '{{ "params.name" }}']) == set()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def listing(**changes):
|
|
38
|
-
return {"status": "ready", "user_parameters_schema": {"required": ["host", "token"]}, **changes}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_required_names_in_the_schema_do_not_count_as_use():
|
|
42
|
-
errors = validate_required_parameter_usage(
|
|
43
|
-
listing(), {"upstream_access_config": {"free": {"api_key": "${ customer_secrets.TOKEN }"}}}
|
|
44
|
-
)
|
|
45
|
-
assert len(errors) == 1
|
|
46
|
-
assert "host, token are used by no channel" in errors[0]
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_partial_use_is_rejected():
|
|
50
|
-
errors = validate_required_parameter_usage(listing(), {"host": "{{ params.host }}"})
|
|
51
|
-
assert "token are used by no channel" in errors[0]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def test_channel_interface_and_document_reads_count_without_proving_attribution():
|
|
55
|
-
data = listing(documents={"example": {"body": "{{ params.token }}"}})
|
|
56
|
-
assert validate_required_parameter_usage(data, {"host": "{{ params.host }}"}) == []
|
|
57
|
-
assert (
|
|
58
|
-
validate_required_parameter_usage(
|
|
59
|
-
listing(user_access_interfaces={"url": "{{ params.host }}"}, documents={"body": "{{ params.token }}"}), {}
|
|
60
|
-
)
|
|
61
|
-
== []
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_required_parameter_can_have_punctuation_in_its_name():
|
|
66
|
-
assert (
|
|
67
|
-
validate_required_parameter_usage(
|
|
68
|
-
listing(user_parameters_schema={"required": ["host-name"]}), {"host": "{{ params['host-name'] }}"}
|
|
69
|
-
)
|
|
70
|
-
== []
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def test_deprecated_content_can_be_retired():
|
|
75
|
-
assert validate_required_parameter_usage(listing(status="deprecated"), {}) == []
|
|
76
|
-
assert validate_required_parameter_usage(listing(), {"status": "deprecated"}) == []
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def test_no_required_parameters():
|
|
80
|
-
assert validate_required_parameter_usage({}, {}) == []
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@pytest.mark.parametrize("suffix", ["json", "toml"])
|
|
84
|
-
def test_file_validator_checks_paired_offering(tmp_path, suffix):
|
|
85
|
-
schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
|
|
86
|
-
validator = DataValidator(tmp_path, schema_dir)
|
|
87
|
-
data = listing()
|
|
88
|
-
(tmp_path / "listing.json").write_text(json.dumps(data))
|
|
89
|
-
path = tmp_path / f"offering.{suffix}"
|
|
90
|
-
path.write_text("{}" if suffix == "json" else "")
|
|
91
|
-
errors = validator.validate_required_parameter_usage(data, tmp_path / "listing.json")
|
|
92
|
-
assert "host, token" in errors[0]
|
|
93
|
-
path.write_text(
|
|
94
|
-
json.dumps({"host": "{{ params.host }}", "token": "{{ params.token }}"})
|
|
95
|
-
if suffix == "json"
|
|
96
|
-
else 'host = "{{ params.host }}"\ntoken = "{{ params.token }}"\n'
|
|
97
|
-
)
|
|
98
|
-
assert validator.validate_required_parameter_usage(data, tmp_path / "listing.json") == []
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def test_validation_pipeline_invokes_the_shared_rule(tmp_path, monkeypatch):
|
|
102
|
-
schema_dir = Path(__file__).parents[1] / "src/unitysvc_core/schema"
|
|
103
|
-
validator = DataValidator(tmp_path, schema_dir)
|
|
104
|
-
path = tmp_path / "listing.json"
|
|
105
|
-
path.write_text(json.dumps({"name": "labs/test"}))
|
|
106
|
-
calls = []
|
|
107
|
-
|
|
108
|
-
def check(data, file_path):
|
|
109
|
-
calls.append(file_path)
|
|
110
|
-
return ["unused-param-sentinel"]
|
|
111
|
-
|
|
112
|
-
monkeypatch.setattr(validator, "validate_required_parameter_usage", check)
|
|
113
|
-
valid, errors = validator.validate_data_file(path, "listing_v1")
|
|
114
|
-
assert not valid
|
|
115
|
-
assert "unused-param-sentinel" in errors
|
|
116
|
-
assert calls == [path]
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_invalid_jinja_does_not_bypass_the_rule():
|
|
120
|
-
errors = validate_required_parameter_usage(listing(), {"host": "{{ params.host"})
|
|
121
|
-
assert "invalid Jinja template" in errors[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/models/service_group_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core/schema/service_group_v1.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.34 → unitysvc_core-0.2.35}/src/unitysvc_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|