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,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum API Server - API server for Quantum microservices
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from flask import Flask, jsonify, request
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
|
|
11
|
+
# Fix imports
|
|
12
|
+
|
|
13
|
+
from quantum.core.ast_nodes import ApplicationNode, QuantumRoute
|
|
14
|
+
|
|
15
|
+
class QuantumAPIServer:
|
|
16
|
+
"""API server for Quantum microservices"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, port: int = 8080):
|
|
19
|
+
self.port = port
|
|
20
|
+
self.app = Flask(__name__)
|
|
21
|
+
self.routes: Dict[str, Dict[str, Any]] = {}
|
|
22
|
+
self._setup_default_routes()
|
|
23
|
+
|
|
24
|
+
def _setup_default_routes(self):
|
|
25
|
+
"""Configure default API routes"""
|
|
26
|
+
@self.app.route('/health', methods=['GET'])
|
|
27
|
+
def health_check():
|
|
28
|
+
return jsonify({
|
|
29
|
+
"status": "healthy",
|
|
30
|
+
"server": "Quantum API",
|
|
31
|
+
"routes": list(self.routes.keys())
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
def configure_from_ast(self, app_node: ApplicationNode):
|
|
35
|
+
"""Configure API based on application AST"""
|
|
36
|
+
for route in app_node.routes:
|
|
37
|
+
self._add_route_from_ast(route)
|
|
38
|
+
|
|
39
|
+
def _add_route_from_ast(self, route: QuantumRoute):
|
|
40
|
+
"""Add API route based on AST"""
|
|
41
|
+
path = route.path
|
|
42
|
+
method = route.method
|
|
43
|
+
|
|
44
|
+
# Get first q:return from route
|
|
45
|
+
content = "{}"
|
|
46
|
+
if route.returns:
|
|
47
|
+
content = route.returns[0].value or "{}"
|
|
48
|
+
|
|
49
|
+
self._add_api_route(path, method, content)
|
|
50
|
+
|
|
51
|
+
def _add_api_route(self, path: str, method: str, content: str):
|
|
52
|
+
"""Add API route to Flask"""
|
|
53
|
+
self.routes[path] = {'method': method, 'content': content}
|
|
54
|
+
|
|
55
|
+
def api_handler():
|
|
56
|
+
return self._process_json_content(content)
|
|
57
|
+
|
|
58
|
+
# Unique name for function
|
|
59
|
+
api_handler.__name__ = f"api_{path.replace('/', '_').replace('-', '_')}"
|
|
60
|
+
|
|
61
|
+
# Add to Flask
|
|
62
|
+
self.app.add_url_rule(path, api_handler.__name__, api_handler, methods=[method])
|
|
63
|
+
|
|
64
|
+
def _process_json_content(self, content: str):
|
|
65
|
+
"""Process content as JSON"""
|
|
66
|
+
try:
|
|
67
|
+
# Try to parse JSON
|
|
68
|
+
parsed = json.loads(content)
|
|
69
|
+
return jsonify(parsed)
|
|
70
|
+
except json.JSONDecodeError:
|
|
71
|
+
# If not valid JSON, return as text
|
|
72
|
+
return jsonify({"result": content, "type": "text"})
|
|
73
|
+
|
|
74
|
+
def start(self):
|
|
75
|
+
"""Start API server"""
|
|
76
|
+
print(f"🛠️ Quantum API Server starting on port {self.port}")
|
|
77
|
+
print(f"🔗 API: http://localhost:{self.port}")
|
|
78
|
+
print(f"🩺 Health: http://localhost:{self.port}/health")
|
|
79
|
+
print("🛑 To stop: Ctrl+C")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
self.app.run(host='0.0.0.0', port=self.port, debug=False)
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
print("\n👋 API server stopped by user")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"❌ Server error: {e}")
|
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Cache - Phase 2 Performance Optimization
|
|
3
|
+
|
|
4
|
+
Caches parsed AST structures for faster component loading.
|
|
5
|
+
Uses mtime-based invalidation to detect file changes.
|
|
6
|
+
|
|
7
|
+
Performance Impact:
|
|
8
|
+
- 10-50x speedup on subsequent file loads
|
|
9
|
+
- Automatic invalidation when files change
|
|
10
|
+
- LRU eviction with configurable size
|
|
11
|
+
- Thread-safe implementation
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import hashlib
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
import pickle
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, Optional, Tuple, List
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from functools import lru_cache
|
|
23
|
+
import weakref
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class CacheEntry:
|
|
28
|
+
"""A cached AST entry with metadata"""
|
|
29
|
+
ast: Any # ComponentNode
|
|
30
|
+
mtime: float # File modification time
|
|
31
|
+
size: int # File size in bytes
|
|
32
|
+
hash: str # Content hash for extra validation
|
|
33
|
+
created_at: float = field(default_factory=time.time)
|
|
34
|
+
access_count: int = 0
|
|
35
|
+
last_accessed: float = field(default_factory=time.time)
|
|
36
|
+
|
|
37
|
+
def touch(self):
|
|
38
|
+
"""Update access statistics"""
|
|
39
|
+
self.access_count += 1
|
|
40
|
+
self.last_accessed = time.time()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class CacheStats:
|
|
45
|
+
"""Statistics for cache performance monitoring"""
|
|
46
|
+
hits: int = 0
|
|
47
|
+
misses: int = 0
|
|
48
|
+
invalidations: int = 0
|
|
49
|
+
evictions: int = 0
|
|
50
|
+
total_parse_time_ms: float = 0.0
|
|
51
|
+
total_cache_time_ms: float = 0.0
|
|
52
|
+
entries_count: int = 0
|
|
53
|
+
memory_estimate_kb: float = 0.0
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def hit_rate(self) -> float:
|
|
57
|
+
total = self.hits + self.misses
|
|
58
|
+
return self.hits / total if total > 0 else 0.0
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def avg_parse_time_ms(self) -> float:
|
|
62
|
+
return self.total_parse_time_ms / self.misses if self.misses > 0 else 0.0
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
'hits': self.hits,
|
|
67
|
+
'misses': self.misses,
|
|
68
|
+
'hit_rate': f"{self.hit_rate:.1%}",
|
|
69
|
+
'invalidations': self.invalidations,
|
|
70
|
+
'evictions': self.evictions,
|
|
71
|
+
'entries_count': self.entries_count,
|
|
72
|
+
'memory_estimate_kb': f"{self.memory_estimate_kb:.1f}",
|
|
73
|
+
'avg_parse_time_ms': f"{self.avg_parse_time_ms:.2f}",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ASTCache:
|
|
78
|
+
"""
|
|
79
|
+
Thread-safe LRU cache for parsed AST structures.
|
|
80
|
+
|
|
81
|
+
Features:
|
|
82
|
+
- Caches parsed ComponentNode objects by file path
|
|
83
|
+
- Automatic invalidation based on file mtime and size
|
|
84
|
+
- Optional content hash validation for extra safety
|
|
85
|
+
- LRU eviction with configurable max entries
|
|
86
|
+
- Statistics tracking for monitoring
|
|
87
|
+
- Filesystem watch integration support
|
|
88
|
+
|
|
89
|
+
Usage:
|
|
90
|
+
cache = ASTCache(max_entries=100)
|
|
91
|
+
|
|
92
|
+
# Get cached AST or parse if needed
|
|
93
|
+
ast = cache.get_or_parse("app.q", parser)
|
|
94
|
+
|
|
95
|
+
# Manual invalidation
|
|
96
|
+
cache.invalidate("app.q")
|
|
97
|
+
|
|
98
|
+
# Check statistics
|
|
99
|
+
print(cache.stats.hit_rate)
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
max_entries: int = 100,
|
|
105
|
+
enable_hash_validation: bool = False,
|
|
106
|
+
enable_stats: bool = True,
|
|
107
|
+
cache_dir: Optional[Path] = None,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Initialize the AST cache.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
max_entries: Maximum number of AST entries to cache
|
|
114
|
+
enable_hash_validation: Whether to validate content hash (slower but safer)
|
|
115
|
+
enable_stats: Whether to track performance statistics
|
|
116
|
+
cache_dir: Optional directory for persistent cache (not implemented yet)
|
|
117
|
+
"""
|
|
118
|
+
self._max_entries = max_entries
|
|
119
|
+
self._enable_hash = enable_hash_validation
|
|
120
|
+
self._enable_stats = enable_stats
|
|
121
|
+
self._cache_dir = cache_dir
|
|
122
|
+
|
|
123
|
+
self._cache: Dict[str, CacheEntry] = {}
|
|
124
|
+
self._stats = CacheStats()
|
|
125
|
+
self._lock = threading.RLock()
|
|
126
|
+
|
|
127
|
+
# For dependency tracking (imports)
|
|
128
|
+
self._dependencies: Dict[str, set] = {} # file -> set of files it imports
|
|
129
|
+
|
|
130
|
+
def _normalize_path(self, file_path: Any) -> str:
|
|
131
|
+
"""Normalize a file path to a canonical string key"""
|
|
132
|
+
if isinstance(file_path, Path):
|
|
133
|
+
return str(file_path.resolve())
|
|
134
|
+
return str(Path(file_path).resolve())
|
|
135
|
+
|
|
136
|
+
def _compute_hash(self, content: str) -> str:
|
|
137
|
+
"""Compute a hash of the file content"""
|
|
138
|
+
return hashlib.md5(content.encode('utf-8')).hexdigest()
|
|
139
|
+
|
|
140
|
+
def _get_file_info(self, file_path: str) -> Tuple[float, int]:
|
|
141
|
+
"""Get file mtime and size"""
|
|
142
|
+
try:
|
|
143
|
+
stat = os.stat(file_path)
|
|
144
|
+
return stat.st_mtime, stat.st_size
|
|
145
|
+
except OSError:
|
|
146
|
+
return 0.0, 0
|
|
147
|
+
|
|
148
|
+
def _is_valid(self, entry: CacheEntry, file_path: str, content: Optional[str] = None) -> bool:
|
|
149
|
+
"""Check if a cache entry is still valid"""
|
|
150
|
+
mtime, size = self._get_file_info(file_path)
|
|
151
|
+
|
|
152
|
+
# Check mtime and size
|
|
153
|
+
if entry.mtime != mtime or entry.size != size:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# Optionally check content hash
|
|
157
|
+
if self._enable_hash and content is not None:
|
|
158
|
+
content_hash = self._compute_hash(content)
|
|
159
|
+
if entry.hash != content_hash:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def _evict_lru(self):
|
|
165
|
+
"""Evict least recently used entries if over capacity"""
|
|
166
|
+
while len(self._cache) >= self._max_entries:
|
|
167
|
+
# Find LRU entry
|
|
168
|
+
lru_key = min(
|
|
169
|
+
self._cache.keys(),
|
|
170
|
+
key=lambda k: self._cache[k].last_accessed
|
|
171
|
+
)
|
|
172
|
+
del self._cache[lru_key]
|
|
173
|
+
if self._enable_stats:
|
|
174
|
+
self._stats.evictions += 1
|
|
175
|
+
|
|
176
|
+
def _estimate_memory(self, ast: Any) -> float:
|
|
177
|
+
"""Estimate memory usage of an AST in KB"""
|
|
178
|
+
try:
|
|
179
|
+
# Rough estimate using pickle size
|
|
180
|
+
data = pickle.dumps(ast)
|
|
181
|
+
return len(data) / 1024
|
|
182
|
+
except:
|
|
183
|
+
return 10.0 # Default estimate
|
|
184
|
+
|
|
185
|
+
def get(self, file_path: Any) -> Optional[Any]:
|
|
186
|
+
"""
|
|
187
|
+
Get a cached AST if available and valid.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
file_path: Path to the .q file
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Cached ComponentNode or None if not cached/invalid
|
|
194
|
+
"""
|
|
195
|
+
key = self._normalize_path(file_path)
|
|
196
|
+
|
|
197
|
+
with self._lock:
|
|
198
|
+
if key not in self._cache:
|
|
199
|
+
if self._enable_stats:
|
|
200
|
+
self._stats.misses += 1
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
entry = self._cache[key]
|
|
204
|
+
|
|
205
|
+
# Validate entry
|
|
206
|
+
if not self._is_valid(entry, key):
|
|
207
|
+
del self._cache[key]
|
|
208
|
+
if self._enable_stats:
|
|
209
|
+
self._stats.misses += 1
|
|
210
|
+
self._stats.invalidations += 1
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
# Cache hit
|
|
214
|
+
entry.touch()
|
|
215
|
+
if self._enable_stats:
|
|
216
|
+
self._stats.hits += 1
|
|
217
|
+
|
|
218
|
+
return entry.ast
|
|
219
|
+
|
|
220
|
+
def get_or_parse(
|
|
221
|
+
self,
|
|
222
|
+
file_path: Any,
|
|
223
|
+
parser: Any,
|
|
224
|
+
content: Optional[str] = None,
|
|
225
|
+
) -> Any:
|
|
226
|
+
"""
|
|
227
|
+
Get cached AST or parse the file if not cached.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
file_path: Path to the .q file
|
|
231
|
+
parser: QuantumParser instance to use for parsing
|
|
232
|
+
content: Optional file content (if already read)
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
ComponentNode (either cached or freshly parsed)
|
|
236
|
+
"""
|
|
237
|
+
key = self._normalize_path(file_path)
|
|
238
|
+
|
|
239
|
+
with self._lock:
|
|
240
|
+
# Try cache first
|
|
241
|
+
cached = self.get(key)
|
|
242
|
+
if cached is not None:
|
|
243
|
+
return cached
|
|
244
|
+
|
|
245
|
+
# Need to parse
|
|
246
|
+
start_time = time.perf_counter()
|
|
247
|
+
|
|
248
|
+
# Read content if not provided
|
|
249
|
+
if content is None:
|
|
250
|
+
with open(key, 'r', encoding='utf-8') as f:
|
|
251
|
+
content = f.read()
|
|
252
|
+
|
|
253
|
+
# Parse
|
|
254
|
+
ast = parser.parse(content)
|
|
255
|
+
|
|
256
|
+
parse_time = (time.perf_counter() - start_time) * 1000
|
|
257
|
+
|
|
258
|
+
# Get file info
|
|
259
|
+
mtime, size = self._get_file_info(key)
|
|
260
|
+
content_hash = self._compute_hash(content) if self._enable_hash else ""
|
|
261
|
+
|
|
262
|
+
# Evict if needed
|
|
263
|
+
self._evict_lru()
|
|
264
|
+
|
|
265
|
+
# Cache the result
|
|
266
|
+
entry = CacheEntry(
|
|
267
|
+
ast=ast,
|
|
268
|
+
mtime=mtime,
|
|
269
|
+
size=size,
|
|
270
|
+
hash=content_hash,
|
|
271
|
+
)
|
|
272
|
+
self._cache[key] = entry
|
|
273
|
+
|
|
274
|
+
# Update stats
|
|
275
|
+
if self._enable_stats:
|
|
276
|
+
self._stats.total_parse_time_ms += parse_time
|
|
277
|
+
self._stats.entries_count = len(self._cache)
|
|
278
|
+
self._stats.memory_estimate_kb += self._estimate_memory(ast)
|
|
279
|
+
|
|
280
|
+
return ast
|
|
281
|
+
|
|
282
|
+
def put(self, file_path: Any, ast: Any, content: Optional[str] = None):
|
|
283
|
+
"""
|
|
284
|
+
Manually put an AST into the cache.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
file_path: Path to the .q file
|
|
288
|
+
ast: The parsed ComponentNode
|
|
289
|
+
content: Optional file content for hash computation
|
|
290
|
+
"""
|
|
291
|
+
key = self._normalize_path(file_path)
|
|
292
|
+
|
|
293
|
+
with self._lock:
|
|
294
|
+
mtime, size = self._get_file_info(key)
|
|
295
|
+
content_hash = ""
|
|
296
|
+
if self._enable_hash and content is not None:
|
|
297
|
+
content_hash = self._compute_hash(content)
|
|
298
|
+
|
|
299
|
+
self._evict_lru()
|
|
300
|
+
|
|
301
|
+
entry = CacheEntry(
|
|
302
|
+
ast=ast,
|
|
303
|
+
mtime=mtime,
|
|
304
|
+
size=size,
|
|
305
|
+
hash=content_hash,
|
|
306
|
+
)
|
|
307
|
+
self._cache[key] = entry
|
|
308
|
+
|
|
309
|
+
if self._enable_stats:
|
|
310
|
+
self._stats.entries_count = len(self._cache)
|
|
311
|
+
|
|
312
|
+
def invalidate(self, file_path: Optional[Any] = None):
|
|
313
|
+
"""
|
|
314
|
+
Invalidate cache entries.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
file_path: Specific file to invalidate, or None for all
|
|
318
|
+
"""
|
|
319
|
+
with self._lock:
|
|
320
|
+
if file_path is None:
|
|
321
|
+
count = len(self._cache)
|
|
322
|
+
self._cache.clear()
|
|
323
|
+
self._dependencies.clear()
|
|
324
|
+
if self._enable_stats:
|
|
325
|
+
self._stats.invalidations += count
|
|
326
|
+
self._stats.entries_count = 0
|
|
327
|
+
else:
|
|
328
|
+
key = self._normalize_path(file_path)
|
|
329
|
+
if key in self._cache:
|
|
330
|
+
del self._cache[key]
|
|
331
|
+
if self._enable_stats:
|
|
332
|
+
self._stats.invalidations += 1
|
|
333
|
+
self._stats.entries_count = len(self._cache)
|
|
334
|
+
|
|
335
|
+
# Also invalidate dependents
|
|
336
|
+
self._invalidate_dependents(key)
|
|
337
|
+
|
|
338
|
+
def _invalidate_dependents(self, file_path: str):
|
|
339
|
+
"""Invalidate all files that depend on this file"""
|
|
340
|
+
dependents = []
|
|
341
|
+
for path, deps in self._dependencies.items():
|
|
342
|
+
if file_path in deps:
|
|
343
|
+
dependents.append(path)
|
|
344
|
+
|
|
345
|
+
for dep_path in dependents:
|
|
346
|
+
if dep_path in self._cache:
|
|
347
|
+
del self._cache[dep_path]
|
|
348
|
+
if self._enable_stats:
|
|
349
|
+
self._stats.invalidations += 1
|
|
350
|
+
|
|
351
|
+
def register_dependency(self, file_path: Any, depends_on: Any):
|
|
352
|
+
"""
|
|
353
|
+
Register that file_path depends on depends_on.
|
|
354
|
+
Used for import tracking.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
file_path: The file that has a dependency
|
|
358
|
+
depends_on: The file it depends on (imports)
|
|
359
|
+
"""
|
|
360
|
+
key = self._normalize_path(file_path)
|
|
361
|
+
dep_key = self._normalize_path(depends_on)
|
|
362
|
+
|
|
363
|
+
with self._lock:
|
|
364
|
+
if key not in self._dependencies:
|
|
365
|
+
self._dependencies[key] = set()
|
|
366
|
+
self._dependencies[key].add(dep_key)
|
|
367
|
+
|
|
368
|
+
def preload(self, file_paths: List[Any], parser: Any):
|
|
369
|
+
"""
|
|
370
|
+
Preload multiple files into the cache.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
file_paths: List of paths to preload
|
|
374
|
+
parser: QuantumParser instance
|
|
375
|
+
"""
|
|
376
|
+
for file_path in file_paths:
|
|
377
|
+
try:
|
|
378
|
+
self.get_or_parse(file_path, parser)
|
|
379
|
+
except Exception:
|
|
380
|
+
pass # Ignore errors during preload
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def stats(self) -> CacheStats:
|
|
384
|
+
"""Get cache statistics"""
|
|
385
|
+
with self._lock:
|
|
386
|
+
self._stats.entries_count = len(self._cache)
|
|
387
|
+
return self._stats
|
|
388
|
+
|
|
389
|
+
def reset_stats(self):
|
|
390
|
+
"""Reset statistics counters"""
|
|
391
|
+
with self._lock:
|
|
392
|
+
entries = self._stats.entries_count
|
|
393
|
+
memory = self._stats.memory_estimate_kb
|
|
394
|
+
self._stats = CacheStats()
|
|
395
|
+
self._stats.entries_count = entries
|
|
396
|
+
self._stats.memory_estimate_kb = memory
|
|
397
|
+
|
|
398
|
+
def clear(self):
|
|
399
|
+
"""Clear the entire cache"""
|
|
400
|
+
with self._lock:
|
|
401
|
+
self._cache.clear()
|
|
402
|
+
self._dependencies.clear()
|
|
403
|
+
if self._enable_stats:
|
|
404
|
+
self._stats.entries_count = 0
|
|
405
|
+
self._stats.memory_estimate_kb = 0
|
|
406
|
+
|
|
407
|
+
def cache_info(self) -> Dict[str, Any]:
|
|
408
|
+
"""Get detailed cache information"""
|
|
409
|
+
with self._lock:
|
|
410
|
+
entries = []
|
|
411
|
+
for path, entry in self._cache.items():
|
|
412
|
+
entries.append({
|
|
413
|
+
'path': path,
|
|
414
|
+
'mtime': entry.mtime,
|
|
415
|
+
'size': entry.size,
|
|
416
|
+
'access_count': entry.access_count,
|
|
417
|
+
'age_seconds': time.time() - entry.created_at,
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
'max_entries': self._max_entries,
|
|
422
|
+
'current_entries': len(self._cache),
|
|
423
|
+
'hash_validation': self._enable_hash,
|
|
424
|
+
'entries': entries,
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# Global singleton instance
|
|
429
|
+
_global_cache: Optional[ASTCache] = None
|
|
430
|
+
_global_cache_lock = threading.Lock()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def get_ast_cache(max_entries: int = 100) -> ASTCache:
|
|
434
|
+
"""
|
|
435
|
+
Get the global AST cache instance (singleton pattern).
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
max_entries: Maximum cache size (only used on first call)
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
The global ASTCache instance
|
|
442
|
+
"""
|
|
443
|
+
global _global_cache
|
|
444
|
+
|
|
445
|
+
if _global_cache is None:
|
|
446
|
+
with _global_cache_lock:
|
|
447
|
+
if _global_cache is None:
|
|
448
|
+
_global_cache = ASTCache(max_entries=max_entries)
|
|
449
|
+
|
|
450
|
+
return _global_cache
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def invalidate_ast_cache(file_path: Optional[Any] = None):
|
|
454
|
+
"""
|
|
455
|
+
Convenience function to invalidate the global AST cache.
|
|
456
|
+
|
|
457
|
+
Args:
|
|
458
|
+
file_path: Specific file to invalidate, or None for all
|
|
459
|
+
"""
|
|
460
|
+
cache = get_ast_cache()
|
|
461
|
+
cache.invalidate(file_path)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Integration with hot reload
|
|
465
|
+
class ASTCacheWatcher:
|
|
466
|
+
"""
|
|
467
|
+
Watches for file changes and invalidates cache accordingly.
|
|
468
|
+
Integrates with the hot reload system.
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
def __init__(self, cache: Optional[ASTCache] = None):
|
|
472
|
+
self._cache = cache or get_ast_cache()
|
|
473
|
+
self._watching: Dict[str, float] = {} # path -> last known mtime
|
|
474
|
+
|
|
475
|
+
def check_and_invalidate(self, file_path: Any) -> bool:
|
|
476
|
+
"""
|
|
477
|
+
Check if a file has changed and invalidate if so.
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if the file was invalidated
|
|
481
|
+
"""
|
|
482
|
+
key = str(Path(file_path).resolve())
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
current_mtime = os.stat(key).st_mtime
|
|
486
|
+
except OSError:
|
|
487
|
+
return False
|
|
488
|
+
|
|
489
|
+
if key in self._watching:
|
|
490
|
+
if current_mtime != self._watching[key]:
|
|
491
|
+
self._cache.invalidate(key)
|
|
492
|
+
self._watching[key] = current_mtime
|
|
493
|
+
return True
|
|
494
|
+
else:
|
|
495
|
+
self._watching[key] = current_mtime
|
|
496
|
+
|
|
497
|
+
return False
|
|
498
|
+
|
|
499
|
+
def on_file_changed(self, file_path: Any):
|
|
500
|
+
"""
|
|
501
|
+
Callback for file change events (from hot reload).
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
file_path: Path to the changed file
|
|
505
|
+
"""
|
|
506
|
+
self._cache.invalidate(file_path)
|