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,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Dependency Resolver
|
|
3
|
+
|
|
4
|
+
Resolves package dependencies with version constraints.
|
|
5
|
+
|
|
6
|
+
Supported version constraints:
|
|
7
|
+
- Exact: "1.0.0"
|
|
8
|
+
- Caret: "^1.0.0" (compatible with 1.x.x)
|
|
9
|
+
- Tilde: "~1.0.0" (compatible with 1.0.x)
|
|
10
|
+
- Range: ">=1.0.0", ">1.0.0", "<=1.0.0", "<1.0.0"
|
|
11
|
+
- Any: "*"
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
17
|
+
|
|
18
|
+
from .manifest import PackageManifest
|
|
19
|
+
from .registry import PackageRegistry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResolutionError(Exception):
|
|
23
|
+
"""Error resolving package dependencies."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class VersionConstraint:
|
|
29
|
+
"""Represents a version constraint."""
|
|
30
|
+
operator: str # exact, caret, tilde, >=, >, <=, <, any
|
|
31
|
+
major: int
|
|
32
|
+
minor: int
|
|
33
|
+
patch: int
|
|
34
|
+
prerelease: Optional[str] = None
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def parse(cls, constraint: str) -> 'VersionConstraint':
|
|
38
|
+
"""Parse version constraint string."""
|
|
39
|
+
constraint = constraint.strip()
|
|
40
|
+
|
|
41
|
+
if constraint == '*':
|
|
42
|
+
return cls(operator='any', major=0, minor=0, patch=0)
|
|
43
|
+
|
|
44
|
+
# Extract operator
|
|
45
|
+
if constraint.startswith('^'):
|
|
46
|
+
operator = 'caret'
|
|
47
|
+
version_str = constraint[1:]
|
|
48
|
+
elif constraint.startswith('~'):
|
|
49
|
+
operator = 'tilde'
|
|
50
|
+
version_str = constraint[1:]
|
|
51
|
+
elif constraint.startswith('>='):
|
|
52
|
+
operator = '>='
|
|
53
|
+
version_str = constraint[2:]
|
|
54
|
+
elif constraint.startswith('<='):
|
|
55
|
+
operator = '<='
|
|
56
|
+
version_str = constraint[2:]
|
|
57
|
+
elif constraint.startswith('>'):
|
|
58
|
+
operator = '>'
|
|
59
|
+
version_str = constraint[1:]
|
|
60
|
+
elif constraint.startswith('<'):
|
|
61
|
+
operator = '<'
|
|
62
|
+
version_str = constraint[1:]
|
|
63
|
+
else:
|
|
64
|
+
operator = 'exact'
|
|
65
|
+
version_str = constraint
|
|
66
|
+
|
|
67
|
+
# Parse version
|
|
68
|
+
match = re.match(r'^(\d+)\.(\d+)\.(\d+)(-[a-zA-Z0-9.]+)?$', version_str)
|
|
69
|
+
if not match:
|
|
70
|
+
raise ResolutionError(f"Invalid version format: {constraint}")
|
|
71
|
+
|
|
72
|
+
return cls(
|
|
73
|
+
operator=operator,
|
|
74
|
+
major=int(match.group(1)),
|
|
75
|
+
minor=int(match.group(2)),
|
|
76
|
+
patch=int(match.group(3)),
|
|
77
|
+
prerelease=match.group(4)[1:] if match.group(4) else None
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def satisfies(self, version: str) -> bool:
|
|
81
|
+
"""Check if a version satisfies this constraint."""
|
|
82
|
+
match = re.match(r'^(\d+)\.(\d+)\.(\d+)(-[a-zA-Z0-9.]+)?$', version)
|
|
83
|
+
if not match:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
v_major = int(match.group(1))
|
|
87
|
+
v_minor = int(match.group(2))
|
|
88
|
+
v_patch = int(match.group(3))
|
|
89
|
+
v_prerelease = match.group(4)[1:] if match.group(4) else None
|
|
90
|
+
|
|
91
|
+
if self.operator == 'any':
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
if self.operator == 'exact':
|
|
95
|
+
return (v_major == self.major and
|
|
96
|
+
v_minor == self.minor and
|
|
97
|
+
v_patch == self.patch)
|
|
98
|
+
|
|
99
|
+
if self.operator == 'caret':
|
|
100
|
+
# ^1.2.3 means >=1.2.3 and <2.0.0 (for major > 0)
|
|
101
|
+
# ^0.2.3 means >=0.2.3 and <0.3.0 (for major == 0)
|
|
102
|
+
if self.major > 0:
|
|
103
|
+
if v_major != self.major:
|
|
104
|
+
return False
|
|
105
|
+
return self._version_tuple(v_major, v_minor, v_patch) >= \
|
|
106
|
+
self._version_tuple(self.major, self.minor, self.patch)
|
|
107
|
+
else:
|
|
108
|
+
if v_major != 0 or v_minor != self.minor:
|
|
109
|
+
return False
|
|
110
|
+
return v_patch >= self.patch
|
|
111
|
+
|
|
112
|
+
if self.operator == 'tilde':
|
|
113
|
+
# ~1.2.3 means >=1.2.3 and <1.3.0
|
|
114
|
+
if v_major != self.major or v_minor != self.minor:
|
|
115
|
+
return False
|
|
116
|
+
return v_patch >= self.patch
|
|
117
|
+
|
|
118
|
+
if self.operator == '>=':
|
|
119
|
+
return self._version_tuple(v_major, v_minor, v_patch) >= \
|
|
120
|
+
self._version_tuple(self.major, self.minor, self.patch)
|
|
121
|
+
|
|
122
|
+
if self.operator == '>':
|
|
123
|
+
return self._version_tuple(v_major, v_minor, v_patch) > \
|
|
124
|
+
self._version_tuple(self.major, self.minor, self.patch)
|
|
125
|
+
|
|
126
|
+
if self.operator == '<=':
|
|
127
|
+
return self._version_tuple(v_major, v_minor, v_patch) <= \
|
|
128
|
+
self._version_tuple(self.major, self.minor, self.patch)
|
|
129
|
+
|
|
130
|
+
if self.operator == '<':
|
|
131
|
+
return self._version_tuple(v_major, v_minor, v_patch) < \
|
|
132
|
+
self._version_tuple(self.major, self.minor, self.patch)
|
|
133
|
+
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def _version_tuple(major: int, minor: int, patch: int) -> Tuple[int, int, int]:
|
|
138
|
+
return (major, minor, patch)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class ResolvedPackage:
|
|
143
|
+
"""A resolved package with specific version."""
|
|
144
|
+
name: str
|
|
145
|
+
version: str
|
|
146
|
+
manifest: PackageManifest
|
|
147
|
+
dependencies: List['ResolvedPackage']
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DependencyResolver:
|
|
151
|
+
"""
|
|
152
|
+
Resolves package dependencies.
|
|
153
|
+
|
|
154
|
+
Uses a simple depth-first resolution strategy with version constraints.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(self, registry: PackageRegistry):
|
|
158
|
+
self.registry = registry
|
|
159
|
+
self._resolution_cache: Dict[str, ResolvedPackage] = {}
|
|
160
|
+
self._resolution_stack: Set[str] = set()
|
|
161
|
+
|
|
162
|
+
def resolve(
|
|
163
|
+
self,
|
|
164
|
+
package_name: str,
|
|
165
|
+
version_constraint: str = '*'
|
|
166
|
+
) -> ResolvedPackage:
|
|
167
|
+
"""
|
|
168
|
+
Resolve a package and all its dependencies.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
package_name: Package to resolve
|
|
172
|
+
version_constraint: Version constraint string
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
ResolvedPackage with all dependencies resolved
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
ResolutionError: If resolution fails
|
|
179
|
+
"""
|
|
180
|
+
# Check for circular dependencies
|
|
181
|
+
if package_name in self._resolution_stack:
|
|
182
|
+
raise ResolutionError(
|
|
183
|
+
f"Circular dependency detected: {package_name}"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Check cache
|
|
187
|
+
cache_key = f"{package_name}@{version_constraint}"
|
|
188
|
+
if cache_key in self._resolution_cache:
|
|
189
|
+
return self._resolution_cache[cache_key]
|
|
190
|
+
|
|
191
|
+
# Add to resolution stack
|
|
192
|
+
self._resolution_stack.add(package_name)
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# Find matching version
|
|
196
|
+
version = self._find_matching_version(package_name, version_constraint)
|
|
197
|
+
if not version:
|
|
198
|
+
raise ResolutionError(
|
|
199
|
+
f"No matching version found for {package_name} {version_constraint}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Get manifest
|
|
203
|
+
manifest = self.registry.get_package(package_name, version)
|
|
204
|
+
if not manifest:
|
|
205
|
+
raise ResolutionError(
|
|
206
|
+
f"Package {package_name}@{version} not found in registry"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Resolve dependencies recursively
|
|
210
|
+
resolved_deps = []
|
|
211
|
+
for dep_name, dep_constraint in manifest.dependencies.items():
|
|
212
|
+
resolved_dep = self.resolve(dep_name, dep_constraint)
|
|
213
|
+
resolved_deps.append(resolved_dep)
|
|
214
|
+
|
|
215
|
+
# Create resolved package
|
|
216
|
+
resolved = ResolvedPackage(
|
|
217
|
+
name=package_name,
|
|
218
|
+
version=version,
|
|
219
|
+
manifest=manifest,
|
|
220
|
+
dependencies=resolved_deps
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Cache result
|
|
224
|
+
self._resolution_cache[cache_key] = resolved
|
|
225
|
+
|
|
226
|
+
return resolved
|
|
227
|
+
|
|
228
|
+
finally:
|
|
229
|
+
# Remove from resolution stack
|
|
230
|
+
self._resolution_stack.discard(package_name)
|
|
231
|
+
|
|
232
|
+
def resolve_all(
|
|
233
|
+
self,
|
|
234
|
+
dependencies: Dict[str, str]
|
|
235
|
+
) -> List[ResolvedPackage]:
|
|
236
|
+
"""
|
|
237
|
+
Resolve multiple dependencies.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
dependencies: Dict of package name -> version constraint
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
List of resolved packages
|
|
244
|
+
"""
|
|
245
|
+
resolved = []
|
|
246
|
+
for name, constraint in dependencies.items():
|
|
247
|
+
resolved.append(self.resolve(name, constraint))
|
|
248
|
+
return resolved
|
|
249
|
+
|
|
250
|
+
def _find_matching_version(
|
|
251
|
+
self,
|
|
252
|
+
package_name: str,
|
|
253
|
+
version_constraint: str
|
|
254
|
+
) -> Optional[str]:
|
|
255
|
+
"""Find the best matching version for a constraint."""
|
|
256
|
+
constraint = VersionConstraint.parse(version_constraint)
|
|
257
|
+
|
|
258
|
+
# Get all installed versions
|
|
259
|
+
versions = self.registry.get_all_versions(package_name)
|
|
260
|
+
if not versions:
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
# Find matching versions
|
|
264
|
+
matching = [v for v in versions if constraint.satisfies(v)]
|
|
265
|
+
if not matching:
|
|
266
|
+
return None
|
|
267
|
+
|
|
268
|
+
# Return highest matching version (already sorted in descending order)
|
|
269
|
+
return matching[0]
|
|
270
|
+
|
|
271
|
+
def get_install_order(self, resolved: ResolvedPackage) -> List[ResolvedPackage]:
|
|
272
|
+
"""
|
|
273
|
+
Get packages in installation order (dependencies first).
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
resolved: Resolved package with dependencies
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of packages in installation order
|
|
280
|
+
"""
|
|
281
|
+
order = []
|
|
282
|
+
visited = set()
|
|
283
|
+
|
|
284
|
+
def visit(pkg: ResolvedPackage):
|
|
285
|
+
if pkg.name in visited:
|
|
286
|
+
return
|
|
287
|
+
visited.add(pkg.name)
|
|
288
|
+
|
|
289
|
+
# Visit dependencies first
|
|
290
|
+
for dep in pkg.dependencies:
|
|
291
|
+
visit(dep)
|
|
292
|
+
|
|
293
|
+
order.append(pkg)
|
|
294
|
+
|
|
295
|
+
visit(resolved)
|
|
296
|
+
return order
|
|
297
|
+
|
|
298
|
+
def check_conflicts(
|
|
299
|
+
self,
|
|
300
|
+
packages: List[ResolvedPackage]
|
|
301
|
+
) -> List[str]:
|
|
302
|
+
"""
|
|
303
|
+
Check for version conflicts in resolved packages.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
packages: List of resolved packages
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
List of conflict descriptions (empty if no conflicts)
|
|
310
|
+
"""
|
|
311
|
+
conflicts = []
|
|
312
|
+
versions: Dict[str, Set[str]] = {}
|
|
313
|
+
|
|
314
|
+
def collect_versions(pkg: ResolvedPackage):
|
|
315
|
+
if pkg.name not in versions:
|
|
316
|
+
versions[pkg.name] = set()
|
|
317
|
+
versions[pkg.name].add(pkg.version)
|
|
318
|
+
|
|
319
|
+
for dep in pkg.dependencies:
|
|
320
|
+
collect_versions(dep)
|
|
321
|
+
|
|
322
|
+
for pkg in packages:
|
|
323
|
+
collect_versions(pkg)
|
|
324
|
+
|
|
325
|
+
for name, vers in versions.items():
|
|
326
|
+
if len(vers) > 1:
|
|
327
|
+
conflicts.append(
|
|
328
|
+
f"Conflicting versions of {name}: {', '.join(sorted(vers))}"
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return conflicts
|
|
332
|
+
|
|
333
|
+
def clear_cache(self):
|
|
334
|
+
"""Clear resolution cache."""
|
|
335
|
+
self._resolution_cache.clear()
|
|
336
|
+
self._resolution_stack.clear()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Plugin System
|
|
3
|
+
|
|
4
|
+
Provides extensible architecture for custom AST nodes, tags, adapters,
|
|
5
|
+
lifecycle hooks, and CLI commands.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from quantum.plugins import PluginLoader, PluginRegistry, HookManager
|
|
9
|
+
|
|
10
|
+
# Load plugins from directory
|
|
11
|
+
loader = PluginLoader()
|
|
12
|
+
loader.discover_plugins('./plugins')
|
|
13
|
+
|
|
14
|
+
# Access registry
|
|
15
|
+
registry = loader.registry
|
|
16
|
+
tag_handler = registry.get_tag_handler('my', 'custom-tag')
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .manifest import PluginManifest, ManifestError
|
|
20
|
+
from .registry import PluginRegistry, PluginInfo
|
|
21
|
+
from .hooks import HookManager, HookType
|
|
22
|
+
from .loader import PluginLoader, PluginLoadError
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
'PluginManifest',
|
|
26
|
+
'ManifestError',
|
|
27
|
+
'PluginRegistry',
|
|
28
|
+
'PluginInfo',
|
|
29
|
+
'HookManager',
|
|
30
|
+
'HookType',
|
|
31
|
+
'PluginLoader',
|
|
32
|
+
'PluginLoadError',
|
|
33
|
+
]
|
quantum/plugins/hooks.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Plugin Hook System
|
|
3
|
+
|
|
4
|
+
Provides lifecycle hooks that plugins can use to intercept and modify
|
|
5
|
+
the parsing, rendering, and execution phases.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Callable, Any, Dict, List, Optional
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HookType(Enum):
|
|
17
|
+
"""Available hook types in the Quantum lifecycle"""
|
|
18
|
+
|
|
19
|
+
# Parsing phase
|
|
20
|
+
BEFORE_PARSE = "before_parse" # Before XML is parsed
|
|
21
|
+
AFTER_PARSE = "after_parse" # After AST is created
|
|
22
|
+
|
|
23
|
+
# Rendering phase
|
|
24
|
+
BEFORE_RENDER = "before_render" # Before HTML generation
|
|
25
|
+
AFTER_RENDER = "after_render" # After HTML generation
|
|
26
|
+
|
|
27
|
+
# Execution phase
|
|
28
|
+
BEFORE_EXECUTE = "before_execute" # Before component execution
|
|
29
|
+
AFTER_EXECUTE = "after_execute" # After component execution
|
|
30
|
+
|
|
31
|
+
# Error handling
|
|
32
|
+
ON_ERROR = "on_error" # When an error occurs
|
|
33
|
+
|
|
34
|
+
# Lifecycle
|
|
35
|
+
ON_INIT = "on_init" # Plugin initialization
|
|
36
|
+
ON_SHUTDOWN = "on_shutdown" # Application shutdown
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class HookRegistration:
|
|
41
|
+
"""A registered hook callback"""
|
|
42
|
+
hook_type: HookType
|
|
43
|
+
callback: Callable
|
|
44
|
+
plugin_name: str
|
|
45
|
+
priority: int = 100 # Lower = earlier execution
|
|
46
|
+
|
|
47
|
+
def __lt__(self, other):
|
|
48
|
+
"""Allow sorting by priority"""
|
|
49
|
+
return self.priority < other.priority
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class HookContext:
|
|
54
|
+
"""Context passed to hook callbacks"""
|
|
55
|
+
hook_type: HookType
|
|
56
|
+
plugin_name: str = ""
|
|
57
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
# Parsing context
|
|
60
|
+
source: Optional[str] = None # Original source code
|
|
61
|
+
ast: Optional[Any] = None # Parsed AST
|
|
62
|
+
|
|
63
|
+
# Rendering context
|
|
64
|
+
component: Optional[Any] = None # Component being rendered
|
|
65
|
+
html: Optional[str] = None # Rendered HTML
|
|
66
|
+
|
|
67
|
+
# Execution context
|
|
68
|
+
params: Optional[Dict] = None # Execution parameters
|
|
69
|
+
result: Optional[Any] = None # Execution result
|
|
70
|
+
|
|
71
|
+
# Error context
|
|
72
|
+
error: Optional[Exception] = None # Error that occurred
|
|
73
|
+
|
|
74
|
+
# Control flow
|
|
75
|
+
skip: bool = False # Skip remaining processing
|
|
76
|
+
modified: bool = False # Data was modified
|
|
77
|
+
|
|
78
|
+
def set_result(self, value: Any):
|
|
79
|
+
"""Set result and mark as modified"""
|
|
80
|
+
self.result = value
|
|
81
|
+
self.modified = True
|
|
82
|
+
|
|
83
|
+
def set_html(self, value: str):
|
|
84
|
+
"""Set HTML and mark as modified"""
|
|
85
|
+
self.html = value
|
|
86
|
+
self.modified = True
|
|
87
|
+
|
|
88
|
+
def set_ast(self, value: Any):
|
|
89
|
+
"""Set AST and mark as modified"""
|
|
90
|
+
self.ast = value
|
|
91
|
+
self.modified = True
|
|
92
|
+
|
|
93
|
+
def skip_remaining(self):
|
|
94
|
+
"""Skip remaining hooks and processing"""
|
|
95
|
+
self.skip = True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class HookManager:
|
|
99
|
+
"""
|
|
100
|
+
Manages plugin lifecycle hooks.
|
|
101
|
+
|
|
102
|
+
Example usage:
|
|
103
|
+
manager = HookManager()
|
|
104
|
+
|
|
105
|
+
# Register a hook
|
|
106
|
+
def my_before_render(ctx: HookContext):
|
|
107
|
+
print(f"Rendering: {ctx.component}")
|
|
108
|
+
ctx.data['custom'] = 'value'
|
|
109
|
+
|
|
110
|
+
manager.register(HookType.BEFORE_RENDER, my_before_render, "my-plugin")
|
|
111
|
+
|
|
112
|
+
# Execute hooks
|
|
113
|
+
ctx = HookContext(hook_type=HookType.BEFORE_RENDER)
|
|
114
|
+
ctx.component = my_component
|
|
115
|
+
manager.execute(HookType.BEFORE_RENDER, ctx)
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self):
|
|
119
|
+
self._hooks: Dict[HookType, List[HookRegistration]] = {
|
|
120
|
+
hook_type: [] for hook_type in HookType
|
|
121
|
+
}
|
|
122
|
+
self._enabled = True
|
|
123
|
+
|
|
124
|
+
def register(
|
|
125
|
+
self,
|
|
126
|
+
hook_type: HookType,
|
|
127
|
+
callback: Callable[[HookContext], None],
|
|
128
|
+
plugin_name: str,
|
|
129
|
+
priority: int = 100
|
|
130
|
+
) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Register a hook callback.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
hook_type: Type of hook to register
|
|
136
|
+
callback: Function to call when hook is triggered
|
|
137
|
+
plugin_name: Name of the plugin registering the hook
|
|
138
|
+
priority: Execution priority (lower = earlier)
|
|
139
|
+
"""
|
|
140
|
+
registration = HookRegistration(
|
|
141
|
+
hook_type=hook_type,
|
|
142
|
+
callback=callback,
|
|
143
|
+
plugin_name=plugin_name,
|
|
144
|
+
priority=priority
|
|
145
|
+
)
|
|
146
|
+
self._hooks[hook_type].append(registration)
|
|
147
|
+
# Keep sorted by priority
|
|
148
|
+
self._hooks[hook_type].sort()
|
|
149
|
+
|
|
150
|
+
logger.debug(f"Registered hook: {hook_type.value} from {plugin_name} (priority={priority})")
|
|
151
|
+
|
|
152
|
+
def unregister(self, hook_type: HookType, plugin_name: str) -> int:
|
|
153
|
+
"""
|
|
154
|
+
Unregister all hooks of a type for a plugin.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Number of hooks removed
|
|
158
|
+
"""
|
|
159
|
+
original_count = len(self._hooks[hook_type])
|
|
160
|
+
self._hooks[hook_type] = [
|
|
161
|
+
h for h in self._hooks[hook_type]
|
|
162
|
+
if h.plugin_name != plugin_name
|
|
163
|
+
]
|
|
164
|
+
removed = original_count - len(self._hooks[hook_type])
|
|
165
|
+
if removed:
|
|
166
|
+
logger.debug(f"Unregistered {removed} hooks for {plugin_name}")
|
|
167
|
+
return removed
|
|
168
|
+
|
|
169
|
+
def unregister_all(self, plugin_name: str) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Unregister all hooks for a plugin.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Total number of hooks removed
|
|
175
|
+
"""
|
|
176
|
+
total_removed = 0
|
|
177
|
+
for hook_type in HookType:
|
|
178
|
+
total_removed += self.unregister(hook_type, plugin_name)
|
|
179
|
+
return total_removed
|
|
180
|
+
|
|
181
|
+
def execute(self, hook_type: HookType, context: HookContext) -> HookContext:
|
|
182
|
+
"""
|
|
183
|
+
Execute all registered hooks of a type.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
hook_type: Type of hook to execute
|
|
187
|
+
context: Context to pass to callbacks
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Modified context after all hooks execute
|
|
191
|
+
"""
|
|
192
|
+
if not self._enabled:
|
|
193
|
+
return context
|
|
194
|
+
|
|
195
|
+
hooks = self._hooks.get(hook_type, [])
|
|
196
|
+
if not hooks:
|
|
197
|
+
return context
|
|
198
|
+
|
|
199
|
+
logger.debug(f"Executing {len(hooks)} hooks for {hook_type.value}")
|
|
200
|
+
|
|
201
|
+
for registration in hooks:
|
|
202
|
+
if context.skip:
|
|
203
|
+
logger.debug(f"Skipping remaining hooks after {context.plugin_name}")
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
context.plugin_name = registration.plugin_name
|
|
208
|
+
registration.callback(context)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Hook error in {registration.plugin_name}: {e}")
|
|
211
|
+
# Don't propagate hook errors unless it's an ON_ERROR hook
|
|
212
|
+
if hook_type != HookType.ON_ERROR:
|
|
213
|
+
# Execute error hooks
|
|
214
|
+
error_ctx = HookContext(
|
|
215
|
+
hook_type=HookType.ON_ERROR,
|
|
216
|
+
error=e,
|
|
217
|
+
data={'original_hook': hook_type.value}
|
|
218
|
+
)
|
|
219
|
+
self.execute(HookType.ON_ERROR, error_ctx)
|
|
220
|
+
|
|
221
|
+
return context
|
|
222
|
+
|
|
223
|
+
def execute_before_parse(self, source: str) -> str:
|
|
224
|
+
"""Execute before_parse hooks and return potentially modified source"""
|
|
225
|
+
ctx = HookContext(hook_type=HookType.BEFORE_PARSE, source=source)
|
|
226
|
+
ctx = self.execute(HookType.BEFORE_PARSE, ctx)
|
|
227
|
+
return ctx.source if ctx.modified else source
|
|
228
|
+
|
|
229
|
+
def execute_after_parse(self, ast: Any) -> Any:
|
|
230
|
+
"""Execute after_parse hooks and return potentially modified AST"""
|
|
231
|
+
ctx = HookContext(hook_type=HookType.AFTER_PARSE, ast=ast)
|
|
232
|
+
ctx = self.execute(HookType.AFTER_PARSE, ctx)
|
|
233
|
+
return ctx.ast if ctx.modified else ast
|
|
234
|
+
|
|
235
|
+
def execute_before_render(self, component: Any, params: Dict = None) -> HookContext:
|
|
236
|
+
"""Execute before_render hooks"""
|
|
237
|
+
ctx = HookContext(
|
|
238
|
+
hook_type=HookType.BEFORE_RENDER,
|
|
239
|
+
component=component,
|
|
240
|
+
params=params or {}
|
|
241
|
+
)
|
|
242
|
+
return self.execute(HookType.BEFORE_RENDER, ctx)
|
|
243
|
+
|
|
244
|
+
def execute_after_render(self, component: Any, html: str) -> str:
|
|
245
|
+
"""Execute after_render hooks and return potentially modified HTML"""
|
|
246
|
+
ctx = HookContext(
|
|
247
|
+
hook_type=HookType.AFTER_RENDER,
|
|
248
|
+
component=component,
|
|
249
|
+
html=html
|
|
250
|
+
)
|
|
251
|
+
ctx = self.execute(HookType.AFTER_RENDER, ctx)
|
|
252
|
+
return ctx.html if ctx.modified else html
|
|
253
|
+
|
|
254
|
+
def execute_before_execute(self, component: Any, params: Dict = None) -> HookContext:
|
|
255
|
+
"""Execute before_execute hooks"""
|
|
256
|
+
ctx = HookContext(
|
|
257
|
+
hook_type=HookType.BEFORE_EXECUTE,
|
|
258
|
+
component=component,
|
|
259
|
+
params=params or {}
|
|
260
|
+
)
|
|
261
|
+
return self.execute(HookType.BEFORE_EXECUTE, ctx)
|
|
262
|
+
|
|
263
|
+
def execute_after_execute(self, component: Any, result: Any) -> Any:
|
|
264
|
+
"""Execute after_execute hooks and return potentially modified result"""
|
|
265
|
+
ctx = HookContext(
|
|
266
|
+
hook_type=HookType.AFTER_EXECUTE,
|
|
267
|
+
component=component,
|
|
268
|
+
result=result
|
|
269
|
+
)
|
|
270
|
+
ctx = self.execute(HookType.AFTER_EXECUTE, ctx)
|
|
271
|
+
return ctx.result if ctx.modified else result
|
|
272
|
+
|
|
273
|
+
def get_hooks(self, hook_type: HookType) -> List[HookRegistration]:
|
|
274
|
+
"""Get all registered hooks of a type"""
|
|
275
|
+
return self._hooks.get(hook_type, [])
|
|
276
|
+
|
|
277
|
+
def get_all_hooks(self) -> Dict[HookType, List[HookRegistration]]:
|
|
278
|
+
"""Get all registered hooks"""
|
|
279
|
+
return self._hooks
|
|
280
|
+
|
|
281
|
+
def enable(self):
|
|
282
|
+
"""Enable hook execution"""
|
|
283
|
+
self._enabled = True
|
|
284
|
+
|
|
285
|
+
def disable(self):
|
|
286
|
+
"""Disable hook execution"""
|
|
287
|
+
self._enabled = False
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def is_enabled(self) -> bool:
|
|
291
|
+
"""Check if hooks are enabled"""
|
|
292
|
+
return self._enabled
|
|
293
|
+
|
|
294
|
+
def clear(self):
|
|
295
|
+
"""Clear all registered hooks"""
|
|
296
|
+
for hook_type in HookType:
|
|
297
|
+
self._hooks[hook_type] = []
|
|
298
|
+
|
|
299
|
+
def count(self, hook_type: HookType = None) -> int:
|
|
300
|
+
"""
|
|
301
|
+
Count registered hooks.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
hook_type: Specific type to count, or None for total
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Number of registered hooks
|
|
308
|
+
"""
|
|
309
|
+
if hook_type:
|
|
310
|
+
return len(self._hooks.get(hook_type, []))
|
|
311
|
+
return sum(len(hooks) for hooks in self._hooks.values())
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# Global hook manager instance
|
|
315
|
+
_global_hook_manager: Optional[HookManager] = None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def get_hook_manager() -> HookManager:
|
|
319
|
+
"""Get the global hook manager instance"""
|
|
320
|
+
global _global_hook_manager
|
|
321
|
+
if _global_hook_manager is None:
|
|
322
|
+
_global_hook_manager = HookManager()
|
|
323
|
+
return _global_hook_manager
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def set_hook_manager(manager: HookManager) -> None:
|
|
327
|
+
"""Set the global hook manager instance"""
|
|
328
|
+
global _global_hook_manager
|
|
329
|
+
_global_hook_manager = manager
|