nthlayer-common 2.1.0__tar.gz → 2.1.2__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.
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/PKG-INFO +1 -1
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/pyproject.toml +1 -1
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/__init__.py +3 -3
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/scan.py +56 -7
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common.egg-info/PKG-INFO +1 -1
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_manifest_scan.py +145 -4
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/README.md +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/setup.cfg +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/api_client.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/clients/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/clients/base.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/clients/cortex.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/clients/mimir.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/clients/pagerduty.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/clients/slack.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/cloudevents.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/config.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/dependency_models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/domain_models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/errors.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/explanation.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/gate_models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/governance_bridge/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/governance_bridge/emitter.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/governance_bridge/models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/normalizer.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/ownership.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/ownership_providers/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/ownership_providers/backstage.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/ownership_providers/base.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/ownership_providers/kubernetes.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/ownership_providers/pagerduty.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/identity/resolver.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/llm.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/llm_structured.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/llm_stub.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/openslo/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/openslo/parser.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/_shared.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/loader.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/v1.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/v2.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/target_validation.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/v1_compat.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/metrics.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/outcomes.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/overrides/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/overrides/ingestion.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/overrides/models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/parsing.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/prompts.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/base.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/grafana.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/lock.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/mimir.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/pagerduty.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/prometheus.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/providers/registry.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/py.typed +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/errors.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/hashing.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/sqlite_store.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/store.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/verdict_bridge.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/verification.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/slack.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/slack_web.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/slo_models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/telemetry.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/tiers.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/verdicts/__init__.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/verdicts/core.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/verdicts/models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/verdicts/serialise.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/verdicts/sqlite_store.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/verdicts/store.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common.egg-info/SOURCES.txt +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common.egg-info/dependency_links.txt +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common.egg-info/requires.txt +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common.egg-info/top_level.txt +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_api_client.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_cloudevents.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_config.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_errors.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_explanation.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_governance_bridge.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_identity.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_llm.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_llm_structured.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_llm_stub.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_manifest_models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_manifest_parser.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_manifest_real_specs.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_manifest_service_type.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_manifest_v2_archetypes.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_metrics.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_outcomes.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_overrides.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_prompts.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_providers.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_slack.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_slack_web.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_target_validation.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_telemetry.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_tier_errors.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_tiers.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_v1_to_v2_migration.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_verdicts_core.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_verdicts_models.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_verdicts_serialise.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_verdicts_sqlite_concurrency.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_verdicts_store.py +0 -0
- {nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/tests/test_verdicts_v15_fields.py +0 -0
|
@@ -98,12 +98,12 @@ __all__ = [
|
|
|
98
98
|
"VALID_TIERS",
|
|
99
99
|
# Service-type helpers
|
|
100
100
|
"is_valid_service_type",
|
|
101
|
-
|
|
101
|
+
"resolve_service_type",
|
|
102
|
+
"valid_service_types_phrase",
|
|
103
|
+
# Directory scanning
|
|
102
104
|
"MANIFEST_SUFFIXES",
|
|
103
105
|
"foreign_yaml_reason",
|
|
104
106
|
"iter_manifest_files",
|
|
105
|
-
"resolve_service_type",
|
|
106
|
-
"valid_service_types_phrase",
|
|
107
107
|
# Enums
|
|
108
108
|
"DependencyCriticality",
|
|
109
109
|
"SourceFormat",
|
|
@@ -26,6 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
|
|
27
27
|
import yaml
|
|
28
28
|
|
|
29
|
+
from nthlayer_common.manifest.models import is_valid_service_type
|
|
29
30
|
from nthlayer_common.manifest.parser.v1 import is_srm_v1_format
|
|
30
31
|
from nthlayer_common.manifest.parser.v2 import is_opensrm_v2_format
|
|
31
32
|
|
|
@@ -38,8 +39,10 @@ MANIFEST_SUFFIXES = (".yaml", ".yml")
|
|
|
38
39
|
def iter_manifest_files(specs_dir: str | Path) -> list[Path]:
|
|
39
40
|
"""Every ``.yaml``/``.yml`` file directly under ``specs_dir``, sorted.
|
|
40
41
|
|
|
41
|
-
Sorted
|
|
42
|
-
|
|
42
|
+
Sorted so the order is stable across machines. That matters to any
|
|
43
|
+
caller that dedupes first-wins — learn/retrospective does; observe's
|
|
44
|
+
spec_loader does not, and whether it should is opensrm-3470's follow-up
|
|
45
|
+
rather than a property this function can assume.
|
|
43
46
|
|
|
44
47
|
Returns empty for a path that is not a directory rather than raising,
|
|
45
48
|
so a caller that wants its own error message for that case keeps the
|
|
@@ -48,7 +51,21 @@ def iter_manifest_files(specs_dir: str | Path) -> list[Path]:
|
|
|
48
51
|
path = Path(specs_dir)
|
|
49
52
|
if not path.is_dir():
|
|
50
53
|
return []
|
|
51
|
-
|
|
54
|
+
# `not p.is_dir()` rather than `p.is_file()`: excluding DIRECTORIES was
|
|
55
|
+
# the point — a directory named `foo.yaml` would be counted as a broken
|
|
56
|
+
# manifest by every caller. is_file() also excludes a dangling symlink,
|
|
57
|
+
# which silently drops a stale overlay link that previously reached
|
|
58
|
+
# load_manifest and was counted.
|
|
59
|
+
#
|
|
60
|
+
# Suffix compared case-folded: `.YAML` was excluded outright — never
|
|
61
|
+
# loaded, never counted, never logged — which is the silent-subset-by-
|
|
62
|
+
# extension failure MANIFEST_SUFFIXES exists to prevent, one case-fold
|
|
63
|
+
# away.
|
|
64
|
+
return sorted(
|
|
65
|
+
p
|
|
66
|
+
for p in path.iterdir()
|
|
67
|
+
if not p.is_dir() and p.suffix.lower() in MANIFEST_SUFFIXES
|
|
68
|
+
)
|
|
52
69
|
|
|
53
70
|
|
|
54
71
|
def foreign_yaml_reason(spec_file: str | Path) -> str | None:
|
|
@@ -77,9 +94,10 @@ def foreign_yaml_reason(spec_file: str | Path) -> str | None:
|
|
|
77
94
|
its body is indistinguishable in principle from any other headerless
|
|
78
95
|
YAML mapping — both are dicts with no apiVersion, no kind, no
|
|
79
96
|
``spec.service``. No content heuristic separates those, and adding more
|
|
80
|
-
markers would not change it.
|
|
81
|
-
record it
|
|
82
|
-
|
|
97
|
+
markers would not change it. For such a file this returns a reason, so
|
|
98
|
+
the caller can record it rather than drop it without trace — which is
|
|
99
|
+
what lets an investigator chasing a number that looks wrong pick the
|
|
100
|
+
trail back up.
|
|
83
101
|
"""
|
|
84
102
|
path = Path(spec_file)
|
|
85
103
|
try:
|
|
@@ -117,11 +135,42 @@ def foreign_yaml_reason(spec_file: str | Path) -> str | None:
|
|
|
117
135
|
if near_miss:
|
|
118
136
|
return None
|
|
119
137
|
|
|
138
|
+
# Body recovery: a bad merge or a mis-indent can strip the header while
|
|
139
|
+
# leaving the body intact, and that is the common corruption.
|
|
140
|
+
#
|
|
141
|
+
# The key names differ by format, and getting them wrong makes this whole
|
|
142
|
+
# branch inert — which it was until opensrm-3470's edge-cases pass:
|
|
143
|
+
# v1 spec.slos a MAPPING of name -> config (parser/v1 does
|
|
144
|
+
# slos_data.items()), not a list
|
|
145
|
+
# v2 spec.slo a list of OpenSLO documents
|
|
146
|
+
# spec.judgment_slo a list
|
|
147
|
+
# spec.service a MAPPING; OpenSLO writes the same key as a
|
|
148
|
+
# plain string, which is what keeps OpenSLO out
|
|
149
|
+
# spec.outcomes a mapping
|
|
150
|
+
# v1 spec.type REQUIRED by parse_srm_v1 (v1.py:91), and
|
|
151
|
+
# checked against is_valid_service_type rather
|
|
152
|
+
# than "is a string": a Backstage catalog-info
|
|
153
|
+
# carries spec.type too (`service`, `library`),
|
|
154
|
+
# and counting those makes every Backstage
|
|
155
|
+
# Component in the directory a broken manifest
|
|
156
|
+
# v2 spec.owner REQUIRED by parse_opensrm_v2 (v2.py:104)
|
|
157
|
+
# spec.slos is accepted as either shape: v1 writes a mapping, and a
|
|
158
|
+
# partially-converted file is exactly the kind of thing that lands here.
|
|
159
|
+
#
|
|
160
|
+
# The required keys matter most: a file carrying the one key its format
|
|
161
|
+
# cannot be parsed without makes a stronger claim to being a manifest
|
|
162
|
+
# than any of the optional ones above. Checking only the optional keys
|
|
163
|
+
# was the same mistake as testing spec.slos for the wrong type —
|
|
164
|
+
# reasoning about shapes without asking what the parsers demand.
|
|
120
165
|
spec = data.get("spec")
|
|
121
166
|
if isinstance(spec, dict) and (
|
|
122
167
|
isinstance(spec.get("service"), dict)
|
|
123
|
-
or isinstance(spec.get("slos"), list)
|
|
168
|
+
or isinstance(spec.get("slos"), (dict, list))
|
|
169
|
+
or isinstance(spec.get("slo"), list)
|
|
170
|
+
or isinstance(spec.get("judgment_slo"), list)
|
|
124
171
|
or isinstance(spec.get("outcomes"), dict)
|
|
172
|
+
or is_valid_service_type(spec.get("type"))
|
|
173
|
+
or isinstance(spec.get("owner"), dict)
|
|
125
174
|
):
|
|
126
175
|
return None
|
|
127
176
|
|
|
@@ -16,9 +16,8 @@ have now happened in nthlayer-workers:
|
|
|
16
16
|
|
|
17
17
|
``foreign_yaml_reason`` is that question, promoted here from
|
|
18
18
|
``nthlayer_workers.learn.retrospective`` so the three call sites that need
|
|
19
|
-
it stop reaching into each other (opensrm-3470). ``iter_manifest_files``
|
|
20
|
-
|
|
21
|
-
reached by file extension rather than parse error.
|
|
19
|
+
it stop reaching into each other (opensrm-3470). ``iter_manifest_files`` comes with it — see MANIFEST_SUFFIXES for why both
|
|
20
|
+
suffixes, always.
|
|
22
21
|
|
|
23
22
|
These tests are ported from the workers suite deliberately: the promotion
|
|
24
23
|
must be behaviour-preserving, and porting the contract is how that is
|
|
@@ -54,7 +53,10 @@ _AIMING = {
|
|
|
54
53
|
# Header stripped by a bad merge or a mis-indent, body intact. This is the
|
|
55
54
|
# common corruption, and the case the gate's last extension exists for.
|
|
56
55
|
"headerless, spec.service mapping": "spec:\n service:\n name: s\n type: api\n",
|
|
57
|
-
|
|
56
|
+
# v1 writes spec.slos as a MAPPING of name -> config. Encoding it as a
|
|
57
|
+
# list here is what let the inert-guard bug hide: the fixture agreed
|
|
58
|
+
# with the broken predicate instead of with the parser.
|
|
59
|
+
"headerless, spec.slos mapping": "spec:\n slos:\n availability:\n target: 99.9\n",
|
|
58
60
|
"headerless, spec.outcomes": "spec:\n outcomes:\n decision_value: {}\n",
|
|
59
61
|
# A typo'd kind or a drifted API group is an ordinary way a real manifest
|
|
60
62
|
# breaks: aiming at us and missing still counts.
|
|
@@ -158,3 +160,142 @@ def test_iter_manifest_files_on_a_missing_directory_is_empty(tmp_path):
|
|
|
158
160
|
"""Callers check is_dir() for their own error messages; this returning
|
|
159
161
|
empty rather than raising keeps that their decision."""
|
|
160
162
|
assert iter_manifest_files(tmp_path / "nope") == []
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_iter_manifest_files_skips_directories(tmp_path):
|
|
166
|
+
"""A DIRECTORY named ``foo.yaml`` is not a manifest file — every caller
|
|
167
|
+
would otherwise count it as a manifest that failed to load."""
|
|
168
|
+
(tmp_path / "subdir.yaml").mkdir()
|
|
169
|
+
(tmp_path / "real.yaml").write_text("spec: {}\n")
|
|
170
|
+
|
|
171
|
+
assert [p.name for p in iter_manifest_files(tmp_path)] == ["real.yaml"]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# =============================================================================
|
|
175
|
+
# The body-recovery step must fire for REAL manifest shapes
|
|
176
|
+
# =============================================================================
|
|
177
|
+
#
|
|
178
|
+
# opensrm-oh27's fifth and final gate iteration added body recovery: a bad
|
|
179
|
+
# merge or a mis-indent can strip the header while leaving the body intact,
|
|
180
|
+
# and that is the common corruption. It was landed as "the last extension"
|
|
181
|
+
# on the grounds that the remaining leak was unclosable in principle.
|
|
182
|
+
#
|
|
183
|
+
# It was inert. `spec.slos` was tested with isinstance(..., list), but v1
|
|
184
|
+
# writes it as a MAPPING (parser/v1.py does slos_data.items()), and v2 does
|
|
185
|
+
# not use `spec.slos` at all — its keys are `spec.slo` and
|
|
186
|
+
# `spec.judgment_slo`. So the branch never fired for any real manifest, in
|
|
187
|
+
# either format, and a header-stripped manifest was dropped as foreign with
|
|
188
|
+
# parse_failures == 0: the exact silent subset this bead family exists to
|
|
189
|
+
# remove, inside the guard meant to prevent it.
|
|
190
|
+
#
|
|
191
|
+
# Found by opensrm-3470's edge-cases pass. Not caught earlier because the
|
|
192
|
+
# promotion was verified against the ORIGINAL, which had the same defect —
|
|
193
|
+
# faithful copying proves faithfulness, not correctness.
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@pytest.mark.parametrize(
|
|
197
|
+
("label", "body"),
|
|
198
|
+
[
|
|
199
|
+
# v1: spec.slos is a mapping of name -> config
|
|
200
|
+
("v1 slos mapping", "spec:\n type: api\n slos:\n availability:\n target: 99.9\n"),
|
|
201
|
+
# v2: a list of OpenSLO documents
|
|
202
|
+
("v2 slo list", "spec:\n slo:\n - apiVersion: openslo/v1\n kind: SLO\n"),
|
|
203
|
+
# v2: judgment SLOs
|
|
204
|
+
("v2 judgment_slo", "spec:\n judgment_slo:\n - metadata: {name: j}\n"),
|
|
205
|
+
# already covered, kept so a regression in either direction shows here
|
|
206
|
+
("v2 service mapping", "spec:\n service:\n name: s\n type: api\n"),
|
|
207
|
+
("v2 outcomes", "spec:\n outcomes:\n decision_value: {}\n"),
|
|
208
|
+
],
|
|
209
|
+
)
|
|
210
|
+
def test_headerless_manifest_bodies_are_recovered(tmp_path, label, body):
|
|
211
|
+
"""A header-stripped manifest must COUNT, not be dropped as foreign."""
|
|
212
|
+
path = tmp_path / "x.yaml"
|
|
213
|
+
path.write_text(body)
|
|
214
|
+
|
|
215
|
+
assert foreign_yaml_reason(path) is None, (
|
|
216
|
+
f"{label}: a headerless manifest body was classified as foreign YAML, "
|
|
217
|
+
f"so it would be dropped with parse_failures == 0"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_openslo_still_excluded_after_widening_the_body_check(tmp_path):
|
|
222
|
+
"""Widening body recovery must not start swallowing OpenSLO documents.
|
|
223
|
+
|
|
224
|
+
OpenSLO writes spec.service as a STRING and has spec.objectives; it has
|
|
225
|
+
no spec.slos/slo/judgment_slo. If widening broke this, every OpenSLO file
|
|
226
|
+
in a specs directory would start counting as a broken manifest.
|
|
227
|
+
"""
|
|
228
|
+
path = tmp_path / "x.yaml"
|
|
229
|
+
path.write_text("spec:\n service: my-svc\n objectives:\n - target: 0.99\n")
|
|
230
|
+
|
|
231
|
+
assert foreign_yaml_reason(path) is not None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.mark.parametrize("name", ["a.YAML", "b.Yml", "c.YmL"])
|
|
235
|
+
def test_iter_manifest_files_is_case_insensitive_on_suffix(tmp_path, name):
|
|
236
|
+
"""`.YAML` was excluded — never loaded, never counted, never logged.
|
|
237
|
+
|
|
238
|
+
The same silent-subset-by-extension failure MANIFEST_SUFFIXES exists to
|
|
239
|
+
prevent, one case-fold away."""
|
|
240
|
+
(tmp_path / name).write_text("spec: {}\n")
|
|
241
|
+
|
|
242
|
+
assert [p.name for p in iter_manifest_files(tmp_path)] == [name]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_a_dangling_symlink_is_still_listed(tmp_path):
|
|
246
|
+
"""is_file() follows symlinks, so a broken one vanished from the listing.
|
|
247
|
+
|
|
248
|
+
Before the is_file() guard it reached load_manifest and raised, so it was
|
|
249
|
+
counted. Silently dropping a stale overlay symlink is a regression the
|
|
250
|
+
guard introduced; excluding directories was the point, not excluding
|
|
251
|
+
everything that is not a readable file.
|
|
252
|
+
"""
|
|
253
|
+
(tmp_path / "broken.yaml").symlink_to(tmp_path / "does-not-exist.yaml")
|
|
254
|
+
|
|
255
|
+
assert [p.name for p in iter_manifest_files(tmp_path)] == ["broken.yaml"]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@pytest.mark.parametrize(
|
|
259
|
+
("label", "body"),
|
|
260
|
+
[
|
|
261
|
+
# parser/v1.py:91 — "spec.type is required"
|
|
262
|
+
("v1 required type", "spec:\n type: api\n dependencies:\n - name: x\n"),
|
|
263
|
+
# parser/v2.py:104 — "spec.owner is required in v2 manifests"
|
|
264
|
+
("v2 required owner", "spec:\n owner:\n group: group:default/t\n contracts: []\n"),
|
|
265
|
+
],
|
|
266
|
+
)
|
|
267
|
+
def test_body_recovery_covers_the_keys_the_parsers_require(tmp_path, label, body):
|
|
268
|
+
"""Body recovery checked OPTIONAL keys and omitted the required ones.
|
|
269
|
+
|
|
270
|
+
A file carrying the one key its format cannot be parsed without is about
|
|
271
|
+
as strong a claim to being a manifest as a body can make — stronger than
|
|
272
|
+
the optional keys already accepted. Same class as the inert-list bug: a
|
|
273
|
+
predicate reasoning about shapes without checking what the parsers
|
|
274
|
+
actually demand.
|
|
275
|
+
"""
|
|
276
|
+
path = tmp_path / "x.yaml"
|
|
277
|
+
path.write_text(body)
|
|
278
|
+
|
|
279
|
+
assert foreign_yaml_reason(path) is None, f"{label} was dropped as foreign"
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_backstage_component_is_not_a_manifest(tmp_path):
|
|
283
|
+
"""A Backstage catalog-info.yaml carries `spec.type` and `spec.owner` too.
|
|
284
|
+
|
|
285
|
+
Accepting any string `spec.type` as a manifest marker counts every
|
|
286
|
+
Backstage Component in a specs directory as a broken manifest — the
|
|
287
|
+
noisy direction, which is how a coverage caveat stops being read.
|
|
288
|
+
|
|
289
|
+
`spec.type` only counts when it is a value OpenSRM would accept:
|
|
290
|
+
`service` is not one, `api` is. Backstage's `spec.owner` is a string
|
|
291
|
+
where v2's is a mapping, so that marker already discriminates.
|
|
292
|
+
"""
|
|
293
|
+
path = tmp_path / "catalog-info.yaml"
|
|
294
|
+
path.write_text(
|
|
295
|
+
"apiVersion: backstage.io/v1alpha1\n"
|
|
296
|
+
"kind: Component\n"
|
|
297
|
+
"metadata: {name: svc}\n"
|
|
298
|
+
"spec: {type: service, lifecycle: production, owner: team-a}\n"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
assert foreign_yaml_reason(path) is not None
|
|
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
|
|
File without changes
|
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/governance_bridge/__init__.py
RENAMED
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/governance_bridge/emitter.py
RENAMED
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/governance_bridge/models.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/openslo/__init__.py
RENAMED
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/openslo/parser.py
RENAMED
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/__init__.py
RENAMED
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/_shared.py
RENAMED
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/parser/loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/manifest/target_validation.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
|
|
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
|
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/records/verdict_bridge.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common/verdicts/sqlite_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nthlayer_common-2.1.0 → nthlayer_common-2.1.2}/src/nthlayer_common.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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|