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.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. 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())