quantum-framework 0.9.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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Plugin Loader
|
|
3
|
+
|
|
4
|
+
Discovers and loads plugins from:
|
|
5
|
+
1. Local plugin directories
|
|
6
|
+
2. Installed Python packages (quantum-plugin-* naming convention)
|
|
7
|
+
3. Explicit plugin paths
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import importlib
|
|
12
|
+
import importlib.util
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Dict, Any, Optional, Callable
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from .manifest import PluginManifest, ManifestError
|
|
18
|
+
from .registry import PluginRegistry, PluginInfo, TagHandler, get_registry
|
|
19
|
+
from .hooks import HookManager, HookType, get_hook_manager
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PluginLoadError(Exception):
|
|
25
|
+
"""Error loading a plugin"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PluginLoader:
|
|
30
|
+
"""
|
|
31
|
+
Discovers and loads Quantum plugins.
|
|
32
|
+
|
|
33
|
+
Plugins can be loaded from:
|
|
34
|
+
1. Local directories containing quantum-plugin.yaml
|
|
35
|
+
2. Installed Python packages with quantum_plugin entry point
|
|
36
|
+
3. Explicit paths to plugin directories
|
|
37
|
+
|
|
38
|
+
Example usage:
|
|
39
|
+
loader = PluginLoader()
|
|
40
|
+
|
|
41
|
+
# Discover plugins in a directory
|
|
42
|
+
loader.discover_plugins('./plugins')
|
|
43
|
+
|
|
44
|
+
# Load a specific plugin
|
|
45
|
+
loader.load_plugin('./plugins/my-plugin')
|
|
46
|
+
|
|
47
|
+
# Access loaded plugins
|
|
48
|
+
plugin = loader.registry.get_plugin('my-plugin')
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
MANIFEST_FILENAME = 'quantum-plugin.yaml'
|
|
52
|
+
PACKAGE_PREFIX = 'quantum_plugin_'
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
registry: PluginRegistry = None,
|
|
57
|
+
hook_manager: HookManager = None
|
|
58
|
+
):
|
|
59
|
+
self.registry = registry or get_registry()
|
|
60
|
+
self.hook_manager = hook_manager or get_hook_manager()
|
|
61
|
+
self._loaded_paths: set = set()
|
|
62
|
+
|
|
63
|
+
def discover_plugins(self, search_path: str) -> List[str]:
|
|
64
|
+
"""
|
|
65
|
+
Discover and load all plugins in a directory.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
search_path: Path to search for plugins
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of loaded plugin names
|
|
72
|
+
"""
|
|
73
|
+
search_dir = Path(search_path)
|
|
74
|
+
if not search_dir.exists():
|
|
75
|
+
logger.warning(f"Plugin search path does not exist: {search_path}")
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
loaded = []
|
|
79
|
+
for item in search_dir.iterdir():
|
|
80
|
+
if item.is_dir():
|
|
81
|
+
manifest_path = item / self.MANIFEST_FILENAME
|
|
82
|
+
if manifest_path.exists():
|
|
83
|
+
try:
|
|
84
|
+
name = self.load_plugin(str(item))
|
|
85
|
+
loaded.append(name)
|
|
86
|
+
except PluginLoadError as e:
|
|
87
|
+
logger.error(f"Failed to load plugin {item.name}: {e}")
|
|
88
|
+
|
|
89
|
+
logger.info(f"Discovered {len(loaded)} plugins in {search_path}")
|
|
90
|
+
return loaded
|
|
91
|
+
|
|
92
|
+
def discover_installed_plugins(self) -> List[str]:
|
|
93
|
+
"""
|
|
94
|
+
Discover plugins installed as Python packages.
|
|
95
|
+
|
|
96
|
+
Looks for packages with names starting with 'quantum_plugin_'
|
|
97
|
+
or using the 'quantum.plugins' entry point group.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
List of loaded plugin names
|
|
101
|
+
"""
|
|
102
|
+
loaded = []
|
|
103
|
+
|
|
104
|
+
# Method 1: Look for quantum_plugin_* packages
|
|
105
|
+
try:
|
|
106
|
+
import pkgutil
|
|
107
|
+
for importer, modname, ispkg in pkgutil.iter_modules():
|
|
108
|
+
if modname.startswith(self.PACKAGE_PREFIX):
|
|
109
|
+
try:
|
|
110
|
+
name = self._load_installed_plugin(modname)
|
|
111
|
+
if name:
|
|
112
|
+
loaded.append(name)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Failed to load installed plugin {modname}: {e}")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.debug(f"Error scanning for installed plugins: {e}")
|
|
117
|
+
|
|
118
|
+
# Method 2: Use entry points (Python 3.9+)
|
|
119
|
+
try:
|
|
120
|
+
from importlib.metadata import entry_points
|
|
121
|
+
eps = entry_points()
|
|
122
|
+
|
|
123
|
+
# Handle different Python versions
|
|
124
|
+
if hasattr(eps, 'select'):
|
|
125
|
+
# Python 3.10+
|
|
126
|
+
quantum_plugins = eps.select(group='quantum.plugins')
|
|
127
|
+
else:
|
|
128
|
+
# Python 3.9
|
|
129
|
+
quantum_plugins = eps.get('quantum.plugins', [])
|
|
130
|
+
|
|
131
|
+
for ep in quantum_plugins:
|
|
132
|
+
try:
|
|
133
|
+
plugin_module = ep.load()
|
|
134
|
+
if hasattr(plugin_module, 'get_manifest'):
|
|
135
|
+
manifest_data = plugin_module.get_manifest()
|
|
136
|
+
manifest = PluginManifest.from_dict(manifest_data)
|
|
137
|
+
self._register_plugin_from_module(manifest, plugin_module)
|
|
138
|
+
loaded.append(manifest.name)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Failed to load entry point {ep.name}: {e}")
|
|
141
|
+
except ImportError:
|
|
142
|
+
pass # importlib.metadata not available
|
|
143
|
+
|
|
144
|
+
logger.info(f"Discovered {len(loaded)} installed plugins")
|
|
145
|
+
return loaded
|
|
146
|
+
|
|
147
|
+
def load_plugin(self, plugin_path: str) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Load a plugin from a directory.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
plugin_path: Path to plugin directory
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Loaded plugin name
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
PluginLoadError: If plugin cannot be loaded
|
|
159
|
+
"""
|
|
160
|
+
plugin_dir = Path(plugin_path).resolve()
|
|
161
|
+
|
|
162
|
+
# Check if already loaded
|
|
163
|
+
if str(plugin_dir) in self._loaded_paths:
|
|
164
|
+
logger.debug(f"Plugin already loaded: {plugin_dir}")
|
|
165
|
+
manifest = PluginManifest.from_file(plugin_dir / self.MANIFEST_FILENAME)
|
|
166
|
+
return manifest.name
|
|
167
|
+
|
|
168
|
+
# Find and parse manifest
|
|
169
|
+
manifest_path = plugin_dir / self.MANIFEST_FILENAME
|
|
170
|
+
if not manifest_path.exists():
|
|
171
|
+
raise PluginLoadError(f"Manifest not found: {manifest_path}")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
manifest = PluginManifest.from_file(manifest_path)
|
|
175
|
+
except ManifestError as e:
|
|
176
|
+
raise PluginLoadError(f"Invalid manifest: {e}")
|
|
177
|
+
|
|
178
|
+
# Validate manifest
|
|
179
|
+
errors = manifest.validate()
|
|
180
|
+
if errors:
|
|
181
|
+
raise PluginLoadError(f"Manifest validation failed: {errors}")
|
|
182
|
+
|
|
183
|
+
# Create plugin info
|
|
184
|
+
info = PluginInfo(
|
|
185
|
+
name=manifest.name,
|
|
186
|
+
version=manifest.version,
|
|
187
|
+
manifest=manifest,
|
|
188
|
+
source_path=plugin_dir
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Add plugin directory to Python path
|
|
192
|
+
plugin_src = plugin_dir / 'src'
|
|
193
|
+
if plugin_src.exists():
|
|
194
|
+
sys.path.insert(0, str(plugin_src))
|
|
195
|
+
sys.path.insert(0, str(plugin_dir))
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
# Load components
|
|
199
|
+
self._load_tag_handlers(manifest, info, plugin_dir)
|
|
200
|
+
self._load_node_classes(manifest, info, plugin_dir)
|
|
201
|
+
self._load_adapters(manifest, info, plugin_dir)
|
|
202
|
+
self._load_cli_commands(manifest, info, plugin_dir)
|
|
203
|
+
self._load_hooks(manifest, info, plugin_dir)
|
|
204
|
+
|
|
205
|
+
# Register plugin
|
|
206
|
+
self.registry.register_plugin(info)
|
|
207
|
+
self._loaded_paths.add(str(plugin_dir))
|
|
208
|
+
|
|
209
|
+
# Execute on_init hooks
|
|
210
|
+
from .hooks import HookContext
|
|
211
|
+
ctx = HookContext(
|
|
212
|
+
hook_type=HookType.ON_INIT,
|
|
213
|
+
data={'plugin': info}
|
|
214
|
+
)
|
|
215
|
+
self.hook_manager.execute(HookType.ON_INIT, ctx)
|
|
216
|
+
|
|
217
|
+
logger.info(f"Loaded plugin: {manifest.name}@{manifest.version}")
|
|
218
|
+
return manifest.name
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
# Clean up on failure
|
|
222
|
+
if str(plugin_src) in sys.path:
|
|
223
|
+
sys.path.remove(str(plugin_src))
|
|
224
|
+
if str(plugin_dir) in sys.path:
|
|
225
|
+
sys.path.remove(str(plugin_dir))
|
|
226
|
+
raise PluginLoadError(f"Failed to load plugin components: {e}")
|
|
227
|
+
|
|
228
|
+
def unload_plugin(self, name: str) -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Unload a plugin.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
name: Plugin name
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if plugin was unloaded
|
|
237
|
+
"""
|
|
238
|
+
info = self.registry.get_plugin(name)
|
|
239
|
+
if not info:
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# Execute on_shutdown hooks for this plugin
|
|
243
|
+
from .hooks import HookContext
|
|
244
|
+
ctx = HookContext(
|
|
245
|
+
hook_type=HookType.ON_SHUTDOWN,
|
|
246
|
+
data={'plugin': info}
|
|
247
|
+
)
|
|
248
|
+
self.hook_manager.execute(HookType.ON_SHUTDOWN, ctx)
|
|
249
|
+
|
|
250
|
+
# Unregister hooks
|
|
251
|
+
self.hook_manager.unregister_all(name)
|
|
252
|
+
|
|
253
|
+
# Unregister from registry
|
|
254
|
+
self.registry.unregister_plugin(name)
|
|
255
|
+
|
|
256
|
+
# Remove from loaded paths
|
|
257
|
+
if info.source_path:
|
|
258
|
+
path_str = str(info.source_path)
|
|
259
|
+
if path_str in self._loaded_paths:
|
|
260
|
+
self._loaded_paths.remove(path_str)
|
|
261
|
+
|
|
262
|
+
logger.info(f"Unloaded plugin: {name}")
|
|
263
|
+
return True
|
|
264
|
+
|
|
265
|
+
def reload_plugin(self, name: str) -> str:
|
|
266
|
+
"""
|
|
267
|
+
Reload a plugin (unload and load again).
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
name: Plugin name
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Plugin name
|
|
274
|
+
|
|
275
|
+
Raises:
|
|
276
|
+
PluginLoadError: If plugin cannot be reloaded
|
|
277
|
+
"""
|
|
278
|
+
info = self.registry.get_plugin(name)
|
|
279
|
+
if not info:
|
|
280
|
+
raise PluginLoadError(f"Plugin not found: {name}")
|
|
281
|
+
|
|
282
|
+
source_path = str(info.source_path)
|
|
283
|
+
self.unload_plugin(name)
|
|
284
|
+
return self.load_plugin(source_path)
|
|
285
|
+
|
|
286
|
+
def _load_installed_plugin(self, module_name: str) -> Optional[str]:
|
|
287
|
+
"""Load a plugin from an installed package"""
|
|
288
|
+
try:
|
|
289
|
+
module = importlib.import_module(module_name)
|
|
290
|
+
if hasattr(module, 'get_manifest'):
|
|
291
|
+
manifest_data = module.get_manifest()
|
|
292
|
+
manifest = PluginManifest.from_dict(manifest_data)
|
|
293
|
+
self._register_plugin_from_module(manifest, module)
|
|
294
|
+
return manifest.name
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Error loading installed plugin {module_name}: {e}")
|
|
297
|
+
return None
|
|
298
|
+
|
|
299
|
+
def _register_plugin_from_module(self, manifest: PluginManifest, module: Any) -> None:
|
|
300
|
+
"""Register a plugin loaded from a Python module"""
|
|
301
|
+
info = PluginInfo(
|
|
302
|
+
name=manifest.name,
|
|
303
|
+
version=manifest.version,
|
|
304
|
+
manifest=manifest,
|
|
305
|
+
source_path=Path(module.__file__).parent if hasattr(module, '__file__') else Path('.')
|
|
306
|
+
)
|
|
307
|
+
self.registry.register_plugin(info)
|
|
308
|
+
|
|
309
|
+
def _load_module_from_handler(self, handler_str: str, plugin_dir: Path) -> tuple:
|
|
310
|
+
"""
|
|
311
|
+
Load a module and get the specified object.
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
handler_str: Handler string in format "module.path:ClassName"
|
|
315
|
+
plugin_dir: Plugin directory
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Tuple of (module, object)
|
|
319
|
+
"""
|
|
320
|
+
if ':' not in handler_str:
|
|
321
|
+
raise PluginLoadError(f"Invalid handler format: {handler_str}. Expected 'module:Class'")
|
|
322
|
+
|
|
323
|
+
module_path, obj_name = handler_str.rsplit(':', 1)
|
|
324
|
+
|
|
325
|
+
# Try to import the module
|
|
326
|
+
try:
|
|
327
|
+
module = importlib.import_module(module_path)
|
|
328
|
+
except ImportError:
|
|
329
|
+
# Try as relative path from plugin dir
|
|
330
|
+
try:
|
|
331
|
+
module_file = plugin_dir / (module_path.replace('.', '/') + '.py')
|
|
332
|
+
if not module_file.exists():
|
|
333
|
+
module_file = plugin_dir / 'src' / (module_path.replace('.', '/') + '.py')
|
|
334
|
+
|
|
335
|
+
if not module_file.exists():
|
|
336
|
+
raise PluginLoadError(f"Module not found: {module_path}")
|
|
337
|
+
|
|
338
|
+
spec = importlib.util.spec_from_file_location(module_path, module_file)
|
|
339
|
+
module = importlib.util.module_from_spec(spec)
|
|
340
|
+
spec.loader.exec_module(module)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
raise PluginLoadError(f"Failed to import module {module_path}: {e}")
|
|
343
|
+
|
|
344
|
+
# Get the object
|
|
345
|
+
if not hasattr(module, obj_name):
|
|
346
|
+
raise PluginLoadError(f"Object not found in module: {obj_name}")
|
|
347
|
+
|
|
348
|
+
return module, getattr(module, obj_name)
|
|
349
|
+
|
|
350
|
+
def _load_tag_handlers(self, manifest: PluginManifest, info: PluginInfo, plugin_dir: Path) -> None:
|
|
351
|
+
"""Load tag handlers from manifest"""
|
|
352
|
+
for tag_def in manifest.tags:
|
|
353
|
+
try:
|
|
354
|
+
module, handler_class = self._load_module_from_handler(tag_def.handler, plugin_dir)
|
|
355
|
+
|
|
356
|
+
# Instantiate handler
|
|
357
|
+
if isinstance(handler_class, type):
|
|
358
|
+
handler = handler_class()
|
|
359
|
+
else:
|
|
360
|
+
handler = handler_class
|
|
361
|
+
|
|
362
|
+
# Set prefix if not set
|
|
363
|
+
if hasattr(handler, 'prefix') and not handler.prefix:
|
|
364
|
+
handler.prefix = tag_def.prefix
|
|
365
|
+
|
|
366
|
+
# Register
|
|
367
|
+
self.registry.register_tag_handler(tag_def.prefix, handler, manifest.name)
|
|
368
|
+
info.tag_handlers[tag_def.prefix] = handler
|
|
369
|
+
info.loaded_modules[tag_def.handler] = module
|
|
370
|
+
|
|
371
|
+
logger.debug(f"Loaded tag handler: {tag_def.prefix}:* from {tag_def.handler}")
|
|
372
|
+
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(f"Failed to load tag handler {tag_def.prefix}: {e}")
|
|
375
|
+
raise
|
|
376
|
+
|
|
377
|
+
def _load_node_classes(self, manifest: PluginManifest, info: PluginInfo, plugin_dir: Path) -> None:
|
|
378
|
+
"""Load AST node classes from manifest"""
|
|
379
|
+
for node_def in manifest.nodes:
|
|
380
|
+
try:
|
|
381
|
+
module, node_class = self._load_module_from_handler(node_def.handler, plugin_dir)
|
|
382
|
+
|
|
383
|
+
self.registry.register_node_class(node_def.name, node_class, manifest.name)
|
|
384
|
+
info.node_classes[node_def.name] = node_class
|
|
385
|
+
info.loaded_modules[node_def.handler] = module
|
|
386
|
+
|
|
387
|
+
logger.debug(f"Loaded node class: {node_def.name}")
|
|
388
|
+
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.error(f"Failed to load node class {node_def.name}: {e}")
|
|
391
|
+
raise
|
|
392
|
+
|
|
393
|
+
def _load_adapters(self, manifest: PluginManifest, info: PluginInfo, plugin_dir: Path) -> None:
|
|
394
|
+
"""Load adapters from manifest"""
|
|
395
|
+
for adapter_def in manifest.adapters:
|
|
396
|
+
try:
|
|
397
|
+
module, adapter_class = self._load_module_from_handler(adapter_def.handler, plugin_dir)
|
|
398
|
+
|
|
399
|
+
# Instantiate if class
|
|
400
|
+
if isinstance(adapter_class, type):
|
|
401
|
+
adapter = adapter_class()
|
|
402
|
+
else:
|
|
403
|
+
adapter = adapter_class
|
|
404
|
+
|
|
405
|
+
self.registry.register_adapter(adapter_def.name, adapter, manifest.name)
|
|
406
|
+
info.adapters[adapter_def.name] = adapter
|
|
407
|
+
info.loaded_modules[adapter_def.handler] = module
|
|
408
|
+
|
|
409
|
+
logger.debug(f"Loaded adapter: {adapter_def.name}")
|
|
410
|
+
|
|
411
|
+
except Exception as e:
|
|
412
|
+
logger.error(f"Failed to load adapter {adapter_def.name}: {e}")
|
|
413
|
+
raise
|
|
414
|
+
|
|
415
|
+
def _load_cli_commands(self, manifest: PluginManifest, info: PluginInfo, plugin_dir: Path) -> None:
|
|
416
|
+
"""Load CLI commands from manifest"""
|
|
417
|
+
for cmd_def in manifest.cli_commands:
|
|
418
|
+
try:
|
|
419
|
+
module, handler = self._load_module_from_handler(cmd_def.handler, plugin_dir)
|
|
420
|
+
|
|
421
|
+
self.registry.register_cli_command(
|
|
422
|
+
cmd_def.name,
|
|
423
|
+
handler,
|
|
424
|
+
manifest.name,
|
|
425
|
+
cmd_def.aliases
|
|
426
|
+
)
|
|
427
|
+
info.cli_commands[cmd_def.name] = handler
|
|
428
|
+
info.loaded_modules[cmd_def.handler] = module
|
|
429
|
+
|
|
430
|
+
logger.debug(f"Loaded CLI command: {cmd_def.name}")
|
|
431
|
+
|
|
432
|
+
except Exception as e:
|
|
433
|
+
logger.error(f"Failed to load CLI command {cmd_def.name}: {e}")
|
|
434
|
+
raise
|
|
435
|
+
|
|
436
|
+
def _load_hooks(self, manifest: PluginManifest, info: PluginInfo, plugin_dir: Path) -> None:
|
|
437
|
+
"""Load lifecycle hooks from manifest"""
|
|
438
|
+
for hook_def in manifest.hooks:
|
|
439
|
+
try:
|
|
440
|
+
module, handler = self._load_module_from_handler(hook_def.handler, plugin_dir)
|
|
441
|
+
|
|
442
|
+
# Convert string to HookType
|
|
443
|
+
hook_type = HookType(hook_def.hook_type)
|
|
444
|
+
|
|
445
|
+
self.hook_manager.register(
|
|
446
|
+
hook_type,
|
|
447
|
+
handler,
|
|
448
|
+
manifest.name,
|
|
449
|
+
hook_def.priority
|
|
450
|
+
)
|
|
451
|
+
info.loaded_modules[hook_def.handler] = module
|
|
452
|
+
|
|
453
|
+
logger.debug(f"Loaded hook: {hook_def.hook_type} from {hook_def.handler}")
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
logger.error(f"Failed to load hook {hook_def.hook_type}: {e}")
|
|
457
|
+
raise
|
|
458
|
+
|
|
459
|
+
def get_loaded_plugins(self) -> List[str]:
|
|
460
|
+
"""Get names of all loaded plugins"""
|
|
461
|
+
return list(self.registry.get_all_plugins().keys())
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Global loader instance
|
|
465
|
+
_global_loader: Optional[PluginLoader] = None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def get_loader() -> PluginLoader:
|
|
469
|
+
"""Get the global plugin loader instance"""
|
|
470
|
+
global _global_loader
|
|
471
|
+
if _global_loader is None:
|
|
472
|
+
_global_loader = PluginLoader()
|
|
473
|
+
return _global_loader
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def set_loader(loader: PluginLoader) -> None:
|
|
477
|
+
"""Set the global plugin loader instance"""
|
|
478
|
+
global _global_loader
|
|
479
|
+
_global_loader = loader
|