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,312 @@
1
+ """
2
+ Data Fetching Feature - AST Nodes
3
+ Defines FetchNode and related nodes for q:fetch declarative data fetching
4
+ """
5
+
6
+ from typing import List, Dict, Any, Optional
7
+ from dataclasses import dataclass, field
8
+ from quantum.core.ast_nodes import QuantumNode
9
+
10
+
11
+ @dataclass
12
+ class FetchHeaderNode(QuantumNode):
13
+ """Represents a <q:header> within q:fetch for HTTP headers."""
14
+
15
+ name: str
16
+ value: str
17
+
18
+ def to_dict(self) -> Dict[str, Any]:
19
+ return {
20
+ "type": "fetch_header",
21
+ "name": self.name,
22
+ "value": self.value[:50] + "..." if len(str(self.value)) > 50 else self.value
23
+ }
24
+
25
+ def validate(self) -> List[str]:
26
+ errors = []
27
+ if not self.name:
28
+ errors.append("Fetch header name is required")
29
+ if self.value is None:
30
+ errors.append(f"Fetch header '{self.name}' value is required")
31
+ return errors
32
+
33
+
34
+ @dataclass
35
+ class FetchLoadingNode(QuantumNode):
36
+ """Represents <q:loading> within q:fetch - content shown during loading."""
37
+
38
+ children: List[QuantumNode] = field(default_factory=list)
39
+
40
+ def add_child(self, child: QuantumNode):
41
+ """Add a child node to loading content."""
42
+ self.children.append(child)
43
+
44
+ def to_dict(self) -> Dict[str, Any]:
45
+ return {
46
+ "type": "fetch_loading",
47
+ "children_count": len(self.children)
48
+ }
49
+
50
+ def validate(self) -> List[str]:
51
+ errors = []
52
+ for child in self.children:
53
+ if hasattr(child, 'validate'):
54
+ errors.extend(child.validate())
55
+ return errors
56
+
57
+
58
+ @dataclass
59
+ class FetchErrorNode(QuantumNode):
60
+ """Represents <q:error> within q:fetch - content shown on error."""
61
+
62
+ children: List[QuantumNode] = field(default_factory=list)
63
+
64
+ def add_child(self, child: QuantumNode):
65
+ """Add a child node to error content."""
66
+ self.children.append(child)
67
+
68
+ def to_dict(self) -> Dict[str, Any]:
69
+ return {
70
+ "type": "fetch_error",
71
+ "children_count": len(self.children)
72
+ }
73
+
74
+ def validate(self) -> List[str]:
75
+ errors = []
76
+ for child in self.children:
77
+ if hasattr(child, 'validate'):
78
+ errors.extend(child.validate())
79
+ return errors
80
+
81
+
82
+ @dataclass
83
+ class FetchSuccessNode(QuantumNode):
84
+ """Represents <q:success> within q:fetch - content shown on success."""
85
+
86
+ children: List[QuantumNode] = field(default_factory=list)
87
+
88
+ def add_child(self, child: QuantumNode):
89
+ """Add a child node to success content."""
90
+ self.children.append(child)
91
+
92
+ def to_dict(self) -> Dict[str, Any]:
93
+ return {
94
+ "type": "fetch_success",
95
+ "children_count": len(self.children)
96
+ }
97
+
98
+ def validate(self) -> List[str]:
99
+ errors = []
100
+ for child in self.children:
101
+ if hasattr(child, 'validate'):
102
+ errors.extend(child.validate())
103
+ return errors
104
+
105
+
106
+ class FetchNode(QuantumNode):
107
+ """
108
+ Represents a <q:fetch> - Declarative data fetching with state management.
109
+
110
+ Examples:
111
+ <q:fetch name="users" url="/api/users" method="GET" cache="5m">
112
+ <q:loading><ui:skeleton /></q:loading>
113
+ <q:error><ui:alert variant="danger">{error}</ui:alert></q:error>
114
+ <q:success>
115
+ <ui:list source="{users}" />
116
+ </q:success>
117
+ </q:fetch>
118
+
119
+ <q:fetch name="user" url="/api/users/{userId}" method="GET">
120
+ <q:header name="Authorization" value="Bearer {token}" />
121
+ <q:success>
122
+ <div>Name: {user.name}</div>
123
+ </q:success>
124
+ </q:fetch>
125
+
126
+ <q:fetch name="orders" url="/api/orders" method="POST" body="{orderData}">
127
+ <q:loading>Creating order...</q:loading>
128
+ <q:error>Failed: {error}</q:error>
129
+ <q:success>Order created: {orders.id}</q:success>
130
+ </q:fetch>
131
+ """
132
+
133
+ def __init__(self, name: str, url: str):
134
+ self.name = name # Variable name for result data
135
+ self.url = url # Fetch URL (supports databinding)
136
+
137
+ # HTTP configuration
138
+ self.method = "GET" # GET, POST, PUT, DELETE, PATCH
139
+ self.headers: List[FetchHeaderNode] = []
140
+ self.body = None # Request body (JSON string or databinding)
141
+ self.content_type = "application/json"
142
+
143
+ # Caching
144
+ self.cache = None # Cache TTL (e.g., "5m", "1h", "30s", "0" for no cache)
145
+ self.cache_key = None # Custom cache key (default: url + method)
146
+
147
+ # Polling / Refetch
148
+ self.interval = None # Polling interval (e.g., "10s", "1m")
149
+ self.refetch_on = None # Events that trigger refetch
150
+
151
+ # Request configuration
152
+ self.timeout = 30000 # Timeout in ms (default 30s)
153
+ self.retry = 0 # Number of retries on failure
154
+ self.retry_delay = 1000 # Delay between retries in ms
155
+
156
+ # Response handling
157
+ self.transform = None # Transform expression for response
158
+ self.response_format = "auto" # auto, json, text, blob
159
+
160
+ # Callbacks
161
+ self.on_success = None # Function to call on success
162
+ self.on_error = None # Function to call on error
163
+
164
+ # State children (loading/error/success)
165
+ self.loading_node: Optional[FetchLoadingNode] = None
166
+ self.error_node: Optional[FetchErrorNode] = None
167
+ self.success_node: Optional[FetchSuccessNode] = None
168
+
169
+ # Fallback content (if no q:loading/error/success provided)
170
+ self.fallback_children: List[QuantumNode] = []
171
+
172
+ # Abort on unmount (for SPA/reactive)
173
+ self.abort_on_unmount = True
174
+
175
+ # Lazy loading (don't fetch until visible)
176
+ self.lazy = False
177
+
178
+ # Credentials mode (for cookies)
179
+ self.credentials = None # "same-origin", "include", "omit"
180
+
181
+ def add_header(self, header: FetchHeaderNode):
182
+ """Add HTTP header to the fetch request."""
183
+ self.headers.append(header)
184
+
185
+ def set_loading(self, node: FetchLoadingNode):
186
+ """Set the loading state content."""
187
+ self.loading_node = node
188
+
189
+ def set_error(self, node: FetchErrorNode):
190
+ """Set the error state content."""
191
+ self.error_node = node
192
+
193
+ def set_success(self, node: FetchSuccessNode):
194
+ """Set the success state content."""
195
+ self.success_node = node
196
+
197
+ def add_fallback_child(self, child: QuantumNode):
198
+ """Add fallback content for simple fetch without states."""
199
+ self.fallback_children.append(child)
200
+
201
+ def get_cache_seconds(self) -> Optional[int]:
202
+ """Parse cache TTL string to seconds. Returns None if no caching."""
203
+ if not self.cache:
204
+ return None
205
+
206
+ cache = self.cache.strip().lower()
207
+
208
+ if cache == "0" or cache == "false" or cache == "no":
209
+ return 0
210
+
211
+ # Parse duration string (e.g., "5m", "1h", "30s")
212
+ import re
213
+ match = re.match(r'^(\d+)(s|m|h|d)?$', cache)
214
+ if match:
215
+ value = int(match.group(1))
216
+ unit = match.group(2) or 's'
217
+
218
+ multipliers = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
219
+ return value * multipliers.get(unit, 1)
220
+
221
+ # If it's just a number, treat as seconds
222
+ try:
223
+ return int(cache)
224
+ except ValueError:
225
+ return None
226
+
227
+ def get_interval_ms(self) -> Optional[int]:
228
+ """Parse interval string to milliseconds. Returns None if no polling."""
229
+ if not self.interval:
230
+ return None
231
+
232
+ interval = self.interval.strip().lower()
233
+
234
+ # Parse duration string (e.g., "10s", "1m", "500ms")
235
+ import re
236
+ match = re.match(r'^(\d+)(ms|s|m)?$', interval)
237
+ if match:
238
+ value = int(match.group(1))
239
+ unit = match.group(2) or 'ms'
240
+
241
+ multipliers = {'ms': 1, 's': 1000, 'm': 60000}
242
+ return value * multipliers.get(unit, 1)
243
+
244
+ # If it's just a number, treat as milliseconds
245
+ try:
246
+ return int(interval)
247
+ except ValueError:
248
+ return None
249
+
250
+ def to_dict(self) -> Dict[str, Any]:
251
+ return {
252
+ "type": "fetch",
253
+ "name": self.name,
254
+ "url": self.url[:50] + "..." if len(self.url) > 50 else self.url,
255
+ "method": self.method,
256
+ "headers_count": len(self.headers),
257
+ "cache": self.cache,
258
+ "interval": self.interval,
259
+ "timeout": self.timeout,
260
+ "retry": self.retry,
261
+ "transform": self.transform,
262
+ "has_loading": self.loading_node is not None,
263
+ "has_error": self.error_node is not None,
264
+ "has_success": self.success_node is not None,
265
+ "on_success": self.on_success,
266
+ "on_error": self.on_error,
267
+ }
268
+
269
+ def validate(self) -> List[str]:
270
+ errors = []
271
+
272
+ if not self.name:
273
+ errors.append("Fetch name is required")
274
+
275
+ if not self.url:
276
+ errors.append("Fetch URL is required")
277
+
278
+ # Validate HTTP method
279
+ valid_methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']
280
+ if self.method.upper() not in valid_methods:
281
+ errors.append(f"Invalid HTTP method: {self.method}. Must be one of {valid_methods}")
282
+
283
+ # Validate headers
284
+ for header in self.headers:
285
+ errors.extend(header.validate())
286
+
287
+ # Validate state nodes
288
+ if self.loading_node:
289
+ errors.extend(self.loading_node.validate())
290
+ if self.error_node:
291
+ errors.extend(self.error_node.validate())
292
+ if self.success_node:
293
+ errors.extend(self.success_node.validate())
294
+
295
+ # Validate fallback children
296
+ for child in self.fallback_children:
297
+ if hasattr(child, 'validate'):
298
+ errors.extend(child.validate())
299
+
300
+ # Validate response format
301
+ valid_formats = ['auto', 'json', 'text', 'blob']
302
+ if self.response_format not in valid_formats:
303
+ errors.append(f"Invalid response format: {self.response_format}. Must be one of {valid_formats}")
304
+
305
+ # Validate credentials mode
306
+ if self.credentials and self.credentials not in ['same-origin', 'include', 'omit']:
307
+ errors.append(f"Invalid credentials mode: {self.credentials}. Must be same-origin, include, or omit")
308
+
309
+ return errors
310
+
311
+ def __repr__(self):
312
+ return f'<FetchNode name={self.name} url={self.url[:30]}... method={self.method}>'
@@ -0,0 +1,351 @@
1
+ """
2
+ Data Fetching Feature - Desktop Adapter
3
+ Generates Python code using requests library for the Desktop (pywebview) target.
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, Any, Optional
8
+ from .ast_node import FetchNode
9
+
10
+
11
+ class FetchDesktopAdapter:
12
+ """Generates Python code for q:fetch nodes in desktop applications."""
13
+
14
+ def __init__(self):
15
+ self._fetch_counter = 0
16
+ self._fetches: Dict[str, FetchNode] = {}
17
+
18
+ def register_fetch(self, node: FetchNode) -> str:
19
+ """
20
+ Register a FetchNode and return its unique ID.
21
+
22
+ Args:
23
+ node: FetchNode to register
24
+
25
+ Returns:
26
+ Unique fetch ID
27
+ """
28
+ self._fetch_counter += 1
29
+ fetch_id = f"fetch_{self._fetch_counter}"
30
+ self._fetches[fetch_id] = node
31
+ return fetch_id
32
+
33
+ def generate_fetch_class(self) -> str:
34
+ """
35
+ Generate the QuantumFetch class for handling fetch requests.
36
+
37
+ Returns:
38
+ Python code for the fetch management class
39
+ """
40
+ return QUANTUM_FETCH_CLASS
41
+
42
+ def generate_fetch_methods(self) -> str:
43
+ """
44
+ Generate Python methods for each registered fetch.
45
+
46
+ Returns:
47
+ Python code with fetch methods
48
+ """
49
+ if not self._fetches:
50
+ return ' # No fetch nodes defined'
51
+
52
+ methods = []
53
+ for fetch_id, node in self._fetches.items():
54
+ method = self._generate_fetch_method(fetch_id, node)
55
+ methods.append(method)
56
+
57
+ return '\n\n'.join(methods)
58
+
59
+ def _generate_fetch_method(self, fetch_id: str, node: FetchNode) -> str:
60
+ """
61
+ Generate a Python method for a specific fetch.
62
+
63
+ Args:
64
+ fetch_id: Unique fetch identifier
65
+ node: FetchNode to generate code for
66
+
67
+ Returns:
68
+ Python method code
69
+ """
70
+ # Build headers dict
71
+ headers_dict = {}
72
+ for header in node.headers:
73
+ headers_dict[header.name] = header.value
74
+
75
+ # Escape URL and other strings
76
+ url_escaped = node.url.replace("'", "\\'")
77
+
78
+ lines = [
79
+ f" def {fetch_id}(self, params=None):",
80
+ f' """Fetch: {node.name} from {node.url[:50]}"""',
81
+ f" url = '{url_escaped}'",
82
+ f"",
83
+ f" # Resolve URL parameters from state",
84
+ f" url = self._resolve_url(url, params)",
85
+ f"",
86
+ ]
87
+
88
+ # Check cache first
89
+ cache_seconds = node.get_cache_seconds()
90
+ if cache_seconds and cache_seconds > 0:
91
+ lines.extend([
92
+ f" # Check cache",
93
+ f" cache_key = f'{node.method}:{{url}}'",
94
+ f" cached = self._check_cache(cache_key, {cache_seconds})",
95
+ f" if cached is not None:",
96
+ f" self.state.set('{node.name}', cached)",
97
+ f" self.state.set('{node.name}_loading', False)",
98
+ f" self.state.set('{node.name}_error', None)",
99
+ f" return cached",
100
+ f"",
101
+ ])
102
+
103
+ # Set loading state
104
+ lines.extend([
105
+ f" # Set loading state",
106
+ f" self.state.set('{node.name}_loading', True)",
107
+ f" self.state.set('{node.name}_error', None)",
108
+ f"",
109
+ ])
110
+
111
+ # Build request
112
+ lines.extend([
113
+ f" try:",
114
+ f" headers = {repr(headers_dict)}",
115
+ f" headers['Content-Type'] = '{node.content_type}'",
116
+ f"",
117
+ ])
118
+
119
+ # Add request body for non-GET methods
120
+ if node.method.upper() in ['POST', 'PUT', 'PATCH']:
121
+ if node.body:
122
+ lines.extend([
123
+ f" # Request body",
124
+ f" body = self._resolve_value('{node.body}')",
125
+ f" if isinstance(body, dict):",
126
+ f" body = json.dumps(body)",
127
+ f"",
128
+ ])
129
+ else:
130
+ lines.append(f" body = None")
131
+
132
+ # Execute request
133
+ method_lower = node.method.lower()
134
+ if node.method.upper() in ['POST', 'PUT', 'PATCH']:
135
+ lines.extend([
136
+ f" response = requests.{method_lower}(",
137
+ f" url,",
138
+ f" headers=headers,",
139
+ f" data=body,",
140
+ f" timeout={node.timeout / 1000}",
141
+ f" )",
142
+ ])
143
+ else:
144
+ lines.extend([
145
+ f" response = requests.{method_lower}(",
146
+ f" url,",
147
+ f" headers=headers,",
148
+ f" timeout={node.timeout / 1000}",
149
+ f" )",
150
+ ])
151
+
152
+ lines.extend([
153
+ f" response.raise_for_status()",
154
+ f"",
155
+ ])
156
+
157
+ # Parse response
158
+ if node.response_format == 'text':
159
+ lines.append(f" data = response.text")
160
+ elif node.response_format == 'blob':
161
+ lines.append(f" data = response.content")
162
+ else:
163
+ lines.extend([
164
+ f" # Parse response",
165
+ f" content_type = response.headers.get('content-type', '')",
166
+ f" if 'application/json' in content_type:",
167
+ f" data = response.json()",
168
+ f" else:",
169
+ f" data = response.text",
170
+ ])
171
+
172
+ # Apply transform
173
+ if node.transform:
174
+ lines.extend([
175
+ f"",
176
+ f" # Apply transform",
177
+ f" data = self._apply_transform(data, '{node.transform}')",
178
+ ])
179
+
180
+ # Cache result
181
+ if cache_seconds and cache_seconds > 0:
182
+ lines.extend([
183
+ f"",
184
+ f" # Cache result",
185
+ f" self._set_cache(cache_key, data)",
186
+ ])
187
+
188
+ # Update state
189
+ lines.extend([
190
+ f"",
191
+ f" # Update state",
192
+ f" self.state.set('{node.name}', data)",
193
+ f" self.state.set('{node.name}_loading', False)",
194
+ f" self.state.set('{node.name}_error', None)",
195
+ f"",
196
+ ])
197
+
198
+ # Call success callback
199
+ if node.on_success:
200
+ lines.extend([
201
+ f" # Success callback",
202
+ f" if hasattr(self.api, '{node.on_success}'):",
203
+ f" getattr(self.api, '{node.on_success}')(data)",
204
+ f"",
205
+ ])
206
+
207
+ lines.append(f" return data")
208
+
209
+ # Error handling
210
+ lines.extend([
211
+ f"",
212
+ f" except requests.RequestException as e:",
213
+ f" error_msg = str(e)",
214
+ f" self.state.set('{node.name}_loading', False)",
215
+ f" self.state.set('{node.name}_error', error_msg)",
216
+ f" self.state.set('{node.name}', None)",
217
+ ])
218
+
219
+ # Call error callback
220
+ if node.on_error:
221
+ lines.extend([
222
+ f" # Error callback",
223
+ f" if hasattr(self.api, '{node.on_error}'):",
224
+ f" getattr(self.api, '{node.on_error}')(error_msg)",
225
+ ])
226
+
227
+ lines.extend([
228
+ f" return None",
229
+ ])
230
+
231
+ return '\n'.join(lines)
232
+
233
+ def generate_initial_fetches(self) -> str:
234
+ """
235
+ Generate code to execute initial fetches on app start.
236
+
237
+ Returns:
238
+ Python code for initial fetch execution
239
+ """
240
+ if not self._fetches:
241
+ return ' pass # No initial fetches'
242
+
243
+ lines = []
244
+ for fetch_id, node in self._fetches.items():
245
+ if not node.lazy:
246
+ lines.append(f" self.fetch.{fetch_id}()")
247
+
248
+ return '\n'.join(lines) if lines else ' pass # All fetches are lazy'
249
+
250
+
251
+ # Python class template for fetch management
252
+ QUANTUM_FETCH_CLASS = '''
253
+ import json
254
+ import threading
255
+ import time
256
+ from typing import Any, Dict, Optional
257
+ import requests
258
+
259
+
260
+ class QuantumFetch:
261
+ """Manages fetch requests with caching and state updates."""
262
+
263
+ def __init__(self, state, api=None):
264
+ self.state = state
265
+ self.api = api
266
+ self._cache: Dict[str, tuple] = {} # key -> (data, expires_at)
267
+ self._polling_threads: Dict[str, threading.Event] = {}
268
+
269
+ def _resolve_url(self, url: str, params: Optional[Dict] = None) -> str:
270
+ """Resolve URL parameters from state and params dict."""
271
+ import re
272
+
273
+ def replace_param(match):
274
+ var_name = match.group(1)
275
+ # Check params first
276
+ if params and var_name in params:
277
+ return str(params[var_name])
278
+ # Then check state
279
+ value = self.state.get(var_name)
280
+ return str(value) if value is not None else match.group(0)
281
+
282
+ return re.sub(r'\\{([^}]+)\\}', replace_param, url)
283
+
284
+ def _resolve_value(self, value_expr: str) -> Any:
285
+ """Resolve a value expression from state."""
286
+ if not value_expr:
287
+ return None
288
+
289
+ # Check if it's a state reference
290
+ if value_expr.startswith('{') and value_expr.endswith('}'):
291
+ var_name = value_expr[1:-1].strip()
292
+ return self.state.get(var_name)
293
+
294
+ return value_expr
295
+
296
+ def _check_cache(self, key: str, ttl_seconds: int) -> Optional[Any]:
297
+ """Check if cached data exists and is not expired."""
298
+ if key in self._cache:
299
+ data, expires_at = self._cache[key]
300
+ if time.time() < expires_at:
301
+ return data
302
+ else:
303
+ del self._cache[key]
304
+ return None
305
+
306
+ def _set_cache(self, key: str, data: Any, ttl_seconds: int = 300):
307
+ """Cache data with expiration."""
308
+ self._cache[key] = (data, time.time() + ttl_seconds)
309
+
310
+ def _apply_transform(self, data: Any, transform_expr: str) -> Any:
311
+ """Apply a transform expression to data."""
312
+ # Simple transform support - can be extended
313
+ if transform_expr.startswith('.'):
314
+ # Property access like ".data" or ".results"
315
+ for prop in transform_expr[1:].split('.'):
316
+ if isinstance(data, dict) and prop in data:
317
+ data = data[prop]
318
+ elif hasattr(data, prop):
319
+ data = getattr(data, prop)
320
+ return data
321
+
322
+ def start_polling(self, fetch_id: str, fetch_method, interval_ms: int):
323
+ """Start polling a fetch at specified interval."""
324
+ if fetch_id in self._polling_threads:
325
+ self.stop_polling(fetch_id)
326
+
327
+ stop_event = threading.Event()
328
+ self._polling_threads[fetch_id] = stop_event
329
+
330
+ def poll():
331
+ while not stop_event.is_set():
332
+ try:
333
+ fetch_method()
334
+ except Exception:
335
+ pass
336
+ stop_event.wait(interval_ms / 1000)
337
+
338
+ thread = threading.Thread(target=poll, daemon=True)
339
+ thread.start()
340
+
341
+ def stop_polling(self, fetch_id: str):
342
+ """Stop polling for a specific fetch."""
343
+ if fetch_id in self._polling_threads:
344
+ self._polling_threads[fetch_id].set()
345
+ del self._polling_threads[fetch_id]
346
+
347
+ def stop_all_polling(self):
348
+ """Stop all polling threads."""
349
+ for fetch_id in list(self._polling_threads.keys()):
350
+ self.stop_polling(fetch_id)
351
+ '''