mcp-proxy-adapter 3.1.6__py3-none-any.whl → 4.1.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.
Files changed (118) hide show
  1. mcp_proxy_adapter/api/app.py +65 -27
  2. mcp_proxy_adapter/api/handlers.py +1 -1
  3. mcp_proxy_adapter/api/middleware/error_handling.py +11 -10
  4. mcp_proxy_adapter/api/tool_integration.py +5 -2
  5. mcp_proxy_adapter/api/tools.py +3 -3
  6. mcp_proxy_adapter/commands/base.py +19 -1
  7. mcp_proxy_adapter/commands/command_registry.py +254 -8
  8. mcp_proxy_adapter/commands/hooks.py +260 -0
  9. mcp_proxy_adapter/commands/reload_command.py +211 -0
  10. mcp_proxy_adapter/commands/reload_settings_command.py +125 -0
  11. mcp_proxy_adapter/commands/settings_command.py +189 -0
  12. mcp_proxy_adapter/config.py +16 -1
  13. mcp_proxy_adapter/core/__init__.py +44 -0
  14. mcp_proxy_adapter/core/logging.py +87 -34
  15. mcp_proxy_adapter/core/settings.py +376 -0
  16. mcp_proxy_adapter/core/utils.py +2 -2
  17. mcp_proxy_adapter/custom_openapi.py +81 -2
  18. mcp_proxy_adapter/examples/README.md +124 -0
  19. mcp_proxy_adapter/examples/__init__.py +7 -0
  20. mcp_proxy_adapter/examples/basic_server/README.md +60 -0
  21. mcp_proxy_adapter/examples/basic_server/__init__.py +7 -0
  22. mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +39 -0
  23. mcp_proxy_adapter/examples/basic_server/config.json +35 -0
  24. mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +238 -0
  25. mcp_proxy_adapter/examples/basic_server/server.py +98 -0
  26. mcp_proxy_adapter/examples/custom_commands/README.md +127 -0
  27. mcp_proxy_adapter/examples/custom_commands/__init__.py +27 -0
  28. mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +250 -0
  29. mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +6 -0
  30. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +103 -0
  31. mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +111 -0
  32. mcp_proxy_adapter/examples/custom_commands/config.json +62 -0
  33. mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +169 -0
  34. mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +215 -0
  35. mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +76 -0
  36. mcp_proxy_adapter/examples/custom_commands/custom_settings.json +96 -0
  37. mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +241 -0
  38. mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +135 -0
  39. mcp_proxy_adapter/examples/custom_commands/echo_command.py +122 -0
  40. mcp_proxy_adapter/examples/custom_commands/hooks.py +230 -0
  41. mcp_proxy_adapter/examples/custom_commands/intercept_command.py +123 -0
  42. mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +103 -0
  43. mcp_proxy_adapter/examples/custom_commands/server.py +223 -0
  44. mcp_proxy_adapter/examples/custom_commands/test_hooks.py +176 -0
  45. mcp_proxy_adapter/examples/deployment/README.md +49 -0
  46. mcp_proxy_adapter/examples/deployment/__init__.py +7 -0
  47. mcp_proxy_adapter/examples/deployment/config.development.json +8 -0
  48. {examples/basic_example → mcp_proxy_adapter/examples/deployment}/config.json +11 -7
  49. mcp_proxy_adapter/examples/deployment/config.production.json +12 -0
  50. mcp_proxy_adapter/examples/deployment/config.staging.json +11 -0
  51. mcp_proxy_adapter/examples/deployment/docker-compose.yml +31 -0
  52. mcp_proxy_adapter/examples/deployment/run.sh +43 -0
  53. mcp_proxy_adapter/examples/deployment/run_docker.sh +84 -0
  54. mcp_proxy_adapter/openapi.py +3 -2
  55. mcp_proxy_adapter/tests/api/test_custom_openapi.py +617 -0
  56. mcp_proxy_adapter/tests/api/test_handlers.py +522 -0
  57. mcp_proxy_adapter/tests/api/test_schemas.py +546 -0
  58. mcp_proxy_adapter/tests/api/test_tool_integration.py +531 -0
  59. mcp_proxy_adapter/tests/unit/test_base_command.py +391 -85
  60. mcp_proxy_adapter/version.py +1 -1
  61. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/METADATA +3 -3
  62. mcp_proxy_adapter-4.1.0.dist-info/RECORD +110 -0
  63. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/WHEEL +1 -1
  64. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/top_level.txt +0 -1
  65. examples/__init__.py +0 -19
  66. examples/anti_patterns/README.md +0 -51
  67. examples/anti_patterns/__init__.py +0 -9
  68. examples/anti_patterns/bad_design/README.md +0 -72
  69. examples/anti_patterns/bad_design/global_state.py +0 -170
  70. examples/anti_patterns/bad_design/monolithic_command.py +0 -272
  71. examples/basic_example/README.md +0 -245
  72. examples/basic_example/__init__.py +0 -8
  73. examples/basic_example/commands/__init__.py +0 -5
  74. examples/basic_example/commands/echo_command.py +0 -95
  75. examples/basic_example/commands/math_command.py +0 -151
  76. examples/basic_example/commands/time_command.py +0 -152
  77. examples/basic_example/docs/EN/README.md +0 -177
  78. examples/basic_example/docs/RU/README.md +0 -177
  79. examples/basic_example/server.py +0 -151
  80. examples/basic_example/tests/conftest.py +0 -243
  81. examples/check_vstl_schema.py +0 -106
  82. examples/commands/echo_command.py +0 -52
  83. examples/commands/echo_command_di.py +0 -152
  84. examples/commands/echo_result.py +0 -65
  85. examples/commands/get_date_command.py +0 -98
  86. examples/commands/new_uuid4_command.py +0 -91
  87. examples/complete_example/Dockerfile +0 -24
  88. examples/complete_example/README.md +0 -92
  89. examples/complete_example/__init__.py +0 -8
  90. examples/complete_example/commands/__init__.py +0 -5
  91. examples/complete_example/commands/system_command.py +0 -328
  92. examples/complete_example/config.json +0 -41
  93. examples/complete_example/configs/config.dev.yaml +0 -40
  94. examples/complete_example/configs/config.docker.yaml +0 -40
  95. examples/complete_example/docker-compose.yml +0 -35
  96. examples/complete_example/requirements.txt +0 -20
  97. examples/complete_example/server.py +0 -113
  98. examples/di_example/.pytest_cache/README.md +0 -8
  99. examples/di_example/server.py +0 -249
  100. examples/fix_vstl_help.py +0 -123
  101. examples/minimal_example/README.md +0 -65
  102. examples/minimal_example/__init__.py +0 -8
  103. examples/minimal_example/config.json +0 -14
  104. examples/minimal_example/main.py +0 -136
  105. examples/minimal_example/simple_server.py +0 -163
  106. examples/minimal_example/tests/conftest.py +0 -171
  107. examples/minimal_example/tests/test_hello_command.py +0 -111
  108. examples/minimal_example/tests/test_integration.py +0 -181
  109. examples/patch_vstl_service.py +0 -105
  110. examples/patch_vstl_service_mcp.py +0 -108
  111. examples/server.py +0 -69
  112. examples/simple_server.py +0 -128
  113. examples/test_package_3.1.4.py +0 -177
  114. examples/test_server.py +0 -134
  115. examples/tool_description_example.py +0 -82
  116. mcp_proxy_adapter/py.typed +0 -0
  117. mcp_proxy_adapter-3.1.6.dist-info/RECORD +0 -118
  118. {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -18,7 +18,7 @@ from mcp_proxy_adapter.config import config
18
18
  from mcp_proxy_adapter.core.errors import MicroserviceError, NotFoundError
19
19
  from mcp_proxy_adapter.core.logging import logger, RequestLogger
20
20
  from mcp_proxy_adapter.commands.command_registry import registry
21
- from mcp_proxy_adapter.custom_openapi import custom_openapi
21
+ from mcp_proxy_adapter.custom_openapi import custom_openapi_with_fallback
22
22
 
23
23
 
24
24
  @asynccontextmanager
@@ -29,12 +29,16 @@ async def lifespan(app: FastAPI):
29
29
  # Startup events
30
30
  from mcp_proxy_adapter.commands.command_registry import registry
31
31
  from mcp_proxy_adapter.commands.help_command import HelpCommand
32
+ from mcp_proxy_adapter.commands.health_command import HealthCommand
32
33
 
33
- # Register built-in commands (help should be registered first)
34
+ # Register built-in commands if they don't exist (user can override them)
34
35
  if not registry.command_exists("help"):
35
36
  registry.register(HelpCommand)
36
37
 
37
- # Register other commands
38
+ if not registry.command_exists("health"):
39
+ registry.register(HealthCommand)
40
+
41
+ # Discover and register additional commands automatically
38
42
  registry.discover_commands()
39
43
 
40
44
  logger.info(f"Application started with {len(registry.get_all_commands())} commands registered")
@@ -45,18 +49,28 @@ async def lifespan(app: FastAPI):
45
49
  logger.info("Application shutting down")
46
50
 
47
51
 
48
- def create_app() -> FastAPI:
52
+ def create_app(title: Optional[str] = None, description: Optional[str] = None, version: Optional[str] = None) -> FastAPI:
49
53
  """
50
54
  Creates and configures FastAPI application.
51
55
 
56
+ Args:
57
+ title: Application title (default: "MCP Proxy Adapter")
58
+ description: Application description (default: "JSON-RPC API for interacting with MCP Proxy")
59
+ version: Application version (default: "1.0.0")
60
+
52
61
  Returns:
53
62
  Configured FastAPI application.
54
63
  """
64
+ # Use provided parameters or defaults
65
+ app_title = title or "MCP Proxy Adapter"
66
+ app_description = description or "JSON-RPC API for interacting with MCP Proxy"
67
+ app_version = version or "1.0.0"
68
+
55
69
  # Create application
56
70
  app = FastAPI(
57
- title="MCP Proxy Adapter",
58
- description="JSON-RPC API for interacting with MCP Proxy",
59
- version="1.0.0",
71
+ title=app_title,
72
+ description=app_description,
73
+ version=app_version,
60
74
  docs_url="/docs",
61
75
  redoc_url="/redoc",
62
76
  lifespan=lifespan,
@@ -75,7 +89,7 @@ def create_app() -> FastAPI:
75
89
  setup_middleware(app)
76
90
 
77
91
  # Use custom OpenAPI schema
78
- app.openapi = lambda: custom_openapi(app)
92
+ app.openapi = lambda: custom_openapi_with_fallback(app)
79
93
 
80
94
  # Explicit endpoint for OpenAPI schema
81
95
  @app.get("/openapi.json")
@@ -83,8 +97,8 @@ def create_app() -> FastAPI:
83
97
  """
84
98
  Returns optimized OpenAPI schema compatible with MCP-Proxy.
85
99
  """
86
- return custom_openapi(app)
87
-
100
+ return custom_openapi_with_fallback(app)
101
+
88
102
  # JSON-RPC handler
89
103
  @app.post("/api/jsonrpc", response_model=Union[JsonRpcSuccessResponse, JsonRpcErrorResponse, List[Union[JsonRpcSuccessResponse, JsonRpcErrorResponse]]])
90
104
  async def jsonrpc_endpoint(request: Request, request_data: Union[Dict[str, Any], List[Dict[str, Any]]] = Body(...)):
@@ -119,7 +133,7 @@ def create_app() -> FastAPI:
119
133
  else:
120
134
  # Process single request
121
135
  return await handle_json_rpc(request_data, request_id)
122
-
136
+
123
137
  # Command execution endpoint (/cmd)
124
138
  @app.post("/cmd")
125
139
  async def cmd_endpoint(request: Request, command_data: Dict[str, Any] = Body(...)):
@@ -245,7 +259,7 @@ def create_app() -> FastAPI:
245
259
  }
246
260
  }
247
261
  )
248
-
262
+
249
263
  # Direct command call
250
264
  @app.post("/api/command/{command_name}")
251
265
  async def command_endpoint(request: Request, command_name: str, params: Dict[str, Any] = Body(default={})):
@@ -259,29 +273,50 @@ def create_app() -> FastAPI:
259
273
  result = await execute_command(command_name, params, request_id)
260
274
  return result
261
275
  except MicroserviceError as e:
276
+ # Convert to proper HTTP status code
277
+ status_code = 400 if e.code < 0 else e.code
262
278
  return JSONResponse(
263
- status_code=e.code,
279
+ status_code=status_code,
264
280
  content=e.to_dict()
265
281
  )
266
-
282
+
267
283
  # Server health check
268
284
  @app.get("/health", operation_id="health_check")
269
285
  async def health_endpoint():
270
286
  """
271
- Проверить работоспособность сервиса.
272
-
273
- Возвращает информацию о состоянии сервиса.
287
+ Health check endpoint.
288
+ Returns server status and basic information.
274
289
  """
275
- # Получаем базовую информацию о здоровье сервера
276
- health_data = await get_server_health()
277
-
278
- # Возвращаем информацию в формате, соответствующем схеме
279
- return JSONResponse(content={
290
+ return {
280
291
  "status": "ok",
281
292
  "model": "mcp-proxy-adapter",
282
- "version": config.get("version", "1.0.0")
283
- })
293
+ "version": "1.0.0"
294
+ }
284
295
 
296
+ # Graceful shutdown endpoint
297
+ @app.post("/shutdown")
298
+ async def shutdown_endpoint():
299
+ """
300
+ Graceful shutdown endpoint.
301
+ Triggers server shutdown after completing current requests.
302
+ """
303
+ import asyncio
304
+
305
+ # Schedule shutdown after a short delay to allow response
306
+ async def delayed_shutdown():
307
+ await asyncio.sleep(1)
308
+ # This will trigger the lifespan shutdown event
309
+ import os
310
+ os._exit(0)
311
+
312
+ # Start shutdown task
313
+ asyncio.create_task(delayed_shutdown())
314
+
315
+ return {
316
+ "status": "shutting_down",
317
+ "message": "Server shutdown initiated. New requests will be rejected."
318
+ }
319
+
285
320
  # List of available commands
286
321
  @app.get("/api/commands", response_model=CommandListResponse)
287
322
  async def commands_list_endpoint():
@@ -290,7 +325,7 @@ def create_app() -> FastAPI:
290
325
  """
291
326
  commands = await get_commands_list()
292
327
  return {"commands": commands}
293
-
328
+
294
329
  # Get command information by name
295
330
  @app.get("/api/commands/{command_name}")
296
331
  async def command_info_endpoint(request: Request, command_name: str):
@@ -317,7 +352,7 @@ def create_app() -> FastAPI:
317
352
  }
318
353
  }
319
354
  )
320
-
355
+
321
356
  # Get API tool description
322
357
  @app.get("/api/tools/{tool_name}")
323
358
  async def tool_description_endpoint(tool_name: str, format: Optional[str] = "json"):
@@ -368,7 +403,7 @@ def create_app() -> FastAPI:
368
403
  }
369
404
  }
370
405
  )
371
-
406
+
372
407
  # Execute API tool
373
408
  @app.post("/api/tools/{tool_name}")
374
409
  async def execute_tool_endpoint(tool_name: str, params: Dict[str, Any] = Body(...)):
@@ -408,5 +443,8 @@ def create_app() -> FastAPI:
408
443
  return app
409
444
 
410
445
 
446
+
447
+
448
+
411
449
  # Create global application instance
412
450
  app = create_app()
@@ -43,7 +43,7 @@ async def execute_command(command_name: str, params: Dict[str, Any], request_id:
43
43
  start_time = time.time()
44
44
 
45
45
  # Use Command.run that handles instances with dependencies properly
46
- command_class = registry.get_command(command_name)
46
+ command_class = registry.get_command_with_priority(command_name)
47
47
  result = await command_class.run(**params)
48
48
 
49
49
  execution_time = time.time() - start_time
@@ -35,7 +35,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
35
35
  except CommandError as e:
36
36
  # Command error
37
37
  request_id = getattr(request.state, "request_id", "unknown")
38
- logger.error(f"[{request_id}] Command error: {str(e)}")
38
+ logger.debug(f"[{request_id}] Command error: {str(e)}")
39
39
 
40
40
  # Проверяем, является ли запрос JSON-RPC
41
41
  is_jsonrpc = self._is_json_rpc_request(request)
@@ -52,7 +52,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
52
52
  "error": {
53
53
  "code": e.code,
54
54
  "message": str(e),
55
- "data": e.details if hasattr(e, "details") else None
55
+ "data": e.data if hasattr(e, "data") and e.data else None
56
56
  },
57
57
  "id": request_id_jsonrpc
58
58
  }
@@ -67,7 +67,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
67
67
  except ValidationError as e:
68
68
  # Validation error
69
69
  request_id = getattr(request.state, "request_id", "unknown")
70
- logger.error(f"[{request_id}] Validation error: {str(e)}")
70
+ logger.debug(f"[{request_id}] Validation error: {str(e)}")
71
71
 
72
72
  # Get JSON-RPC request ID if available
73
73
  request_id_jsonrpc = await self._get_json_rpc_id(request)
@@ -81,7 +81,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
81
81
  "error": {
82
82
  "code": -32602,
83
83
  "message": "Invalid params",
84
- "data": e.details if hasattr(e, "details") else None
84
+ "data": e.data if hasattr(e, "data") and e.data else None
85
85
  },
86
86
  "id": request_id_jsonrpc
87
87
  }
@@ -96,7 +96,7 @@ class ErrorHandlingMiddleware(BaseMiddleware):
96
96
  except MicroserviceError as e:
97
97
  # Other microservice error
98
98
  request_id = getattr(request.state, "request_id", "unknown")
99
- logger.error(f"[{request_id}] Microservice error: {str(e)}")
99
+ logger.debug(f"[{request_id}] Microservice error: {str(e)}")
100
100
 
101
101
  # Get JSON-RPC request ID if available
102
102
  request_id_jsonrpc = await self._get_json_rpc_id(request)
@@ -104,13 +104,13 @@ class ErrorHandlingMiddleware(BaseMiddleware):
104
104
  # If request was JSON-RPC
105
105
  if self._is_json_rpc_request(request):
106
106
  return JSONResponse(
107
- status_code=e.code,
107
+ status_code=400,
108
108
  content={
109
109
  "jsonrpc": "2.0",
110
110
  "error": {
111
111
  "code": -32000,
112
112
  "message": str(e),
113
- "data": e.details if hasattr(e, "details") else None
113
+ "data": e.data if hasattr(e, "data") and e.data else None
114
114
  },
115
115
  "id": request_id_jsonrpc
116
116
  }
@@ -118,14 +118,14 @@ class ErrorHandlingMiddleware(BaseMiddleware):
118
118
 
119
119
  # Regular API error
120
120
  return JSONResponse(
121
- status_code=e.code,
121
+ status_code=400,
122
122
  content=e.to_dict()
123
123
  )
124
124
 
125
125
  except Exception as e:
126
126
  # Unexpected error
127
127
  request_id = getattr(request.state, "request_id", "unknown")
128
- logger.exception(f"[{request_id}] Unexpected error: {str(e)}")
128
+ logger.debug(f"[{request_id}] Unexpected error: {str(e)}")
129
129
 
130
130
  # Get JSON-RPC request ID if available
131
131
  request_id_jsonrpc = await self._get_json_rpc_id(request)
@@ -165,7 +165,8 @@ class ErrorHandlingMiddleware(BaseMiddleware):
165
165
  Returns:
166
166
  True if request is JSON-RPC, False otherwise.
167
167
  """
168
- return request.url.path.endswith("/jsonrpc")
168
+ # Only requests to /api/jsonrpc are JSON-RPC requests
169
+ return request.url.path == "/api/jsonrpc"
169
170
 
170
171
  async def _get_json_rpc_id(self, request: Request) -> Optional[Any]:
171
172
  """
@@ -126,7 +126,7 @@ class ToolIntegration:
126
126
 
127
127
  logger.info(f"Successfully registered tool: {tool_name}")
128
128
  except Exception as e:
129
- logger.error(f"Error registering tool {tool_name}: {e}")
129
+ logger.debug(f"Error registering tool {tool_name}: {e}")
130
130
  results[tool_name] = {
131
131
  "status": "error",
132
132
  "error": str(e)
@@ -149,7 +149,10 @@ class ToolIntegration:
149
149
 
150
150
  # Формируем словарь типов для всех параметров всех команд
151
151
  for cmd_name, cmd_info in commands.items():
152
- for param_name, param_info in cmd_info.get("params", {}).items():
152
+ params = cmd_info.get("params", {})
153
+ if params is None:
154
+ continue
155
+ for param_name, param_info in params.items():
153
156
  param_type = param_info.get("type", "значение")
154
157
 
155
158
  # Преобразуем русские типы в типы JSON Schema
@@ -50,14 +50,14 @@ class TSTCommandExecutor:
50
50
 
51
51
  try:
52
52
  # Проверяем существование команды
53
- if not registry.command_exists(command):
53
+ if not registry.command_exists_with_priority(command):
54
54
  raise NotFoundError(f"Команда '{command}' не найдена")
55
55
 
56
56
  # Получаем класс команды
57
- command_class = registry.get_command(command)
57
+ command_class = registry.get_command_with_priority(command)
58
58
 
59
59
  # Выполняем команду
60
- result = await command_class.run(**params)
60
+ result = await command_class.execute(**params)
61
61
 
62
62
  # Возвращаем результат
63
63
  return result.to_dict()
@@ -13,6 +13,7 @@ from mcp_proxy_adapter.core.errors import (
13
13
  CommandError, InternalError, InvalidParamsError, NotFoundError, ValidationError
14
14
  )
15
15
  from mcp_proxy_adapter.core.logging import logger
16
+ from .hooks import hooks, HookContext
16
17
 
17
18
 
18
19
  T = TypeVar("T", bound=CommandResult)
@@ -136,6 +137,20 @@ class Command(ABC):
136
137
  # Parameters validation
137
138
  validated_params = cls.validate_params(kwargs)
138
139
 
140
+ # Execute before hooks
141
+ hook_context = hooks.execute_before_hooks(command_name, validated_params)
142
+
143
+ # Check if standard processing should be skipped
144
+ if not hook_context.standard_processing:
145
+ logger.debug(f"Standard processing skipped for command {command_name} due to hook")
146
+ # Return the params as result if standard processing is disabled
147
+ return SuccessResult(data=validated_params)
148
+
149
+ # Get command with priority (custom commands first, then built-in)
150
+ priority_command_class = registry.get_priority_command(command_name)
151
+ if priority_command_class is None:
152
+ raise NotFoundError(f"Command '{command_name}' not found")
153
+
139
154
  # Check if we have a registered instance for this command
140
155
  if registry.has_instance(command_name):
141
156
  # Use existing instance with dependencies
@@ -143,9 +158,12 @@ class Command(ABC):
143
158
  result = await command.execute(**validated_params)
144
159
  else:
145
160
  # Create new instance for commands without dependencies
146
- command = cls()
161
+ command = priority_command_class()
147
162
  result = await command.execute(**validated_params)
148
163
 
164
+ # Execute after hooks
165
+ hooks.execute_after_hooks(command_name, validated_params, result)
166
+
149
167
  logger.debug(f"Command {cls.__name__} executed successfully")
150
168
  return result
151
169
  except ValidationError as e: