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,326 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase H: File Upload Service
|
|
3
|
+
|
|
4
|
+
Handles file uploads with validation, storage, and naming strategies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
import mimetypes
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, Optional, List
|
|
12
|
+
from werkzeug.utils import secure_filename
|
|
13
|
+
from werkzeug.datastructures import FileStorage
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileUploadError(Exception):
|
|
17
|
+
"""Raised when file upload fails"""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FileUploadService:
|
|
22
|
+
"""Service for handling file uploads with validation"""
|
|
23
|
+
|
|
24
|
+
# File size constants
|
|
25
|
+
KB = 1024
|
|
26
|
+
MB = 1024 * KB
|
|
27
|
+
GB = 1024 * MB
|
|
28
|
+
|
|
29
|
+
def __init__(self, root: str = None):
|
|
30
|
+
"""Initialize file upload service.
|
|
31
|
+
|
|
32
|
+
`root` confines every path this service touches. Defaults to
|
|
33
|
+
./uploads under the current working directory.
|
|
34
|
+
"""
|
|
35
|
+
self.max_file_size = 10 * self.MB # Default 10MB
|
|
36
|
+
self.allowed_extensions = None # None = allow all
|
|
37
|
+
self.root = Path(root or 'uploads').resolve()
|
|
38
|
+
|
|
39
|
+
def resolve_within_root(self, filepath: str) -> Path:
|
|
40
|
+
"""Resolve `filepath` and refuse anything outside the root.
|
|
41
|
+
|
|
42
|
+
q:file's path can come straight from databinding — a form field, a
|
|
43
|
+
query parameter. delete_file used to do `Path(filepath).unlink()` with
|
|
44
|
+
no containment at all, so
|
|
45
|
+
|
|
46
|
+
<q:file action="delete" file="{nomeArquivo}" />
|
|
47
|
+
|
|
48
|
+
with nomeArquivo = "uploads/../banco-importante.db" deleted a file
|
|
49
|
+
outside the upload directory and reported
|
|
50
|
+
{'success': True, 'message': 'File deleted successfully'}.
|
|
51
|
+
Reproduced before fixing.
|
|
52
|
+
|
|
53
|
+
Same rule as the component router's _is_within_components: reject the
|
|
54
|
+
traversal token outright, then resolve and confirm containment.
|
|
55
|
+
"""
|
|
56
|
+
raw = str(filepath or '')
|
|
57
|
+
if not raw:
|
|
58
|
+
raise FileUploadError("empty file path")
|
|
59
|
+
if '..' in raw.replace('\\', '/').split('/'):
|
|
60
|
+
raise FileUploadError(
|
|
61
|
+
f"path {raw!r} escapes the upload directory"
|
|
62
|
+
)
|
|
63
|
+
candidate = Path(raw)
|
|
64
|
+
target = (candidate if candidate.is_absolute()
|
|
65
|
+
else self.root / candidate).resolve()
|
|
66
|
+
if target != self.root and self.root not in target.parents:
|
|
67
|
+
raise FileUploadError(
|
|
68
|
+
f"path {raw!r} resolves outside the upload directory "
|
|
69
|
+
f"({self.root})"
|
|
70
|
+
)
|
|
71
|
+
return target
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def parse_size(size_str: str) -> int:
|
|
75
|
+
"""
|
|
76
|
+
Parse file size string like '5MB', '100KB', '1GB' to bytes
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
size_str: Size string (e.g., '5MB', '100KB')
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Size in bytes
|
|
83
|
+
"""
|
|
84
|
+
size_str = size_str.strip().upper()
|
|
85
|
+
|
|
86
|
+
if size_str.endswith('GB'):
|
|
87
|
+
return int(float(size_str[:-2]) * FileUploadService.GB)
|
|
88
|
+
elif size_str.endswith('MB'):
|
|
89
|
+
return int(float(size_str[:-2]) * FileUploadService.MB)
|
|
90
|
+
elif size_str.endswith('KB'):
|
|
91
|
+
return int(float(size_str[:-2]) * FileUploadService.KB)
|
|
92
|
+
elif size_str.endswith('B'):
|
|
93
|
+
return int(size_str[:-1])
|
|
94
|
+
else:
|
|
95
|
+
# Assume bytes if no unit
|
|
96
|
+
return int(size_str)
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def get_file_extension(filename: str) -> str:
|
|
100
|
+
"""Get file extension from filename"""
|
|
101
|
+
return Path(filename).suffix.lower()
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def is_allowed_extension(filename: str, allowed: List[str]) -> bool:
|
|
105
|
+
"""
|
|
106
|
+
Check if file extension is allowed
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
filename: Filename to check
|
|
110
|
+
allowed: List of allowed extensions (e.g., ['.jpg', '.png', '.pdf'])
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
True if allowed, False otherwise
|
|
114
|
+
"""
|
|
115
|
+
if not allowed:
|
|
116
|
+
return True # No restrictions
|
|
117
|
+
|
|
118
|
+
ext = FileUploadService.get_file_extension(filename)
|
|
119
|
+
return ext in [a.lower() for a in allowed]
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def is_allowed_mimetype(file: FileStorage, allowed_patterns: List[str]) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Check if file MIME type matches allowed patterns
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
file: FileStorage object
|
|
128
|
+
allowed_patterns: List of MIME patterns (e.g., ['image/*', 'application/pdf'])
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if allowed, False otherwise
|
|
132
|
+
"""
|
|
133
|
+
if not allowed_patterns:
|
|
134
|
+
return True # No restrictions
|
|
135
|
+
|
|
136
|
+
file_mimetype = file.content_type or mimetypes.guess_type(file.filename)[0]
|
|
137
|
+
|
|
138
|
+
if not file_mimetype:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
for pattern in allowed_patterns:
|
|
142
|
+
if pattern == '*/*':
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
if '*' in pattern:
|
|
146
|
+
# Handle wildcards like 'image/*'
|
|
147
|
+
main_type = pattern.split('/')[0]
|
|
148
|
+
if file_mimetype.startswith(main_type + '/'):
|
|
149
|
+
return True
|
|
150
|
+
elif file_mimetype == pattern:
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
def validate_file(
|
|
156
|
+
self,
|
|
157
|
+
file: FileStorage,
|
|
158
|
+
max_size: Optional[str] = None,
|
|
159
|
+
allowed_extensions: Optional[List[str]] = None,
|
|
160
|
+
allowed_mimetypes: Optional[List[str]] = None
|
|
161
|
+
) -> Dict[str, Any]:
|
|
162
|
+
"""
|
|
163
|
+
Validate uploaded file
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
file: FileStorage object from Flask
|
|
167
|
+
max_size: Maximum file size (e.g., '5MB', '100KB')
|
|
168
|
+
allowed_extensions: List of allowed extensions
|
|
169
|
+
allowed_mimetypes: List of allowed MIME types
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dict with validation result
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
FileUploadError: If validation fails
|
|
176
|
+
"""
|
|
177
|
+
if not file or not file.filename:
|
|
178
|
+
raise FileUploadError("No file provided")
|
|
179
|
+
|
|
180
|
+
# Check file size
|
|
181
|
+
file.seek(0, os.SEEK_END)
|
|
182
|
+
file_size = file.tell()
|
|
183
|
+
file.seek(0) # Reset to beginning
|
|
184
|
+
|
|
185
|
+
if max_size:
|
|
186
|
+
max_bytes = self.parse_size(max_size)
|
|
187
|
+
if file_size > max_bytes:
|
|
188
|
+
raise FileUploadError(
|
|
189
|
+
f"File size ({file_size / self.MB:.2f}MB) exceeds maximum ({max_size})"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Check extension
|
|
193
|
+
if allowed_extensions and not self.is_allowed_extension(file.filename, allowed_extensions):
|
|
194
|
+
raise FileUploadError(
|
|
195
|
+
f"File type not allowed. Allowed: {', '.join(allowed_extensions)}"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Check MIME type
|
|
199
|
+
if allowed_mimetypes and not self.is_allowed_mimetype(file, allowed_mimetypes):
|
|
200
|
+
raise FileUploadError(
|
|
201
|
+
f"File MIME type not allowed. Allowed: {', '.join(allowed_mimetypes)}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
'valid': True,
|
|
206
|
+
'filename': file.filename,
|
|
207
|
+
'size': file_size,
|
|
208
|
+
'mimetype': file.content_type
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
def upload_file(
|
|
212
|
+
self,
|
|
213
|
+
file: FileStorage,
|
|
214
|
+
destination: str,
|
|
215
|
+
name_conflict: str = "error",
|
|
216
|
+
custom_filename: Optional[str] = None,
|
|
217
|
+
max_size: Optional[str] = None,
|
|
218
|
+
allowed_extensions: Optional[List[str]] = None,
|
|
219
|
+
allowed_mimetypes: Optional[List[str]] = None
|
|
220
|
+
) -> Dict[str, Any]:
|
|
221
|
+
"""
|
|
222
|
+
Upload file to destination
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
file: FileStorage object from Flask
|
|
226
|
+
destination: Destination directory
|
|
227
|
+
name_conflict: Strategy for naming conflicts (error, overwrite, skip, makeUnique)
|
|
228
|
+
custom_filename: Optional custom filename
|
|
229
|
+
max_size: Maximum file size
|
|
230
|
+
allowed_extensions: Allowed file extensions
|
|
231
|
+
allowed_mimetypes: Allowed MIME types
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Dict with upload result
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
FileUploadError: If upload fails
|
|
238
|
+
"""
|
|
239
|
+
# Validate file
|
|
240
|
+
validation = self.validate_file(file, max_size, allowed_extensions, allowed_mimetypes)
|
|
241
|
+
|
|
242
|
+
# Secure the filename
|
|
243
|
+
original_filename = custom_filename or file.filename
|
|
244
|
+
secure_name = secure_filename(original_filename)
|
|
245
|
+
|
|
246
|
+
# Create destination directory if it doesn't exist.
|
|
247
|
+
#
|
|
248
|
+
# `destination` is databound, so it can carry a form value. It used to
|
|
249
|
+
# be Path(destination).mkdir(parents=True) with no containment, which
|
|
250
|
+
# let an upload create and write directories anywhere on disk. Every
|
|
251
|
+
# destination= in this repo already lives under ./uploads/, so
|
|
252
|
+
# confining to the root breaks no real usage.
|
|
253
|
+
dest_path = self.resolve_within_root(destination)
|
|
254
|
+
dest_path.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
|
|
256
|
+
# Handle name conflicts
|
|
257
|
+
final_filename = secure_name
|
|
258
|
+
file_path = dest_path / final_filename
|
|
259
|
+
|
|
260
|
+
if file_path.exists():
|
|
261
|
+
if name_conflict == "error":
|
|
262
|
+
raise FileUploadError(f"File '{final_filename}' already exists")
|
|
263
|
+
elif name_conflict == "skip":
|
|
264
|
+
return {
|
|
265
|
+
'success': False,
|
|
266
|
+
'skipped': True,
|
|
267
|
+
'filename': final_filename,
|
|
268
|
+
'message': 'File already exists and was skipped'
|
|
269
|
+
}
|
|
270
|
+
elif name_conflict == "makeUnique":
|
|
271
|
+
# Generate unique filename
|
|
272
|
+
name_part = file_path.stem
|
|
273
|
+
ext_part = file_path.suffix
|
|
274
|
+
unique_id = uuid.uuid4().hex[:8]
|
|
275
|
+
final_filename = f"{name_part}_{unique_id}{ext_part}"
|
|
276
|
+
file_path = dest_path / final_filename
|
|
277
|
+
# elif name_conflict == "overwrite": just use the same filename
|
|
278
|
+
|
|
279
|
+
# Save file
|
|
280
|
+
try:
|
|
281
|
+
file.save(str(file_path))
|
|
282
|
+
except Exception as e:
|
|
283
|
+
raise FileUploadError(f"Failed to save file: {e}")
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
'success': True,
|
|
287
|
+
'filename': final_filename,
|
|
288
|
+
'filepath': str(file_path),
|
|
289
|
+
'original_filename': original_filename,
|
|
290
|
+
'size': validation['size'],
|
|
291
|
+
'mimetype': validation['mimetype'],
|
|
292
|
+
'url': f"/uploads/{final_filename}" # Relative URL
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
def delete_file(self, filepath: str) -> Dict[str, Any]:
|
|
296
|
+
"""
|
|
297
|
+
Delete a file
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
filepath: Path to file to delete
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Dict with deletion result
|
|
304
|
+
"""
|
|
305
|
+
try:
|
|
306
|
+
file_path = self.resolve_within_root(filepath)
|
|
307
|
+
except FileUploadError as e:
|
|
308
|
+
return {'success': False, 'error': str(e)}
|
|
309
|
+
|
|
310
|
+
if not file_path.exists():
|
|
311
|
+
return {
|
|
312
|
+
'success': False,
|
|
313
|
+
'error': 'File not found'
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
file_path.unlink()
|
|
318
|
+
return {
|
|
319
|
+
'success': True,
|
|
320
|
+
'message': 'File deleted successfully'
|
|
321
|
+
}
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return {
|
|
324
|
+
'success': False,
|
|
325
|
+
'error': str(e)
|
|
326
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Function Registry - Manages function definitions across scopes
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Optional, List
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# Fix imports
|
|
10
|
+
|
|
11
|
+
from quantum.core.ast_nodes import ComponentNode
|
|
12
|
+
from quantum.core.features.functions.src.ast_node import FunctionNode
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FunctionRegistry:
|
|
16
|
+
"""Global registry for all functions across scopes"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
# Escopo por container
|
|
20
|
+
self.components: Dict[str, Dict[str, FunctionNode]] = {} # component_name -> {func_name: func}
|
|
21
|
+
|
|
22
|
+
# Index global (para funções scope="global")
|
|
23
|
+
self.global_functions: Dict[str, FunctionNode] = {}
|
|
24
|
+
|
|
25
|
+
# Imported modules (para q:import)
|
|
26
|
+
self.imported_modules: List[str] = []
|
|
27
|
+
|
|
28
|
+
def register_component(self, component: ComponentNode):
|
|
29
|
+
"""Register all functions from a component"""
|
|
30
|
+
component_name = getattr(component, 'name', None) or getattr(component, 'id', 'unknown')
|
|
31
|
+
if component_name not in self.components:
|
|
32
|
+
self.components[component_name] = {}
|
|
33
|
+
|
|
34
|
+
for func in getattr(component, 'functions', []):
|
|
35
|
+
self.register_function(func, component)
|
|
36
|
+
|
|
37
|
+
def register_function(self, func: FunctionNode, container: ComponentNode):
|
|
38
|
+
"""Register function in appropriate scope"""
|
|
39
|
+
container_name = getattr(container, 'name', None) or getattr(container, 'id', 'unknown')
|
|
40
|
+
if container_name not in self.components:
|
|
41
|
+
self.components[container_name] = {}
|
|
42
|
+
|
|
43
|
+
self.components[container.name][func.name] = func
|
|
44
|
+
|
|
45
|
+
# Functions with scope="global" go to global index
|
|
46
|
+
if func.scope == "global":
|
|
47
|
+
qualified_name = f"{container.name}.{func.name}"
|
|
48
|
+
self.global_functions[qualified_name] = func
|
|
49
|
+
|
|
50
|
+
def resolve_function(self, name: str, current_component: Optional[ComponentNode] = None) -> Optional[FunctionNode]:
|
|
51
|
+
"""
|
|
52
|
+
Resolve function name to FunctionNode - respects scope rules
|
|
53
|
+
|
|
54
|
+
Resolution order:
|
|
55
|
+
1. Local component functions
|
|
56
|
+
2. Global functions (qualified name: Component.function)
|
|
57
|
+
3. Imported module functions
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# 1. Check local component first
|
|
61
|
+
if current_component and current_component.name in self.components:
|
|
62
|
+
component_funcs = self.components[current_component.name]
|
|
63
|
+
if name in component_funcs:
|
|
64
|
+
return component_funcs[name]
|
|
65
|
+
|
|
66
|
+
# 2. Check global functions (qualified name: Component.function)
|
|
67
|
+
if name in self.global_functions:
|
|
68
|
+
return self.global_functions[name]
|
|
69
|
+
|
|
70
|
+
# 3. Check if it's a qualified call (Component.function)
|
|
71
|
+
if '.' in name:
|
|
72
|
+
component_name, func_name = name.split('.', 1)
|
|
73
|
+
if component_name in self.components:
|
|
74
|
+
return self.components[component_name].get(func_name)
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def get_rest_endpoints(self) -> List[FunctionNode]:
|
|
79
|
+
"""Get all functions that are REST endpoints"""
|
|
80
|
+
endpoints = []
|
|
81
|
+
|
|
82
|
+
# Functions in components with endpoint attribute
|
|
83
|
+
for component_funcs in self.components.values():
|
|
84
|
+
for func in component_funcs.values():
|
|
85
|
+
if func.is_rest_enabled() and not func.is_private():
|
|
86
|
+
endpoints.append(func)
|
|
87
|
+
|
|
88
|
+
return endpoints
|
|
89
|
+
|
|
90
|
+
def get_event_handlers(self, component: ComponentNode) -> List:
|
|
91
|
+
"""Get all event handlers for a component"""
|
|
92
|
+
return component.event_handlers
|
|
93
|
+
|
|
94
|
+
def import_module(self, module_name: str):
|
|
95
|
+
"""Mark module as imported (for q:import)"""
|
|
96
|
+
if module_name not in self.imported_modules:
|
|
97
|
+
self.imported_modules.append(module_name)
|
|
98
|
+
|
|
99
|
+
def is_module_imported(self, module_name: str) -> bool:
|
|
100
|
+
"""Check if module is imported"""
|
|
101
|
+
return module_name in self.imported_modules
|
|
102
|
+
|
|
103
|
+
def list_functions(self, component_name: Optional[str] = None) -> List[FunctionNode]:
|
|
104
|
+
"""List all functions (optionally filtered by component)"""
|
|
105
|
+
if component_name:
|
|
106
|
+
return list(self.components.get(component_name, {}).values())
|
|
107
|
+
|
|
108
|
+
# List all functions
|
|
109
|
+
all_functions = []
|
|
110
|
+
for component_funcs in self.components.values():
|
|
111
|
+
all_functions.extend(component_funcs.values())
|
|
112
|
+
return all_functions
|
|
113
|
+
|
|
114
|
+
def clear(self):
|
|
115
|
+
"""Clear all registered functions"""
|
|
116
|
+
self.components.clear()
|
|
117
|
+
self.global_functions.clear()
|
|
118
|
+
self.imported_modules.clear()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Game Engine 2D - Builder/Orchestrator
|
|
3
|
+
|
|
4
|
+
Orchestrates the compilation pipeline:
|
|
5
|
+
ApplicationNode (type="game") → extract scenes/behaviors/prefabs → CodeGenerator → output
|
|
6
|
+
|
|
7
|
+
Supports two backends:
|
|
8
|
+
- pixi (default): PIXI.js + Matter.js → standalone HTML file
|
|
9
|
+
- godot: Godot 4 → project directory with .tscn + .gd files
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
builder = GameBuilder() # Default: pixi backend
|
|
13
|
+
builder = GameBuilder(engine='godot') # Godot 4 backend
|
|
14
|
+
builder.build_to_file(app_node)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from quantum.core.ast_nodes import ApplicationNode
|
|
22
|
+
from quantum.core.features.game_engine_2d.src.ast_nodes import (
|
|
23
|
+
SceneNode, BehaviorNode, PrefabNode, EnemyNode,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GameBuildError(Exception):
|
|
28
|
+
"""Error during game build."""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GameBuilder:
|
|
33
|
+
"""Builds a game from a Quantum game ApplicationNode.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
engine: Backend engine to use. 'pixi' (default) or 'godot'.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
VALID_ENGINES = ('pixi', 'godot')
|
|
40
|
+
|
|
41
|
+
def __init__(self, engine: str = 'pixi', source_dir: str = None):
|
|
42
|
+
if engine not in self.VALID_ENGINES:
|
|
43
|
+
raise GameBuildError(
|
|
44
|
+
f"Unknown engine '{engine}'. Valid engines: {', '.join(self.VALID_ENGINES)}"
|
|
45
|
+
)
|
|
46
|
+
self.engine = engine
|
|
47
|
+
self.source_dir = source_dir
|
|
48
|
+
|
|
49
|
+
def build(self, app: ApplicationNode, output_dir: Optional[str] = None) -> str:
|
|
50
|
+
"""Build game from an ApplicationNode with type='game'.
|
|
51
|
+
|
|
52
|
+
For pixi engine: returns HTML string.
|
|
53
|
+
For godot engine: returns output directory path.
|
|
54
|
+
"""
|
|
55
|
+
scenes = getattr(app, 'scenes', [])
|
|
56
|
+
behaviors = getattr(app, 'behaviors', [])
|
|
57
|
+
prefabs = getattr(app, 'prefabs', [])
|
|
58
|
+
enemies = getattr(app, 'enemies', [])
|
|
59
|
+
|
|
60
|
+
if not scenes:
|
|
61
|
+
raise GameBuildError("No scenes found in game application")
|
|
62
|
+
|
|
63
|
+
valid_behaviors = [b for b in behaviors if isinstance(b, BehaviorNode)]
|
|
64
|
+
valid_prefabs = [p for p in prefabs if isinstance(p, PrefabNode)]
|
|
65
|
+
valid_enemies = [e for e in enemies if isinstance(e, EnemyNode)]
|
|
66
|
+
|
|
67
|
+
if self.engine == 'godot':
|
|
68
|
+
return self._build_godot(app, scenes, valid_behaviors, valid_prefabs, output_dir, valid_enemies)
|
|
69
|
+
else:
|
|
70
|
+
return self._build_pixi(app, scenes, valid_behaviors, valid_prefabs)
|
|
71
|
+
|
|
72
|
+
def _build_pixi(self, app: ApplicationNode, scenes, behaviors, prefabs) -> str:
|
|
73
|
+
"""Build with PIXI.js + Matter.js backend (returns HTML string)."""
|
|
74
|
+
from quantum.runtime.game_code_generator import GameCodeGenerator
|
|
75
|
+
|
|
76
|
+
if len(scenes) > 1:
|
|
77
|
+
initial_name = scenes[0].name
|
|
78
|
+
for scene in scenes:
|
|
79
|
+
if isinstance(scene, SceneNode) and scene.active:
|
|
80
|
+
initial_name = scene.name
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
generator = GameCodeGenerator()
|
|
84
|
+
return generator.generate_multi(
|
|
85
|
+
scenes=[s for s in scenes if isinstance(s, SceneNode)],
|
|
86
|
+
initial=initial_name,
|
|
87
|
+
behaviors=behaviors,
|
|
88
|
+
prefabs=prefabs,
|
|
89
|
+
title=app.app_id,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
active_scene = scenes[0]
|
|
93
|
+
for scene in scenes:
|
|
94
|
+
if isinstance(scene, SceneNode) and scene.active:
|
|
95
|
+
active_scene = scene
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
generator = GameCodeGenerator()
|
|
99
|
+
return generator.generate(
|
|
100
|
+
scene=active_scene,
|
|
101
|
+
behaviors=behaviors,
|
|
102
|
+
prefabs=prefabs,
|
|
103
|
+
title=app.app_id,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _build_godot(self, app: ApplicationNode, scenes, behaviors, prefabs,
|
|
107
|
+
output_dir: Optional[str] = None,
|
|
108
|
+
enemies: list = None) -> str:
|
|
109
|
+
"""Build with Godot 4 backend (returns output directory path)."""
|
|
110
|
+
from quantum.runtime.godot_code_generator import GodotCodeGenerator
|
|
111
|
+
|
|
112
|
+
if output_dir is None:
|
|
113
|
+
output_dir = f"projects/{app.app_id}/godot"
|
|
114
|
+
|
|
115
|
+
generator = GodotCodeGenerator(source_dir=self.source_dir)
|
|
116
|
+
persistent = getattr(app, 'persistent', [])
|
|
117
|
+
|
|
118
|
+
if len(scenes) > 1:
|
|
119
|
+
initial_name = scenes[0].name
|
|
120
|
+
for scene in scenes:
|
|
121
|
+
if isinstance(scene, SceneNode) and (getattr(scene, 'initial', False) or scene.active):
|
|
122
|
+
initial_name = scene.name
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
return generator.generate_multi(
|
|
126
|
+
scenes=[s for s in scenes if isinstance(s, SceneNode)],
|
|
127
|
+
initial=initial_name,
|
|
128
|
+
behaviors=behaviors,
|
|
129
|
+
prefabs=prefabs,
|
|
130
|
+
enemies=enemies or [],
|
|
131
|
+
persistent=persistent,
|
|
132
|
+
output_dir=output_dir,
|
|
133
|
+
project_name=app.app_id,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
active_scene = scenes[0]
|
|
137
|
+
for scene in scenes:
|
|
138
|
+
if isinstance(scene, SceneNode) and scene.active:
|
|
139
|
+
active_scene = scene
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
return generator.generate(
|
|
143
|
+
scene=active_scene,
|
|
144
|
+
behaviors=behaviors,
|
|
145
|
+
prefabs=prefabs,
|
|
146
|
+
enemies=enemies or [],
|
|
147
|
+
output_dir=output_dir,
|
|
148
|
+
project_name=app.app_id,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def build_to_file(self, app: ApplicationNode, output_path: Optional[str] = None) -> str:
|
|
152
|
+
"""Build and write output. Returns the output file/directory path."""
|
|
153
|
+
if self.engine == 'godot':
|
|
154
|
+
return self.build(app, output_dir=output_path)
|
|
155
|
+
|
|
156
|
+
# PIXI: write HTML file
|
|
157
|
+
html = self.build(app)
|
|
158
|
+
|
|
159
|
+
if output_path is None:
|
|
160
|
+
output_path = f"{app.app_id}.html"
|
|
161
|
+
|
|
162
|
+
path = Path(output_path)
|
|
163
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
164
|
+
path.write_text(html, encoding='utf-8')
|
|
165
|
+
|
|
166
|
+
return str(path.resolve())
|