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.
- quantum/__init__.py +1 -0
- quantum/cli/__init__.py +2 -0
- quantum/cli/admin.py +131 -0
- quantum/cli/check.py +223 -0
- quantum/cli/commands/__init__.py +15 -0
- quantum/cli/commands/build.py +414 -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 +548 -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 +565 -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 +2619 -0
- quantum/core/config_env.py +54 -0
- quantum/core/docs_links.py +8 -0
- quantum/core/expression_diagnostics.py +87 -0
- quantum/core/expression_stdlib.py +208 -0
- quantum/core/expressions.py +677 -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_import/src/__init__.py +0 -0
- quantum/core/features/data_import/src/ast_node.py +292 -0
- quantum/core/features/data_import/src/runtime.py +533 -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 +147 -0
- quantum/core/features/invocation/src/runtime.py +332 -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/query_validators.py +20 -0
- quantum/core/features/state_management/src/__init__.py +11 -0
- quantum/core/features/state_management/src/ast_node.py +121 -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 +1913 -0
- quantum/core/features/ui_engine/src/parser.py +1192 -0
- quantum/core/features/websocket/src/__init__.py +24 -0
- quantum/core/features/websocket/src/ast_node.py +247 -0
- quantum/core/guards.py +53 -0
- quantum/core/html_compat.py +299 -0
- quantum/core/parser.py +1551 -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 +71 -0
- quantum/core/parsers/control_flow/loop_parser.py +105 -0
- quantum/core/parsers/control_flow/set_parser.py +93 -0
- quantum/core/parsers/data/__init__.py +12 -0
- quantum/core/parsers/data/data_parser.py +229 -0
- quantum/core/parsers/data/invoke_parser.py +133 -0
- quantum/core/parsers/data/query_parser.py +213 -0
- quantum/core/parsers/data/transaction_parser.py +86 -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 +79 -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 +98 -0
- quantum/core/parsers/functions/param_parser.py +73 -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/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 +164 -0
- quantum/core/xml_lines.py +81 -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 +25 -0
- quantum/runtime/action_handler.py +532 -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/ast_cache.py +506 -0
- quantum/runtime/auth_service.py +267 -0
- quantum/runtime/component.py +1048 -0
- quantum/runtime/component_composer.py +133 -0
- quantum/runtime/component_resolver.py +174 -0
- quantum/runtime/conditions.py +54 -0
- quantum/runtime/database_service.py +595 -0
- quantum/runtime/db_schema.py +100 -0
- quantum/runtime/dev_panel.py +178 -0
- quantum/runtime/email_service.py +162 -0
- quantum/runtime/error_handler.py +295 -0
- quantum/runtime/execution_context.py +283 -0
- quantum/runtime/executor_registry.py +178 -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 +264 -0
- quantum/runtime/executors/control_flow/__init__.py +12 -0
- quantum/runtime/executors/control_flow/if_executor.py +98 -0
- quantum/runtime/executors/control_flow/loop_executor.py +308 -0
- quantum/runtime/executors/control_flow/redirect_executor.py +43 -0
- quantum/runtime/executors/control_flow/set_executor.py +464 -0
- quantum/runtime/executors/data/__init__.py +12 -0
- quantum/runtime/executors/data/data_executor.py +156 -0
- quantum/runtime/executors/data/invoke_executor.py +228 -0
- quantum/runtime/executors/data/query_executor.py +290 -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 +546 -0
- quantum/runtime/llm_cache.py +100 -0
- quantum/runtime/llm_providers.py +704 -0
- quantum/runtime/llm_service.py +287 -0
- quantum/runtime/local_server.py +28 -0
- quantum/runtime/logging_setup.py +141 -0
- quantum/runtime/message_broker.py +364 -0
- quantum/runtime/message_queue_service.py +571 -0
- quantum/runtime/param_validation.py +218 -0
- quantum/runtime/pypy_compat.py +317 -0
- quantum/runtime/python_bridge.py +698 -0
- quantum/runtime/query_validators.py +304 -0
- quantum/runtime/renderer.py +734 -0
- quantum/runtime/service_container.py +470 -0
- quantum/runtime/table_params.py +163 -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 +222 -0
- quantum/runtime/ui_console.py +405 -0
- quantum/runtime/ui_desktop.py +56 -0
- quantum/runtime/ui_form_rules.py +193 -0
- quantum/runtime/ui_html_adapter.py +2567 -0
- quantum/runtime/ui_html_templates.py +2106 -0
- quantum/runtime/ui_mobile_adapter.py +1832 -0
- quantum/runtime/ui_mobile_templates.py +1003 -0
- quantum/runtime/ui_page_renderer.py +392 -0
- quantum/runtime/ui_pager.py +73 -0
- quantum/runtime/ui_table_edit.py +139 -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/ui_view.py +221 -0
- quantum/runtime/validators.py +256 -0
- quantum/runtime/web_server.py +2131 -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/services.py +109 -0
- quantum/utils/__init__.py +1 -0
- quantum_admin/__init__.py +4 -0
- quantum_admin/assets/quantum-admin-connectors.js +123 -0
- quantum_admin/assets/quantum-admin.css +2058 -0
- quantum_admin/assets/quantum-source-highlight.js +106 -0
- quantum_admin/backend/__init__.py +4 -0
- quantum_admin/backend/audit_service.py +346 -0
- quantum_admin/backend/auth_service.py +394 -0
- quantum_admin/backend/component_discovery.py +766 -0
- quantum_admin/backend/config_generator.py +586 -0
- quantum_admin/backend/connector_service.py +1016 -0
- quantum_admin/backend/crud.py +557 -0
- quantum_admin/backend/database.py +226 -0
- quantum_admin/backend/db_setup_service.py +193 -0
- quantum_admin/backend/deploy_service.py +698 -0
- quantum_admin/backend/docker_service.py +422 -0
- quantum_admin/backend/environment_service.py +418 -0
- quantum_admin/backend/git_service.py +370 -0
- quantum_admin/backend/health_service.py +390 -0
- quantum_admin/backend/job_service.py +347 -0
- quantum_admin/backend/log_service.py +331 -0
- quantum_admin/backend/main.py +14426 -0
- quantum_admin/backend/models.py +986 -0
- quantum_admin/backend/pipeline_service.py +812 -0
- quantum_admin/backend/resource_manager.py +1118 -0
- quantum_admin/backend/schemas.py +419 -0
- quantum_admin/backend/secret_manager.py +277 -0
- quantum_admin/backend/settings_service.py +488 -0
- quantum_admin/backend/template_service.py +710 -0
- quantum_admin/backend/test_execution_service.py +364 -0
- quantum_admin/backend/test_generator.py +782 -0
- quantum_admin/backend/webhook_service.py +454 -0
- quantum_admin/backend/websocket_manager.py +360 -0
- quantum_admin/components/admin/_layout/AdminShell.q +121 -0
- quantum_admin/components/admin/agents.q +107 -0
- quantum_admin/components/admin/app/[name].q +558 -0
- quantum_admin/components/admin/applications.q +160 -0
- quantum_admin/components/admin/component/[...path].q +205 -0
- quantum_admin/components/admin/components.q +59 -0
- quantum_admin/components/admin/connectors.q +283 -0
- quantum_admin/components/admin/dashboard.q +86 -0
- quantum_admin/components/admin/database.q +95 -0
- quantum_admin/components/admin/features.q +69 -0
- quantum_admin/components/admin/index.q +12 -0
- quantum_admin/components/admin/jobs.q +75 -0
- quantum_admin/components/admin/login.q +65 -0
- quantum_admin/components/admin/logout.q +11 -0
- quantum_admin/components/admin/settings.q +174 -0
- quantum_admin/components/admin/source.q +45 -0
- quantum_admin/components/admin/tests.q +73 -0
- quantum_admin/run.py +13 -0
- quantum_admin/services/__init__.py +16 -0
- quantum_admin/services/_base.py +84 -0
- quantum_admin/services/_component_test_generator.py +205 -0
- quantum_admin/services/_yaml_editor.py +82 -0
- quantum_admin/services/apps.py +330 -0
- quantum_admin/services/auth.py +61 -0
- quantum_admin/services/components.py +210 -0
- quantum_admin/services/connectors.py +164 -0
- quantum_admin/services/projects.py +189 -0
- quantum_admin/services/repository.py +223 -0
- quantum_admin/services/settings.py +150 -0
- quantum_admin/services/yaml_import.py +118 -0
- quantum_framework-0.18.0.dist-info/METADATA +281 -0
- quantum_framework-0.18.0.dist-info/RECORD +333 -0
- quantum_framework-0.18.0.dist-info/WHEEL +5 -0
- quantum_framework-0.18.0.dist-info/entry_points.txt +2 -0
- quantum_framework-0.18.0.dist-info/licenses/LICENSE +21 -0
- quantum_framework-0.18.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data Import Feature - Runtime
|
|
3
|
+
Handles execution of all data import and transformation operations
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import json
|
|
8
|
+
import csv
|
|
9
|
+
import requests
|
|
10
|
+
import xml.etree.ElementTree as ET
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Any, Optional, List
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from io import StringIO
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from quantum.core.expressions import ExpressionEvaluator
|
|
18
|
+
|
|
19
|
+
# Shared with ComponentRuntime and HTMLRenderer, so a {field} expression in a
|
|
20
|
+
# q:data filter means the same thing it means anywhere else in the language.
|
|
21
|
+
_EXPRESSIONS = ExpressionEvaluator()
|
|
22
|
+
_BINDING = re.compile(r'\{([^}]+)\}')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class DataResult:
|
|
27
|
+
"""Result object for all data operations"""
|
|
28
|
+
success: bool
|
|
29
|
+
data: Any = None
|
|
30
|
+
error: Optional[Dict[str, Any]] = None
|
|
31
|
+
recordCount: int = 0
|
|
32
|
+
loadTime: float = 0.0
|
|
33
|
+
cached: bool = False
|
|
34
|
+
source: str = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DataImportService:
|
|
38
|
+
"""Service to handle all types of data import and transformation"""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self.cache: Dict[str, Any] = {} # Simple in-memory cache
|
|
42
|
+
|
|
43
|
+
def import_data(
|
|
44
|
+
self,
|
|
45
|
+
data_type: str,
|
|
46
|
+
source: str,
|
|
47
|
+
params: Dict[str, Any],
|
|
48
|
+
context: Any = None
|
|
49
|
+
) -> DataResult:
|
|
50
|
+
"""Main data import dispatcher"""
|
|
51
|
+
start_time = time.time()
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
# Check cache if enabled
|
|
55
|
+
cache_enabled = params.get('cache', True)
|
|
56
|
+
if cache_enabled:
|
|
57
|
+
cache_key = f"data_{source}_{hash(str(params))}"
|
|
58
|
+
cached_result = self.get_from_cache(cache_key)
|
|
59
|
+
if cached_result is not None:
|
|
60
|
+
cached_result.cached = True
|
|
61
|
+
return cached_result
|
|
62
|
+
|
|
63
|
+
# Dispatch based on type
|
|
64
|
+
if data_type == "csv":
|
|
65
|
+
result = self._import_csv(source, params)
|
|
66
|
+
elif data_type == "json":
|
|
67
|
+
result = self._import_json(source, params)
|
|
68
|
+
elif data_type == "xml":
|
|
69
|
+
result = self._import_xml(source, params)
|
|
70
|
+
elif data_type == "transform":
|
|
71
|
+
result = self._transform_data(source, params, context)
|
|
72
|
+
else:
|
|
73
|
+
return DataResult(
|
|
74
|
+
success=False,
|
|
75
|
+
error={"message": f"Unknown data type: {data_type}"},
|
|
76
|
+
source=source
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Calculate load time
|
|
80
|
+
load_time = (time.time() - start_time) * 1000 # Convert to ms
|
|
81
|
+
result.loadTime = load_time
|
|
82
|
+
|
|
83
|
+
# Apply transformations if specified
|
|
84
|
+
if 'transforms' in params and params['transforms']:
|
|
85
|
+
result = self._apply_transformations(result, params['transforms'], context)
|
|
86
|
+
|
|
87
|
+
# Cache result if enabled
|
|
88
|
+
if cache_enabled and result.success:
|
|
89
|
+
ttl = params.get('ttl')
|
|
90
|
+
self.put_in_cache(cache_key, result, ttl)
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
load_time = (time.time() - start_time) * 1000
|
|
96
|
+
return DataResult(
|
|
97
|
+
success=False,
|
|
98
|
+
error={
|
|
99
|
+
"message": str(e),
|
|
100
|
+
"type": type(e).__name__
|
|
101
|
+
},
|
|
102
|
+
loadTime=load_time,
|
|
103
|
+
source=source
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _import_csv(self, source: str, params: Dict[str, Any]) -> DataResult:
|
|
107
|
+
"""Import CSV data from file or URL"""
|
|
108
|
+
try:
|
|
109
|
+
# Get CSV content
|
|
110
|
+
content = self._get_source_content(source, params.get('headers', []))
|
|
111
|
+
|
|
112
|
+
# Parse CSV
|
|
113
|
+
delimiter = params.get('delimiter', ',')
|
|
114
|
+
quote = params.get('quote', '"')
|
|
115
|
+
has_header = params.get('header', True)
|
|
116
|
+
encoding = params.get('encoding', 'utf-8')
|
|
117
|
+
skip_rows = params.get('skip_rows', 0)
|
|
118
|
+
columns = params.get('columns', [])
|
|
119
|
+
|
|
120
|
+
# Parse CSV content
|
|
121
|
+
csv_reader = csv.DictReader(
|
|
122
|
+
StringIO(content),
|
|
123
|
+
delimiter=delimiter,
|
|
124
|
+
quotechar=quote
|
|
125
|
+
) if has_header else csv.reader(StringIO(content), delimiter=delimiter, quotechar=quote)
|
|
126
|
+
|
|
127
|
+
# Skip rows if needed
|
|
128
|
+
for _ in range(skip_rows):
|
|
129
|
+
try:
|
|
130
|
+
next(csv_reader)
|
|
131
|
+
except StopIteration:
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
# Convert to list of dictionaries
|
|
135
|
+
data = []
|
|
136
|
+
for row in csv_reader:
|
|
137
|
+
if has_header:
|
|
138
|
+
# Row is already a dict
|
|
139
|
+
record = self._convert_csv_row(row, columns)
|
|
140
|
+
else:
|
|
141
|
+
# Row is a list - create dict from column definitions
|
|
142
|
+
if columns:
|
|
143
|
+
record = {}
|
|
144
|
+
for i, col_def in enumerate(columns):
|
|
145
|
+
if i < len(row):
|
|
146
|
+
col_name = col_def.get('name')
|
|
147
|
+
col_value = row[i]
|
|
148
|
+
record[col_name] = self._convert_type(col_value, col_def.get('type', 'string'))
|
|
149
|
+
else:
|
|
150
|
+
# No columns defined - use numeric keys
|
|
151
|
+
record = {str(i): val for i, val in enumerate(row)}
|
|
152
|
+
|
|
153
|
+
data.append(record)
|
|
154
|
+
|
|
155
|
+
return DataResult(
|
|
156
|
+
success=True,
|
|
157
|
+
data=data,
|
|
158
|
+
recordCount=len(data),
|
|
159
|
+
source=source
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
except Exception as e:
|
|
163
|
+
return DataResult(
|
|
164
|
+
success=False,
|
|
165
|
+
error={
|
|
166
|
+
"message": f"CSV import failed: {str(e)}",
|
|
167
|
+
"type": type(e).__name__
|
|
168
|
+
},
|
|
169
|
+
source=source
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _import_json(self, source: str, params: Dict[str, Any]) -> DataResult:
|
|
173
|
+
"""Import JSON data from file or URL"""
|
|
174
|
+
try:
|
|
175
|
+
# Get JSON content
|
|
176
|
+
content = self._get_source_content(source, params.get('headers', []))
|
|
177
|
+
|
|
178
|
+
# Parse JSON
|
|
179
|
+
data = json.loads(content)
|
|
180
|
+
|
|
181
|
+
# Ensure data is a list
|
|
182
|
+
if not isinstance(data, list):
|
|
183
|
+
# If it's a dict, wrap it in a list
|
|
184
|
+
data = [data]
|
|
185
|
+
|
|
186
|
+
return DataResult(
|
|
187
|
+
success=True,
|
|
188
|
+
data=data,
|
|
189
|
+
recordCount=len(data) if isinstance(data, list) else 1,
|
|
190
|
+
source=source
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
except json.JSONDecodeError as e:
|
|
194
|
+
return DataResult(
|
|
195
|
+
success=False,
|
|
196
|
+
error={
|
|
197
|
+
"message": f"Invalid JSON: {str(e)}",
|
|
198
|
+
"type": "JSONDecodeError"
|
|
199
|
+
},
|
|
200
|
+
source=source
|
|
201
|
+
)
|
|
202
|
+
except Exception as e:
|
|
203
|
+
return DataResult(
|
|
204
|
+
success=False,
|
|
205
|
+
error={
|
|
206
|
+
"message": f"JSON import failed: {str(e)}",
|
|
207
|
+
"type": type(e).__name__
|
|
208
|
+
},
|
|
209
|
+
source=source
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def _import_xml(self, source: str, params: Dict[str, Any]) -> DataResult:
|
|
213
|
+
"""Import XML data from file or URL with XPath"""
|
|
214
|
+
try:
|
|
215
|
+
# Get XML content
|
|
216
|
+
content = self._get_source_content(source, params.get('headers', []))
|
|
217
|
+
|
|
218
|
+
# Parse XML
|
|
219
|
+
root = ET.fromstring(content)
|
|
220
|
+
|
|
221
|
+
# Get XPath expression
|
|
222
|
+
xpath = params.get('xpath', '.')
|
|
223
|
+
fields = params.get('fields', [])
|
|
224
|
+
|
|
225
|
+
# Find nodes using XPath (basic support - ET has limited XPath)
|
|
226
|
+
nodes = root.findall(xpath)
|
|
227
|
+
|
|
228
|
+
# Extract data from nodes
|
|
229
|
+
data = []
|
|
230
|
+
for node in nodes:
|
|
231
|
+
record = {}
|
|
232
|
+
for field_def in fields:
|
|
233
|
+
field_name = field_def.get('name')
|
|
234
|
+
field_xpath = field_def.get('xpath')
|
|
235
|
+
field_type = field_def.get('type', 'string')
|
|
236
|
+
|
|
237
|
+
# Extract value using XPath
|
|
238
|
+
value = self._extract_xml_value(node, field_xpath)
|
|
239
|
+
record[field_name] = self._convert_type(value, field_type)
|
|
240
|
+
|
|
241
|
+
data.append(record)
|
|
242
|
+
|
|
243
|
+
return DataResult(
|
|
244
|
+
success=True, # was False: every XML import "failed", so no transform ran on it
|
|
245
|
+
data=data,
|
|
246
|
+
recordCount=len(data),
|
|
247
|
+
source=source
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
except ET.ParseError as e:
|
|
251
|
+
return DataResult(
|
|
252
|
+
success=False,
|
|
253
|
+
error={
|
|
254
|
+
"message": f"Invalid XML: {str(e)}",
|
|
255
|
+
"type": "XMLParseError"
|
|
256
|
+
},
|
|
257
|
+
source=source
|
|
258
|
+
)
|
|
259
|
+
except Exception as e:
|
|
260
|
+
return DataResult(
|
|
261
|
+
success=False,
|
|
262
|
+
error={
|
|
263
|
+
"message": f"XML import failed: {str(e)}",
|
|
264
|
+
"type": type(e).__name__
|
|
265
|
+
},
|
|
266
|
+
source=source
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def _transform_data(self, source: str, params: Dict[str, Any], context: Any) -> DataResult:
|
|
270
|
+
"""Transform existing data (from variable reference)"""
|
|
271
|
+
try:
|
|
272
|
+
# Source should be a variable reference like "{users}"
|
|
273
|
+
# Extract variable name and get data from context
|
|
274
|
+
if source.startswith('{') and source.endswith('}'):
|
|
275
|
+
var_name = source[1:-1] # Remove braces
|
|
276
|
+
else:
|
|
277
|
+
var_name = source
|
|
278
|
+
|
|
279
|
+
# Get data from context
|
|
280
|
+
if hasattr(context, 'get_variable'):
|
|
281
|
+
data = context.get_variable(var_name)
|
|
282
|
+
elif isinstance(context, dict):
|
|
283
|
+
data = context.get(var_name)
|
|
284
|
+
else:
|
|
285
|
+
return DataResult(
|
|
286
|
+
success=False,
|
|
287
|
+
error={"message": f"Cannot access variable '{var_name}' from context"},
|
|
288
|
+
source=source
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if data is None:
|
|
292
|
+
return DataResult(
|
|
293
|
+
success=False,
|
|
294
|
+
error={"message": f"Variable '{var_name}' not found"},
|
|
295
|
+
source=source
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Ensure data is a list
|
|
299
|
+
if not isinstance(data, list):
|
|
300
|
+
data = [data]
|
|
301
|
+
|
|
302
|
+
return DataResult(
|
|
303
|
+
success=True,
|
|
304
|
+
data=data,
|
|
305
|
+
recordCount=len(data),
|
|
306
|
+
source=source
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
return DataResult(
|
|
311
|
+
success=False,
|
|
312
|
+
error={
|
|
313
|
+
"message": f"Transform failed: {str(e)}",
|
|
314
|
+
"type": type(e).__name__
|
|
315
|
+
},
|
|
316
|
+
source=source
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def _apply_transformations(self, result: DataResult, transforms: List[Dict[str, Any]], context: Any) -> DataResult:
|
|
320
|
+
"""Apply transformation operations to data"""
|
|
321
|
+
if not result.success or not result.data:
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
data = result.data
|
|
325
|
+
|
|
326
|
+
for transform_op in transforms:
|
|
327
|
+
op_type = transform_op.get('type')
|
|
328
|
+
|
|
329
|
+
if op_type == 'filter':
|
|
330
|
+
data = self._apply_filter(data, transform_op, context)
|
|
331
|
+
elif op_type == 'sort':
|
|
332
|
+
data = self._apply_sort(data, transform_op)
|
|
333
|
+
elif op_type == 'limit':
|
|
334
|
+
data = self._apply_limit(data, transform_op)
|
|
335
|
+
elif op_type == 'compute':
|
|
336
|
+
data = self._apply_compute(data, transform_op, context)
|
|
337
|
+
|
|
338
|
+
# Update result
|
|
339
|
+
result.data = data
|
|
340
|
+
result.recordCount = len(data)
|
|
341
|
+
return result
|
|
342
|
+
|
|
343
|
+
def _apply_filter(self, data: List[Dict[str, Any]], filter_op: Dict[str, Any], context: Any) -> List[Dict[str, Any]]:
|
|
344
|
+
"""Filter data based on condition"""
|
|
345
|
+
condition = filter_op.get('condition', '')
|
|
346
|
+
|
|
347
|
+
filtered = []
|
|
348
|
+
for record in data:
|
|
349
|
+
# Evaluate condition for this record
|
|
350
|
+
if self._evaluate_condition(condition, record, context):
|
|
351
|
+
filtered.append(record)
|
|
352
|
+
|
|
353
|
+
return filtered
|
|
354
|
+
|
|
355
|
+
def _apply_sort(self, data: List[Dict[str, Any]], sort_op: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
356
|
+
"""Sort data by field"""
|
|
357
|
+
by_field = sort_op.get('by')
|
|
358
|
+
order = sort_op.get('order', 'asc')
|
|
359
|
+
|
|
360
|
+
if not by_field:
|
|
361
|
+
return data
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
# Sort data
|
|
365
|
+
reverse = (order == 'desc')
|
|
366
|
+
sorted_data = sorted(data, key=lambda x: x.get(by_field, ''), reverse=reverse)
|
|
367
|
+
return sorted_data
|
|
368
|
+
except Exception:
|
|
369
|
+
# If sorting fails, return original data
|
|
370
|
+
return data
|
|
371
|
+
|
|
372
|
+
def _apply_limit(self, data: List[Dict[str, Any]], limit_op: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
373
|
+
"""Limit number of records"""
|
|
374
|
+
limit = limit_op.get('value', 0)
|
|
375
|
+
|
|
376
|
+
if isinstance(limit, str):
|
|
377
|
+
try:
|
|
378
|
+
limit = int(limit)
|
|
379
|
+
except ValueError:
|
|
380
|
+
return data
|
|
381
|
+
|
|
382
|
+
return data[:limit]
|
|
383
|
+
|
|
384
|
+
def _apply_compute(self, data: List[Dict[str, Any]], compute_op: Dict[str, Any], context: Any) -> List[Dict[str, Any]]:
|
|
385
|
+
"""Compute derived field"""
|
|
386
|
+
field = compute_op.get('field')
|
|
387
|
+
expression = compute_op.get('expression')
|
|
388
|
+
comp_type = compute_op.get('comp_type', 'string')
|
|
389
|
+
|
|
390
|
+
if not field or not expression:
|
|
391
|
+
return data
|
|
392
|
+
|
|
393
|
+
# Add computed field to each record
|
|
394
|
+
for record in data:
|
|
395
|
+
value = self._evaluate_expression(expression, record, context)
|
|
396
|
+
record[field] = self._convert_type(value, comp_type)
|
|
397
|
+
|
|
398
|
+
return data
|
|
399
|
+
|
|
400
|
+
def _get_source_content(self, source: str, headers: List[Dict[str, str]] = None) -> str:
|
|
401
|
+
"""Get content from file or URL"""
|
|
402
|
+
# Check if source is a URL
|
|
403
|
+
if source.startswith('http://') or source.startswith('https://'):
|
|
404
|
+
# Fetch from URL
|
|
405
|
+
headers_dict = {}
|
|
406
|
+
if headers:
|
|
407
|
+
for header in headers:
|
|
408
|
+
headers_dict[header.get('name')] = header.get('value')
|
|
409
|
+
|
|
410
|
+
response = requests.get(source, headers=headers_dict, timeout=30)
|
|
411
|
+
response.raise_for_status()
|
|
412
|
+
return response.text
|
|
413
|
+
else:
|
|
414
|
+
# Read from file
|
|
415
|
+
file_path = Path(source)
|
|
416
|
+
if not file_path.is_absolute():
|
|
417
|
+
# Relative path - resolve from current working directory
|
|
418
|
+
file_path = Path.cwd() / file_path
|
|
419
|
+
|
|
420
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
421
|
+
return f.read()
|
|
422
|
+
|
|
423
|
+
def _convert_csv_row(self, row: Dict[str, str], columns: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
424
|
+
"""Convert CSV row values to proper types"""
|
|
425
|
+
if not columns:
|
|
426
|
+
return row
|
|
427
|
+
|
|
428
|
+
converted = {}
|
|
429
|
+
for col_def in columns:
|
|
430
|
+
col_name = col_def.get('name')
|
|
431
|
+
col_type = col_def.get('type', 'string')
|
|
432
|
+
|
|
433
|
+
if col_name in row:
|
|
434
|
+
converted[col_name] = self._convert_type(row[col_name], col_type)
|
|
435
|
+
|
|
436
|
+
# Include any columns not defined in schema
|
|
437
|
+
for key, value in row.items():
|
|
438
|
+
if key not in converted:
|
|
439
|
+
converted[key] = value
|
|
440
|
+
|
|
441
|
+
return converted
|
|
442
|
+
|
|
443
|
+
def _convert_type(self, value: Any, target_type: str) -> Any:
|
|
444
|
+
"""Convert value to target type"""
|
|
445
|
+
if value is None or value == '':
|
|
446
|
+
return None
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
if target_type == 'integer':
|
|
450
|
+
return int(value)
|
|
451
|
+
elif target_type == 'decimal':
|
|
452
|
+
return float(value)
|
|
453
|
+
elif target_type == 'boolean':
|
|
454
|
+
if isinstance(value, bool):
|
|
455
|
+
return value
|
|
456
|
+
if isinstance(value, str):
|
|
457
|
+
return value.lower() in ('true', '1', 'yes', 'on')
|
|
458
|
+
return bool(value)
|
|
459
|
+
elif target_type == 'json':
|
|
460
|
+
if isinstance(value, str):
|
|
461
|
+
return json.loads(value)
|
|
462
|
+
return value
|
|
463
|
+
elif target_type == 'array':
|
|
464
|
+
if isinstance(value, list):
|
|
465
|
+
return value
|
|
466
|
+
if isinstance(value, str):
|
|
467
|
+
return json.loads(value)
|
|
468
|
+
return [value]
|
|
469
|
+
else: # string
|
|
470
|
+
return str(value)
|
|
471
|
+
except (ValueError, json.JSONDecodeError):
|
|
472
|
+
return value
|
|
473
|
+
|
|
474
|
+
def _extract_xml_value(self, node: ET.Element, xpath: str) -> Optional[str]:
|
|
475
|
+
"""Extract value from XML node using XPath"""
|
|
476
|
+
# Handle attribute access (@attr)
|
|
477
|
+
if xpath.startswith('@'):
|
|
478
|
+
attr_name = xpath[1:]
|
|
479
|
+
return node.get(attr_name)
|
|
480
|
+
|
|
481
|
+
# Handle text content (text())
|
|
482
|
+
if xpath == 'text()':
|
|
483
|
+
return node.text
|
|
484
|
+
|
|
485
|
+
# child/@attr — ElementTree's find() does not understand the @ step.
|
|
486
|
+
if '/@' in xpath:
|
|
487
|
+
child_path, attr = xpath.rsplit('/@', 1)
|
|
488
|
+
child = node.find(child_path)
|
|
489
|
+
return child.get(attr) if child is not None else None
|
|
490
|
+
|
|
491
|
+
# child/text() — nor text(); it raised SyntaxError on every such field.
|
|
492
|
+
if xpath.endswith('/text()'):
|
|
493
|
+
xpath = xpath[:-len('/text()')]
|
|
494
|
+
|
|
495
|
+
child = node.find(xpath)
|
|
496
|
+
return child.text if child is not None else None
|
|
497
|
+
|
|
498
|
+
def _evaluate_condition(self, condition: str, record: Dict[str, Any], context: Any) -> bool:
|
|
499
|
+
"""Evaluate a q:data filter condition against one imported record.
|
|
500
|
+
|
|
501
|
+
`{field}` names the record's field. The braces are stripped so the
|
|
502
|
+
evaluator resolves the NAME; the record's values are never spliced
|
|
503
|
+
into the expression text.
|
|
504
|
+
|
|
505
|
+
That distinction is the whole fix. This used to build a string by
|
|
506
|
+
interpolating each field value into the condition — quoting strings as
|
|
507
|
+
f"'{value}'" — and then call a bare eval() on it, with full builtins.
|
|
508
|
+
A single quote anywhere in an imported CSV cell closed the literal and
|
|
509
|
+
the rest of the cell ran as Python. The input here is an imported file,
|
|
510
|
+
which is the least trusted thing the framework touches.
|
|
511
|
+
"""
|
|
512
|
+
return bool(self._evaluate_expression(condition, record, context))
|
|
513
|
+
|
|
514
|
+
def _evaluate_expression(self, expression: str, record: Dict[str, Any], context: Any) -> Any:
|
|
515
|
+
"""Evaluate a computed-field expression against one imported record.
|
|
516
|
+
|
|
517
|
+
See _evaluate_condition for why this no longer interpolates values.
|
|
518
|
+
"""
|
|
519
|
+
expr = _BINDING.sub(lambda m: m.group(1), expression).strip()
|
|
520
|
+
return _EXPRESSIONS.try_evaluate(expr, record, default=None)
|
|
521
|
+
|
|
522
|
+
def get_from_cache(self, cache_key: str) -> Optional[DataResult]:
|
|
523
|
+
"""Get value from cache"""
|
|
524
|
+
return self.cache.get(cache_key)
|
|
525
|
+
|
|
526
|
+
def put_in_cache(self, cache_key: str, value: DataResult, ttl: Optional[int] = None):
|
|
527
|
+
"""Put value in cache"""
|
|
528
|
+
# Simple implementation - in production you'd use TTL properly
|
|
529
|
+
self.cache[cache_key] = value
|
|
530
|
+
|
|
531
|
+
def clear_cache(self):
|
|
532
|
+
"""Clear all cache"""
|
|
533
|
+
self.cache.clear()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dump feature for Quantum Language
|
|
3
|
+
|
|
4
|
+
Provides beautiful variable inspection for debugging,
|
|
5
|
+
like ColdFusion's cfdump but better.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .ast_node import DumpNode
|
|
9
|
+
from .parser import parse_dump
|
|
10
|
+
from .runtime import DumpService
|
|
11
|
+
|
|
12
|
+
__all__ = ['DumpNode', 'parse_dump', 'DumpService']
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AST Node for q:dump - Variable Inspection
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from quantum.core.ast_nodes import QuantumNode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DumpNode(QuantumNode):
|
|
10
|
+
"""
|
|
11
|
+
Represents a <q:dump> - Variable inspection statement
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
<q:dump var="{user}" label="User Object" />
|
|
15
|
+
<q:dump var="{data}" format="json" depth="3" />
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
VALID_FORMATS = ['html', 'json', 'text']
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
var: str,
|
|
23
|
+
label: Optional[str] = None,
|
|
24
|
+
format: str = 'html',
|
|
25
|
+
depth: int = 10,
|
|
26
|
+
when: Optional[str] = None,
|
|
27
|
+
expand: int = 2,
|
|
28
|
+
show_types: bool = True
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize a DumpNode
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
var: Variable to inspect (databinding expression)
|
|
35
|
+
label: Optional identifying label
|
|
36
|
+
format: Output format (html, json, text)
|
|
37
|
+
depth: Max depth for nested structures
|
|
38
|
+
when: Optional conditional expression
|
|
39
|
+
expand: Auto-expand depth (Phase 2)
|
|
40
|
+
show_types: Show type information (Phase 2)
|
|
41
|
+
"""
|
|
42
|
+
# Validate var
|
|
43
|
+
if not var:
|
|
44
|
+
raise ValueError("Dump requires 'var' attribute")
|
|
45
|
+
|
|
46
|
+
# Validate format
|
|
47
|
+
if format and format not in self.VALID_FORMATS:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Invalid dump format '{format}'. Must be one of: {', '.join(self.VALID_FORMATS)}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Validate depth
|
|
53
|
+
if depth is not None:
|
|
54
|
+
try:
|
|
55
|
+
depth_int = int(depth)
|
|
56
|
+
if depth_int < 0:
|
|
57
|
+
raise ValueError("Dump depth must be a positive integer")
|
|
58
|
+
depth = depth_int
|
|
59
|
+
except (ValueError, TypeError):
|
|
60
|
+
raise ValueError("Dump depth must be a positive integer")
|
|
61
|
+
|
|
62
|
+
self.var = var
|
|
63
|
+
self.label = label if label else var
|
|
64
|
+
self.format = format
|
|
65
|
+
self.depth = depth
|
|
66
|
+
self.when = when
|
|
67
|
+
self.expand = expand
|
|
68
|
+
self.show_types = show_types
|
|
69
|
+
|
|
70
|
+
def __repr__(self):
|
|
71
|
+
attrs = [f"var={self.var!r}"]
|
|
72
|
+
if self.label != self.var:
|
|
73
|
+
attrs.append(f"label={self.label!r}")
|
|
74
|
+
if self.format != 'html':
|
|
75
|
+
attrs.append(f"format={self.format!r}")
|
|
76
|
+
if self.when:
|
|
77
|
+
attrs.append(f"when={self.when!r}")
|
|
78
|
+
return f"DumpNode({', '.join(attrs)})"
|
|
79
|
+
|
|
80
|
+
def to_dict(self):
|
|
81
|
+
"""Convert node to dictionary for debugging/serialization"""
|
|
82
|
+
return {
|
|
83
|
+
"type": "dump",
|
|
84
|
+
"var": self.var,
|
|
85
|
+
"label": self.label,
|
|
86
|
+
"format": self.format,
|
|
87
|
+
"depth": self.depth,
|
|
88
|
+
"when": self.when,
|
|
89
|
+
"expand": self.expand,
|
|
90
|
+
"show_types": self.show_types
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
def validate(self):
|
|
94
|
+
"""Validate the node and return list of errors"""
|
|
95
|
+
errors = []
|
|
96
|
+
|
|
97
|
+
if not self.var:
|
|
98
|
+
errors.append("Dump requires 'var' attribute")
|
|
99
|
+
|
|
100
|
+
if self.format and self.format not in self.VALID_FORMATS:
|
|
101
|
+
errors.append(f"Invalid dump format '{self.format}'. Must be one of: {', '.join(self.VALID_FORMATS)}")
|
|
102
|
+
|
|
103
|
+
if self.depth is not None and (not isinstance(self.depth, int) or self.depth < 0):
|
|
104
|
+
errors.append("Dump depth must be a positive integer")
|
|
105
|
+
|
|
106
|
+
return errors
|