spring-runtime-mcp 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spring_runtime_mcp/__init__.py +3 -0
- spring_runtime_mcp/adapters/__init__.py +1 -0
- spring_runtime_mcp/adapters/beans.py +159 -0
- spring_runtime_mcp/adapters/conditions.py +333 -0
- spring_runtime_mcp/adapters/config.py +295 -0
- spring_runtime_mcp/adapters/drift.py +790 -0
- spring_runtime_mcp/adapters/health.py +164 -0
- spring_runtime_mcp/adapters/mapping_predicates.py +201 -0
- spring_runtime_mcp/adapters/mappings.py +294 -0
- spring_runtime_mcp/adapters/migrations.py +373 -0
- spring_runtime_mcp/adapters/openapi.py +683 -0
- spring_runtime_mcp/adapters/source_catalog.py +119 -0
- spring_runtime_mcp/adapters/startup.py +202 -0
- spring_runtime_mcp/analyzers/__init__.py +1 -0
- spring_runtime_mcp/analyzers/bean_graph.py +172 -0
- spring_runtime_mcp/analyzers/condition_failures.py +359 -0
- spring_runtime_mcp/cli.py +248 -0
- spring_runtime_mcp/contracts/__init__.py +1 -0
- spring_runtime_mcp/contracts/definitions.py +425 -0
- spring_runtime_mcp/diagnostics.py +254 -0
- spring_runtime_mcp/models/__init__.py +1 -0
- spring_runtime_mcp/models/bean_graph.py +77 -0
- spring_runtime_mcp/models/common.py +99 -0
- spring_runtime_mcp/models/conditions.py +56 -0
- spring_runtime_mcp/models/queries.py +140 -0
- spring_runtime_mcp/models/source_catalog.py +35 -0
- spring_runtime_mcp/models/timestamps.py +71 -0
- spring_runtime_mcp/policy/__init__.py +1 -0
- spring_runtime_mcp/policy/configuration.py +32 -0
- spring_runtime_mcp/policy/output_guard.py +117 -0
- spring_runtime_mcp/policy/sanitization.py +616 -0
- spring_runtime_mcp/py.typed +0 -0
- spring_runtime_mcp/replay.py +220 -0
- spring_runtime_mcp/replay_data/bean_graph/README.md +26 -0
- spring_runtime_mcp/replay_data/bean_graph/factory-method/conditions.json +43 -0
- spring_runtime_mcp/replay_data/bean_graph/factory-method/source-catalog.json +54 -0
- spring_runtime_mcp/replay_data/bean_graph/nested-conditions/beans.json +99 -0
- spring_runtime_mcp/replay_data/bean_graph/nested-conditions/conditions.json +25 -0
- spring_runtime_mcp/replay_data/bean_graph/parent-child/beans.json +92 -0
- spring_runtime_mcp/replay_data/bean_graph/shape-metadata.json +32 -0
- spring_runtime_mcp/replay_data/bean_graph/stripe-missing/conditions.json +43 -0
- spring_runtime_mcp/replay_data/bean_graph/stripe-missing/source-catalog.json +54 -0
- spring_runtime_mcp/replay_data/conformance/README.md +41 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/beans.json +92 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/conditions.json +52 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/configprops.json +20 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/env.json +16 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/flyway.json +40 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/health-v2.json +14 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/health-v3.json +38 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/liquibase.json +30 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/mappings.json +40 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/openapi.json +54 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/source-catalog.json +90 -0
- spring_runtime_mcp/replay_data/conformance/boot-2.7-servlet/startup.json +45 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/beans.json +92 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/conditions.json +52 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/configprops.json +20 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/env.json +16 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/flyway.json +40 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/health-v3.json +38 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/liquibase.json +30 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/mappings.json +40 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/openapi.json +54 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/source-catalog.json +90 -0
- spring_runtime_mcp/replay_data/conformance/boot-3.4-servlet/startup.json +45 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/beans.json +92 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/conditions.json +52 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/configprops.json +20 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/env.json +16 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/flyway.json +40 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/health-v3.json +38 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/liquibase.json +30 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/mappings.json +40 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/openapi.json +54 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/servlet-namespace-comparison.json +4 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/source-catalog.json +90 -0
- spring_runtime_mcp/replay_data/conformance/boot-4.1-webflux/startup.json +45 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_analyze_startup.json +62 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_check_migrations.json +63 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_detect_drift.json +64 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_diagnose_health.json +59 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_inspect_beans.json +56 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_inspect_config.json +62 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_search_openapi.json +80 -0
- spring_runtime_mcp/replay_data/conformance/contracts/springboot_verify_mappings.json +58 -0
- spring_runtime_mcp/replay_data/conformance/faults/cases.json +63 -0
- spring_runtime_mcp/replay_data/conformance/faults/health-hidden.expected.json +54 -0
- spring_runtime_mcp/replay_data/conformance/faults/health-hidden.json +3 -0
- spring_runtime_mcp/replay_data/conformance/large/beans-2500.json +22506 -0
- spring_runtime_mcp/replay_data/conformance/manifest.json +46 -0
- spring_runtime_mcp/schemas/tools-contract.json +1902 -0
- spring_runtime_mcp/server.py +322 -0
- spring_runtime_mcp/services.py +44 -0
- spring_runtime_mcp/snapshots.py +340 -0
- spring_runtime_mcp/tools/__init__.py +1 -0
- spring_runtime_mcp/tools/analyze_startup.py +79 -0
- spring_runtime_mcp/tools/check_migrations.py +99 -0
- spring_runtime_mcp/tools/detect_drift.py +323 -0
- spring_runtime_mcp/tools/diagnose_health.py +88 -0
- spring_runtime_mcp/tools/inspect_beans.py +479 -0
- spring_runtime_mcp/tools/inspect_config.py +102 -0
- spring_runtime_mcp/tools/registry.py +48 -0
- spring_runtime_mcp/tools/search_openapi.py +79 -0
- spring_runtime_mcp/tools/support.py +166 -0
- spring_runtime_mcp/tools/verify_mappings.py +62 -0
- spring_runtime_mcp/transport/__init__.py +1 -0
- spring_runtime_mcp/transport/actuator_http.py +459 -0
- spring_runtime_mcp/transport/bounded_json.py +144 -0
- spring_runtime_mcp/transport/replay_files.py +71 -0
- spring_runtime_mcp/transport/response_gate.py +186 -0
- spring_runtime_mcp/transport/responses.py +82 -0
- spring_runtime_mcp/transport/stdio.py +44 -0
- spring_runtime_mcp-0.2.0.dist-info/METADATA +320 -0
- spring_runtime_mcp-0.2.0.dist-info/RECORD +118 -0
- spring_runtime_mcp-0.2.0.dist-info/WHEEL +4 -0
- spring_runtime_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- spring_runtime_mcp-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Normalize env precedence and configuration property bindings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Iterator
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..models.common import AnalysisBudget
|
|
11
|
+
|
|
12
|
+
from ..contracts.definitions import glob_matches
|
|
13
|
+
from ..models.queries import ConfigQuery
|
|
14
|
+
from ..policy.sanitization import MARKER, SanitizerPipeline, sensitive_key
|
|
15
|
+
from ..policy.configuration import ConfigReadPolicy
|
|
16
|
+
|
|
17
|
+
_FILE = re.compile(r"class path resource \[([A-Za-z0-9_$./-]+)\](?: - (\d+):(\d+))?")
|
|
18
|
+
_MASK = re.compile(r"^\*+$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _kind(source_name: str) -> str:
|
|
22
|
+
lowered = source_name.casefold().replace("_", "")
|
|
23
|
+
if "applicationconfig" in lowered or "classpath" in lowered:
|
|
24
|
+
return "configurationFile"
|
|
25
|
+
if "systemenvironment" in lowered:
|
|
26
|
+
return "environmentVariable"
|
|
27
|
+
if "systemproperties" in lowered:
|
|
28
|
+
return "systemProperty"
|
|
29
|
+
if "commandline" in lowered:
|
|
30
|
+
return "commandLine"
|
|
31
|
+
return "other" if source_name else "unknown"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _origin(raw: Any, source_name: str) -> dict[str, Any]:
|
|
35
|
+
kind = _kind(source_name)
|
|
36
|
+
file = line = column = None
|
|
37
|
+
if isinstance(raw, str):
|
|
38
|
+
matched = _FILE.search(raw)
|
|
39
|
+
if matched:
|
|
40
|
+
file = matched.group(1).rsplit("/", 1)[-1][:256]
|
|
41
|
+
line = int(matched.group(2)) if matched.group(2) else None
|
|
42
|
+
column = int(matched.group(3)) if matched.group(3) else None
|
|
43
|
+
kind = "configurationFile"
|
|
44
|
+
return {
|
|
45
|
+
"kind": kind,
|
|
46
|
+
"file": file,
|
|
47
|
+
"line": line,
|
|
48
|
+
"column": column,
|
|
49
|
+
"environmentVariable": None,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
_NESTED_DEPTH = 4
|
|
54
|
+
_NESTED_LEAVES = 500
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _nested_leaves(
|
|
58
|
+
prefix: str,
|
|
59
|
+
node: dict[str, Any],
|
|
60
|
+
*,
|
|
61
|
+
gaps: set[str],
|
|
62
|
+
budget: list[int],
|
|
63
|
+
depth: int = 0,
|
|
64
|
+
) -> Iterator[tuple[str, Any]]:
|
|
65
|
+
"""Expand nested config objects to dotted leaves under fixed budgets."""
|
|
66
|
+
for leaf in sorted(node):
|
|
67
|
+
if not isinstance(leaf, str):
|
|
68
|
+
gaps.add("CONFIGPROPS_NESTED_UNSUPPORTED")
|
|
69
|
+
continue
|
|
70
|
+
key = f"{prefix}.{leaf}" if prefix else leaf
|
|
71
|
+
value = node[leaf]
|
|
72
|
+
if isinstance(value, dict):
|
|
73
|
+
if depth >= _NESTED_DEPTH:
|
|
74
|
+
gaps.add("CONFIGPROPS_NESTED_LIMIT")
|
|
75
|
+
continue
|
|
76
|
+
yield from _nested_leaves(
|
|
77
|
+
key, value, gaps=gaps, budget=budget, depth=depth + 1
|
|
78
|
+
)
|
|
79
|
+
continue
|
|
80
|
+
if isinstance(value, list):
|
|
81
|
+
gaps.add("CONFIGPROPS_NESTED_UNSUPPORTED")
|
|
82
|
+
continue
|
|
83
|
+
if budget[0] >= _NESTED_LEAVES:
|
|
84
|
+
gaps.add("CONFIGPROPS_NESTED_LIMIT")
|
|
85
|
+
continue
|
|
86
|
+
budget[0] += 1
|
|
87
|
+
yield key, value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _selected(query: ConfigQuery, key: str) -> bool:
|
|
91
|
+
if query.prefix and key != query.prefix and not key.startswith(query.prefix + "."):
|
|
92
|
+
return False
|
|
93
|
+
return not (
|
|
94
|
+
query.property_key_pattern and not glob_matches(query.property_key_pattern, key)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _project(
|
|
99
|
+
raw: Any, include: bool, key: str, pipeline: SanitizerPipeline
|
|
100
|
+
) -> tuple[Any, str]:
|
|
101
|
+
if not include:
|
|
102
|
+
return None, "notReported"
|
|
103
|
+
if sensitive_key(key) or (
|
|
104
|
+
isinstance(raw, str) and (_MASK.fullmatch(raw) or pipeline.high_risk(raw))
|
|
105
|
+
):
|
|
106
|
+
return MARKER, "redacted"
|
|
107
|
+
if isinstance(raw, str):
|
|
108
|
+
if any(char.isspace() for char in raw) or any(ord(char) < 32 for char in raw):
|
|
109
|
+
return MARKER, "redacted"
|
|
110
|
+
return raw[:512], "visible"
|
|
111
|
+
if isinstance(raw, bool) or type(raw) is int:
|
|
112
|
+
return raw, "visible"
|
|
113
|
+
if isinstance(raw, float) and raw == raw:
|
|
114
|
+
return raw, "visible"
|
|
115
|
+
return None, "notReported"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def normalize_config(
|
|
119
|
+
env: Any | None,
|
|
120
|
+
configprops: Any | None,
|
|
121
|
+
query: ConfigQuery,
|
|
122
|
+
pipeline: SanitizerPipeline,
|
|
123
|
+
*,
|
|
124
|
+
policy: ConfigReadPolicy | None = None,
|
|
125
|
+
) -> tuple[list[str] | None, list[dict[str, Any]], list[str]]:
|
|
126
|
+
gaps: set[str] = set()
|
|
127
|
+
analysis_budget = AnalysisBudget()
|
|
128
|
+
profiles: list[str] | None = None
|
|
129
|
+
rows: list[dict[str, Any]] = []
|
|
130
|
+
if isinstance(env, dict):
|
|
131
|
+
raw_profiles = env.get("activeProfiles")
|
|
132
|
+
if isinstance(raw_profiles, list):
|
|
133
|
+
profiles = [
|
|
134
|
+
item
|
|
135
|
+
for item in raw_profiles
|
|
136
|
+
if isinstance(item, str)
|
|
137
|
+
and item
|
|
138
|
+
and not any(char.isspace() for char in item)
|
|
139
|
+
][:20]
|
|
140
|
+
sources = env.get("propertySources")
|
|
141
|
+
if not isinstance(sources, list):
|
|
142
|
+
gaps.add("ENV_SOURCES_UNOBSERVED")
|
|
143
|
+
else:
|
|
144
|
+
seen: set[str] = set()
|
|
145
|
+
for source in sources:
|
|
146
|
+
if not isinstance(source, dict):
|
|
147
|
+
gaps.add("ENV_SOURCE_UNREADABLE")
|
|
148
|
+
continue
|
|
149
|
+
raw_name = source.get("name")
|
|
150
|
+
name = raw_name if isinstance(raw_name, str) else ""
|
|
151
|
+
properties = source.get("properties")
|
|
152
|
+
if not isinstance(properties, dict):
|
|
153
|
+
gaps.add("ENV_PROPERTIES_UNOBSERVED")
|
|
154
|
+
continue
|
|
155
|
+
for key in sorted(properties):
|
|
156
|
+
analysis_budget.consume()
|
|
157
|
+
if not isinstance(key, str) or not _selected(query, key):
|
|
158
|
+
continue
|
|
159
|
+
if policy is not None and not policy.allows(key):
|
|
160
|
+
gaps.add("CONFIG_SCOPE_LIMITED")
|
|
161
|
+
continue
|
|
162
|
+
if query.context_id is not None:
|
|
163
|
+
continue
|
|
164
|
+
spec = properties[key]
|
|
165
|
+
raw_value = spec.get("value") if isinstance(spec, dict) else spec
|
|
166
|
+
origin = spec.get("origin") if isinstance(spec, dict) else None
|
|
167
|
+
value, state = _project(
|
|
168
|
+
raw_value,
|
|
169
|
+
query.include_values
|
|
170
|
+
and (policy is None or policy.allow_values),
|
|
171
|
+
key,
|
|
172
|
+
pipeline,
|
|
173
|
+
)
|
|
174
|
+
rows.append(
|
|
175
|
+
{
|
|
176
|
+
"endpointId": "env",
|
|
177
|
+
"contextId": None,
|
|
178
|
+
"parentContextId": None,
|
|
179
|
+
"propertyKey": key[:256],
|
|
180
|
+
"prefix": key.rsplit(".", 1)[0] if "." in key else None,
|
|
181
|
+
"configurationBean": None,
|
|
182
|
+
"effectiveValue": value,
|
|
183
|
+
"valueState": state,
|
|
184
|
+
"bindingValue": None,
|
|
185
|
+
"bindingState": "notReported",
|
|
186
|
+
"origin": _origin(origin, name),
|
|
187
|
+
"sourceKind": _origin(origin, name)["kind"],
|
|
188
|
+
"effective": "no" if key in seen else "yes",
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
seen.add(key)
|
|
192
|
+
elif env is not None:
|
|
193
|
+
gaps.add("ENV_SOURCES_UNOBSERVED")
|
|
194
|
+
if isinstance(configprops, dict):
|
|
195
|
+
contexts = configprops.get("contexts")
|
|
196
|
+
if not isinstance(contexts, dict):
|
|
197
|
+
gaps.add("CONFIGPROPS_UNOBSERVED")
|
|
198
|
+
else:
|
|
199
|
+
for context_name in sorted(contexts):
|
|
200
|
+
if query.context_id and context_name != query.context_id:
|
|
201
|
+
continue
|
|
202
|
+
context = contexts[context_name]
|
|
203
|
+
beans = context.get("beans") if isinstance(context, dict) else None
|
|
204
|
+
if not isinstance(beans, dict):
|
|
205
|
+
gaps.add("CONFIGPROPS_UNOBSERVED")
|
|
206
|
+
continue
|
|
207
|
+
parent = (
|
|
208
|
+
context.get("parentId")
|
|
209
|
+
if isinstance(context.get("parentId"), str)
|
|
210
|
+
else None
|
|
211
|
+
)
|
|
212
|
+
for bean_name in sorted(beans):
|
|
213
|
+
bean = beans[bean_name]
|
|
214
|
+
if not isinstance(bean, dict):
|
|
215
|
+
continue
|
|
216
|
+
prefix = (
|
|
217
|
+
bean.get("prefix")
|
|
218
|
+
if isinstance(bean.get("prefix"), str)
|
|
219
|
+
else None
|
|
220
|
+
)
|
|
221
|
+
properties = bean.get("properties")
|
|
222
|
+
if not isinstance(properties, dict):
|
|
223
|
+
gaps.add("CONFIGPROPS_PROPERTIES_UNOBSERVED")
|
|
224
|
+
continue
|
|
225
|
+
budget = [0]
|
|
226
|
+
for key, raw_value in _nested_leaves(
|
|
227
|
+
prefix or "", properties, gaps=gaps, budget=budget
|
|
228
|
+
):
|
|
229
|
+
analysis_budget.consume()
|
|
230
|
+
if not _selected(query, key):
|
|
231
|
+
continue
|
|
232
|
+
if policy is not None and not policy.allows(key):
|
|
233
|
+
gaps.add("CONFIG_SCOPE_LIMITED")
|
|
234
|
+
continue
|
|
235
|
+
value, state = _project(
|
|
236
|
+
raw_value,
|
|
237
|
+
query.include_values
|
|
238
|
+
and (policy is None or policy.allow_values),
|
|
239
|
+
key,
|
|
240
|
+
pipeline,
|
|
241
|
+
)
|
|
242
|
+
target = next(
|
|
243
|
+
(
|
|
244
|
+
row
|
|
245
|
+
for row in rows
|
|
246
|
+
if row["propertyKey"] == key
|
|
247
|
+
and row["effective"] == "yes"
|
|
248
|
+
),
|
|
249
|
+
None,
|
|
250
|
+
)
|
|
251
|
+
if target is None:
|
|
252
|
+
rows.append(
|
|
253
|
+
{
|
|
254
|
+
"endpointId": "configprops",
|
|
255
|
+
"contextId": context_name
|
|
256
|
+
if isinstance(context_name, str)
|
|
257
|
+
else None,
|
|
258
|
+
"parentContextId": parent,
|
|
259
|
+
"propertyKey": key[:256],
|
|
260
|
+
"prefix": prefix,
|
|
261
|
+
"configurationBean": str(bean_name)[:256],
|
|
262
|
+
"effectiveValue": None,
|
|
263
|
+
"valueState": "notReported",
|
|
264
|
+
"bindingValue": value,
|
|
265
|
+
"bindingState": state,
|
|
266
|
+
"origin": {
|
|
267
|
+
"kind": "unknown",
|
|
268
|
+
"file": None,
|
|
269
|
+
"line": None,
|
|
270
|
+
"column": None,
|
|
271
|
+
"environmentVariable": None,
|
|
272
|
+
},
|
|
273
|
+
"sourceKind": "unknown",
|
|
274
|
+
"effective": "unknown",
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
if target["configurationBean"] is not None:
|
|
279
|
+
target = deepcopy(target)
|
|
280
|
+
rows.append(target)
|
|
281
|
+
target["configurationBean"] = str(bean_name)[:256]
|
|
282
|
+
target["prefix"] = prefix or target["prefix"]
|
|
283
|
+
target["bindingValue"] = value
|
|
284
|
+
target["bindingState"] = state
|
|
285
|
+
target["contextId"] = (
|
|
286
|
+
context_name if isinstance(context_name, str) else None
|
|
287
|
+
)
|
|
288
|
+
target["parentContextId"] = parent
|
|
289
|
+
elif configprops is not None:
|
|
290
|
+
gaps.add("CONFIGPROPS_UNOBSERVED")
|
|
291
|
+
items = []
|
|
292
|
+
for index, row in enumerate(rows, start=1):
|
|
293
|
+
row["id"] = f"config_{index:06d}"
|
|
294
|
+
items.append(row)
|
|
295
|
+
return profiles if policy is None or policy.profiles else None, items, sorted(gaps)
|