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.
- mcp_proxy_adapter/api/app.py +65 -27
- mcp_proxy_adapter/api/handlers.py +1 -1
- mcp_proxy_adapter/api/middleware/error_handling.py +11 -10
- mcp_proxy_adapter/api/tool_integration.py +5 -2
- mcp_proxy_adapter/api/tools.py +3 -3
- mcp_proxy_adapter/commands/base.py +19 -1
- mcp_proxy_adapter/commands/command_registry.py +254 -8
- mcp_proxy_adapter/commands/hooks.py +260 -0
- mcp_proxy_adapter/commands/reload_command.py +211 -0
- mcp_proxy_adapter/commands/reload_settings_command.py +125 -0
- mcp_proxy_adapter/commands/settings_command.py +189 -0
- mcp_proxy_adapter/config.py +16 -1
- mcp_proxy_adapter/core/__init__.py +44 -0
- mcp_proxy_adapter/core/logging.py +87 -34
- mcp_proxy_adapter/core/settings.py +376 -0
- mcp_proxy_adapter/core/utils.py +2 -2
- mcp_proxy_adapter/custom_openapi.py +81 -2
- mcp_proxy_adapter/examples/README.md +124 -0
- mcp_proxy_adapter/examples/__init__.py +7 -0
- mcp_proxy_adapter/examples/basic_server/README.md +60 -0
- mcp_proxy_adapter/examples/basic_server/__init__.py +7 -0
- mcp_proxy_adapter/examples/basic_server/basic_custom_settings.json +39 -0
- mcp_proxy_adapter/examples/basic_server/config.json +35 -0
- mcp_proxy_adapter/examples/basic_server/custom_settings_example.py +238 -0
- mcp_proxy_adapter/examples/basic_server/server.py +98 -0
- mcp_proxy_adapter/examples/custom_commands/README.md +127 -0
- mcp_proxy_adapter/examples/custom_commands/__init__.py +27 -0
- mcp_proxy_adapter/examples/custom_commands/advanced_hooks.py +250 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/__init__.py +6 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_echo_command.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/auto_commands/auto_info_command.py +111 -0
- mcp_proxy_adapter/examples/custom_commands/config.json +62 -0
- mcp_proxy_adapter/examples/custom_commands/custom_health_command.py +169 -0
- mcp_proxy_adapter/examples/custom_commands/custom_help_command.py +215 -0
- mcp_proxy_adapter/examples/custom_commands/custom_openapi_generator.py +76 -0
- mcp_proxy_adapter/examples/custom_commands/custom_settings.json +96 -0
- mcp_proxy_adapter/examples/custom_commands/custom_settings_manager.py +241 -0
- mcp_proxy_adapter/examples/custom_commands/data_transform_command.py +135 -0
- mcp_proxy_adapter/examples/custom_commands/echo_command.py +122 -0
- mcp_proxy_adapter/examples/custom_commands/hooks.py +230 -0
- mcp_proxy_adapter/examples/custom_commands/intercept_command.py +123 -0
- mcp_proxy_adapter/examples/custom_commands/manual_echo_command.py +103 -0
- mcp_proxy_adapter/examples/custom_commands/server.py +223 -0
- mcp_proxy_adapter/examples/custom_commands/test_hooks.py +176 -0
- mcp_proxy_adapter/examples/deployment/README.md +49 -0
- mcp_proxy_adapter/examples/deployment/__init__.py +7 -0
- mcp_proxy_adapter/examples/deployment/config.development.json +8 -0
- {examples/basic_example → mcp_proxy_adapter/examples/deployment}/config.json +11 -7
- mcp_proxy_adapter/examples/deployment/config.production.json +12 -0
- mcp_proxy_adapter/examples/deployment/config.staging.json +11 -0
- mcp_proxy_adapter/examples/deployment/docker-compose.yml +31 -0
- mcp_proxy_adapter/examples/deployment/run.sh +43 -0
- mcp_proxy_adapter/examples/deployment/run_docker.sh +84 -0
- mcp_proxy_adapter/openapi.py +3 -2
- mcp_proxy_adapter/tests/api/test_custom_openapi.py +617 -0
- mcp_proxy_adapter/tests/api/test_handlers.py +522 -0
- mcp_proxy_adapter/tests/api/test_schemas.py +546 -0
- mcp_proxy_adapter/tests/api/test_tool_integration.py +531 -0
- mcp_proxy_adapter/tests/unit/test_base_command.py +391 -85
- mcp_proxy_adapter/version.py +1 -1
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/METADATA +3 -3
- mcp_proxy_adapter-4.1.0.dist-info/RECORD +110 -0
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/WHEEL +1 -1
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/top_level.txt +0 -1
- examples/__init__.py +0 -19
- examples/anti_patterns/README.md +0 -51
- examples/anti_patterns/__init__.py +0 -9
- examples/anti_patterns/bad_design/README.md +0 -72
- examples/anti_patterns/bad_design/global_state.py +0 -170
- examples/anti_patterns/bad_design/monolithic_command.py +0 -272
- examples/basic_example/README.md +0 -245
- examples/basic_example/__init__.py +0 -8
- examples/basic_example/commands/__init__.py +0 -5
- examples/basic_example/commands/echo_command.py +0 -95
- examples/basic_example/commands/math_command.py +0 -151
- examples/basic_example/commands/time_command.py +0 -152
- examples/basic_example/docs/EN/README.md +0 -177
- examples/basic_example/docs/RU/README.md +0 -177
- examples/basic_example/server.py +0 -151
- examples/basic_example/tests/conftest.py +0 -243
- examples/check_vstl_schema.py +0 -106
- examples/commands/echo_command.py +0 -52
- examples/commands/echo_command_di.py +0 -152
- examples/commands/echo_result.py +0 -65
- examples/commands/get_date_command.py +0 -98
- examples/commands/new_uuid4_command.py +0 -91
- examples/complete_example/Dockerfile +0 -24
- examples/complete_example/README.md +0 -92
- examples/complete_example/__init__.py +0 -8
- examples/complete_example/commands/__init__.py +0 -5
- examples/complete_example/commands/system_command.py +0 -328
- examples/complete_example/config.json +0 -41
- examples/complete_example/configs/config.dev.yaml +0 -40
- examples/complete_example/configs/config.docker.yaml +0 -40
- examples/complete_example/docker-compose.yml +0 -35
- examples/complete_example/requirements.txt +0 -20
- examples/complete_example/server.py +0 -113
- examples/di_example/.pytest_cache/README.md +0 -8
- examples/di_example/server.py +0 -249
- examples/fix_vstl_help.py +0 -123
- examples/minimal_example/README.md +0 -65
- examples/minimal_example/__init__.py +0 -8
- examples/minimal_example/config.json +0 -14
- examples/minimal_example/main.py +0 -136
- examples/minimal_example/simple_server.py +0 -163
- examples/minimal_example/tests/conftest.py +0 -171
- examples/minimal_example/tests/test_hello_command.py +0 -111
- examples/minimal_example/tests/test_integration.py +0 -181
- examples/patch_vstl_service.py +0 -105
- examples/patch_vstl_service_mcp.py +0 -108
- examples/server.py +0 -69
- examples/simple_server.py +0 -128
- examples/test_package_3.1.4.py +0 -177
- examples/test_server.py +0 -134
- examples/tool_description_example.py +0 -82
- mcp_proxy_adapter/py.typed +0 -0
- mcp_proxy_adapter-3.1.6.dist-info/RECORD +0 -118
- {mcp_proxy_adapter-3.1.6.dist-info → mcp_proxy_adapter-4.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -41,6 +41,7 @@ class CommandRegistry:
|
|
41
41
|
"""
|
42
42
|
self._commands: Dict[str, Type[Command]] = {}
|
43
43
|
self._instances: Dict[str, Command] = {}
|
44
|
+
self._custom_commands: Dict[str, Type[Command]] = {} # Custom commands with priority
|
44
45
|
|
45
46
|
def register(self, command: Union[Type[Command], Command]) -> None:
|
46
47
|
"""
|
@@ -196,7 +197,7 @@ class CommandRegistry:
|
|
196
197
|
Raises:
|
197
198
|
NotFoundError: If command is not found.
|
198
199
|
"""
|
199
|
-
command_class = self.
|
200
|
+
command_class = self.get_command_with_priority(command_name)
|
200
201
|
|
201
202
|
return {
|
202
203
|
"name": command_name,
|
@@ -219,7 +220,7 @@ class CommandRegistry:
|
|
219
220
|
Raises:
|
220
221
|
NotFoundError: If command is not found
|
221
222
|
"""
|
222
|
-
command_class = self.
|
223
|
+
command_class = self.get_command_with_priority(command_name)
|
223
224
|
return command_class.get_metadata()
|
224
225
|
|
225
226
|
def get_all_metadata(self) -> Dict[str, Dict[str, Any]]:
|
@@ -230,8 +231,13 @@ class CommandRegistry:
|
|
230
231
|
Dict with command names as keys and metadata as values
|
231
232
|
"""
|
232
233
|
metadata = {}
|
233
|
-
|
234
|
+
# Add custom commands first (they have priority)
|
235
|
+
for name, command_class in self._custom_commands.items():
|
234
236
|
metadata[name] = command_class.get_metadata()
|
237
|
+
# Add built-in commands (custom commands will override if same name)
|
238
|
+
for name, command_class in self._commands.items():
|
239
|
+
if name not in self._custom_commands: # Only add if not overridden by custom
|
240
|
+
metadata[name] = command_class.get_metadata()
|
235
241
|
return metadata
|
236
242
|
|
237
243
|
def get_all_commands_info(self) -> Dict[str, Dict[str, Any]]:
|
@@ -242,19 +248,29 @@ class CommandRegistry:
|
|
242
248
|
Dictionary with information about all commands.
|
243
249
|
"""
|
244
250
|
commands_info = {}
|
245
|
-
|
251
|
+
# Add custom commands first (they have priority)
|
252
|
+
for name in self._custom_commands:
|
246
253
|
commands_info[name] = self.get_command_info(name)
|
254
|
+
# Add built-in commands (custom commands will override if same name)
|
255
|
+
for name in self._commands:
|
256
|
+
if name not in self._custom_commands: # Only add if not overridden by custom
|
257
|
+
commands_info[name] = self.get_command_info(name)
|
247
258
|
return commands_info
|
248
259
|
|
249
|
-
def discover_commands(self, package_path: str = "mcp_proxy_adapter.commands") ->
|
260
|
+
def discover_commands(self, package_path: str = "mcp_proxy_adapter.commands") -> int:
|
250
261
|
"""
|
251
262
|
Automatically discovers and registers commands in the specified package.
|
252
263
|
|
253
264
|
Args:
|
254
265
|
package_path: Path to package with commands.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
Number of commands discovered and registered.
|
255
269
|
"""
|
256
270
|
logger.info(f"Discovering commands in package: {package_path}")
|
257
271
|
|
272
|
+
commands_discovered = 0
|
273
|
+
|
258
274
|
try:
|
259
275
|
package = importlib.import_module(package_path)
|
260
276
|
package_dir = os.path.dirname(package.__file__ or "")
|
@@ -262,7 +278,7 @@ class CommandRegistry:
|
|
262
278
|
for _, module_name, is_pkg in pkgutil.iter_modules([package_dir]):
|
263
279
|
if is_pkg:
|
264
280
|
# Recursively traverse subpackages
|
265
|
-
self.discover_commands(f"{package_path}.{module_name}")
|
281
|
+
commands_discovered += self.discover_commands(f"{package_path}.{module_name}")
|
266
282
|
elif module_name.endswith("_command"):
|
267
283
|
# Import only command modules
|
268
284
|
module_path = f"{package_path}.{module_name}"
|
@@ -286,6 +302,8 @@ class CommandRegistry:
|
|
286
302
|
# Register the command only if it doesn't exist
|
287
303
|
if not self.command_exists(command_name):
|
288
304
|
self.register(cast(Type[Command], obj))
|
305
|
+
commands_discovered += 1
|
306
|
+
logger.debug(f"Registered command: {command_name}")
|
289
307
|
else:
|
290
308
|
logger.debug(f"Command '{command_name}' is already registered, skipping")
|
291
309
|
except ValueError as e:
|
@@ -296,13 +314,241 @@ class CommandRegistry:
|
|
296
314
|
except Exception as e:
|
297
315
|
logger.error(f"Error discovering commands: {e}")
|
298
316
|
|
317
|
+
return commands_discovered
|
318
|
+
|
319
|
+
def register_custom_command(self, command: Union[Type[Command], Command]) -> None:
|
320
|
+
"""
|
321
|
+
Register a custom command with priority over built-in commands.
|
322
|
+
|
323
|
+
Args:
|
324
|
+
command: Command class or instance to register.
|
325
|
+
|
326
|
+
Raises:
|
327
|
+
ValueError: If command with the same name is already registered.
|
328
|
+
"""
|
329
|
+
# Determine if this is a class or an instance
|
330
|
+
if isinstance(command, type) and issubclass(command, Command):
|
331
|
+
command_class = command
|
332
|
+
command_instance = None
|
333
|
+
elif isinstance(command, Command):
|
334
|
+
command_class = command.__class__
|
335
|
+
command_instance = command
|
336
|
+
else:
|
337
|
+
raise ValueError(f"Invalid command type: {type(command)}. Expected Command class or instance.")
|
338
|
+
|
339
|
+
# Get command name
|
340
|
+
if not hasattr(command_class, "name") or not command_class.name:
|
341
|
+
# Use class name if name attribute is not set
|
342
|
+
command_name = command_class.__name__.lower()
|
343
|
+
if command_name.endswith("command"):
|
344
|
+
command_name = command_name[:-7] # Remove "command" suffix
|
345
|
+
else:
|
346
|
+
command_name = command_class.name
|
347
|
+
|
348
|
+
if command_name in self._custom_commands:
|
349
|
+
logger.debug(f"Custom command '{command_name}' is already registered, skipping")
|
350
|
+
raise ValueError(f"Custom command '{command_name}' is already registered")
|
351
|
+
|
352
|
+
logger.debug(f"Registering custom command: {command_name}")
|
353
|
+
self._custom_commands[command_name] = command_class
|
354
|
+
|
355
|
+
# Store instance if provided
|
356
|
+
if command_instance:
|
357
|
+
logger.debug(f"Storing custom instance for command: {command_name}")
|
358
|
+
self._instances[command_name] = command_instance
|
359
|
+
|
360
|
+
def unregister_custom_command(self, command_name: str) -> None:
|
361
|
+
"""
|
362
|
+
Remove custom command from registry.
|
363
|
+
|
364
|
+
Args:
|
365
|
+
command_name: Command name to remove.
|
366
|
+
|
367
|
+
Raises:
|
368
|
+
NotFoundError: If command is not found.
|
369
|
+
"""
|
370
|
+
if command_name not in self._custom_commands:
|
371
|
+
raise NotFoundError(f"Custom command '{command_name}' not found")
|
372
|
+
|
373
|
+
logger.debug(f"Unregistering custom command: {command_name}")
|
374
|
+
del self._custom_commands[command_name]
|
375
|
+
|
376
|
+
# Also remove from instances if present
|
377
|
+
if command_name in self._instances:
|
378
|
+
del self._instances[command_name]
|
379
|
+
|
380
|
+
def custom_command_exists(self, command_name: str) -> bool:
|
381
|
+
"""
|
382
|
+
Check if custom command exists.
|
383
|
+
|
384
|
+
Args:
|
385
|
+
command_name: Command name to check.
|
386
|
+
|
387
|
+
Returns:
|
388
|
+
True if custom command exists, False otherwise.
|
389
|
+
"""
|
390
|
+
return command_name in self._custom_commands
|
391
|
+
|
392
|
+
def get_custom_command(self, command_name: str) -> Type[Command]:
|
393
|
+
"""
|
394
|
+
Get custom command class.
|
395
|
+
|
396
|
+
Args:
|
397
|
+
command_name: Command name.
|
398
|
+
|
399
|
+
Returns:
|
400
|
+
Command class.
|
401
|
+
|
402
|
+
Raises:
|
403
|
+
NotFoundError: If command is not found.
|
404
|
+
"""
|
405
|
+
if command_name not in self._custom_commands:
|
406
|
+
raise NotFoundError(f"Custom command '{command_name}' not found")
|
407
|
+
return self._custom_commands[command_name]
|
408
|
+
|
409
|
+
def get_all_custom_commands(self) -> Dict[str, Type[Command]]:
|
410
|
+
"""
|
411
|
+
Get all custom commands.
|
412
|
+
|
413
|
+
Returns:
|
414
|
+
Dictionary with custom command names as keys and classes as values.
|
415
|
+
"""
|
416
|
+
return self._custom_commands.copy()
|
417
|
+
|
418
|
+
def get_priority_command(self, command_name: str) -> Optional[Type[Command]]:
|
419
|
+
"""
|
420
|
+
Get command with priority (custom commands first, then built-in).
|
421
|
+
|
422
|
+
Args:
|
423
|
+
command_name: Command name.
|
424
|
+
|
425
|
+
Returns:
|
426
|
+
Command class if found, None otherwise.
|
427
|
+
"""
|
428
|
+
# First check custom commands
|
429
|
+
if command_name in self._custom_commands:
|
430
|
+
return self._custom_commands[command_name]
|
431
|
+
|
432
|
+
# Then check built-in commands
|
433
|
+
if command_name in self._commands:
|
434
|
+
return self._commands[command_name]
|
435
|
+
|
436
|
+
return None
|
437
|
+
|
438
|
+
def command_exists_with_priority(self, command_name: str) -> bool:
|
439
|
+
"""
|
440
|
+
Check if command exists (custom or built-in).
|
441
|
+
|
442
|
+
Args:
|
443
|
+
command_name: Command name to check.
|
444
|
+
|
445
|
+
Returns:
|
446
|
+
True if command exists, False otherwise.
|
447
|
+
"""
|
448
|
+
return (command_name in self._custom_commands or
|
449
|
+
command_name in self._commands)
|
450
|
+
|
451
|
+
def get_command_with_priority(self, command_name: str) -> Type[Command]:
|
452
|
+
"""
|
453
|
+
Get command with priority (custom commands first, then built-in).
|
454
|
+
|
455
|
+
Args:
|
456
|
+
command_name: Command name.
|
457
|
+
|
458
|
+
Returns:
|
459
|
+
Command class.
|
460
|
+
|
461
|
+
Raises:
|
462
|
+
NotFoundError: If command is not found.
|
463
|
+
"""
|
464
|
+
# First check custom commands
|
465
|
+
if command_name in self._custom_commands:
|
466
|
+
return self._custom_commands[command_name]
|
467
|
+
|
468
|
+
# Then check built-in commands
|
469
|
+
if command_name in self._commands:
|
470
|
+
return self._commands[command_name]
|
471
|
+
|
472
|
+
raise NotFoundError(f"Command '{command_name}' not found")
|
473
|
+
|
299
474
|
def clear(self) -> None:
|
300
475
|
"""
|
301
|
-
|
476
|
+
Clear all registered commands.
|
302
477
|
"""
|
303
|
-
logger.debug("Clearing
|
478
|
+
logger.debug("Clearing all registered commands")
|
304
479
|
self._commands.clear()
|
305
480
|
self._instances.clear()
|
481
|
+
self._custom_commands.clear()
|
482
|
+
|
483
|
+
def reload_config_and_commands(self, package_path: str = "mcp_proxy_adapter.commands") -> Dict[str, Any]:
|
484
|
+
"""
|
485
|
+
Reload configuration and rediscover commands.
|
486
|
+
|
487
|
+
Args:
|
488
|
+
package_path: Path to package with commands.
|
489
|
+
|
490
|
+
Returns:
|
491
|
+
Dictionary with reload information including:
|
492
|
+
- config_reloaded: Whether config was reloaded
|
493
|
+
- commands_discovered: Number of commands discovered
|
494
|
+
- custom_commands_preserved: Number of custom commands preserved
|
495
|
+
- total_commands: Total number of commands after reload
|
496
|
+
"""
|
497
|
+
logger.info("🔄 Starting configuration and commands reload...")
|
498
|
+
|
499
|
+
# Store current custom commands
|
500
|
+
custom_commands_backup = self._custom_commands.copy()
|
501
|
+
|
502
|
+
# Reload configuration
|
503
|
+
try:
|
504
|
+
from mcp_proxy_adapter.config import config
|
505
|
+
config.load_config()
|
506
|
+
config_reloaded = True
|
507
|
+
logger.info("✅ Configuration reloaded successfully")
|
508
|
+
except Exception as e:
|
509
|
+
logger.error(f"❌ Failed to reload configuration: {e}")
|
510
|
+
config_reloaded = False
|
511
|
+
|
512
|
+
# Reinitialize logging with new configuration
|
513
|
+
try:
|
514
|
+
from mcp_proxy_adapter.core.logging import setup_logging
|
515
|
+
setup_logging()
|
516
|
+
logger.info("✅ Logging reinitialized with new configuration")
|
517
|
+
except Exception as e:
|
518
|
+
logger.error(f"❌ Failed to reinitialize logging: {e}")
|
519
|
+
|
520
|
+
# Clear all commands except custom ones
|
521
|
+
self._commands.clear()
|
522
|
+
self._instances.clear()
|
523
|
+
|
524
|
+
# Restore custom commands
|
525
|
+
self._custom_commands = custom_commands_backup
|
526
|
+
custom_commands_preserved = len(custom_commands_backup)
|
527
|
+
|
528
|
+
# Rediscover commands
|
529
|
+
try:
|
530
|
+
commands_discovered = self.discover_commands(package_path)
|
531
|
+
logger.info(f"✅ Rediscovered {commands_discovered} commands")
|
532
|
+
except Exception as e:
|
533
|
+
logger.error(f"❌ Failed to rediscover commands: {e}")
|
534
|
+
commands_discovered = 0
|
535
|
+
|
536
|
+
# Get final counts
|
537
|
+
total_commands = len(self._commands)
|
538
|
+
built_in_commands = total_commands - custom_commands_preserved
|
539
|
+
custom_commands = custom_commands_preserved
|
540
|
+
|
541
|
+
result = {
|
542
|
+
"config_reloaded": config_reloaded,
|
543
|
+
"commands_discovered": commands_discovered,
|
544
|
+
"custom_commands_preserved": custom_commands_preserved,
|
545
|
+
"total_commands": total_commands,
|
546
|
+
"built_in_commands": built_in_commands,
|
547
|
+
"custom_commands": custom_commands
|
548
|
+
}
|
549
|
+
|
550
|
+
logger.info(f"🔄 Reload completed: {result}")
|
551
|
+
return result
|
306
552
|
|
307
553
|
|
308
554
|
# Global command registry instance
|
@@ -0,0 +1,260 @@
|
|
1
|
+
"""
|
2
|
+
Module for command execution hooks.
|
3
|
+
|
4
|
+
This module provides a hook system that allows intercepting command execution
|
5
|
+
before and after the actual command runs.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
9
|
+
from dataclasses import dataclass
|
10
|
+
from enum import Enum
|
11
|
+
|
12
|
+
from mcp_proxy_adapter.core.logging import logger
|
13
|
+
|
14
|
+
|
15
|
+
class HookType(Enum):
|
16
|
+
"""Types of hooks."""
|
17
|
+
BEFORE_EXECUTION = "before_execution"
|
18
|
+
AFTER_EXECUTION = "after_execution"
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class HookContext:
|
23
|
+
"""Context passed to hook functions."""
|
24
|
+
command_name: str
|
25
|
+
params: Dict[str, Any]
|
26
|
+
hook_type: HookType
|
27
|
+
standard_processing: bool = True
|
28
|
+
result: Optional[Any] = None
|
29
|
+
|
30
|
+
|
31
|
+
class CommandHooks:
|
32
|
+
"""
|
33
|
+
Manages command execution hooks.
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(self):
|
37
|
+
"""Initialize hooks manager."""
|
38
|
+
self._before_hooks: Dict[str, List[Callable]] = {}
|
39
|
+
self._after_hooks: Dict[str, List[Callable]] = {}
|
40
|
+
self._global_before_hooks: List[Callable] = []
|
41
|
+
self._global_after_hooks: List[Callable] = []
|
42
|
+
|
43
|
+
def register_before_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
|
44
|
+
"""
|
45
|
+
Register a hook to be executed before command execution.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
command_name: Name of the command to hook into
|
49
|
+
hook: Hook function that takes HookContext as parameter
|
50
|
+
"""
|
51
|
+
if command_name not in self._before_hooks:
|
52
|
+
self._before_hooks[command_name] = []
|
53
|
+
self._before_hooks[command_name].append(hook)
|
54
|
+
logger.debug(f"Registered before hook for command: {command_name}")
|
55
|
+
|
56
|
+
def register_after_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
|
57
|
+
"""
|
58
|
+
Register a hook to be executed after command execution.
|
59
|
+
|
60
|
+
Args:
|
61
|
+
command_name: Name of the command to hook into
|
62
|
+
hook: Hook function that takes HookContext as parameter
|
63
|
+
"""
|
64
|
+
if command_name not in self._after_hooks:
|
65
|
+
self._after_hooks[command_name] = []
|
66
|
+
self._after_hooks[command_name].append(hook)
|
67
|
+
logger.debug(f"Registered after hook for command: {command_name}")
|
68
|
+
|
69
|
+
def register_global_before_hook(self, hook: Callable[[HookContext], None]) -> None:
|
70
|
+
"""
|
71
|
+
Register a global hook to be executed before any command.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
hook: Hook function that takes HookContext as parameter
|
75
|
+
"""
|
76
|
+
self._global_before_hooks.append(hook)
|
77
|
+
logger.debug("Registered global before hook")
|
78
|
+
|
79
|
+
def register_global_after_hook(self, hook: Callable[[HookContext], None]) -> None:
|
80
|
+
"""
|
81
|
+
Register a global hook to be executed after any command.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
hook: Hook function that takes HookContext as parameter
|
85
|
+
"""
|
86
|
+
self._global_after_hooks.append(hook)
|
87
|
+
logger.debug("Registered global after hook")
|
88
|
+
|
89
|
+
def unregister_before_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
|
90
|
+
"""
|
91
|
+
Unregister a before hook for a specific command.
|
92
|
+
|
93
|
+
Args:
|
94
|
+
command_name: Name of the command
|
95
|
+
hook: Hook function to unregister
|
96
|
+
"""
|
97
|
+
if command_name in self._before_hooks:
|
98
|
+
try:
|
99
|
+
self._before_hooks[command_name].remove(hook)
|
100
|
+
logger.debug(f"Unregistered before hook for command: {command_name}")
|
101
|
+
# Remove the command key if no hooks remain
|
102
|
+
if not self._before_hooks[command_name]:
|
103
|
+
del self._before_hooks[command_name]
|
104
|
+
except ValueError:
|
105
|
+
logger.warning(f"Hook not found for command: {command_name}")
|
106
|
+
|
107
|
+
def unregister_after_hook(self, command_name: str, hook: Callable[[HookContext], None]) -> None:
|
108
|
+
"""
|
109
|
+
Unregister an after hook for a specific command.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
command_name: Name of the command
|
113
|
+
hook: Hook function to unregister
|
114
|
+
"""
|
115
|
+
if command_name in self._after_hooks:
|
116
|
+
try:
|
117
|
+
self._after_hooks[command_name].remove(hook)
|
118
|
+
logger.debug(f"Unregistered after hook for command: {command_name}")
|
119
|
+
# Remove the command key if no hooks remain
|
120
|
+
if not self._after_hooks[command_name]:
|
121
|
+
del self._after_hooks[command_name]
|
122
|
+
except ValueError:
|
123
|
+
logger.warning(f"Hook not found for command: {command_name}")
|
124
|
+
|
125
|
+
def unregister_global_before_hook(self, hook: Callable[[HookContext], None]) -> None:
|
126
|
+
"""
|
127
|
+
Unregister a global before hook.
|
128
|
+
|
129
|
+
Args:
|
130
|
+
hook: Hook function to unregister
|
131
|
+
"""
|
132
|
+
try:
|
133
|
+
self._global_before_hooks.remove(hook)
|
134
|
+
logger.debug("Unregistered global before hook")
|
135
|
+
except ValueError:
|
136
|
+
logger.warning("Global before hook not found")
|
137
|
+
|
138
|
+
def unregister_global_after_hook(self, hook: Callable[[HookContext], None]) -> None:
|
139
|
+
"""
|
140
|
+
Unregister a global after hook.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
hook: Hook function to unregister
|
144
|
+
"""
|
145
|
+
try:
|
146
|
+
self._global_after_hooks.remove(hook)
|
147
|
+
logger.debug("Unregistered global after hook")
|
148
|
+
except ValueError:
|
149
|
+
logger.warning("Global after hook not found")
|
150
|
+
|
151
|
+
def execute_before_hooks(self, command_name: str, params: Dict[str, Any]) -> HookContext:
|
152
|
+
"""
|
153
|
+
Execute all before hooks for a command.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
command_name: Name of the command
|
157
|
+
params: Command parameters
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
HookContext with execution results
|
161
|
+
"""
|
162
|
+
context = HookContext(
|
163
|
+
command_name=command_name,
|
164
|
+
params=params,
|
165
|
+
hook_type=HookType.BEFORE_EXECUTION
|
166
|
+
)
|
167
|
+
|
168
|
+
# Execute global before hooks
|
169
|
+
for hook in self._global_before_hooks:
|
170
|
+
try:
|
171
|
+
hook(context)
|
172
|
+
except Exception as e:
|
173
|
+
logger.error(f"Error in global before hook for command {command_name}: {e}")
|
174
|
+
|
175
|
+
# Execute command-specific before hooks
|
176
|
+
if command_name in self._before_hooks:
|
177
|
+
for hook in self._before_hooks[command_name]:
|
178
|
+
try:
|
179
|
+
hook(context)
|
180
|
+
except Exception as e:
|
181
|
+
logger.error(f"Error in before hook for command {command_name}: {e}")
|
182
|
+
|
183
|
+
return context
|
184
|
+
|
185
|
+
def execute_after_hooks(self, command_name: str, params: Dict[str, Any], result: Any) -> HookContext:
|
186
|
+
"""
|
187
|
+
Execute all after hooks for a command.
|
188
|
+
|
189
|
+
Args:
|
190
|
+
command_name: Name of the command
|
191
|
+
params: Command parameters
|
192
|
+
result: Command execution result
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
HookContext with execution results
|
196
|
+
"""
|
197
|
+
context = HookContext(
|
198
|
+
command_name=command_name,
|
199
|
+
params=params,
|
200
|
+
hook_type=HookType.AFTER_EXECUTION,
|
201
|
+
result=result
|
202
|
+
)
|
203
|
+
|
204
|
+
# Execute command-specific after hooks
|
205
|
+
if command_name in self._after_hooks:
|
206
|
+
for hook in self._after_hooks[command_name]:
|
207
|
+
try:
|
208
|
+
hook(context)
|
209
|
+
except Exception as e:
|
210
|
+
logger.error(f"Error in after hook for command {command_name}: {e}")
|
211
|
+
|
212
|
+
# Execute global after hooks
|
213
|
+
for hook in self._global_after_hooks:
|
214
|
+
try:
|
215
|
+
hook(context)
|
216
|
+
except Exception as e:
|
217
|
+
logger.error(f"Error in global after hook for command {command_name}: {e}")
|
218
|
+
|
219
|
+
return context
|
220
|
+
|
221
|
+
def clear_hooks(self, command_name: Optional[str] = None) -> None:
|
222
|
+
"""
|
223
|
+
Clear all hooks or hooks for a specific command.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
command_name: If provided, clear hooks only for this command.
|
227
|
+
If None, clear all hooks.
|
228
|
+
"""
|
229
|
+
if command_name is None:
|
230
|
+
# Clear all hooks
|
231
|
+
self._before_hooks.clear()
|
232
|
+
self._after_hooks.clear()
|
233
|
+
self._global_before_hooks.clear()
|
234
|
+
self._global_after_hooks.clear()
|
235
|
+
logger.debug("Cleared all hooks")
|
236
|
+
else:
|
237
|
+
# Clear hooks for specific command
|
238
|
+
if command_name in self._before_hooks:
|
239
|
+
del self._before_hooks[command_name]
|
240
|
+
if command_name in self._after_hooks:
|
241
|
+
del self._after_hooks[command_name]
|
242
|
+
logger.debug(f"Cleared hooks for command: {command_name}")
|
243
|
+
|
244
|
+
def get_hook_info(self) -> Dict[str, Any]:
|
245
|
+
"""
|
246
|
+
Get information about registered hooks.
|
247
|
+
|
248
|
+
Returns:
|
249
|
+
Dictionary with hook information
|
250
|
+
"""
|
251
|
+
return {
|
252
|
+
"before_hooks": {cmd: len(hooks) for cmd, hooks in self._before_hooks.items()},
|
253
|
+
"after_hooks": {cmd: len(hooks) for cmd, hooks in self._after_hooks.items()},
|
254
|
+
"global_before_hooks": len(self._global_before_hooks),
|
255
|
+
"global_after_hooks": len(self._global_after_hooks)
|
256
|
+
}
|
257
|
+
|
258
|
+
|
259
|
+
# Global hooks instance
|
260
|
+
hooks = CommandHooks()
|