quantum-framework 0.18.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 (333) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/admin.py +131 -0
  4. quantum/cli/check.py +223 -0
  5. quantum/cli/commands/__init__.py +15 -0
  6. quantum/cli/commands/build.py +414 -0
  7. quantum/cli/commands/dev.py +185 -0
  8. quantum/cli/commands/docs.py +329 -0
  9. quantum/cli/commands/lint.py +523 -0
  10. quantum/cli/commands/migrate.py +548 -0
  11. quantum/cli/commands/new.py +622 -0
  12. quantum/cli/commands/serve.py +190 -0
  13. quantum/cli/commands/test.py +193 -0
  14. quantum/cli/deploy.py +810 -0
  15. quantum/cli/hot_reload.py +951 -0
  16. quantum/cli/jobs.py +356 -0
  17. quantum/cli/mq.py +582 -0
  18. quantum/cli/pkg.py +390 -0
  19. quantum/cli/runner.py +565 -0
  20. quantum/cli/server_process.py +159 -0
  21. quantum/cli/utils.py +334 -0
  22. quantum/compiler/__init__.py +30 -0
  23. quantum/compiler/base_generator.py +367 -0
  24. quantum/compiler/cli.py +295 -0
  25. quantum/compiler/expression_transformer.py +444 -0
  26. quantum/compiler/javascript/__init__.py +10 -0
  27. quantum/compiler/javascript/generator.py +659 -0
  28. quantum/compiler/optimizer.py +270 -0
  29. quantum/compiler/python/__init__.py +10 -0
  30. quantum/compiler/python/generator.py +883 -0
  31. quantum/compiler/python/runtime.py +863 -0
  32. quantum/compiler/transpiler.py +330 -0
  33. quantum/core/__init__.py +3 -0
  34. quantum/core/ast_nodes.py +2619 -0
  35. quantum/core/config_env.py +54 -0
  36. quantum/core/docs_links.py +8 -0
  37. quantum/core/expression_diagnostics.py +87 -0
  38. quantum/core/expression_stdlib.py +208 -0
  39. quantum/core/expressions.py +677 -0
  40. quantum/core/features/agents/src/__init__.py +30 -0
  41. quantum/core/features/agents/src/ast_node.py +540 -0
  42. quantum/core/features/conditionals/src/__init__.py +8 -0
  43. quantum/core/features/conditionals/src/ast_node.py +68 -0
  44. quantum/core/features/data_import/src/__init__.py +0 -0
  45. quantum/core/features/data_import/src/ast_node.py +292 -0
  46. quantum/core/features/data_import/src/runtime.py +533 -0
  47. quantum/core/features/dump/src/__init__.py +12 -0
  48. quantum/core/features/dump/src/ast_node.py +106 -0
  49. quantum/core/features/dump/src/parser.py +61 -0
  50. quantum/core/features/dump/src/runtime.py +246 -0
  51. quantum/core/features/functions/src/__init__.py +8 -0
  52. quantum/core/features/functions/src/ast_node.py +149 -0
  53. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  54. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  55. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  56. quantum/core/features/invocation/src/__init__.py +0 -0
  57. quantum/core/features/invocation/src/ast_node.py +147 -0
  58. quantum/core/features/invocation/src/runtime.py +332 -0
  59. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  60. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  61. quantum/core/features/knowledge_base/src/parser.py +82 -0
  62. quantum/core/features/logging/src/__init__.py +12 -0
  63. quantum/core/features/logging/src/ast_node.py +111 -0
  64. quantum/core/features/logging/src/parser.py +50 -0
  65. quantum/core/features/logging/src/runtime.py +190 -0
  66. quantum/core/features/loops/src/__init__.py +8 -0
  67. quantum/core/features/loops/src/ast_node.py +60 -0
  68. quantum/core/features/query/src/__init__.py +0 -0
  69. quantum/core/features/query/src/query_validators.py +20 -0
  70. quantum/core/features/state_management/src/__init__.py +11 -0
  71. quantum/core/features/state_management/src/ast_node.py +121 -0
  72. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  73. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  74. quantum/core/features/terminal_engine/src/parser.py +361 -0
  75. quantum/core/features/testing_engine/src/__init__.py +41 -0
  76. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  77. quantum/core/features/testing_engine/src/parser.py +604 -0
  78. quantum/core/features/theming/src/__init__.py +48 -0
  79. quantum/core/features/theming/src/ast_node.py +137 -0
  80. quantum/core/features/theming/src/presets.py +405 -0
  81. quantum/core/features/ui_engine/src/__init__.py +20 -0
  82. quantum/core/features/ui_engine/src/ast_nodes.py +1913 -0
  83. quantum/core/features/ui_engine/src/parser.py +1192 -0
  84. quantum/core/features/websocket/src/__init__.py +24 -0
  85. quantum/core/features/websocket/src/ast_node.py +247 -0
  86. quantum/core/guards.py +53 -0
  87. quantum/core/html_compat.py +299 -0
  88. quantum/core/parser.py +1551 -0
  89. quantum/core/parser_registry.py +213 -0
  90. quantum/core/parsers/__init__.py +76 -0
  91. quantum/core/parsers/ai/__init__.py +12 -0
  92. quantum/core/parsers/ai/agent_parser.py +106 -0
  93. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  94. quantum/core/parsers/ai/llm_parser.py +78 -0
  95. quantum/core/parsers/ai/team_parser.py +88 -0
  96. quantum/core/parsers/base.py +322 -0
  97. quantum/core/parsers/composition/__init__.py +10 -0
  98. quantum/core/parsers/composition/import_parser.py +50 -0
  99. quantum/core/parsers/composition/slot_parser.py +48 -0
  100. quantum/core/parsers/control_flow/__init__.py +11 -0
  101. quantum/core/parsers/control_flow/if_parser.py +71 -0
  102. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  103. quantum/core/parsers/control_flow/set_parser.py +93 -0
  104. quantum/core/parsers/data/__init__.py +12 -0
  105. quantum/core/parsers/data/data_parser.py +229 -0
  106. quantum/core/parsers/data/invoke_parser.py +133 -0
  107. quantum/core/parsers/data/query_parser.py +213 -0
  108. quantum/core/parsers/data/transaction_parser.py +86 -0
  109. quantum/core/parsers/events/__init__.py +9 -0
  110. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  111. quantum/core/parsers/forms/__init__.py +11 -0
  112. quantum/core/parsers/forms/action_parser.py +79 -0
  113. quantum/core/parsers/forms/flash_parser.py +34 -0
  114. quantum/core/parsers/forms/redirect_parser.py +33 -0
  115. quantum/core/parsers/functions/__init__.py +11 -0
  116. quantum/core/parsers/functions/function_parser.py +98 -0
  117. quantum/core/parsers/functions/param_parser.py +73 -0
  118. quantum/core/parsers/functions/return_parser.py +31 -0
  119. quantum/core/parsers/html/__init__.py +10 -0
  120. quantum/core/parsers/html/component_call_parser.py +130 -0
  121. quantum/core/parsers/html/html_parser.py +115 -0
  122. quantum/core/parsers/jobs/__init__.py +11 -0
  123. quantum/core/parsers/jobs/job_parser.py +71 -0
  124. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  125. quantum/core/parsers/jobs/thread_parser.py +57 -0
  126. quantum/core/parsers/messaging/__init__.py +17 -0
  127. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  128. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_parser.py +114 -0
  130. quantum/core/parsers/messaging/queue_parser.py +61 -0
  131. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  132. quantum/core/parsers/routing/__init__.py +9 -0
  133. quantum/core/parsers/routing/route_parser.py +41 -0
  134. quantum/core/parsers/scripting/__init__.py +12 -0
  135. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  136. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  137. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  138. quantum/core/parsers/scripting/python_parser.py +49 -0
  139. quantum/core/parsers/services/__init__.py +12 -0
  140. quantum/core/parsers/services/dump_parser.py +52 -0
  141. quantum/core/parsers/services/file_parser.py +48 -0
  142. quantum/core/parsers/services/log_parser.py +46 -0
  143. quantum/core/parsers/services/mail_parser.py +65 -0
  144. quantum/core/tiers.py +164 -0
  145. quantum/core/xml_lines.py +81 -0
  146. quantum/packages/__init__.py +28 -0
  147. quantum/packages/manager.py +413 -0
  148. quantum/packages/manifest.py +351 -0
  149. quantum/packages/registry.py +399 -0
  150. quantum/packages/resolver.py +336 -0
  151. quantum/plugins/__init__.py +33 -0
  152. quantum/plugins/hooks.py +329 -0
  153. quantum/plugins/loader.py +479 -0
  154. quantum/plugins/manifest.py +336 -0
  155. quantum/plugins/registry.py +371 -0
  156. quantum/runtime/__init__.py +25 -0
  157. quantum/runtime/action_handler.py +532 -0
  158. quantum/runtime/adapters/__init__.py +88 -0
  159. quantum/runtime/adapters/memory_adapter.py +690 -0
  160. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  161. quantum/runtime/adapters/redis_adapter.py +582 -0
  162. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  163. quantum/runtime/agent_service.py +1133 -0
  164. quantum/runtime/ast_cache.py +506 -0
  165. quantum/runtime/auth_service.py +267 -0
  166. quantum/runtime/component.py +1048 -0
  167. quantum/runtime/component_composer.py +133 -0
  168. quantum/runtime/component_resolver.py +174 -0
  169. quantum/runtime/conditions.py +54 -0
  170. quantum/runtime/database_service.py +595 -0
  171. quantum/runtime/db_schema.py +100 -0
  172. quantum/runtime/dev_panel.py +178 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +283 -0
  176. quantum/runtime/executor_registry.py +178 -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 +264 -0
  184. quantum/runtime/executors/control_flow/__init__.py +12 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +98 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +308 -0
  187. quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
  188. quantum/runtime/executors/control_flow/set_executor.py +464 -0
  189. quantum/runtime/executors/data/__init__.py +12 -0
  190. quantum/runtime/executors/data/data_executor.py +156 -0
  191. quantum/runtime/executors/data/invoke_executor.py +228 -0
  192. quantum/runtime/executors/data/query_executor.py +290 -0
  193. quantum/runtime/executors/data/transaction_executor.py +91 -0
  194. quantum/runtime/executors/jobs/__init__.py +11 -0
  195. quantum/runtime/executors/jobs/job_executor.py +190 -0
  196. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  197. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  198. quantum/runtime/executors/messaging/__init__.py +17 -0
  199. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  200. quantum/runtime/executors/messaging/message_executor.py +174 -0
  201. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  202. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  203. quantum/runtime/executors/scripting/__init__.py +11 -0
  204. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  205. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  206. quantum/runtime/executors/scripting/python_executor.py +249 -0
  207. quantum/runtime/executors/services/__init__.py +12 -0
  208. quantum/runtime/executors/services/dump_executor.py +72 -0
  209. quantum/runtime/executors/services/file_executor.py +89 -0
  210. quantum/runtime/executors/services/log_executor.py +77 -0
  211. quantum/runtime/executors/services/mail_executor.py +81 -0
  212. quantum/runtime/expression_cache.py +498 -0
  213. quantum/runtime/file_upload_service.py +326 -0
  214. quantum/runtime/function_registry.py +118 -0
  215. quantum/runtime/game_builder.py +166 -0
  216. quantum/runtime/game_code_generator.py +2371 -0
  217. quantum/runtime/game_templates.py +2006 -0
  218. quantum/runtime/godot_code_generator.py +4681 -0
  219. quantum/runtime/godot_templates.py +1449 -0
  220. quantum/runtime/job_executor.py +1599 -0
  221. quantum/runtime/knowledge_service.py +546 -0
  222. quantum/runtime/llm_cache.py +100 -0
  223. quantum/runtime/llm_providers.py +704 -0
  224. quantum/runtime/llm_service.py +287 -0
  225. quantum/runtime/local_server.py +28 -0
  226. quantum/runtime/logging_setup.py +141 -0
  227. quantum/runtime/message_broker.py +364 -0
  228. quantum/runtime/message_queue_service.py +571 -0
  229. quantum/runtime/param_validation.py +218 -0
  230. quantum/runtime/pypy_compat.py +317 -0
  231. quantum/runtime/python_bridge.py +698 -0
  232. quantum/runtime/query_validators.py +304 -0
  233. quantum/runtime/renderer.py +734 -0
  234. quantum/runtime/service_container.py +470 -0
  235. quantum/runtime/table_params.py +163 -0
  236. quantum/runtime/terminal_builder.py +76 -0
  237. quantum/runtime/terminal_code_generator.py +607 -0
  238. quantum/runtime/terminal_templates.py +243 -0
  239. quantum/runtime/testing_builder.py +77 -0
  240. quantum/runtime/testing_code_generator.py +833 -0
  241. quantum/runtime/testing_templates.py +85 -0
  242. quantum/runtime/ui_builder.py +222 -0
  243. quantum/runtime/ui_console.py +405 -0
  244. quantum/runtime/ui_desktop.py +56 -0
  245. quantum/runtime/ui_form_rules.py +193 -0
  246. quantum/runtime/ui_html_adapter.py +2567 -0
  247. quantum/runtime/ui_html_templates.py +2106 -0
  248. quantum/runtime/ui_mobile_adapter.py +1832 -0
  249. quantum/runtime/ui_mobile_templates.py +1003 -0
  250. quantum/runtime/ui_page_renderer.py +392 -0
  251. quantum/runtime/ui_pager.py +73 -0
  252. quantum/runtime/ui_table_edit.py +139 -0
  253. quantum/runtime/ui_textual_adapter.py +1866 -0
  254. quantum/runtime/ui_textual_templates.py +45 -0
  255. quantum/runtime/ui_tokens.py +465 -0
  256. quantum/runtime/ui_validator.py +365 -0
  257. quantum/runtime/ui_view.py +221 -0
  258. quantum/runtime/validators.py +256 -0
  259. quantum/runtime/web_server.py +2131 -0
  260. quantum/runtime/websocket_adapter.py +501 -0
  261. quantum/runtime/websocket_service.py +585 -0
  262. quantum/runtime/websocket_transport.py +289 -0
  263. quantum/runtime/wsgi.py +101 -0
  264. quantum/services.py +109 -0
  265. quantum/utils/__init__.py +1 -0
  266. quantum_admin/__init__.py +4 -0
  267. quantum_admin/assets/quantum-admin-connectors.js +123 -0
  268. quantum_admin/assets/quantum-admin.css +2058 -0
  269. quantum_admin/assets/quantum-source-highlight.js +106 -0
  270. quantum_admin/backend/__init__.py +4 -0
  271. quantum_admin/backend/audit_service.py +346 -0
  272. quantum_admin/backend/auth_service.py +394 -0
  273. quantum_admin/backend/component_discovery.py +766 -0
  274. quantum_admin/backend/config_generator.py +586 -0
  275. quantum_admin/backend/connector_service.py +1016 -0
  276. quantum_admin/backend/crud.py +557 -0
  277. quantum_admin/backend/database.py +226 -0
  278. quantum_admin/backend/db_setup_service.py +193 -0
  279. quantum_admin/backend/deploy_service.py +698 -0
  280. quantum_admin/backend/docker_service.py +422 -0
  281. quantum_admin/backend/environment_service.py +418 -0
  282. quantum_admin/backend/git_service.py +370 -0
  283. quantum_admin/backend/health_service.py +390 -0
  284. quantum_admin/backend/job_service.py +347 -0
  285. quantum_admin/backend/log_service.py +331 -0
  286. quantum_admin/backend/main.py +14426 -0
  287. quantum_admin/backend/models.py +986 -0
  288. quantum_admin/backend/pipeline_service.py +812 -0
  289. quantum_admin/backend/resource_manager.py +1118 -0
  290. quantum_admin/backend/schemas.py +419 -0
  291. quantum_admin/backend/secret_manager.py +277 -0
  292. quantum_admin/backend/settings_service.py +488 -0
  293. quantum_admin/backend/template_service.py +710 -0
  294. quantum_admin/backend/test_execution_service.py +364 -0
  295. quantum_admin/backend/test_generator.py +782 -0
  296. quantum_admin/backend/webhook_service.py +454 -0
  297. quantum_admin/backend/websocket_manager.py +360 -0
  298. quantum_admin/components/admin/_layout/AdminShell.q +121 -0
  299. quantum_admin/components/admin/agents.q +107 -0
  300. quantum_admin/components/admin/app/[name].q +558 -0
  301. quantum_admin/components/admin/applications.q +160 -0
  302. quantum_admin/components/admin/component/[...path].q +205 -0
  303. quantum_admin/components/admin/components.q +59 -0
  304. quantum_admin/components/admin/connectors.q +283 -0
  305. quantum_admin/components/admin/dashboard.q +86 -0
  306. quantum_admin/components/admin/database.q +95 -0
  307. quantum_admin/components/admin/features.q +69 -0
  308. quantum_admin/components/admin/index.q +12 -0
  309. quantum_admin/components/admin/jobs.q +75 -0
  310. quantum_admin/components/admin/login.q +65 -0
  311. quantum_admin/components/admin/logout.q +11 -0
  312. quantum_admin/components/admin/settings.q +174 -0
  313. quantum_admin/components/admin/source.q +45 -0
  314. quantum_admin/components/admin/tests.q +73 -0
  315. quantum_admin/run.py +13 -0
  316. quantum_admin/services/__init__.py +16 -0
  317. quantum_admin/services/_base.py +84 -0
  318. quantum_admin/services/_component_test_generator.py +205 -0
  319. quantum_admin/services/_yaml_editor.py +82 -0
  320. quantum_admin/services/apps.py +330 -0
  321. quantum_admin/services/auth.py +61 -0
  322. quantum_admin/services/components.py +210 -0
  323. quantum_admin/services/connectors.py +164 -0
  324. quantum_admin/services/projects.py +189 -0
  325. quantum_admin/services/repository.py +223 -0
  326. quantum_admin/services/settings.py +150 -0
  327. quantum_admin/services/yaml_import.py +118 -0
  328. quantum_framework-0.18.0.dist-info/METADATA +281 -0
  329. quantum_framework-0.18.0.dist-info/RECORD +333 -0
  330. quantum_framework-0.18.0.dist-info/WHEEL +5 -0
  331. quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
  332. quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
  333. quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,766 @@
1
+ """
2
+ Component Discovery Service - Automatically discover and parse Quantum components
3
+
4
+ Features:
5
+ - Scan project directories for .q files
6
+ - Parse .q files to extract metadata
7
+ - Extract endpoints, functions, queries
8
+ - Validate component syntax
9
+ - Track component dependencies
10
+ """
11
+
12
+ import os
13
+ import re
14
+ import logging
15
+ from pathlib import Path
16
+ from typing import Dict, List, Optional, Any, Tuple
17
+ from datetime import datetime
18
+ from dataclasses import dataclass, field
19
+ from xml.etree import ElementTree as ET
20
+ from sqlalchemy.orm import Session
21
+
22
+ try:
23
+ from .models import Project, Component, Endpoint, ComponentTest
24
+ except ImportError:
25
+ from models import Project, Component, Endpoint, ComponentTest
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ @dataclass
31
+ class DiscoveredFunction:
32
+ """Represents a function discovered in a component"""
33
+ name: str
34
+ params: List[str] = field(default_factory=list)
35
+ return_type: Optional[str] = None
36
+ description: Optional[str] = None
37
+ line_number: int = 0
38
+
39
+
40
+ @dataclass
41
+ class DiscoveredEndpoint:
42
+ """Represents an endpoint discovered in a component"""
43
+ method: str # GET, POST, PUT, DELETE
44
+ path: str
45
+ function_name: str
46
+ description: Optional[str] = None
47
+ line_number: int = 0
48
+
49
+
50
+ @dataclass
51
+ class DiscoveredQuery:
52
+ """Represents a database query discovered in a component"""
53
+ name: str
54
+ datasource: Optional[str] = None
55
+ query_type: str = "select" # select, insert, update, delete, exec
56
+ line_number: int = 0
57
+
58
+
59
+ @dataclass
60
+ class DiscoveredDependency:
61
+ """Represents a component dependency"""
62
+ component_name: str
63
+ import_path: Optional[str] = None
64
+ line_number: int = 0
65
+
66
+
67
+ @dataclass
68
+ class ComponentMetadata:
69
+ """Complete metadata for a discovered component"""
70
+ name: str
71
+ file_path: str
72
+ title: Optional[str] = None
73
+ description: Optional[str] = None
74
+
75
+ # Discovered elements
76
+ functions: List[DiscoveredFunction] = field(default_factory=list)
77
+ endpoints: List[DiscoveredEndpoint] = field(default_factory=list)
78
+ queries: List[DiscoveredQuery] = field(default_factory=list)
79
+ dependencies: List[DiscoveredDependency] = field(default_factory=list)
80
+
81
+ # Variables and state
82
+ variables: List[str] = field(default_factory=list)
83
+
84
+ # Validation
85
+ is_valid: bool = True
86
+ errors: List[str] = field(default_factory=list)
87
+ warnings: List[str] = field(default_factory=list)
88
+
89
+ # Stats
90
+ line_count: int = 0
91
+ last_modified: Optional[datetime] = None
92
+
93
+
94
+ class QuantumParser:
95
+ """Parser for Quantum .q files"""
96
+
97
+ # Quantum namespace
98
+ Q_NAMESPACE = "q"
99
+
100
+ # Tag patterns for extraction
101
+ COMPONENT_PATTERN = re.compile(r'<q:component\s+name=["\']([^"\']+)["\']', re.IGNORECASE)
102
+ FUNCTION_PATTERN = re.compile(r'<q:function\s+name=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
103
+ ENDPOINT_PATTERN = re.compile(r'<q:endpoint\s+([^>]+)>', re.IGNORECASE)
104
+ QUERY_PATTERN = re.compile(r'<q:query\s+name=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
105
+ SET_PATTERN = re.compile(r'<q:set\s+var=["\']([^"\']+)["\']', re.IGNORECASE)
106
+ IMPORT_PATTERN = re.compile(r'<q:import\s+component=["\']([^"\']+)["\']', re.IGNORECASE)
107
+ INCLUDE_PATTERN = re.compile(r'<q:include\s+file=["\']([^"\']+)["\']', re.IGNORECASE)
108
+
109
+ # HTTP method patterns
110
+ HTTP_GET_PATTERN = re.compile(r'<q:http-get\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
111
+ HTTP_POST_PATTERN = re.compile(r'<q:http-post\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
112
+ HTTP_PUT_PATTERN = re.compile(r'<q:http-put\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
113
+ HTTP_DELETE_PATTERN = re.compile(r'<q:http-delete\s+path=["\']([^"\']+)["\']([^>]*)>', re.IGNORECASE)
114
+
115
+ @classmethod
116
+ def parse_file(cls, file_path: str) -> ComponentMetadata:
117
+ """Parse a .q file and extract metadata"""
118
+ path = Path(file_path)
119
+
120
+ if not path.exists():
121
+ return ComponentMetadata(
122
+ name=path.stem,
123
+ file_path=str(path),
124
+ is_valid=False,
125
+ errors=["File not found"]
126
+ )
127
+
128
+ try:
129
+ content = path.read_text(encoding='utf-8')
130
+ lines = content.split('\n')
131
+ line_count = len(lines)
132
+ last_modified = datetime.fromtimestamp(path.stat().st_mtime)
133
+
134
+ # Extract component name
135
+ name_match = cls.COMPONENT_PATTERN.search(content)
136
+ component_name = name_match.group(1) if name_match else path.stem
137
+
138
+ metadata = ComponentMetadata(
139
+ name=component_name,
140
+ file_path=str(path),
141
+ line_count=line_count,
142
+ last_modified=last_modified
143
+ )
144
+
145
+ # Parse functions
146
+ metadata.functions = cls._parse_functions(content)
147
+
148
+ # Parse endpoints (both explicit and HTTP method tags)
149
+ metadata.endpoints = cls._parse_endpoints(content)
150
+
151
+ # Parse queries
152
+ metadata.queries = cls._parse_queries(content)
153
+
154
+ # Parse dependencies
155
+ metadata.dependencies = cls._parse_dependencies(content)
156
+
157
+ # Parse variables
158
+ metadata.variables = cls._parse_variables(content)
159
+
160
+ # Validate XML structure
161
+ validation_errors = cls._validate_xml(content)
162
+ if validation_errors:
163
+ metadata.warnings.extend(validation_errors)
164
+
165
+ return metadata
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error parsing {file_path}: {e}")
169
+ return ComponentMetadata(
170
+ name=path.stem,
171
+ file_path=str(path),
172
+ is_valid=False,
173
+ errors=[str(e)]
174
+ )
175
+
176
+ @classmethod
177
+ def _parse_functions(cls, content: str) -> List[DiscoveredFunction]:
178
+ """Extract function definitions from content"""
179
+ functions = []
180
+ lines = content.split('\n')
181
+
182
+ for i, line in enumerate(lines):
183
+ match = cls.FUNCTION_PATTERN.search(line)
184
+ if match:
185
+ name = match.group(1)
186
+ attrs = match.group(2)
187
+
188
+ # Extract parameters
189
+ params = []
190
+ params_match = re.search(r'params=["\']([^"\']+)["\']', attrs)
191
+ if params_match:
192
+ params = [p.strip() for p in params_match.group(1).split(',')]
193
+
194
+ # Extract return type
195
+ return_type = None
196
+ return_match = re.search(r'returns=["\']([^"\']+)["\']', attrs)
197
+ if return_match:
198
+ return_type = return_match.group(1)
199
+
200
+ functions.append(DiscoveredFunction(
201
+ name=name,
202
+ params=params,
203
+ return_type=return_type,
204
+ line_number=i + 1
205
+ ))
206
+
207
+ return functions
208
+
209
+ @classmethod
210
+ def _parse_endpoints(cls, content: str) -> List[DiscoveredEndpoint]:
211
+ """Extract endpoint definitions from content"""
212
+ endpoints = []
213
+ lines = content.split('\n')
214
+
215
+ # Parse explicit q:endpoint tags
216
+ for i, line in enumerate(lines):
217
+ match = cls.ENDPOINT_PATTERN.search(line)
218
+ if match:
219
+ attrs = match.group(1)
220
+
221
+ method_match = re.search(r'method=["\']([^"\']+)["\']', attrs)
222
+ path_match = re.search(r'path=["\']([^"\']+)["\']', attrs)
223
+ func_match = re.search(r'function=["\']([^"\']+)["\']', attrs)
224
+
225
+ if path_match:
226
+ endpoints.append(DiscoveredEndpoint(
227
+ method=method_match.group(1).upper() if method_match else "GET",
228
+ path=path_match.group(1),
229
+ function_name=func_match.group(1) if func_match else "",
230
+ line_number=i + 1
231
+ ))
232
+
233
+ # Parse HTTP method tags (q:http-get, q:http-post, etc.)
234
+ http_patterns = [
235
+ (cls.HTTP_GET_PATTERN, "GET"),
236
+ (cls.HTTP_POST_PATTERN, "POST"),
237
+ (cls.HTTP_PUT_PATTERN, "PUT"),
238
+ (cls.HTTP_DELETE_PATTERN, "DELETE"),
239
+ ]
240
+
241
+ for i, line in enumerate(lines):
242
+ for pattern, method in http_patterns:
243
+ match = pattern.search(line)
244
+ if match:
245
+ path = match.group(1)
246
+ attrs = match.group(2)
247
+
248
+ func_match = re.search(r'function=["\']([^"\']+)["\']', attrs)
249
+
250
+ endpoints.append(DiscoveredEndpoint(
251
+ method=method,
252
+ path=path,
253
+ function_name=func_match.group(1) if func_match else "",
254
+ line_number=i + 1
255
+ ))
256
+
257
+ return endpoints
258
+
259
+ @classmethod
260
+ def _parse_queries(cls, content: str) -> List[DiscoveredQuery]:
261
+ """Extract query definitions from content"""
262
+ queries = []
263
+ lines = content.split('\n')
264
+
265
+ for i, line in enumerate(lines):
266
+ match = cls.QUERY_PATTERN.search(line)
267
+ if match:
268
+ name = match.group(1)
269
+ attrs = match.group(2)
270
+
271
+ datasource = None
272
+ ds_match = re.search(r'datasource=["\']([^"\']+)["\']', attrs)
273
+ if ds_match:
274
+ datasource = ds_match.group(1)
275
+
276
+ # Determine query type from content
277
+ query_type = "select"
278
+ if re.search(r'type=["\'](\w+)["\']', attrs):
279
+ type_match = re.search(r'type=["\'](\w+)["\']', attrs)
280
+ query_type = type_match.group(1).lower()
281
+
282
+ queries.append(DiscoveredQuery(
283
+ name=name,
284
+ datasource=datasource,
285
+ query_type=query_type,
286
+ line_number=i + 1
287
+ ))
288
+
289
+ return queries
290
+
291
+ @classmethod
292
+ def _parse_dependencies(cls, content: str) -> List[DiscoveredDependency]:
293
+ """Extract component dependencies from content"""
294
+ dependencies = []
295
+ lines = content.split('\n')
296
+
297
+ for i, line in enumerate(lines):
298
+ # Check q:import
299
+ import_match = cls.IMPORT_PATTERN.search(line)
300
+ if import_match:
301
+ dependencies.append(DiscoveredDependency(
302
+ component_name=import_match.group(1),
303
+ line_number=i + 1
304
+ ))
305
+
306
+ # Check q:include
307
+ include_match = cls.INCLUDE_PATTERN.search(line)
308
+ if include_match:
309
+ dependencies.append(DiscoveredDependency(
310
+ component_name=include_match.group(1),
311
+ import_path=include_match.group(1),
312
+ line_number=i + 1
313
+ ))
314
+
315
+ return dependencies
316
+
317
+ @classmethod
318
+ def _parse_variables(cls, content: str) -> List[str]:
319
+ """Extract variable names from content"""
320
+ variables = set()
321
+
322
+ for match in cls.SET_PATTERN.finditer(content):
323
+ variables.add(match.group(1))
324
+
325
+ return list(variables)
326
+
327
+ @classmethod
328
+ def _validate_xml(cls, content: str) -> List[str]:
329
+ """Validate XML structure and return any errors"""
330
+ errors = []
331
+
332
+ # A MESMA normalizacao que o parser do framework faz.
333
+ #
334
+ # Sem ela, este validador reprovava arquivos .q que o proprio Quantum
335
+ # carrega sem reclamar: `<input required>`, `<br>`, `Forms & Actions`,
336
+ # `condition="n < 0"`, o corpo Python de um <q:python>. O componente
337
+ # aparecia na interface do admin marcado com "XML parse error" e o
338
+ # autor nao tinha o que consertar — o arquivo estava certo.
339
+ try:
340
+ from quantum.core.html_compat import normalise_html
341
+ content = normalise_html(content)
342
+ except ImportError: # pragma: no cover
343
+ pass
344
+
345
+ # A declaracao XML tem de sair antes de embrulhar. Um arquivo que
346
+ # comeca com `<?xml version="1.0"?>` — dezenas dos entregues — virava
347
+ # `<root ...><?xml ...?>`, e o erro reportado era "XML or text
348
+ # declaration not at start of entity", que descreve o embrulho, nao o
349
+ # arquivo.
350
+ content = re.sub(r'^\s*<\?xml[^?]*\?>\s*', '', content)
351
+
352
+ # E todo prefixo usado no documento precisa estar declarado no
353
+ # embrulho. So `q:` estava, entao `qg:` (jogos), `qt:` (terminal),
354
+ # `ui:` e `qtest:` davam "unbound prefix" — de novo um defeito do
355
+ # embrulho, num arquivo correto. Declarar o que o documento usa
356
+ # mantem os erros ESTRUTURAIS visiveis (tag nao fechada, token
357
+ # invalido) e elimina os inventados.
358
+ prefixos = sorted(set(re.findall(r'</?([A-Za-z_][\w.-]*):', content)))
359
+ ns = ' '.join(f"xmlns:{p}='quantum-{p}'" for p in prefixos) or ''
360
+ wrapped = f"<root {ns}>{content}</root>"
361
+
362
+ try:
363
+ ET.fromstring(wrapped)
364
+ except ET.ParseError as e:
365
+ errors.append(f"XML parse error: {e}")
366
+
367
+ return errors
368
+
369
+
370
+ class ComponentDiscoveryService:
371
+ """Service for discovering components in a project"""
372
+
373
+ def __init__(self, db: Session):
374
+ self.db = db
375
+ self.parser = QuantumParser()
376
+
377
+ def discover_components(
378
+ self,
379
+ project_id: int,
380
+ base_path: Optional[str] = None,
381
+ include_patterns: Optional[List[str]] = None,
382
+ exclude_patterns: Optional[List[str]] = None
383
+ ) -> List[ComponentMetadata]:
384
+ """
385
+ Discover all components in a project
386
+
387
+ Args:
388
+ project_id: Project ID
389
+ base_path: Base path to scan (defaults to project path)
390
+ include_patterns: Glob patterns to include
391
+ exclude_patterns: Glob patterns to exclude
392
+
393
+ Returns:
394
+ List of discovered component metadata
395
+ """
396
+ project = self.db.query(Project).filter(Project.id == project_id).first()
397
+ if not project:
398
+ raise ValueError(f"Project {project_id} not found")
399
+
400
+ # Determine base path - try project settings or use current directory
401
+ if not base_path:
402
+ # Project model may not have path field - use current directory as fallback
403
+ base_path = getattr(project, 'path', None) or getattr(project, 'root_path', None) or "."
404
+
405
+ if not os.path.isabs(base_path):
406
+ base_path = os.path.abspath(base_path)
407
+
408
+ if not os.path.exists(base_path):
409
+ logger.warning(f"Project path does not exist: {base_path}")
410
+ return []
411
+
412
+ # Default patterns
413
+ if include_patterns is None:
414
+ include_patterns = ["**/*.q", "**/*.quantum"]
415
+
416
+ if exclude_patterns is None:
417
+ exclude_patterns = ["**/node_modules/**", "**/.git/**", "**/venv/**", "**/__pycache__/**"]
418
+
419
+ # Find all .q files
420
+ discovered = []
421
+ base = Path(base_path)
422
+
423
+ for pattern in include_patterns:
424
+ for file_path in base.glob(pattern):
425
+ # Check exclusions
426
+ rel_path = str(file_path.relative_to(base))
427
+ excluded = False
428
+
429
+ for exclude in exclude_patterns:
430
+ if file_path.match(exclude):
431
+ excluded = True
432
+ break
433
+
434
+ if not excluded:
435
+ metadata = self.parser.parse_file(str(file_path))
436
+ discovered.append(metadata)
437
+
438
+ return discovered
439
+
440
+ def sync_components(
441
+ self,
442
+ project_id: int,
443
+ base_path: Optional[str] = None
444
+ ) -> Tuple[int, int, int]:
445
+ """
446
+ Sync discovered components with database
447
+
448
+ Returns:
449
+ Tuple of (created, updated, deleted) counts
450
+ """
451
+ # Discover components
452
+ discovered = self.discover_components(project_id, base_path)
453
+ discovered_paths = {m.file_path for m in discovered}
454
+
455
+ # Get existing components
456
+ existing = self.db.query(Component).filter(
457
+ Component.project_id == project_id
458
+ ).all()
459
+ existing_map = {c.file_path: c for c in existing}
460
+
461
+ created = 0
462
+ updated = 0
463
+ deleted = 0
464
+
465
+ # Process discovered components
466
+ for metadata in discovered:
467
+ if metadata.file_path in existing_map:
468
+ # Update existing
469
+ component = existing_map[metadata.file_path]
470
+ component.name = metadata.name
471
+ component.status = "active" if metadata.is_valid else "error"
472
+ component.error_message = "; ".join(metadata.errors) if metadata.errors else None
473
+ component.last_compiled = metadata.last_modified
474
+
475
+ # Update counters
476
+ component.function_count = len(metadata.functions)
477
+ component.endpoint_count = len(metadata.endpoints)
478
+ component.query_count = len(metadata.queries)
479
+
480
+ updated += 1
481
+
482
+ # Sync endpoints
483
+ self._sync_endpoints(component.id, project_id, metadata.endpoints)
484
+ else:
485
+ # Create new
486
+ component = Component(
487
+ project_id=project_id,
488
+ name=metadata.name,
489
+ file_path=metadata.file_path,
490
+ status="active" if metadata.is_valid else "error",
491
+ error_message="; ".join(metadata.errors) if metadata.errors else None,
492
+ last_compiled=metadata.last_modified,
493
+ function_count=len(metadata.functions),
494
+ endpoint_count=len(metadata.endpoints),
495
+ query_count=len(metadata.queries)
496
+ )
497
+ self.db.add(component)
498
+ self.db.flush() # Get component ID
499
+ created += 1
500
+
501
+ # Create endpoints
502
+ self._sync_endpoints(component.id, project_id, metadata.endpoints)
503
+
504
+ # Remove deleted components
505
+ for file_path, component in existing_map.items():
506
+ if file_path not in discovered_paths:
507
+ # Delete endpoints first
508
+ self.db.query(Endpoint).filter(
509
+ Endpoint.component_id == component.id
510
+ ).delete()
511
+ self.db.delete(component)
512
+ deleted += 1
513
+
514
+ self.db.commit()
515
+
516
+ return created, updated, deleted
517
+
518
+ def _sync_endpoints(
519
+ self,
520
+ component_id: int,
521
+ project_id: int,
522
+ endpoints: List[DiscoveredEndpoint]
523
+ ):
524
+ """Sync endpoints for a component"""
525
+ # Delete existing endpoints for this component
526
+ self.db.query(Endpoint).filter(
527
+ Endpoint.component_id == component_id
528
+ ).delete()
529
+
530
+ # Create new endpoints
531
+ for ep in endpoints:
532
+ endpoint = Endpoint(
533
+ project_id=project_id,
534
+ component_id=component_id,
535
+ method=ep.method,
536
+ path=ep.path,
537
+ function_name=ep.function_name,
538
+ description=ep.description
539
+ )
540
+ self.db.add(endpoint)
541
+
542
+ def get_component_details(
543
+ self,
544
+ project_id: int,
545
+ component_id: int
546
+ ) -> Optional[ComponentMetadata]:
547
+ """Get detailed metadata for a specific component"""
548
+ component = self.db.query(Component).filter(
549
+ Component.id == component_id,
550
+ Component.project_id == project_id
551
+ ).first()
552
+
553
+ if not component:
554
+ return None
555
+
556
+ return self.parser.parse_file(component.file_path)
557
+
558
+ def validate_component(
559
+ self,
560
+ file_path: str
561
+ ) -> Tuple[bool, List[str], List[str]]:
562
+ """
563
+ Validate a component file
564
+
565
+ Returns:
566
+ Tuple of (is_valid, errors, warnings)
567
+ """
568
+ metadata = self.parser.parse_file(file_path)
569
+ return metadata.is_valid, metadata.errors, metadata.warnings
570
+
571
+ def get_dependency_graph(
572
+ self,
573
+ project_id: int,
574
+ base_path: Optional[str] = None
575
+ ) -> Dict[str, List[str]]:
576
+ """
577
+ Build dependency graph for all components
578
+
579
+ Args:
580
+ project_id: Project ID
581
+ base_path: Optional base path to scan
582
+
583
+ Returns:
584
+ Dict mapping component names to their dependencies
585
+ """
586
+ discovered = self.discover_components(project_id, base_path)
587
+
588
+ graph = {}
589
+ for metadata in discovered:
590
+ deps = [d.component_name for d in metadata.dependencies]
591
+ graph[metadata.name] = deps
592
+
593
+ return graph
594
+
595
+ def find_unused_components(
596
+ self,
597
+ project_id: int,
598
+ base_path: Optional[str] = None
599
+ ) -> List[str]:
600
+ """Find components that are not imported by any other component"""
601
+ graph = self.get_dependency_graph(project_id, base_path)
602
+
603
+ # Get all component names
604
+ all_components = set(graph.keys())
605
+
606
+ # Get all referenced components
607
+ referenced = set()
608
+ for deps in graph.values():
609
+ referenced.update(deps)
610
+
611
+ # Find unused (not referenced by any other)
612
+ unused = all_components - referenced
613
+
614
+ return list(unused)
615
+
616
+ def discover_existing_tests(
617
+ self,
618
+ project_id: int,
619
+ tests_dir: Optional[str] = None
620
+ ) -> Dict[str, List[Dict[str, Any]]]:
621
+ """
622
+ Discover existing test files and match them to components
623
+
624
+ Args:
625
+ project_id: Project ID
626
+ tests_dir: Directory containing tests (defaults to project/tests)
627
+
628
+ Returns:
629
+ Dict mapping component names to list of discovered test info
630
+ """
631
+ project = self.db.query(Project).filter(Project.id == project_id).first()
632
+ if not project:
633
+ return {}
634
+
635
+ # Determine tests directory
636
+ if not tests_dir:
637
+ base = project.source_path or "."
638
+ tests_dir = os.path.join(base, "tests")
639
+
640
+ if not os.path.exists(tests_dir):
641
+ return {}
642
+
643
+ discovered_tests: Dict[str, List[Dict[str, Any]]] = {}
644
+
645
+ # Scan for test files
646
+ test_path = Path(tests_dir)
647
+ for test_file in test_path.glob("**/test_*.py"):
648
+ # Parse test file to extract test functions
649
+ try:
650
+ content = test_file.read_text(encoding='utf-8')
651
+ file_path = str(test_file)
652
+
653
+ # Extract component name from file (test_<component>.py)
654
+ file_stem = test_file.stem # test_user_login
655
+ component_name = file_stem.replace("test_", "").replace("_", "")
656
+
657
+ # Find test functions
658
+ test_pattern = re.compile(r'def (test_\w+)\s*\(')
659
+ for match in test_pattern.finditer(content):
660
+ test_name = match.group(1)
661
+
662
+ # Determine test type from path or name
663
+ test_type = "unit"
664
+ if "integration" in str(test_file).lower():
665
+ test_type = "integration"
666
+ elif "api" in str(test_file).lower() or "endpoint" in test_name.lower():
667
+ test_type = "api"
668
+ elif "e2e" in str(test_file).lower():
669
+ test_type = "e2e"
670
+
671
+ if component_name not in discovered_tests:
672
+ discovered_tests[component_name] = []
673
+
674
+ discovered_tests[component_name].append({
675
+ "test_file": file_path,
676
+ "test_name": test_name,
677
+ "test_type": test_type
678
+ })
679
+
680
+ except Exception as e:
681
+ logger.warning(f"Error parsing test file {test_file}: {e}")
682
+
683
+ return discovered_tests
684
+
685
+ def sync_component_tests(
686
+ self,
687
+ project_id: int,
688
+ tests_dir: Optional[str] = None
689
+ ) -> Tuple[int, int]:
690
+ """
691
+ Sync discovered tests with database
692
+
693
+ Returns:
694
+ Tuple of (tests_synced, components_updated)
695
+ """
696
+ from datetime import datetime
697
+
698
+ discovered = self.discover_existing_tests(project_id, tests_dir)
699
+ tests_synced = 0
700
+ components_updated = 0
701
+
702
+ # Get all components for this project
703
+ components = self.db.query(Component).filter(
704
+ Component.project_id == project_id
705
+ ).all()
706
+
707
+ component_map = {c.name.lower(): c for c in components}
708
+
709
+ for comp_name_key, tests in discovered.items():
710
+ # Try to find matching component
711
+ component = component_map.get(comp_name_key.lower())
712
+ if not component:
713
+ # Try with underscores removed
714
+ for name, comp in component_map.items():
715
+ if name.replace("_", "") == comp_name_key.lower():
716
+ component = comp
717
+ break
718
+
719
+ if not component:
720
+ continue
721
+
722
+ # Get existing tests for this component
723
+ existing_tests = self.db.query(ComponentTest).filter(
724
+ ComponentTest.component_id == component.id
725
+ ).all()
726
+ existing_map = {(t.test_file, t.test_name): t for t in existing_tests}
727
+
728
+ for test_info in tests:
729
+ key = (test_info["test_file"], test_info["test_name"])
730
+ if key not in existing_map:
731
+ # Create new test record
732
+ test = ComponentTest(
733
+ component_id=component.id,
734
+ test_file=test_info["test_file"],
735
+ test_name=test_info["test_name"],
736
+ test_type=test_info["test_type"],
737
+ generated_by="discovered",
738
+ last_status="pending"
739
+ )
740
+ self.db.add(test)
741
+ tests_synced += 1
742
+
743
+ # Update component test count
744
+ new_test_count = len(tests)
745
+ if component.test_count != new_test_count:
746
+ component.test_count = new_test_count
747
+ components_updated += 1
748
+
749
+ self.db.commit()
750
+ return tests_synced, components_updated
751
+
752
+
753
+ # Singleton instance
754
+ _discovery_service = None
755
+
756
+
757
+ def get_discovery_service(db: Session = None) -> ComponentDiscoveryService:
758
+ """Get component discovery service instance"""
759
+ if db:
760
+ return ComponentDiscoveryService(db)
761
+
762
+ global _discovery_service
763
+ if _discovery_service is None:
764
+ from .database import SessionLocal
765
+ _discovery_service = ComponentDiscoveryService(SessionLocal())
766
+ return _discovery_service