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,178 @@
|
|
|
1
|
+
"""The /_dev panel (M12, DEV-1): what the last requests did, while developing.
|
|
2
|
+
|
|
3
|
+
With `server.debug: true`, the server records each request — the component,
|
|
4
|
+
the action it ran, the queries (SQL, parameters, time, rows), the variables of
|
|
5
|
+
each scope, the redirect and the flash — and `/_dev` shows the last ones. With
|
|
6
|
+
`debug: false` nothing is recorded and `/_dev` does not exist.
|
|
7
|
+
|
|
8
|
+
Recording goes through a context variable, so a query executed deep in the
|
|
9
|
+
runtime finds the request it belongs to without anything being passed down,
|
|
10
|
+
and two requests in two threads never mix.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import contextvars
|
|
14
|
+
import html
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from collections import deque
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
_ATUAL: contextvars.ContextVar = contextvars.ContextVar('quantum_dev_request', default=None)
|
|
21
|
+
|
|
22
|
+
# A value longer than this is cut in the panel (a query of 10k rows, a blob).
|
|
23
|
+
_MAX_TEXTO = 300
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _curto(valor: Any) -> str:
|
|
27
|
+
texto = repr(valor) if not isinstance(valor, str) else valor
|
|
28
|
+
return texto if len(texto) <= _MAX_TEXTO else texto[:_MAX_TEXTO] + f'… ({len(texto)} chars)'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _escopo(variaveis: Dict[str, Any]) -> Dict[str, str]:
|
|
32
|
+
return {nome: _curto(valor) for nome, valor in sorted((variaveis or {}).items())
|
|
33
|
+
if not str(nome).startswith('_')}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# -- what the runtime reports ------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def note(**campos) -> None:
|
|
39
|
+
"""Add facts to the request being recorded (no-op when nothing records)."""
|
|
40
|
+
registro = _ATUAL.get()
|
|
41
|
+
if registro is not None:
|
|
42
|
+
registro.update(campos)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def note_query(datasource: str, sql: str, params: Optional[Dict[str, Any]], ms: float,
|
|
46
|
+
rows: Optional[int], error: Optional[str] = None) -> None:
|
|
47
|
+
registro = _ATUAL.get()
|
|
48
|
+
if registro is not None:
|
|
49
|
+
registro['queries'].append({
|
|
50
|
+
'datasource': datasource, 'sql': ' '.join(sql.split()),
|
|
51
|
+
'params': {k: _curto(v) for k, v in (params or {}).items()},
|
|
52
|
+
'ms': round(ms, 2), 'rows': rows, 'error': error,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def note_scopes(execution_context, label: str = 'page') -> None:
|
|
57
|
+
"""The variables the page — or the action (`label='action'`) — ended with,
|
|
58
|
+
and the session and application scopes as they were then."""
|
|
59
|
+
registro = _ATUAL.get()
|
|
60
|
+
if registro is None or execution_context is None:
|
|
61
|
+
return
|
|
62
|
+
proprias = dict(getattr(execution_context, 'component_vars', {}) or {})
|
|
63
|
+
proprias.update(getattr(execution_context, 'local_vars', {}) or {})
|
|
64
|
+
escopos = registro['scopes'] or {}
|
|
65
|
+
escopos[label] = _escopo({k: v for k, v in proprias.items() if '.' not in str(k)})
|
|
66
|
+
escopos['session'] = _escopo(getattr(execution_context, 'session_vars', {}))
|
|
67
|
+
escopos['application'] = _escopo(getattr(execution_context, 'application_vars', {}))
|
|
68
|
+
# action, then page, then the scopes that outlive them
|
|
69
|
+
ordem = ['action', 'page', 'session', 'application']
|
|
70
|
+
registro['scopes'] = {k: escopos[k] for k in ordem if k in escopos}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# -- the recorder ------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
class DevPanel:
|
|
76
|
+
"""The last requests of this server process, newest first."""
|
|
77
|
+
|
|
78
|
+
def __init__(self, keep: int = 30):
|
|
79
|
+
self.requests: deque = deque(maxlen=keep)
|
|
80
|
+
self._numero = 0
|
|
81
|
+
self._trava = threading.Lock()
|
|
82
|
+
|
|
83
|
+
def start(self, method: str, path: str):
|
|
84
|
+
with self._trava:
|
|
85
|
+
self._numero += 1
|
|
86
|
+
numero = self._numero
|
|
87
|
+
registro = {'n': numero, 'method': method, 'path': path, 'component': None,
|
|
88
|
+
'action': None, 'queries': [], 'scopes': None, 'status': None,
|
|
89
|
+
'redirect': None, 'flash': None, 'error': None, 't0': time.perf_counter()}
|
|
90
|
+
return _ATUAL.set(registro)
|
|
91
|
+
|
|
92
|
+
def finish(self, token, status: int, location: Optional[str], flash: Optional[dict]) -> None:
|
|
93
|
+
registro = _ATUAL.get()
|
|
94
|
+
_ATUAL.reset(token)
|
|
95
|
+
if registro is None:
|
|
96
|
+
return
|
|
97
|
+
registro['status'] = status
|
|
98
|
+
registro['redirect'] = location
|
|
99
|
+
registro['flash'] = flash
|
|
100
|
+
registro['ms'] = round((time.perf_counter() - registro.pop('t0')) * 1000, 1)
|
|
101
|
+
with self._trava:
|
|
102
|
+
self.requests.appendleft(registro)
|
|
103
|
+
|
|
104
|
+
def get(self, numero: int) -> Optional[dict]:
|
|
105
|
+
return next((r for r in list(self.requests) if r['n'] == numero), None)
|
|
106
|
+
|
|
107
|
+
# -- the page ------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
def render(self, numero: Optional[int] = None) -> str:
|
|
110
|
+
requisicoes = list(self.requests)
|
|
111
|
+
escolhida = self.get(numero) if numero is not None else (requisicoes[0] if requisicoes else None)
|
|
112
|
+
e = html.escape
|
|
113
|
+
linhas = []
|
|
114
|
+
for r in requisicoes:
|
|
115
|
+
atual = escolhida is not None and r['n'] == escolhida['n']
|
|
116
|
+
linhas.append(
|
|
117
|
+
f'<tr class="{"atual" if atual else ""}"><td><a href="/_dev/{r["n"]}">#{r["n"]}</a></td>'
|
|
118
|
+
f'<td>{e(r["method"])}</td><td>{e(r["path"])}</td><td>{r["status"]}</td>'
|
|
119
|
+
f'<td>{e(r["action"] or "")}</td><td>{len(r["queries"])}</td><td>{r["ms"]} ms</td></tr>')
|
|
120
|
+
detalhe = self._detalhe(escolhida) if escolhida else '<p>No requests yet. Open a page of the app.</p>'
|
|
121
|
+
return f'''<!DOCTYPE html>
|
|
122
|
+
<html lang="en"><head><meta charset="UTF-8"><title>Quantum /_dev</title>
|
|
123
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
124
|
+
<style>
|
|
125
|
+
body {{ font: 14px/1.45 system-ui, sans-serif; margin: 0; padding: 16px; color: #1f2328; background: #f6f8fa; }}
|
|
126
|
+
h1 {{ font-size: 18px; margin: 0 0 4px; }} h2 {{ font-size: 15px; margin: 20px 0 6px; }}
|
|
127
|
+
.nota {{ color: #57606a; margin: 0 0 12px; }}
|
|
128
|
+
table {{ border-collapse: collapse; width: 100%; background: #fff; }}
|
|
129
|
+
td, th {{ border: 1px solid #d0d7de; padding: 4px 8px; text-align: left; vertical-align: top; }}
|
|
130
|
+
tr.atual td {{ background: #ddf4ff; }}
|
|
131
|
+
code, pre {{ font: 12px/1.4 ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; margin: 0; }}
|
|
132
|
+
th code {{ white-space: nowrap; word-break: normal; }}
|
|
133
|
+
.erro {{ color: #cf222e; }}
|
|
134
|
+
.caixa {{ overflow-x: auto; }}
|
|
135
|
+
</style></head><body>
|
|
136
|
+
<h1>Quantum /_dev</h1>
|
|
137
|
+
<p class="nota">The last {len(requisicoes)} requests of this server process. Shown because
|
|
138
|
+
<code>server.debug</code> is true; with <code>debug: false</code> this page does not exist.</p>
|
|
139
|
+
<div class="caixa"><table><tr><th>#</th><th>Method</th><th>Path</th><th>Status</th><th>Action</th>
|
|
140
|
+
<th>Queries</th><th>Time</th></tr>{"".join(linhas)}</table></div>
|
|
141
|
+
{detalhe}
|
|
142
|
+
</body></html>'''
|
|
143
|
+
|
|
144
|
+
def _detalhe(self, r: dict) -> str:
|
|
145
|
+
e = html.escape
|
|
146
|
+
partes = [f'<h2>#{r["n"]} {e(r["method"])} {e(r["path"])} → {r["status"]} ({r["ms"]} ms)</h2>',
|
|
147
|
+
'<div class="caixa"><table>',
|
|
148
|
+
f'<tr><th>Component</th><td><code>{e(r["component"] or "—")}</code></td></tr>',
|
|
149
|
+
f'<tr><th>Action</th><td>{e(r["action"] or "—")}</td></tr>',
|
|
150
|
+
f'<tr><th>Redirect</th><td>{e(r["redirect"] or "—")}</td></tr>']
|
|
151
|
+
flash = r.get('flash')
|
|
152
|
+
partes.append('<tr><th>Flash</th><td>' + (
|
|
153
|
+
f'{e(str(flash.get("message", "")))} <em>({e(str(flash.get("type", "")))})</em>' if flash else '—')
|
|
154
|
+
+ '</td></tr>')
|
|
155
|
+
if r.get('error'):
|
|
156
|
+
partes.append(f'<tr><th>Error</th><td class="erro"><pre>{e(r["error"])}</pre></td></tr>')
|
|
157
|
+
partes.append('</table></div>')
|
|
158
|
+
partes.append(f'<h2>Queries ({len(r["queries"])})</h2>')
|
|
159
|
+
if r['queries']:
|
|
160
|
+
partes.append('<div class="caixa"><table><tr><th>Datasource</th><th>SQL</th><th>Params</th>'
|
|
161
|
+
'<th>Rows</th><th>Time</th></tr>')
|
|
162
|
+
for q in r['queries']:
|
|
163
|
+
params = ', '.join(f'{e(k)}={e(v)}' for k, v in q['params'].items())
|
|
164
|
+
resultado = f'<span class="erro">{e(q["error"])}</span>' if q['error'] else (q['rows'] if q['rows'] is not None else '—')
|
|
165
|
+
partes.append(f'<tr><td>{e(q["datasource"])}</td><td><pre>{e(q["sql"])}</pre></td>'
|
|
166
|
+
f'<td><code>{params}</code></td><td>{resultado}</td><td>{q["ms"]} ms</td></tr>')
|
|
167
|
+
partes.append('</table></div>')
|
|
168
|
+
else:
|
|
169
|
+
partes.append('<p>None.</p>')
|
|
170
|
+
for escopo, variaveis in (r.get('scopes') or {}).items():
|
|
171
|
+
partes.append(f'<h2>{e(escopo)} ({len(variaveis)})</h2>')
|
|
172
|
+
if variaveis:
|
|
173
|
+
partes.append('<div class="caixa"><table>' + ''.join(
|
|
174
|
+
f'<tr><th><code>{e(k)}</code></th><td><code>{e(v)}</code></td></tr>'
|
|
175
|
+
for k, v in variaveis.items()) + '</table></div>')
|
|
176
|
+
else:
|
|
177
|
+
partes.append('<p>None.</p>')
|
|
178
|
+
return '\n'.join(partes)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase I: Email Service
|
|
3
|
+
|
|
4
|
+
ColdFusion cfmail-inspired email sending with SMTP support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import smtplib
|
|
8
|
+
import os
|
|
9
|
+
from email.mime.text import MIMEText
|
|
10
|
+
from email.mime.multipart import MIMEMultipart
|
|
11
|
+
from email.mime.base import MIMEBase
|
|
12
|
+
from email import encoders
|
|
13
|
+
from typing import List, Dict, Any, Optional
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EmailError(Exception):
|
|
18
|
+
"""Raised when email sending fails"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EmailService:
|
|
23
|
+
"""Service for sending emails via SMTP"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
"""Initialize email service with config from environment"""
|
|
27
|
+
self.smtp_host = os.getenv('SMTP_HOST', 'localhost')
|
|
28
|
+
self.smtp_port = int(os.getenv('SMTP_PORT', '587'))
|
|
29
|
+
self.smtp_user = os.getenv('SMTP_USER', '')
|
|
30
|
+
self.smtp_password = os.getenv('SMTP_PASSWORD', '')
|
|
31
|
+
self.smtp_use_tls = os.getenv('SMTP_USE_TLS', 'true').lower() == 'true'
|
|
32
|
+
self.default_from = os.getenv('SMTP_FROM', 'noreply@quantum.dev')
|
|
33
|
+
|
|
34
|
+
def send_email(
|
|
35
|
+
self,
|
|
36
|
+
to: str,
|
|
37
|
+
subject: str,
|
|
38
|
+
body: str,
|
|
39
|
+
from_addr: Optional[str] = None,
|
|
40
|
+
cc: Optional[str] = None,
|
|
41
|
+
bcc: Optional[str] = None,
|
|
42
|
+
reply_to: Optional[str] = None,
|
|
43
|
+
email_type: str = "html",
|
|
44
|
+
attachments: Optional[List[str]] = None
|
|
45
|
+
) -> Dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Send email via SMTP
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
to: Recipient email(s) (comma-separated)
|
|
51
|
+
subject: Email subject
|
|
52
|
+
body: Email body content
|
|
53
|
+
from_addr: Sender email (uses default if not provided)
|
|
54
|
+
cc: CC recipients (comma-separated)
|
|
55
|
+
bcc: BCC recipients (comma-separated)
|
|
56
|
+
reply_to: Reply-To address
|
|
57
|
+
email_type: 'html' or 'text'
|
|
58
|
+
attachments: List of file paths to attach
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dict with send result
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
EmailError: If sending fails
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
# Create message
|
|
68
|
+
msg = MIMEMultipart()
|
|
69
|
+
msg['From'] = from_addr or self.default_from
|
|
70
|
+
msg['To'] = to
|
|
71
|
+
msg['Subject'] = subject
|
|
72
|
+
|
|
73
|
+
if cc:
|
|
74
|
+
msg['Cc'] = cc
|
|
75
|
+
if reply_to:
|
|
76
|
+
msg['Reply-To'] = reply_to
|
|
77
|
+
|
|
78
|
+
# Attach body
|
|
79
|
+
mime_type = 'html' if email_type == 'html' else 'plain'
|
|
80
|
+
msg.attach(MIMEText(body, mime_type, 'utf-8'))
|
|
81
|
+
|
|
82
|
+
# Attach files if provided
|
|
83
|
+
if attachments:
|
|
84
|
+
for file_path in attachments:
|
|
85
|
+
self._attach_file(msg, file_path)
|
|
86
|
+
|
|
87
|
+
# Prepare recipients
|
|
88
|
+
recipients = self._parse_recipients(to, cc, bcc)
|
|
89
|
+
|
|
90
|
+
# Send email
|
|
91
|
+
if os.getenv('EMAIL_MOCK', 'true').lower() == 'true':
|
|
92
|
+
# Mock mode for development (don't actually send)
|
|
93
|
+
print(f"[MOCK] Email sent to {to}")
|
|
94
|
+
print(f" Subject: {subject}")
|
|
95
|
+
print(f" From: {msg['From']}")
|
|
96
|
+
print(f" Type: {email_type}")
|
|
97
|
+
return {
|
|
98
|
+
'success': True,
|
|
99
|
+
'mock': True,
|
|
100
|
+
'to': to,
|
|
101
|
+
'subject': subject,
|
|
102
|
+
'message': 'Email mock-sent successfully'
|
|
103
|
+
}
|
|
104
|
+
else:
|
|
105
|
+
# Real SMTP sending
|
|
106
|
+
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
|
|
107
|
+
if self.smtp_use_tls:
|
|
108
|
+
server.starttls()
|
|
109
|
+
|
|
110
|
+
if self.smtp_user and self.smtp_password:
|
|
111
|
+
server.login(self.smtp_user, self.smtp_password)
|
|
112
|
+
|
|
113
|
+
server.sendmail(msg['From'], recipients, msg.as_string())
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
'success': True,
|
|
117
|
+
'to': to,
|
|
118
|
+
'subject': subject,
|
|
119
|
+
'message': 'Email sent successfully'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise EmailError(f"Failed to send email: {e}")
|
|
124
|
+
|
|
125
|
+
def _attach_file(self, msg: MIMEMultipart, file_path: str):
|
|
126
|
+
"""Attach a file to email message"""
|
|
127
|
+
try:
|
|
128
|
+
path = Path(file_path)
|
|
129
|
+
if not path.exists():
|
|
130
|
+
raise EmailError(f"Attachment file not found: {file_path}")
|
|
131
|
+
|
|
132
|
+
with open(file_path, 'rb') as f:
|
|
133
|
+
part = MIMEBase('application', 'octet-stream')
|
|
134
|
+
part.set_payload(f.read())
|
|
135
|
+
|
|
136
|
+
encoders.encode_base64(part)
|
|
137
|
+
part.add_header(
|
|
138
|
+
'Content-Disposition',
|
|
139
|
+
f'attachment; filename= {path.name}'
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
msg.attach(part)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
raise EmailError(f"Failed to attach file {file_path}: {e}")
|
|
145
|
+
|
|
146
|
+
def _parse_recipients(
|
|
147
|
+
self,
|
|
148
|
+
to: str,
|
|
149
|
+
cc: Optional[str] = None,
|
|
150
|
+
bcc: Optional[str] = None
|
|
151
|
+
) -> List[str]:
|
|
152
|
+
"""Parse comma-separated email addresses into list"""
|
|
153
|
+
recipients = []
|
|
154
|
+
|
|
155
|
+
if to:
|
|
156
|
+
recipients.extend([email.strip() for email in to.split(',')])
|
|
157
|
+
if cc:
|
|
158
|
+
recipients.extend([email.strip() for email in cc.split(',')])
|
|
159
|
+
if bcc:
|
|
160
|
+
recipients.extend([email.strip() for email in bcc.split(',')])
|
|
161
|
+
|
|
162
|
+
return recipients
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced Error Handler - Phase C: Developer Experience
|
|
3
|
+
|
|
4
|
+
Provides better error messages with:
|
|
5
|
+
- Context (line numbers, code snippets)
|
|
6
|
+
- Suggestions (did you mean...?)
|
|
7
|
+
- Helpful hints
|
|
8
|
+
- Color-coded output
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, List
|
|
14
|
+
import difflib
|
|
15
|
+
|
|
16
|
+
class QuantumError(Exception):
|
|
17
|
+
"""Base class for Quantum errors with enhanced context"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, file_path: Optional[str] = None,
|
|
20
|
+
line: Optional[int] = None, column: Optional[int] = None,
|
|
21
|
+
suggestion: Optional[str] = None, hint: Optional[str] = None):
|
|
22
|
+
self.message = message
|
|
23
|
+
self.file_path = file_path
|
|
24
|
+
self.line = line
|
|
25
|
+
self.column = column
|
|
26
|
+
self.suggestion = suggestion
|
|
27
|
+
self.hint = hint
|
|
28
|
+
super().__init__(self.format_error())
|
|
29
|
+
|
|
30
|
+
def format_error(self) -> str:
|
|
31
|
+
"""Format error message with context"""
|
|
32
|
+
parts = []
|
|
33
|
+
|
|
34
|
+
# Header
|
|
35
|
+
parts.append("=" * 60)
|
|
36
|
+
parts.append("❌ QUANTUM ERROR")
|
|
37
|
+
parts.append("=" * 60)
|
|
38
|
+
|
|
39
|
+
# Location
|
|
40
|
+
if self.file_path:
|
|
41
|
+
location = f"📂 File: {self.file_path}"
|
|
42
|
+
if self.line:
|
|
43
|
+
location += f":{self.line}"
|
|
44
|
+
if self.column:
|
|
45
|
+
location += f":{self.column}"
|
|
46
|
+
parts.append(location)
|
|
47
|
+
|
|
48
|
+
# Error message
|
|
49
|
+
parts.append(f"\n💥 {self.message}")
|
|
50
|
+
|
|
51
|
+
# Code context
|
|
52
|
+
if self.file_path and self.line:
|
|
53
|
+
context = self._get_code_context()
|
|
54
|
+
if context:
|
|
55
|
+
parts.append("\n📝 Code:")
|
|
56
|
+
parts.append(context)
|
|
57
|
+
|
|
58
|
+
# Suggestion
|
|
59
|
+
if self.suggestion:
|
|
60
|
+
parts.append(f"\n💡 Suggestion: {self.suggestion}")
|
|
61
|
+
|
|
62
|
+
# Hint
|
|
63
|
+
if self.hint:
|
|
64
|
+
parts.append(f"\n🔍 Hint: {self.hint}")
|
|
65
|
+
|
|
66
|
+
parts.append("=" * 60)
|
|
67
|
+
|
|
68
|
+
return "\n".join(parts)
|
|
69
|
+
|
|
70
|
+
def _get_code_context(self, context_lines: int = 3) -> Optional[str]:
|
|
71
|
+
"""Get code context around error line"""
|
|
72
|
+
try:
|
|
73
|
+
file_path = Path(self.file_path)
|
|
74
|
+
if not file_path.exists():
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
lines = file_path.read_text(encoding='utf-8').splitlines()
|
|
78
|
+
|
|
79
|
+
start = max(0, self.line - context_lines - 1)
|
|
80
|
+
end = min(len(lines), self.line + context_lines)
|
|
81
|
+
|
|
82
|
+
context_parts = []
|
|
83
|
+
for i in range(start, end):
|
|
84
|
+
line_num = i + 1
|
|
85
|
+
line_content = lines[i]
|
|
86
|
+
|
|
87
|
+
# Mark error line with arrow
|
|
88
|
+
if line_num == self.line:
|
|
89
|
+
context_parts.append(f" → {line_num:4d} | {line_content}")
|
|
90
|
+
# Add column pointer if available
|
|
91
|
+
if self.column:
|
|
92
|
+
pointer = " " * (self.column + 10) + "^"
|
|
93
|
+
context_parts.append(pointer)
|
|
94
|
+
else:
|
|
95
|
+
context_parts.append(f" {line_num:4d} | {line_content}")
|
|
96
|
+
|
|
97
|
+
return "\n".join(context_parts)
|
|
98
|
+
|
|
99
|
+
except Exception:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class ComponentNotFoundError(QuantumError):
|
|
104
|
+
"""Component import not found"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, component_name: str, file_path: str, line: int,
|
|
107
|
+
available_components: List[str] = None):
|
|
108
|
+
# Find similar component names
|
|
109
|
+
suggestion = None
|
|
110
|
+
if available_components:
|
|
111
|
+
matches = difflib.get_close_matches(component_name, available_components, n=3, cutoff=0.6)
|
|
112
|
+
if matches:
|
|
113
|
+
if len(matches) == 1:
|
|
114
|
+
suggestion = f"Did you mean '{matches[0]}'?"
|
|
115
|
+
else:
|
|
116
|
+
suggestion = f"Did you mean one of: {', '.join(matches)}?"
|
|
117
|
+
|
|
118
|
+
hint = f"Add: <q:import component=\"{component_name}\" />"
|
|
119
|
+
|
|
120
|
+
super().__init__(
|
|
121
|
+
f"Component '{component_name}' not found",
|
|
122
|
+
file_path=file_path,
|
|
123
|
+
line=line,
|
|
124
|
+
suggestion=suggestion,
|
|
125
|
+
hint=hint
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ValidationErrorWithContext(QuantumError):
|
|
130
|
+
"""Validation error with context"""
|
|
131
|
+
|
|
132
|
+
def __init__(self, field_name: str, validation_type: str,
|
|
133
|
+
value: any, file_path: str = None, line: int = None):
|
|
134
|
+
message = f"Validation failed for field '{field_name}' ({validation_type})"
|
|
135
|
+
|
|
136
|
+
if validation_type == "required":
|
|
137
|
+
suggestion = f"Field '{field_name}' is required but was not provided"
|
|
138
|
+
elif validation_type == "type":
|
|
139
|
+
suggestion = f"Expected type differs from provided value: {value}"
|
|
140
|
+
elif validation_type == "minlength":
|
|
141
|
+
suggestion = f"Value '{value}' is too short"
|
|
142
|
+
elif validation_type == "maxlength":
|
|
143
|
+
suggestion = f"Value '{value}' is too long"
|
|
144
|
+
elif validation_type == "pattern":
|
|
145
|
+
suggestion = f"Value '{value}' doesn't match required pattern"
|
|
146
|
+
else:
|
|
147
|
+
suggestion = f"Validation '{validation_type}' failed for value: {value}"
|
|
148
|
+
|
|
149
|
+
super().__init__(
|
|
150
|
+
message,
|
|
151
|
+
file_path=file_path,
|
|
152
|
+
line=line,
|
|
153
|
+
suggestion=suggestion
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class QueryErrorWithContext(QuantumError):
|
|
158
|
+
"""Query error with SQL context"""
|
|
159
|
+
|
|
160
|
+
def __init__(self, query_name: str, sql: str, error: str,
|
|
161
|
+
file_path: str = None, line: int = None):
|
|
162
|
+
message = f"Query '{query_name}' failed"
|
|
163
|
+
|
|
164
|
+
# Provide SQL-specific suggestions
|
|
165
|
+
suggestion = None
|
|
166
|
+
if "syntax error" in error.lower():
|
|
167
|
+
suggestion = "Check your SQL syntax"
|
|
168
|
+
elif "table" in error.lower() and "doesn't exist" in error.lower():
|
|
169
|
+
suggestion = "Table doesn't exist - check your database schema"
|
|
170
|
+
elif "column" in error.lower():
|
|
171
|
+
suggestion = "Column doesn't exist - check your table structure"
|
|
172
|
+
elif "duplicate" in error.lower():
|
|
173
|
+
suggestion = "Duplicate entry - check for unique constraints"
|
|
174
|
+
|
|
175
|
+
hint = f"SQL: {sql[:100]}..." if len(sql) > 100 else f"SQL: {sql}"
|
|
176
|
+
|
|
177
|
+
super().__init__(
|
|
178
|
+
f"{message}: {error}",
|
|
179
|
+
file_path=file_path,
|
|
180
|
+
line=line,
|
|
181
|
+
suggestion=suggestion,
|
|
182
|
+
hint=hint
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TransactionErrorWithContext(QuantumError):
|
|
187
|
+
"""Transaction error with rollback info"""
|
|
188
|
+
|
|
189
|
+
def __init__(self, error: str, statements_executed: int,
|
|
190
|
+
file_path: str = None, line: int = None):
|
|
191
|
+
message = "Transaction failed and was rolled back"
|
|
192
|
+
|
|
193
|
+
suggestion = f"{statements_executed} statement(s) were executed before failure"
|
|
194
|
+
hint = "All changes have been rolled back. Check the error above and fix the issue."
|
|
195
|
+
|
|
196
|
+
super().__init__(
|
|
197
|
+
f"{message}: {error}",
|
|
198
|
+
file_path=file_path,
|
|
199
|
+
line=line,
|
|
200
|
+
suggestion=suggestion,
|
|
201
|
+
hint=hint
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class FileUploadErrorWithContext(QuantumError):
|
|
206
|
+
"""File upload error with details"""
|
|
207
|
+
|
|
208
|
+
def __init__(self, error: str, filename: str = None,
|
|
209
|
+
file_size: int = None, max_size: int = None,
|
|
210
|
+
file_path: str = None, line: int = None):
|
|
211
|
+
message = f"File upload failed"
|
|
212
|
+
|
|
213
|
+
suggestion = None
|
|
214
|
+
if "too large" in error.lower():
|
|
215
|
+
if file_size and max_size:
|
|
216
|
+
suggestion = f"File is {file_size} bytes but maximum is {max_size} bytes"
|
|
217
|
+
elif "type" in error.lower() or "extension" in error.lower():
|
|
218
|
+
suggestion = "File type not allowed - check allowed extensions"
|
|
219
|
+
|
|
220
|
+
hint = None
|
|
221
|
+
if filename:
|
|
222
|
+
hint = f"Filename: {filename}"
|
|
223
|
+
|
|
224
|
+
super().__init__(
|
|
225
|
+
f"{message}: {error}",
|
|
226
|
+
file_path=file_path,
|
|
227
|
+
line=line,
|
|
228
|
+
suggestion=suggestion,
|
|
229
|
+
hint=hint
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class ErrorHandler:
|
|
234
|
+
"""Central error handler for better error reporting"""
|
|
235
|
+
|
|
236
|
+
_logger = None
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def _get_logger(cls):
|
|
240
|
+
if cls._logger is None:
|
|
241
|
+
import logging
|
|
242
|
+
cls._logger = logging.getLogger('quantum.errors')
|
|
243
|
+
return cls._logger
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def handle_parse_error(error: Exception, file_path: str) -> QuantumError:
|
|
247
|
+
"""Convert parse errors to enhanced errors"""
|
|
248
|
+
error_msg = str(error)
|
|
249
|
+
|
|
250
|
+
# Extract line and column from XML parse errors
|
|
251
|
+
line = None
|
|
252
|
+
column = None
|
|
253
|
+
if "line" in error_msg:
|
|
254
|
+
import re
|
|
255
|
+
match = re.search(r'line (\d+)', error_msg)
|
|
256
|
+
if match:
|
|
257
|
+
line = int(match.group(1))
|
|
258
|
+
match = re.search(r'column (\d+)', error_msg)
|
|
259
|
+
if match:
|
|
260
|
+
column = int(match.group(1))
|
|
261
|
+
|
|
262
|
+
return QuantumError(
|
|
263
|
+
f"Parse error: {error_msg}",
|
|
264
|
+
file_path=file_path,
|
|
265
|
+
line=line,
|
|
266
|
+
column=column,
|
|
267
|
+
suggestion="Check your XML syntax",
|
|
268
|
+
hint="Make sure all tags are properly closed and attributes are quoted"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def handle_runtime_error(error: Exception, component_name: str,
|
|
273
|
+
file_path: str = None) -> QuantumError:
|
|
274
|
+
"""Convert runtime errors to enhanced errors"""
|
|
275
|
+
return QuantumError(
|
|
276
|
+
f"Runtime error in component '{component_name}': {error}",
|
|
277
|
+
file_path=file_path,
|
|
278
|
+
suggestion="Check your component logic and variable names",
|
|
279
|
+
hint="Run the component with `quantum run <file>.q` to see the error without the server"
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@classmethod
|
|
283
|
+
def print_warning(cls, message: str):
|
|
284
|
+
"""Log formatted warning"""
|
|
285
|
+
cls._get_logger().warning(message)
|
|
286
|
+
|
|
287
|
+
@classmethod
|
|
288
|
+
def print_info(cls, message: str):
|
|
289
|
+
"""Log formatted info"""
|
|
290
|
+
cls._get_logger().info(message)
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def print_success(cls, message: str):
|
|
294
|
+
"""Log formatted success"""
|
|
295
|
+
cls._get_logger().info(message)
|