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,79 @@
|
|
|
1
|
+
"""Read-only local OpenAPI search. Remote $ref values are not resolved."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from ..adapters.openapi import normalize_openapi
|
|
10
|
+
from ..contracts.definitions import validate_arguments
|
|
11
|
+
from ..models.common import NormalizationError
|
|
12
|
+
from ..models.queries import OpenApiQuery
|
|
13
|
+
from ..transport.response_gate import EndpointReadError
|
|
14
|
+
from ..services import Services
|
|
15
|
+
from ..diagnostics import failed, partial
|
|
16
|
+
from .support import tool_handler, finalize, read_checked, resume, shell, stamp
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@tool_handler
|
|
20
|
+
async def search_openapi(
|
|
21
|
+
arguments: Mapping[str, Any], services: Services
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
request_target = "local"
|
|
24
|
+
try:
|
|
25
|
+
query = validate_arguments(arguments, "springboot_search_openapi")
|
|
26
|
+
request_target = query.target_id
|
|
27
|
+
assert isinstance(query, OpenApiQuery)
|
|
28
|
+
if query.cursor:
|
|
29
|
+
return resume(services, query)
|
|
30
|
+
snapshot = uuid4().hex
|
|
31
|
+
observation = await read_checked(services, query.target_id, "openapi", snapshot)
|
|
32
|
+
version, candidate, items, gaps, blocked = normalize_openapi(
|
|
33
|
+
observation.body, query
|
|
34
|
+
)
|
|
35
|
+
result = shell(query.target_id, "registered target openapi", "openapi-1")
|
|
36
|
+
stamp(result, observation, "openapi", snapshot)
|
|
37
|
+
for item in items:
|
|
38
|
+
item["endpointId"] = "openapi"
|
|
39
|
+
item["observationId"] = observation.observation_id
|
|
40
|
+
result["data"] = {
|
|
41
|
+
"openapiVersion": version,
|
|
42
|
+
"candidateId": candidate,
|
|
43
|
+
"items": items,
|
|
44
|
+
}
|
|
45
|
+
enumeration_gaps = {
|
|
46
|
+
"OPENAPI_PATH_UNREADABLE",
|
|
47
|
+
"OPENAPI_PATH_FIELD_CONFLICT",
|
|
48
|
+
"OPENAPI_OPERATION_UNREADABLE",
|
|
49
|
+
}
|
|
50
|
+
result["totalMatched"] = (
|
|
51
|
+
None if enumeration_gaps.intersection(gaps) else len(items)
|
|
52
|
+
)
|
|
53
|
+
partial(result, gaps)
|
|
54
|
+
limit_codes = ("REF_DEPTH_LIMIT", "REF_CYCLE", "REF_EXPANSION_LIMIT")
|
|
55
|
+
if any(code in gaps for code in limit_codes):
|
|
56
|
+
reason = next(code for code in limit_codes if code in gaps)
|
|
57
|
+
result["truncated"] = True
|
|
58
|
+
result["truncationReasons"] = [code for code in limit_codes if code in gaps]
|
|
59
|
+
result["nextAction"] = {
|
|
60
|
+
"kind": "refine",
|
|
61
|
+
"reasonCode": reason,
|
|
62
|
+
"filterSuggestions": (
|
|
63
|
+
[{"parameter": "schemaName", "value": blocked}] if blocked else []
|
|
64
|
+
),
|
|
65
|
+
}
|
|
66
|
+
return finalize(result, services, query)
|
|
67
|
+
except NormalizationError as exc:
|
|
68
|
+
return failed(
|
|
69
|
+
exc.code,
|
|
70
|
+
request_target,
|
|
71
|
+
endpoint=exc.endpoint_id if isinstance(exc, EndpointReadError) else None,
|
|
72
|
+
http_status=exc.http_status if isinstance(exc, EndpointReadError) else None,
|
|
73
|
+
)
|
|
74
|
+
except TimeoutError:
|
|
75
|
+
return failed("TARGET_TIMEOUT", request_target)
|
|
76
|
+
except (ValueError, TypeError, KeyError, RecursionError, UnicodeError):
|
|
77
|
+
return failed("MALFORMED_EVIDENCE", request_target)
|
|
78
|
+
except Exception:
|
|
79
|
+
return failed("INTERNAL_ERROR", request_target)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Shared envelope, observation check, and byte-budget paging for diagnostic tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from ..models.common import CALL_BUDGET, CallBudget, NormalizationError
|
|
13
|
+
from ..snapshots import SnapshotStore, page_envelope
|
|
14
|
+
from ..transport.responses import serialize_response
|
|
15
|
+
from ..services import Services
|
|
16
|
+
from ..diagnostics import _check, failed, partial
|
|
17
|
+
|
|
18
|
+
TOOL_TIMEOUT_SECONDS = 15.0
|
|
19
|
+
ToolHandler = Callable[[Mapping[str, Any], Services], Awaitable[dict[str, Any]]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def tool_handler(handler: ToolHandler) -> ToolHandler:
|
|
23
|
+
"""Bound reads, queued work and cooperative analysis under a single scope."""
|
|
24
|
+
|
|
25
|
+
@wraps(handler)
|
|
26
|
+
async def bounded(
|
|
27
|
+
arguments: Mapping[str, Any], services: Services
|
|
28
|
+
) -> dict[str, Any]:
|
|
29
|
+
parent = CALL_BUDGET.get()
|
|
30
|
+
scope = parent or CallBudget(time.monotonic() + TOOL_TIMEOUT_SECONDS)
|
|
31
|
+
token = CALL_BUDGET.set(scope)
|
|
32
|
+
target = (
|
|
33
|
+
arguments.get("targetId", "local")
|
|
34
|
+
if isinstance(arguments, Mapping)
|
|
35
|
+
else "local"
|
|
36
|
+
)
|
|
37
|
+
try:
|
|
38
|
+
async with asyncio.timeout(scope.check()):
|
|
39
|
+
result = await handler(arguments, services)
|
|
40
|
+
scope.check()
|
|
41
|
+
return result
|
|
42
|
+
except TimeoutError:
|
|
43
|
+
return failed("TARGET_TIMEOUT", target)
|
|
44
|
+
except NormalizationError as exc:
|
|
45
|
+
return failed(exc.code, target)
|
|
46
|
+
finally:
|
|
47
|
+
CALL_BUDGET.reset(token)
|
|
48
|
+
|
|
49
|
+
return bounded
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def shell(target: str, scope: str, adapter: str) -> dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"schemaVersion": "2",
|
|
55
|
+
"status": "ok",
|
|
56
|
+
"source": {
|
|
57
|
+
"targetId": target,
|
|
58
|
+
"snapshotId": None,
|
|
59
|
+
"observedAt": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
60
|
+
"endpointIds": [],
|
|
61
|
+
"bootVersion": None,
|
|
62
|
+
"adapterVersion": adapter,
|
|
63
|
+
"policyVersion": adapter,
|
|
64
|
+
},
|
|
65
|
+
"data": {"items": []},
|
|
66
|
+
"totalMatched": 0,
|
|
67
|
+
"truncated": False,
|
|
68
|
+
"truncationReasons": [],
|
|
69
|
+
"coverage": {"state": "complete", "scope": scope, "gaps": []},
|
|
70
|
+
"warnings": [],
|
|
71
|
+
"redactions": [],
|
|
72
|
+
"nextCursor": None,
|
|
73
|
+
"nextAction": {
|
|
74
|
+
"kind": "done",
|
|
75
|
+
"reasonCode": "COMPLETE",
|
|
76
|
+
"filterSuggestions": [],
|
|
77
|
+
},
|
|
78
|
+
"errors": [],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def stamp(
|
|
83
|
+
result: dict[str, Any], observation: Any, endpoint: str, snapshot: str
|
|
84
|
+
) -> None:
|
|
85
|
+
result["source"].update(
|
|
86
|
+
snapshotId=snapshot,
|
|
87
|
+
endpointIds=sorted({*result["source"]["endpointIds"], endpoint}),
|
|
88
|
+
observedAt=datetime.fromisoformat(
|
|
89
|
+
observation.observed_at.replace("Z", "+00:00")
|
|
90
|
+
)
|
|
91
|
+
.astimezone(UTC)
|
|
92
|
+
.isoformat()
|
|
93
|
+
.replace("+00:00", "Z"),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def read_checked(
|
|
98
|
+
services: Services, target: str, endpoint: str, snapshot: str
|
|
99
|
+
) -> Any:
|
|
100
|
+
if target not in services.targets or services.catalog.target_id != target:
|
|
101
|
+
raise NormalizationError("TARGET_NOT_REGISTERED")
|
|
102
|
+
if endpoint in {"env", "configprops"} and services.config_policy is None:
|
|
103
|
+
from ..transport.response_gate import EndpointReadError
|
|
104
|
+
|
|
105
|
+
raise EndpointReadError("CAPABILITY_DISABLED", endpoint)
|
|
106
|
+
observation = await services.read(target, endpoint, snapshot_id=snapshot)
|
|
107
|
+
_check(observation, target, snapshot, endpoint)
|
|
108
|
+
return observation
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def finalize(
|
|
112
|
+
result: dict[str, Any], services: Services, query: Any, *, more: bool = False
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Slice items to the query limit and the serialized response budget."""
|
|
115
|
+
result = services.sanitizer.sanitize(result)
|
|
116
|
+
if not isinstance(result.get("data"), dict):
|
|
117
|
+
return result
|
|
118
|
+
items = list(result["data"].get("items") or [])
|
|
119
|
+
count = min(len(items), query.max_results)
|
|
120
|
+
while True:
|
|
121
|
+
page = page_envelope(result, items[:count])
|
|
122
|
+
remaining = more or count < len(items)
|
|
123
|
+
if remaining:
|
|
124
|
+
page["truncated"] = True
|
|
125
|
+
page["truncationReasons"] = sorted(
|
|
126
|
+
set(page["truncationReasons"] + ["MAX_RESULTS"])
|
|
127
|
+
)[:8]
|
|
128
|
+
partial(page, ["MAX_RESULTS"])
|
|
129
|
+
page["nextAction"] = {
|
|
130
|
+
"kind": "page",
|
|
131
|
+
"reasonCode": "MORE_RESULTS",
|
|
132
|
+
"filterSuggestions": [],
|
|
133
|
+
}
|
|
134
|
+
page["nextCursor"] = "x" * 64
|
|
135
|
+
try:
|
|
136
|
+
serialize_response(page, "\0" * 128, sanitizer=services.sanitizer)
|
|
137
|
+
break
|
|
138
|
+
except NormalizationError as exc:
|
|
139
|
+
if exc.code != "RESPONSE_TOO_LARGE":
|
|
140
|
+
raise
|
|
141
|
+
if count <= 1:
|
|
142
|
+
return failed("RESPONSE_TOO_LARGE", query.target_id)
|
|
143
|
+
count //= 2
|
|
144
|
+
if remaining:
|
|
145
|
+
page["nextCursor"] = (
|
|
146
|
+
services.snapshots.advance(
|
|
147
|
+
query.cursor, query, count, sanitizer=services.sanitizer
|
|
148
|
+
)
|
|
149
|
+
if query.cursor
|
|
150
|
+
else services.snapshots.put_paged(
|
|
151
|
+
result, query, offset=count, sanitizer=services.sanitizer
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
serialize_response(page, "\0" * 128, sanitizer=services.sanitizer)
|
|
155
|
+
return page
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def resume(services: Services, query: Any) -> dict[str, Any]:
|
|
159
|
+
stored, more = services.snapshots.get_page(
|
|
160
|
+
query.cursor, query, sanitizer=services.sanitizer
|
|
161
|
+
)
|
|
162
|
+
return finalize(stored, services, query, more=more)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Re-export for handlers that only need the store type in annotations.
|
|
166
|
+
__all__ = ["SnapshotStore", "finalize", "read_checked", "resume", "shell", "stamp"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Read-only MVC and WebFlux mapping handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from ..adapters.mappings import normalize_mappings
|
|
10
|
+
from ..contracts.definitions import validate_arguments
|
|
11
|
+
from ..models.common import NormalizationError
|
|
12
|
+
from ..models.queries import MappingQuery
|
|
13
|
+
from ..transport.response_gate import EndpointReadError
|
|
14
|
+
from ..services import Services
|
|
15
|
+
from ..diagnostics import failed, partial
|
|
16
|
+
from .support import tool_handler, finalize, read_checked, resume, shell, stamp
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@tool_handler
|
|
20
|
+
async def verify_mappings(
|
|
21
|
+
arguments: Mapping[str, Any], services: Services
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
request_target = "local"
|
|
24
|
+
try:
|
|
25
|
+
query = validate_arguments(arguments, "springboot_verify_mappings")
|
|
26
|
+
request_target = query.target_id
|
|
27
|
+
assert isinstance(query, MappingQuery)
|
|
28
|
+
if query.cursor:
|
|
29
|
+
return resume(services, query)
|
|
30
|
+
snapshot = uuid4().hex
|
|
31
|
+
observation = await read_checked(
|
|
32
|
+
services, query.target_id, "mappings", snapshot
|
|
33
|
+
)
|
|
34
|
+
items, gaps = normalize_mappings(
|
|
35
|
+
observation.body,
|
|
36
|
+
path_pattern=query.path_pattern,
|
|
37
|
+
http_method=query.http_method,
|
|
38
|
+
controller_pattern=query.controller_class_pattern,
|
|
39
|
+
context_id=query.context_id,
|
|
40
|
+
)
|
|
41
|
+
result = shell(query.target_id, "registered target mappings", "mappings-1")
|
|
42
|
+
stamp(result, observation, "mappings", snapshot)
|
|
43
|
+
for item in items:
|
|
44
|
+
item["endpointId"] = "mappings"
|
|
45
|
+
item["observationId"] = observation.observation_id
|
|
46
|
+
result["data"] = {"items": items}
|
|
47
|
+
result["totalMatched"] = None if gaps else len(items)
|
|
48
|
+
partial(result, gaps)
|
|
49
|
+
return finalize(result, services, query)
|
|
50
|
+
except NormalizationError as exc:
|
|
51
|
+
return failed(
|
|
52
|
+
exc.code,
|
|
53
|
+
request_target,
|
|
54
|
+
endpoint=exc.endpoint_id if isinstance(exc, EndpointReadError) else None,
|
|
55
|
+
http_status=exc.http_status if isinstance(exc, EndpointReadError) else None,
|
|
56
|
+
)
|
|
57
|
+
except TimeoutError:
|
|
58
|
+
return failed("TARGET_TIMEOUT", request_target)
|
|
59
|
+
except (ValueError, TypeError, KeyError, RecursionError, UnicodeError):
|
|
60
|
+
return failed("MALFORMED_EVIDENCE", request_target)
|
|
61
|
+
except Exception:
|
|
62
|
+
return failed("INTERNAL_ERROR", request_target)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Explicit reader interfaces; no live network connector is enabled."""
|