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,863 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Runtime for Python
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
Runtime support library for transpiled Quantum code.
|
|
6
|
+
Generated Python code imports this module for common utilities.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union, Callable
|
|
10
|
+
from functools import wraps
|
|
11
|
+
import html as html_module
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from quantum.core.expressions import (
|
|
16
|
+
ExpressionEvaluator, ExpressionError, is_regex_quantifier,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# The same evaluator the interpreter uses, so a {expression} means one thing
|
|
20
|
+
# whether the .q file is interpreted or compiled to Python. This replaced an
|
|
21
|
+
# eval(expr, {'__builtins__': {}}, context): emptying builtins removes names,
|
|
22
|
+
# not the object graph, so ().__class__.__mro__[1].__subclasses__() still
|
|
23
|
+
# reached every loaded class from a compiled template.
|
|
24
|
+
_EXPRESSIONS = ExpressionEvaluator()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# =============================================================================
|
|
28
|
+
# HTML Utilities
|
|
29
|
+
# =============================================================================
|
|
30
|
+
|
|
31
|
+
def escape(value: Any) -> str:
|
|
32
|
+
"""
|
|
33
|
+
Escape HTML entities in a value.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
value: Value to escape
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
HTML-escaped string
|
|
40
|
+
"""
|
|
41
|
+
if value is None:
|
|
42
|
+
return ''
|
|
43
|
+
return html_module.escape(str(value))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def raw(value: Any) -> 'RawHTML':
|
|
47
|
+
"""
|
|
48
|
+
Mark a value as raw HTML (no escaping).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
value: HTML string
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
RawHTML wrapper
|
|
55
|
+
"""
|
|
56
|
+
return RawHTML(value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RawHTML:
|
|
60
|
+
"""Wrapper for raw HTML that should not be escaped."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, content: Any):
|
|
63
|
+
self.content = str(content) if content is not None else ''
|
|
64
|
+
|
|
65
|
+
def __str__(self) -> str:
|
|
66
|
+
return self.content
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return f'RawHTML({self.content!r})'
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# =============================================================================
|
|
73
|
+
# Databinding
|
|
74
|
+
# =============================================================================
|
|
75
|
+
|
|
76
|
+
def bind(template: str, context: Dict[str, Any]) -> str:
|
|
77
|
+
"""
|
|
78
|
+
Apply databinding to a template string.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
template: Template with {expression} placeholders
|
|
82
|
+
context: Variable context
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Resolved string
|
|
86
|
+
"""
|
|
87
|
+
def replace(match):
|
|
88
|
+
expr = match.group(1).strip()
|
|
89
|
+
# {n}, {n,m} are regex quantifiers, not expressions.
|
|
90
|
+
if is_regex_quantifier(expr):
|
|
91
|
+
return match.group(0)
|
|
92
|
+
try:
|
|
93
|
+
result = _EXPRESSIONS.evaluate(expr, context)
|
|
94
|
+
return escape(result) if not isinstance(result, RawHTML) else str(result)
|
|
95
|
+
except ExpressionError:
|
|
96
|
+
return match.group(0)
|
|
97
|
+
|
|
98
|
+
return re.sub(r'\{([^}]+)\}', replace, template)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# =============================================================================
|
|
102
|
+
# Component Base
|
|
103
|
+
# =============================================================================
|
|
104
|
+
|
|
105
|
+
class Component:
|
|
106
|
+
"""
|
|
107
|
+
Base class for Quantum components.
|
|
108
|
+
|
|
109
|
+
Generated component classes inherit from this.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, **props):
|
|
113
|
+
"""
|
|
114
|
+
Initialize component with props.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
**props: Component properties
|
|
118
|
+
"""
|
|
119
|
+
self.props = props
|
|
120
|
+
self._html: List[str] = []
|
|
121
|
+
self._context: Dict[str, Any] = {}
|
|
122
|
+
|
|
123
|
+
def render(self) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Render the component.
|
|
126
|
+
|
|
127
|
+
Override in generated subclasses.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
HTML string
|
|
131
|
+
"""
|
|
132
|
+
raise NotImplementedError("Component.render() must be implemented")
|
|
133
|
+
|
|
134
|
+
def __str__(self) -> str:
|
|
135
|
+
"""String representation returns rendered HTML."""
|
|
136
|
+
return self.render()
|
|
137
|
+
|
|
138
|
+
def __html__(self) -> str:
|
|
139
|
+
"""Jinja2/Flask compatible HTML method."""
|
|
140
|
+
return self.render()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# =============================================================================
|
|
144
|
+
# Flash Messages
|
|
145
|
+
# =============================================================================
|
|
146
|
+
|
|
147
|
+
_flash_messages: List[Dict[str, str]] = []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def flash(message: str, category: str = 'info'):
|
|
151
|
+
"""
|
|
152
|
+
Add a flash message.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
message: The message text
|
|
156
|
+
category: Message category (info, success, warning, error)
|
|
157
|
+
"""
|
|
158
|
+
_flash_messages.append({
|
|
159
|
+
'message': message,
|
|
160
|
+
'category': category
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_flashed_messages(with_categories: bool = False) -> Union[List[str], List[Dict[str, str]]]:
|
|
165
|
+
"""
|
|
166
|
+
Get and clear flash messages.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
with_categories: Include category with each message
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of messages or dicts with message/category
|
|
173
|
+
"""
|
|
174
|
+
global _flash_messages
|
|
175
|
+
messages = _flash_messages.copy()
|
|
176
|
+
_flash_messages = []
|
|
177
|
+
|
|
178
|
+
if with_categories:
|
|
179
|
+
return messages
|
|
180
|
+
return [m['message'] for m in messages]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# =============================================================================
|
|
184
|
+
# Session & Application Scope
|
|
185
|
+
# =============================================================================
|
|
186
|
+
|
|
187
|
+
class ScopeDict(dict):
|
|
188
|
+
"""Dictionary with attribute access for session/application scope."""
|
|
189
|
+
|
|
190
|
+
def __getattr__(self, name: str) -> Any:
|
|
191
|
+
try:
|
|
192
|
+
return self[name]
|
|
193
|
+
except KeyError:
|
|
194
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
|
|
195
|
+
|
|
196
|
+
def __setattr__(self, name: str, value: Any):
|
|
197
|
+
self[name] = value
|
|
198
|
+
|
|
199
|
+
def __delattr__(self, name: str):
|
|
200
|
+
try:
|
|
201
|
+
del self[name]
|
|
202
|
+
except KeyError:
|
|
203
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# Global session and application scopes
|
|
207
|
+
session = ScopeDict()
|
|
208
|
+
application = ScopeDict()
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# =============================================================================
|
|
212
|
+
# Database Utilities
|
|
213
|
+
# =============================================================================
|
|
214
|
+
|
|
215
|
+
class Database:
|
|
216
|
+
"""
|
|
217
|
+
Simple database wrapper.
|
|
218
|
+
|
|
219
|
+
For actual database access, integrate with SQLAlchemy or similar.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
_connections: Dict[str, Any] = {}
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def get_connection(cls, name: str = 'default') -> 'Database':
|
|
226
|
+
"""Get or create a database connection."""
|
|
227
|
+
if name not in cls._connections:
|
|
228
|
+
cls._connections[name] = cls(name)
|
|
229
|
+
return cls._connections[name]
|
|
230
|
+
|
|
231
|
+
def __init__(self, name: str):
|
|
232
|
+
self.name = name
|
|
233
|
+
self._conn = None
|
|
234
|
+
|
|
235
|
+
def execute(self, sql: str, params: List[Any] = None):
|
|
236
|
+
"""
|
|
237
|
+
Execute a SQL query.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
sql: SQL query string
|
|
241
|
+
params: Query parameters
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Query result
|
|
245
|
+
"""
|
|
246
|
+
# This is a placeholder - actual implementation depends on database
|
|
247
|
+
raise NotImplementedError("Database.execute() requires database configuration")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
# =============================================================================
|
|
251
|
+
# Request Context (Flask Integration)
|
|
252
|
+
# =============================================================================
|
|
253
|
+
|
|
254
|
+
class RequestContext:
|
|
255
|
+
"""
|
|
256
|
+
Request context for web applications.
|
|
257
|
+
|
|
258
|
+
In Flask apps, this wraps flask.request.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
def __init__(self):
|
|
262
|
+
self._form: Dict[str, Any] = {}
|
|
263
|
+
self._args: Dict[str, Any] = {}
|
|
264
|
+
self._cookies: Dict[str, str] = {}
|
|
265
|
+
self._headers: Dict[str, str] = {}
|
|
266
|
+
self._method: str = 'GET'
|
|
267
|
+
self._path: str = '/'
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def form(self) -> Dict[str, Any]:
|
|
271
|
+
"""Form data from POST request."""
|
|
272
|
+
try:
|
|
273
|
+
from flask import request as flask_request
|
|
274
|
+
return flask_request.form
|
|
275
|
+
except (ImportError, RuntimeError):
|
|
276
|
+
return self._form
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def args(self) -> Dict[str, Any]:
|
|
280
|
+
"""Query string parameters."""
|
|
281
|
+
try:
|
|
282
|
+
from flask import request as flask_request
|
|
283
|
+
return flask_request.args
|
|
284
|
+
except (ImportError, RuntimeError):
|
|
285
|
+
return self._args
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def method(self) -> str:
|
|
289
|
+
"""HTTP method."""
|
|
290
|
+
try:
|
|
291
|
+
from flask import request as flask_request
|
|
292
|
+
return flask_request.method
|
|
293
|
+
except (ImportError, RuntimeError):
|
|
294
|
+
return self._method
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def path(self) -> str:
|
|
298
|
+
"""Request path."""
|
|
299
|
+
try:
|
|
300
|
+
from flask import request as flask_request
|
|
301
|
+
return flask_request.path
|
|
302
|
+
except (ImportError, RuntimeError):
|
|
303
|
+
return self._path
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# Global request context
|
|
307
|
+
request = RequestContext()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# =============================================================================
|
|
311
|
+
# Redirect
|
|
312
|
+
# =============================================================================
|
|
313
|
+
|
|
314
|
+
def redirect(url: str, code: int = 302):
|
|
315
|
+
"""
|
|
316
|
+
Redirect to a URL.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
url: Target URL
|
|
320
|
+
code: HTTP status code (302 = temporary, 301 = permanent)
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Redirect response
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
from flask import redirect as flask_redirect
|
|
327
|
+
return flask_redirect(url, code=code)
|
|
328
|
+
except ImportError:
|
|
329
|
+
# Fallback for non-Flask usage
|
|
330
|
+
return f'<meta http-equiv="refresh" content="0; url={url}">'
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# =============================================================================
|
|
334
|
+
# Utilities
|
|
335
|
+
# =============================================================================
|
|
336
|
+
|
|
337
|
+
def to_json(value: Any) -> str:
|
|
338
|
+
"""Convert value to JSON string."""
|
|
339
|
+
return json.dumps(value)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def from_json(value: str) -> Any:
|
|
343
|
+
"""Parse JSON string."""
|
|
344
|
+
return json.loads(value)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def safe_get(obj: Any, *keys, default: Any = None) -> Any:
|
|
348
|
+
"""
|
|
349
|
+
Safely get nested values.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
obj: Object to get value from
|
|
353
|
+
*keys: Keys/indices to traverse
|
|
354
|
+
default: Default value if not found
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Value or default
|
|
358
|
+
"""
|
|
359
|
+
current = obj
|
|
360
|
+
for key in keys:
|
|
361
|
+
try:
|
|
362
|
+
if isinstance(current, dict):
|
|
363
|
+
current = current[key]
|
|
364
|
+
elif isinstance(current, (list, tuple)):
|
|
365
|
+
current = current[int(key)]
|
|
366
|
+
else:
|
|
367
|
+
current = getattr(current, key)
|
|
368
|
+
except (KeyError, IndexError, AttributeError, TypeError, ValueError):
|
|
369
|
+
return default
|
|
370
|
+
return current
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# =============================================================================
|
|
374
|
+
# Data Loading (Phase 2)
|
|
375
|
+
# =============================================================================
|
|
376
|
+
|
|
377
|
+
def load_data(source: str, type: str = 'json') -> Any:
|
|
378
|
+
"""
|
|
379
|
+
Load data from a file or URL.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
source: File path or URL
|
|
383
|
+
type: Data type (json, csv, xml)
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Parsed data
|
|
387
|
+
"""
|
|
388
|
+
import os
|
|
389
|
+
|
|
390
|
+
# Check if URL
|
|
391
|
+
if source.startswith(('http://', 'https://')):
|
|
392
|
+
try:
|
|
393
|
+
import urllib.request
|
|
394
|
+
with urllib.request.urlopen(source) as response:
|
|
395
|
+
content = response.read().decode('utf-8')
|
|
396
|
+
except Exception as e:
|
|
397
|
+
raise RuntimeError(f"Failed to fetch {source}: {e}")
|
|
398
|
+
else:
|
|
399
|
+
# File path
|
|
400
|
+
if not os.path.exists(source):
|
|
401
|
+
raise FileNotFoundError(f"Data file not found: {source}")
|
|
402
|
+
with open(source, 'r', encoding='utf-8') as f:
|
|
403
|
+
content = f.read()
|
|
404
|
+
|
|
405
|
+
# Parse based on type
|
|
406
|
+
if type == 'json':
|
|
407
|
+
return json.loads(content)
|
|
408
|
+
elif type == 'csv':
|
|
409
|
+
import csv
|
|
410
|
+
import io
|
|
411
|
+
reader = csv.DictReader(io.StringIO(content))
|
|
412
|
+
return list(reader)
|
|
413
|
+
elif type == 'xml':
|
|
414
|
+
import xml.etree.ElementTree as ET
|
|
415
|
+
return ET.fromstring(content)
|
|
416
|
+
else:
|
|
417
|
+
return content
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# =============================================================================
|
|
421
|
+
# HTTP Invocation (Phase 2)
|
|
422
|
+
# =============================================================================
|
|
423
|
+
|
|
424
|
+
def invoke(url: str, method: str = 'GET', headers: Dict[str, str] = None,
|
|
425
|
+
body: Any = None, timeout: int = 30) -> Any:
|
|
426
|
+
"""
|
|
427
|
+
Make an HTTP request.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
url: Target URL
|
|
431
|
+
method: HTTP method
|
|
432
|
+
headers: Request headers
|
|
433
|
+
body: Request body (will be JSON-encoded)
|
|
434
|
+
timeout: Timeout in seconds
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Response data (JSON-parsed if applicable)
|
|
438
|
+
"""
|
|
439
|
+
import urllib.request
|
|
440
|
+
import urllib.error
|
|
441
|
+
|
|
442
|
+
headers = headers or {}
|
|
443
|
+
|
|
444
|
+
# Add JSON content type if body provided
|
|
445
|
+
if body is not None and 'Content-Type' not in headers:
|
|
446
|
+
headers['Content-Type'] = 'application/json'
|
|
447
|
+
|
|
448
|
+
# Encode body
|
|
449
|
+
data = None
|
|
450
|
+
if body is not None:
|
|
451
|
+
data = json.dumps(body).encode('utf-8')
|
|
452
|
+
|
|
453
|
+
# Create request
|
|
454
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
458
|
+
content = response.read().decode('utf-8')
|
|
459
|
+
content_type = response.headers.get('Content-Type', '')
|
|
460
|
+
|
|
461
|
+
# Parse JSON if applicable
|
|
462
|
+
if 'application/json' in content_type:
|
|
463
|
+
return json.loads(content)
|
|
464
|
+
return content
|
|
465
|
+
except urllib.error.HTTPError as e:
|
|
466
|
+
raise RuntimeError(f"HTTP {e.code}: {e.reason}")
|
|
467
|
+
except urllib.error.URLError as e:
|
|
468
|
+
raise RuntimeError(f"Request failed: {e.reason}")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# =============================================================================
|
|
472
|
+
# File Handling (Phase 2)
|
|
473
|
+
# =============================================================================
|
|
474
|
+
|
|
475
|
+
def handle_file(file: Any, action: str = 'upload', destination: str = './uploads/') -> Dict[str, Any]:
|
|
476
|
+
"""
|
|
477
|
+
Handle file operations.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
file: File object or path
|
|
481
|
+
action: Action type (upload, read, delete)
|
|
482
|
+
destination: Destination directory for uploads
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Result dict with file info
|
|
486
|
+
"""
|
|
487
|
+
import os
|
|
488
|
+
import shutil
|
|
489
|
+
import uuid
|
|
490
|
+
|
|
491
|
+
result = {
|
|
492
|
+
'success': False,
|
|
493
|
+
'message': '',
|
|
494
|
+
'path': None,
|
|
495
|
+
'filename': None,
|
|
496
|
+
'size': 0
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
if action == 'upload':
|
|
501
|
+
# Ensure destination exists
|
|
502
|
+
os.makedirs(destination, exist_ok=True)
|
|
503
|
+
|
|
504
|
+
# Handle werkzeug FileStorage (Flask uploads)
|
|
505
|
+
if hasattr(file, 'save') and hasattr(file, 'filename'):
|
|
506
|
+
filename = file.filename
|
|
507
|
+
# Generate unique name to avoid conflicts
|
|
508
|
+
ext = os.path.splitext(filename)[1]
|
|
509
|
+
unique_name = f"{uuid.uuid4().hex}{ext}"
|
|
510
|
+
filepath = os.path.join(destination, unique_name)
|
|
511
|
+
file.save(filepath)
|
|
512
|
+
result['success'] = True
|
|
513
|
+
result['path'] = filepath
|
|
514
|
+
result['filename'] = unique_name
|
|
515
|
+
result['original_name'] = filename
|
|
516
|
+
result['size'] = os.path.getsize(filepath)
|
|
517
|
+
elif isinstance(file, str) and os.path.exists(file):
|
|
518
|
+
# Copy existing file
|
|
519
|
+
filename = os.path.basename(file)
|
|
520
|
+
filepath = os.path.join(destination, filename)
|
|
521
|
+
shutil.copy2(file, filepath)
|
|
522
|
+
result['success'] = True
|
|
523
|
+
result['path'] = filepath
|
|
524
|
+
result['filename'] = filename
|
|
525
|
+
result['size'] = os.path.getsize(filepath)
|
|
526
|
+
else:
|
|
527
|
+
result['message'] = 'Invalid file input'
|
|
528
|
+
|
|
529
|
+
elif action == 'read':
|
|
530
|
+
if isinstance(file, str) and os.path.exists(file):
|
|
531
|
+
with open(file, 'rb') as f:
|
|
532
|
+
result['content'] = f.read()
|
|
533
|
+
result['success'] = True
|
|
534
|
+
result['size'] = len(result['content'])
|
|
535
|
+
else:
|
|
536
|
+
result['message'] = 'File not found'
|
|
537
|
+
|
|
538
|
+
elif action == 'delete':
|
|
539
|
+
if isinstance(file, str) and os.path.exists(file):
|
|
540
|
+
os.remove(file)
|
|
541
|
+
result['success'] = True
|
|
542
|
+
result['message'] = 'File deleted'
|
|
543
|
+
else:
|
|
544
|
+
result['message'] = 'File not found'
|
|
545
|
+
|
|
546
|
+
except Exception as e:
|
|
547
|
+
result['message'] = str(e)
|
|
548
|
+
|
|
549
|
+
return result
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# =============================================================================
|
|
553
|
+
# Email Sending (Phase 2)
|
|
554
|
+
# =============================================================================
|
|
555
|
+
|
|
556
|
+
def send_mail(to: str, from_addr: str = None, subject: str = '',
|
|
557
|
+
body: str = '', html: bool = False, attachments: List[str] = None) -> bool:
|
|
558
|
+
"""
|
|
559
|
+
Send an email.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
to: Recipient email address(es)
|
|
563
|
+
from_addr: Sender email address
|
|
564
|
+
subject: Email subject
|
|
565
|
+
body: Email body
|
|
566
|
+
html: Whether body is HTML
|
|
567
|
+
attachments: List of file paths to attach
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
True if sent successfully
|
|
571
|
+
"""
|
|
572
|
+
import smtplib
|
|
573
|
+
from email.mime.text import MIMEText
|
|
574
|
+
from email.mime.multipart import MIMEMultipart
|
|
575
|
+
from email.mime.base import MIMEBase
|
|
576
|
+
from email import encoders
|
|
577
|
+
import os
|
|
578
|
+
|
|
579
|
+
# Get SMTP configuration from environment
|
|
580
|
+
smtp_host = os.environ.get('SMTP_HOST', 'localhost')
|
|
581
|
+
smtp_port = int(os.environ.get('SMTP_PORT', '25'))
|
|
582
|
+
smtp_user = os.environ.get('SMTP_USER', '')
|
|
583
|
+
smtp_pass = os.environ.get('SMTP_PASS', '')
|
|
584
|
+
smtp_ssl = os.environ.get('SMTP_SSL', 'false').lower() == 'true'
|
|
585
|
+
|
|
586
|
+
if not from_addr:
|
|
587
|
+
from_addr = os.environ.get('SMTP_FROM', 'noreply@localhost')
|
|
588
|
+
|
|
589
|
+
# Create message
|
|
590
|
+
if attachments:
|
|
591
|
+
msg = MIMEMultipart()
|
|
592
|
+
msg.attach(MIMEText(body, 'html' if html else 'plain'))
|
|
593
|
+
else:
|
|
594
|
+
msg = MIMEText(body, 'html' if html else 'plain')
|
|
595
|
+
|
|
596
|
+
msg['Subject'] = subject
|
|
597
|
+
msg['From'] = from_addr
|
|
598
|
+
msg['To'] = to if isinstance(to, str) else ', '.join(to)
|
|
599
|
+
|
|
600
|
+
# Add attachments
|
|
601
|
+
if attachments:
|
|
602
|
+
for filepath in attachments:
|
|
603
|
+
if os.path.exists(filepath):
|
|
604
|
+
with open(filepath, 'rb') as f:
|
|
605
|
+
part = MIMEBase('application', 'octet-stream')
|
|
606
|
+
part.set_payload(f.read())
|
|
607
|
+
encoders.encode_base64(part)
|
|
608
|
+
part.add_header('Content-Disposition', f'attachment; filename="{os.path.basename(filepath)}"')
|
|
609
|
+
msg.attach(part)
|
|
610
|
+
|
|
611
|
+
try:
|
|
612
|
+
if smtp_ssl:
|
|
613
|
+
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
|
614
|
+
else:
|
|
615
|
+
server = smtplib.SMTP(smtp_host, smtp_port)
|
|
616
|
+
|
|
617
|
+
if smtp_user and smtp_pass:
|
|
618
|
+
server.login(smtp_user, smtp_pass)
|
|
619
|
+
|
|
620
|
+
recipients = [to] if isinstance(to, str) else list(to)
|
|
621
|
+
server.sendmail(from_addr, recipients, msg.as_string())
|
|
622
|
+
server.quit()
|
|
623
|
+
return True
|
|
624
|
+
except Exception as e:
|
|
625
|
+
print(f"Email send failed: {e}")
|
|
626
|
+
return False
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# =============================================================================
|
|
630
|
+
# Job Queue (Phase 3)
|
|
631
|
+
# =============================================================================
|
|
632
|
+
|
|
633
|
+
class JobQueue:
|
|
634
|
+
"""Simple job queue for background processing."""
|
|
635
|
+
|
|
636
|
+
_jobs: Dict[str, Callable] = {}
|
|
637
|
+
_pending: List[Dict[str, Any]] = []
|
|
638
|
+
|
|
639
|
+
@classmethod
|
|
640
|
+
def task(cls, queue: str = 'default', priority: int = 0):
|
|
641
|
+
"""Decorator to register a job task."""
|
|
642
|
+
def decorator(func: Callable):
|
|
643
|
+
cls._jobs[func.__name__] = {
|
|
644
|
+
'func': func,
|
|
645
|
+
'queue': queue,
|
|
646
|
+
'priority': priority
|
|
647
|
+
}
|
|
648
|
+
return func
|
|
649
|
+
return decorator
|
|
650
|
+
|
|
651
|
+
@classmethod
|
|
652
|
+
def dispatch(cls, name: str, delay: str = None, **params):
|
|
653
|
+
"""Dispatch a job for execution."""
|
|
654
|
+
import threading
|
|
655
|
+
|
|
656
|
+
if name not in cls._jobs:
|
|
657
|
+
raise ValueError(f"Job not found: {name}")
|
|
658
|
+
|
|
659
|
+
job = cls._jobs[name]
|
|
660
|
+
|
|
661
|
+
def run_job():
|
|
662
|
+
if delay:
|
|
663
|
+
import time
|
|
664
|
+
# Parse delay (e.g., "5m", "1h", "30s")
|
|
665
|
+
seconds = cls._parse_delay(delay)
|
|
666
|
+
time.sleep(seconds)
|
|
667
|
+
job['func'](**params)
|
|
668
|
+
|
|
669
|
+
thread = threading.Thread(target=run_job, daemon=True)
|
|
670
|
+
thread.start()
|
|
671
|
+
|
|
672
|
+
@classmethod
|
|
673
|
+
def cancel(cls, name: str):
|
|
674
|
+
"""Cancel pending jobs (best effort)."""
|
|
675
|
+
cls._pending = [j for j in cls._pending if j.get('name') != name]
|
|
676
|
+
|
|
677
|
+
@staticmethod
|
|
678
|
+
def _parse_delay(delay: str) -> float:
|
|
679
|
+
"""Parse delay string to seconds."""
|
|
680
|
+
import re
|
|
681
|
+
match = re.match(r'(\d+)([smhd])', delay)
|
|
682
|
+
if not match:
|
|
683
|
+
return 0
|
|
684
|
+
value, unit = int(match.group(1)), match.group(2)
|
|
685
|
+
multipliers = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}
|
|
686
|
+
return value * multipliers.get(unit, 1)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# =============================================================================
|
|
690
|
+
# Scheduler (Phase 3)
|
|
691
|
+
# =============================================================================
|
|
692
|
+
|
|
693
|
+
class Scheduler:
|
|
694
|
+
"""Task scheduler for periodic and one-time tasks."""
|
|
695
|
+
|
|
696
|
+
_tasks: Dict[str, Dict[str, Any]] = {}
|
|
697
|
+
_running: bool = False
|
|
698
|
+
|
|
699
|
+
@classmethod
|
|
700
|
+
def schedule(cls, interval: str = None, cron: str = None, at: str = None, enabled: bool = True):
|
|
701
|
+
"""Decorator to schedule a task."""
|
|
702
|
+
def decorator(func: Callable):
|
|
703
|
+
cls._tasks[func.__name__] = {
|
|
704
|
+
'func': func,
|
|
705
|
+
'interval': interval,
|
|
706
|
+
'cron': cron,
|
|
707
|
+
'at': at,
|
|
708
|
+
'enabled': enabled
|
|
709
|
+
}
|
|
710
|
+
return func
|
|
711
|
+
return decorator
|
|
712
|
+
|
|
713
|
+
@classmethod
|
|
714
|
+
def start(cls):
|
|
715
|
+
"""Start the scheduler."""
|
|
716
|
+
import threading
|
|
717
|
+
|
|
718
|
+
if cls._running:
|
|
719
|
+
return
|
|
720
|
+
|
|
721
|
+
cls._running = True
|
|
722
|
+
|
|
723
|
+
def scheduler_loop():
|
|
724
|
+
import time
|
|
725
|
+
while cls._running:
|
|
726
|
+
for name, task in cls._tasks.items():
|
|
727
|
+
if not task['enabled']:
|
|
728
|
+
continue
|
|
729
|
+
# Simple interval handling
|
|
730
|
+
if task.get('interval'):
|
|
731
|
+
# Would need proper timing logic
|
|
732
|
+
pass
|
|
733
|
+
time.sleep(1)
|
|
734
|
+
|
|
735
|
+
thread = threading.Thread(target=scheduler_loop, daemon=True)
|
|
736
|
+
thread.start()
|
|
737
|
+
|
|
738
|
+
@classmethod
|
|
739
|
+
def stop(cls):
|
|
740
|
+
"""Stop the scheduler."""
|
|
741
|
+
cls._running = False
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
# =============================================================================
|
|
745
|
+
# Async Thread (Phase 3)
|
|
746
|
+
# =============================================================================
|
|
747
|
+
|
|
748
|
+
class AsyncThread:
|
|
749
|
+
"""Wrapper for async thread execution."""
|
|
750
|
+
|
|
751
|
+
_threads: Dict[str, 'AsyncThread'] = {}
|
|
752
|
+
|
|
753
|
+
def __init__(self, name: str, target: Callable, timeout: str = None, on_complete: Callable = None):
|
|
754
|
+
self.name = name
|
|
755
|
+
self.target = target
|
|
756
|
+
self.timeout = timeout
|
|
757
|
+
self.on_complete = on_complete
|
|
758
|
+
self._thread = None
|
|
759
|
+
self._result = None
|
|
760
|
+
AsyncThread._threads[name] = self
|
|
761
|
+
|
|
762
|
+
def start(self):
|
|
763
|
+
"""Start the thread."""
|
|
764
|
+
import threading
|
|
765
|
+
|
|
766
|
+
def wrapper():
|
|
767
|
+
try:
|
|
768
|
+
self._result = self.target()
|
|
769
|
+
if self.on_complete:
|
|
770
|
+
self.on_complete(self._result)
|
|
771
|
+
except Exception as e:
|
|
772
|
+
self._result = e
|
|
773
|
+
|
|
774
|
+
self._thread = threading.Thread(target=wrapper, daemon=True)
|
|
775
|
+
self._thread.start()
|
|
776
|
+
|
|
777
|
+
def join(self, timeout: float = None):
|
|
778
|
+
"""Wait for thread completion."""
|
|
779
|
+
if self._thread:
|
|
780
|
+
timeout_seconds = None
|
|
781
|
+
if timeout:
|
|
782
|
+
timeout_seconds = float(timeout)
|
|
783
|
+
elif self.timeout:
|
|
784
|
+
timeout_seconds = JobQueue._parse_delay(self.timeout)
|
|
785
|
+
self._thread.join(timeout=timeout_seconds)
|
|
786
|
+
|
|
787
|
+
def terminate(self):
|
|
788
|
+
"""Terminate the thread (best effort)."""
|
|
789
|
+
# Python threads can't be forcefully terminated
|
|
790
|
+
# This is a placeholder for cooperative termination
|
|
791
|
+
pass
|
|
792
|
+
|
|
793
|
+
@property
|
|
794
|
+
def result(self):
|
|
795
|
+
"""Get thread result."""
|
|
796
|
+
return self._result
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
def parallel_execute(tasks: List[Callable], max_workers: int = 4) -> List[Any]:
|
|
800
|
+
"""Execute tasks in parallel."""
|
|
801
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
802
|
+
|
|
803
|
+
results = []
|
|
804
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
805
|
+
futures = {executor.submit(task): idx for idx, task in enumerate(tasks)}
|
|
806
|
+
results = [None] * len(tasks)
|
|
807
|
+
for future in as_completed(futures):
|
|
808
|
+
idx = futures[future]
|
|
809
|
+
try:
|
|
810
|
+
results[idx] = future.result()
|
|
811
|
+
except Exception as e:
|
|
812
|
+
results[idx] = e
|
|
813
|
+
return results
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
# =============================================================================
|
|
817
|
+
# Exports
|
|
818
|
+
# =============================================================================
|
|
819
|
+
|
|
820
|
+
__all__ = [
|
|
821
|
+
# HTML
|
|
822
|
+
'escape',
|
|
823
|
+
'raw',
|
|
824
|
+
'RawHTML',
|
|
825
|
+
'bind',
|
|
826
|
+
|
|
827
|
+
# Component
|
|
828
|
+
'Component',
|
|
829
|
+
|
|
830
|
+
# Flash
|
|
831
|
+
'flash',
|
|
832
|
+
'get_flashed_messages',
|
|
833
|
+
|
|
834
|
+
# Scope
|
|
835
|
+
'session',
|
|
836
|
+
'application',
|
|
837
|
+
'ScopeDict',
|
|
838
|
+
|
|
839
|
+
# Database
|
|
840
|
+
'Database',
|
|
841
|
+
|
|
842
|
+
# Request
|
|
843
|
+
'request',
|
|
844
|
+
'RequestContext',
|
|
845
|
+
'redirect',
|
|
846
|
+
|
|
847
|
+
# Data (Phase 2)
|
|
848
|
+
'load_data',
|
|
849
|
+
'invoke',
|
|
850
|
+
'handle_file',
|
|
851
|
+
'send_mail',
|
|
852
|
+
|
|
853
|
+
# Async (Phase 3)
|
|
854
|
+
'JobQueue',
|
|
855
|
+
'Scheduler',
|
|
856
|
+
'AsyncThread',
|
|
857
|
+
'parallel_execute',
|
|
858
|
+
|
|
859
|
+
# Utilities
|
|
860
|
+
'to_json',
|
|
861
|
+
'from_json',
|
|
862
|
+
'safe_get',
|
|
863
|
+
]
|