quantum-framework 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +417 -0
- quantum/cli/commands/dev.py +185 -0
- quantum/cli/commands/docs.py +329 -0
- quantum/cli/commands/lint.py +523 -0
- quantum/cli/commands/migrate.py +527 -0
- quantum/cli/commands/new.py +622 -0
- quantum/cli/commands/serve.py +190 -0
- quantum/cli/commands/test.py +193 -0
- quantum/cli/deploy.py +810 -0
- quantum/cli/hot_reload.py +951 -0
- quantum/cli/jobs.py +356 -0
- quantum/cli/mq.py +582 -0
- quantum/cli/pkg.py +390 -0
- quantum/cli/runner.py +547 -0
- quantum/cli/server_process.py +159 -0
- quantum/cli/utils.py +334 -0
- quantum/compiler/__init__.py +30 -0
- quantum/compiler/base_generator.py +367 -0
- quantum/compiler/cli.py +295 -0
- quantum/compiler/expression_transformer.py +444 -0
- quantum/compiler/javascript/__init__.py +10 -0
- quantum/compiler/javascript/generator.py +659 -0
- quantum/compiler/optimizer.py +270 -0
- quantum/compiler/python/__init__.py +10 -0
- quantum/compiler/python/generator.py +883 -0
- quantum/compiler/python/runtime.py +863 -0
- quantum/compiler/transpiler.py +330 -0
- quantum/core/__init__.py +3 -0
- quantum/core/ast_nodes.py +2611 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +148 -0
- quantum/core/expressions.py +591 -0
- quantum/core/features/agents/src/__init__.py +30 -0
- quantum/core/features/agents/src/ast_node.py +540 -0
- quantum/core/features/conditionals/src/__init__.py +8 -0
- quantum/core/features/conditionals/src/ast_node.py +68 -0
- quantum/core/features/data_fetching/src/__init__.py +21 -0
- quantum/core/features/data_fetching/src/ast_node.py +312 -0
- quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
- quantum/core/features/data_fetching/src/html_adapter.py +474 -0
- quantum/core/features/data_fetching/src/parser.py +225 -0
- quantum/core/features/data_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +291 -0
- quantum/core/features/data_import/src/runtime.py +538 -0
- quantum/core/features/dump/src/__init__.py +12 -0
- quantum/core/features/dump/src/ast_node.py +106 -0
- quantum/core/features/dump/src/parser.py +61 -0
- quantum/core/features/dump/src/runtime.py +246 -0
- quantum/core/features/functions/src/__init__.py +8 -0
- quantum/core/features/functions/src/ast_node.py +149 -0
- quantum/core/features/game_engine_2d/src/__init__.py +19 -0
- quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
- quantum/core/features/game_engine_2d/src/parser.py +983 -0
- quantum/core/features/invocation/src/__init__.py +0 -0
- quantum/core/features/invocation/src/ast_node.py +146 -0
- quantum/core/features/invocation/src/runtime.py +327 -0
- quantum/core/features/knowledge_base/src/__init__.py +6 -0
- quantum/core/features/knowledge_base/src/ast_node.py +113 -0
- quantum/core/features/knowledge_base/src/parser.py +82 -0
- quantum/core/features/logging/src/__init__.py +12 -0
- quantum/core/features/logging/src/ast_node.py +111 -0
- quantum/core/features/logging/src/parser.py +50 -0
- quantum/core/features/logging/src/runtime.py +190 -0
- quantum/core/features/loops/src/__init__.py +8 -0
- quantum/core/features/loops/src/ast_node.py +60 -0
- quantum/core/features/query/src/__init__.py +0 -0
- quantum/core/features/query/src/database_service.py +322 -0
- quantum/core/features/query/src/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +228 -0
- quantum/core/features/terminal_engine/src/__init__.py +21 -0
- quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
- quantum/core/features/terminal_engine/src/parser.py +361 -0
- quantum/core/features/testing_engine/src/__init__.py +41 -0
- quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
- quantum/core/features/testing_engine/src/parser.py +604 -0
- quantum/core/features/theming/src/__init__.py +48 -0
- quantum/core/features/theming/src/ast_node.py +137 -0
- quantum/core/features/theming/src/presets.py +405 -0
- quantum/core/features/ui_engine/src/__init__.py +20 -0
- quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
- quantum/core/features/ui_engine/src/parser.py +1106 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1235 -0
- quantum/core/parser_registry.py +213 -0
- quantum/core/parsers/__init__.py +76 -0
- quantum/core/parsers/ai/__init__.py +12 -0
- quantum/core/parsers/ai/agent_parser.py +106 -0
- quantum/core/parsers/ai/knowledge_parser.py +86 -0
- quantum/core/parsers/ai/llm_parser.py +78 -0
- quantum/core/parsers/ai/team_parser.py +88 -0
- quantum/core/parsers/base.py +322 -0
- quantum/core/parsers/composition/__init__.py +10 -0
- quantum/core/parsers/composition/import_parser.py +50 -0
- quantum/core/parsers/composition/slot_parser.py +48 -0
- quantum/core/parsers/control_flow/__init__.py +11 -0
- quantum/core/parsers/control_flow/if_parser.py +68 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +89 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +217 -0
- quantum/core/parsers/data/invoke_parser.py +116 -0
- quantum/core/parsers/data/query_parser.py +188 -0
- quantum/core/parsers/data/transaction_parser.py +77 -0
- quantum/core/parsers/events/__init__.py +9 -0
- quantum/core/parsers/events/dispatch_event_parser.py +44 -0
- quantum/core/parsers/forms/__init__.py +11 -0
- quantum/core/parsers/forms/action_parser.py +54 -0
- quantum/core/parsers/forms/flash_parser.py +34 -0
- quantum/core/parsers/forms/redirect_parser.py +33 -0
- quantum/core/parsers/functions/__init__.py +11 -0
- quantum/core/parsers/functions/function_parser.py +137 -0
- quantum/core/parsers/functions/param_parser.py +66 -0
- quantum/core/parsers/functions/return_parser.py +31 -0
- quantum/core/parsers/html/__init__.py +10 -0
- quantum/core/parsers/html/component_call_parser.py +130 -0
- quantum/core/parsers/html/html_parser.py +115 -0
- quantum/core/parsers/jobs/__init__.py +11 -0
- quantum/core/parsers/jobs/job_parser.py +71 -0
- quantum/core/parsers/jobs/schedule_parser.py +62 -0
- quantum/core/parsers/jobs/thread_parser.py +57 -0
- quantum/core/parsers/messaging/__init__.py +17 -0
- quantum/core/parsers/messaging/message_ack_parser.py +30 -0
- quantum/core/parsers/messaging/message_nack_parser.py +30 -0
- quantum/core/parsers/messaging/message_parser.py +114 -0
- quantum/core/parsers/messaging/queue_parser.py +61 -0
- quantum/core/parsers/messaging/websocket_parser.py +121 -0
- quantum/core/parsers/persistence/__init__.py +9 -0
- quantum/core/parsers/persistence/persist_parser.py +64 -0
- quantum/core/parsers/routing/__init__.py +9 -0
- quantum/core/parsers/routing/route_parser.py +41 -0
- quantum/core/parsers/scripting/__init__.py +12 -0
- quantum/core/parsers/scripting/pyclass_parser.py +62 -0
- quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
- quantum/core/parsers/scripting/pyimport_parser.py +52 -0
- quantum/core/parsers/scripting/python_parser.py +49 -0
- quantum/core/parsers/services/__init__.py +12 -0
- quantum/core/parsers/services/dump_parser.py +52 -0
- quantum/core/parsers/services/file_parser.py +48 -0
- quantum/core/parsers/services/log_parser.py +46 -0
- quantum/core/parsers/services/mail_parser.py +65 -0
- quantum/core/tiers.py +82 -0
- quantum/packages/__init__.py +28 -0
- quantum/packages/manager.py +413 -0
- quantum/packages/manifest.py +351 -0
- quantum/packages/registry.py +399 -0
- quantum/packages/resolver.py +336 -0
- quantum/plugins/__init__.py +33 -0
- quantum/plugins/hooks.py +329 -0
- quantum/plugins/loader.py +479 -0
- quantum/plugins/manifest.py +336 -0
- quantum/plugins/registry.py +371 -0
- quantum/runtime/__init__.py +28 -0
- quantum/runtime/action_handler.py +443 -0
- quantum/runtime/adapters/__init__.py +88 -0
- quantum/runtime/adapters/memory_adapter.py +690 -0
- quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
- quantum/runtime/adapters/redis_adapter.py +582 -0
- quantum/runtime/adapters/sqlite_adapter.py +414 -0
- quantum/runtime/agent_service.py +1133 -0
- quantum/runtime/api_server.py +86 -0
- quantum/runtime/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +990 -0
- quantum/runtime/component_composer.py +319 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/database_service.py +598 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +286 -0
- quantum/runtime/executor_registry.py +171 -0
- quantum/runtime/executors/__init__.py +71 -0
- quantum/runtime/executors/ai/__init__.py +12 -0
- quantum/runtime/executors/ai/agent_executor.py +217 -0
- quantum/runtime/executors/ai/knowledge_executor.py +114 -0
- quantum/runtime/executors/ai/llm_executor.py +153 -0
- quantum/runtime/executors/ai/team_executor.py +171 -0
- quantum/runtime/executors/base.py +262 -0
- quantum/runtime/executors/control_flow/__init__.py +11 -0
- quantum/runtime/executors/control_flow/if_executor.py +93 -0
- quantum/runtime/executors/control_flow/loop_executor.py +307 -0
- quantum/runtime/executors/control_flow/set_executor.py +412 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +145 -0
- quantum/runtime/executors/data/invoke_executor.py +176 -0
- quantum/runtime/executors/data/query_executor.py +256 -0
- quantum/runtime/executors/data/transaction_executor.py +91 -0
- quantum/runtime/executors/jobs/__init__.py +11 -0
- quantum/runtime/executors/jobs/job_executor.py +190 -0
- quantum/runtime/executors/jobs/schedule_executor.py +132 -0
- quantum/runtime/executors/jobs/thread_executor.py +127 -0
- quantum/runtime/executors/messaging/__init__.py +17 -0
- quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
- quantum/runtime/executors/messaging/message_executor.py +174 -0
- quantum/runtime/executors/messaging/queue_executor.py +103 -0
- quantum/runtime/executors/messaging/websocket_executor.py +197 -0
- quantum/runtime/executors/scripting/__init__.py +11 -0
- quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
- quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
- quantum/runtime/executors/scripting/python_executor.py +249 -0
- quantum/runtime/executors/services/__init__.py +12 -0
- quantum/runtime/executors/services/dump_executor.py +72 -0
- quantum/runtime/executors/services/file_executor.py +89 -0
- quantum/runtime/executors/services/log_executor.py +77 -0
- quantum/runtime/executors/services/mail_executor.py +81 -0
- quantum/runtime/expression_cache.py +498 -0
- quantum/runtime/file_upload_service.py +326 -0
- quantum/runtime/function_registry.py +118 -0
- quantum/runtime/game_builder.py +166 -0
- quantum/runtime/game_code_generator.py +2371 -0
- quantum/runtime/game_templates.py +2006 -0
- quantum/runtime/godot_code_generator.py +4681 -0
- quantum/runtime/godot_templates.py +1449 -0
- quantum/runtime/job_executor.py +1599 -0
- quantum/runtime/knowledge_service.py +500 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/logging_setup.py +140 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +184 -0
- quantum/runtime/pypy_compat.py +315 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +733 -0
- quantum/runtime/service_container.py +444 -0
- quantum/runtime/terminal_builder.py +76 -0
- quantum/runtime/terminal_code_generator.py +607 -0
- quantum/runtime/terminal_templates.py +243 -0
- quantum/runtime/testing_builder.py +77 -0
- quantum/runtime/testing_code_generator.py +833 -0
- quantum/runtime/testing_templates.py +85 -0
- quantum/runtime/ui_builder.py +188 -0
- quantum/runtime/ui_desktop_adapter.py +1730 -0
- quantum/runtime/ui_desktop_templates.py +307 -0
- quantum/runtime/ui_html_adapter.py +2691 -0
- quantum/runtime/ui_html_templates.py +2297 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_textual_adapter.py +1866 -0
- quantum/runtime/ui_textual_templates.py +45 -0
- quantum/runtime/ui_tokens.py +465 -0
- quantum/runtime/ui_validator.py +365 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +1766 -0
- quantum/runtime/websocket_adapter.py +501 -0
- quantum/runtime/websocket_service.py +585 -0
- quantum/runtime/websocket_transport.py +289 -0
- quantum/runtime/wsgi.py +101 -0
- quantum/utils/__init__.py +1 -0
- quantum_framework-0.9.0.dist-info/METADATA +244 -0
- quantum_framework-0.9.0.dist-info/RECORD +262 -0
- quantum_framework-0.9.0.dist-info/WHEEL +5 -0
- quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
'''
|