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,414 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A message broker that survives the process.
|
|
3
|
+
|
|
4
|
+
The in-memory adapter lives inside one interpreter, so `quantum mq publish`
|
|
5
|
+
wrote into a broker that was discarded a moment later and `quantum mq consume`
|
|
6
|
+
created a fresh empty one — every message lost, both commands reporting
|
|
7
|
+
success. Under gunicorn the same thing happens per worker: four workers, four
|
|
8
|
+
private queues.
|
|
9
|
+
|
|
10
|
+
This stores messages in SQLite, the same way JobQueueService already stores
|
|
11
|
+
jobs, so a publish in one process is visible to a consume in another and
|
|
12
|
+
survives a restart. No server to run.
|
|
13
|
+
|
|
14
|
+
It is not a replacement for RabbitMQ or Redis at scale — consumers POLL, and
|
|
15
|
+
there is no clustering. It is the honest default: durable, ordered, with real
|
|
16
|
+
ack/nack semantics, and adequate for the single-host deployments this
|
|
17
|
+
framework targets.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import sqlite3
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
28
|
+
|
|
29
|
+
from quantum.runtime.message_broker import (
|
|
30
|
+
MessageBroker, Message, QueueInfo,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
_SCHEMA = """
|
|
34
|
+
CREATE TABLE IF NOT EXISTS quantum_messages (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
destination TEXT NOT NULL, -- queue name, or topic for pub/sub
|
|
37
|
+
kind TEXT NOT NULL, -- 'queue' | 'topic'
|
|
38
|
+
body TEXT NOT NULL, -- JSON
|
|
39
|
+
headers TEXT NOT NULL, -- JSON
|
|
40
|
+
reply_to TEXT,
|
|
41
|
+
correlation_id TEXT,
|
|
42
|
+
state TEXT NOT NULL DEFAULT 'ready', -- ready | inflight | done
|
|
43
|
+
delivered_at TEXT,
|
|
44
|
+
created_at TEXT NOT NULL
|
|
45
|
+
);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS ix_messages_pick
|
|
47
|
+
ON quantum_messages (destination, kind, state, created_at);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE IF NOT EXISTS quantum_queues (
|
|
50
|
+
name TEXT PRIMARY KEY,
|
|
51
|
+
durable INTEGER NOT NULL DEFAULT 1,
|
|
52
|
+
dlq TEXT,
|
|
53
|
+
created_at TEXT NOT NULL
|
|
54
|
+
);
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SqliteAdapter(MessageBroker):
|
|
59
|
+
"""Durable, cross-process message broker backed by a SQLite file."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, db_path: str = None):
|
|
62
|
+
self.db_path = db_path or 'quantum_messages.db'
|
|
63
|
+
self._connected = False
|
|
64
|
+
self._lock = threading.RLock()
|
|
65
|
+
self._subs: Dict[str, Dict[str, Any]] = {}
|
|
66
|
+
self._threads: Dict[str, threading.Thread] = {}
|
|
67
|
+
self._running = False
|
|
68
|
+
|
|
69
|
+
# -- connection --------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
def _conn(self):
|
|
72
|
+
conn = sqlite3.connect(self.db_path, timeout=30.0)
|
|
73
|
+
conn.row_factory = sqlite3.Row
|
|
74
|
+
return conn
|
|
75
|
+
|
|
76
|
+
def connect(self, config: Dict[str, Any] = None) -> None:
|
|
77
|
+
config = config or {}
|
|
78
|
+
if config.get('path'):
|
|
79
|
+
self.db_path = config['path']
|
|
80
|
+
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
conn = self._conn()
|
|
82
|
+
try:
|
|
83
|
+
conn.executescript(_SCHEMA)
|
|
84
|
+
conn.commit()
|
|
85
|
+
finally:
|
|
86
|
+
conn.close()
|
|
87
|
+
self._connected = True
|
|
88
|
+
self._running = True
|
|
89
|
+
|
|
90
|
+
def disconnect(self) -> None:
|
|
91
|
+
self._running = False
|
|
92
|
+
with self._lock:
|
|
93
|
+
self._subs.clear()
|
|
94
|
+
self._threads.clear()
|
|
95
|
+
self._connected = False
|
|
96
|
+
|
|
97
|
+
def is_connected(self) -> bool:
|
|
98
|
+
return self._connected
|
|
99
|
+
|
|
100
|
+
# -- producing ---------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _store(self, destination: str, kind: str, message: Message) -> None:
|
|
103
|
+
conn = self._conn()
|
|
104
|
+
try:
|
|
105
|
+
conn.execute(
|
|
106
|
+
"INSERT OR REPLACE INTO quantum_messages "
|
|
107
|
+
"(id, destination, kind, body, headers, reply_to, "
|
|
108
|
+
" correlation_id, state, created_at) "
|
|
109
|
+
"VALUES (?,?,?,?,?,?,?,'ready',?)",
|
|
110
|
+
(
|
|
111
|
+
message.id or str(uuid.uuid4()),
|
|
112
|
+
destination,
|
|
113
|
+
kind,
|
|
114
|
+
json.dumps(message.body, default=str),
|
|
115
|
+
json.dumps(message.headers or {}, default=str),
|
|
116
|
+
message.reply_to,
|
|
117
|
+
message.correlation_id,
|
|
118
|
+
datetime.now().isoformat(),
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
conn.commit()
|
|
122
|
+
finally:
|
|
123
|
+
conn.close()
|
|
124
|
+
|
|
125
|
+
def publish(self, topic: str, message: Message) -> None:
|
|
126
|
+
self._store(topic, 'topic', message)
|
|
127
|
+
|
|
128
|
+
def send(self, queue: str, message: Message) -> None:
|
|
129
|
+
self._store(queue, 'queue', message)
|
|
130
|
+
|
|
131
|
+
def request(self, queue: str, message: Message, timeout: int = 5000) -> Message:
|
|
132
|
+
"""Request/reply. Polls the reply queue until the timeout."""
|
|
133
|
+
reply_to = message.reply_to or f"reply.{uuid.uuid4().hex[:12]}"
|
|
134
|
+
message.reply_to = reply_to
|
|
135
|
+
message.correlation_id = message.correlation_id or str(uuid.uuid4())
|
|
136
|
+
self.send(queue, message)
|
|
137
|
+
|
|
138
|
+
deadline = time.time() + (timeout / 1000.0)
|
|
139
|
+
while time.time() < deadline:
|
|
140
|
+
got = self._take_one(reply_to, 'queue')
|
|
141
|
+
if got is not None:
|
|
142
|
+
self._mark_done(got.id)
|
|
143
|
+
return got
|
|
144
|
+
time.sleep(0.05)
|
|
145
|
+
raise TimeoutError(
|
|
146
|
+
f"no reply on {reply_to!r} within {timeout}ms"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# -- consuming ---------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
def _row_to_message(self, row) -> Message:
|
|
152
|
+
return Message(
|
|
153
|
+
id=row['id'],
|
|
154
|
+
topic=row['destination'] if row['kind'] == 'topic' else None,
|
|
155
|
+
queue=row['destination'] if row['kind'] == 'queue' else None,
|
|
156
|
+
body=json.loads(row['body']),
|
|
157
|
+
headers=json.loads(row['headers']),
|
|
158
|
+
reply_to=row['reply_to'],
|
|
159
|
+
correlation_id=row['correlation_id'],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Depois de quanto tempo uma mensagem reivindicada e considerada
|
|
163
|
+
# abandonada. Sem isto, um consumidor que morre entre reivindicar e
|
|
164
|
+
# confirmar prende a mensagem em 'inflight' PARA SEMPRE: ela nao volta
|
|
165
|
+
# para a fila, nao vai para a DLQ e nao aparece na contagem. Some.
|
|
166
|
+
VISIBILITY_TIMEOUT = 300.0 # segundos
|
|
167
|
+
|
|
168
|
+
def _reclaim_abandoned(self, conn, destination: str, kind: str) -> None:
|
|
169
|
+
"""Devolve para a fila o que ficou reivindicado tempo demais."""
|
|
170
|
+
limite = datetime.fromtimestamp(
|
|
171
|
+
time.time() - self.VISIBILITY_TIMEOUT).isoformat()
|
|
172
|
+
conn.execute(
|
|
173
|
+
"UPDATE quantum_messages SET state='ready', delivered_at=NULL "
|
|
174
|
+
"WHERE destination = ? AND kind = ? AND state = 'inflight' "
|
|
175
|
+
" AND delivered_at IS NOT NULL AND delivered_at < ?",
|
|
176
|
+
(destination, kind, limite),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _take_one(self, destination: str, kind: str) -> Optional[Message]:
|
|
180
|
+
"""Claim the oldest ready message, atomically."""
|
|
181
|
+
with self._lock:
|
|
182
|
+
conn = self._conn()
|
|
183
|
+
try:
|
|
184
|
+
self._reclaim_abandoned(conn, destination, kind)
|
|
185
|
+
row = conn.execute(
|
|
186
|
+
"SELECT * FROM quantum_messages "
|
|
187
|
+
"WHERE destination = ? AND kind = ? AND state = 'ready' "
|
|
188
|
+
"ORDER BY created_at ASC LIMIT 1",
|
|
189
|
+
(destination, kind),
|
|
190
|
+
).fetchone()
|
|
191
|
+
if not row:
|
|
192
|
+
return None
|
|
193
|
+
# Claim it. The WHERE state='ready' makes this safe against a
|
|
194
|
+
# second process that read the same row a moment ago.
|
|
195
|
+
changed = conn.execute(
|
|
196
|
+
"UPDATE quantum_messages SET state='inflight', delivered_at=? "
|
|
197
|
+
"WHERE id = ? AND state = 'ready'",
|
|
198
|
+
(datetime.now().isoformat(), row['id']),
|
|
199
|
+
).rowcount
|
|
200
|
+
conn.commit()
|
|
201
|
+
if not changed:
|
|
202
|
+
return None
|
|
203
|
+
return self._row_to_message(row)
|
|
204
|
+
finally:
|
|
205
|
+
conn.close()
|
|
206
|
+
|
|
207
|
+
def _mark_done(self, message_id: str) -> None:
|
|
208
|
+
conn = self._conn()
|
|
209
|
+
try:
|
|
210
|
+
conn.execute(
|
|
211
|
+
"UPDATE quantum_messages SET state='done' WHERE id = ?",
|
|
212
|
+
(message_id,),
|
|
213
|
+
)
|
|
214
|
+
conn.commit()
|
|
215
|
+
finally:
|
|
216
|
+
conn.close()
|
|
217
|
+
|
|
218
|
+
def _mark_ready(self, message_id: str) -> None:
|
|
219
|
+
conn = self._conn()
|
|
220
|
+
try:
|
|
221
|
+
conn.execute(
|
|
222
|
+
"UPDATE quantum_messages SET state='ready', delivered_at=NULL "
|
|
223
|
+
"WHERE id = ?",
|
|
224
|
+
(message_id,),
|
|
225
|
+
)
|
|
226
|
+
conn.commit()
|
|
227
|
+
finally:
|
|
228
|
+
conn.close()
|
|
229
|
+
|
|
230
|
+
def _poll_loop(self, sub_id: str, destination: str, kind: str,
|
|
231
|
+
handler: Callable, poll_interval: float = 0.1):
|
|
232
|
+
while self._running and sub_id in self._subs:
|
|
233
|
+
msg = self._take_one(destination, kind)
|
|
234
|
+
if msg is None:
|
|
235
|
+
time.sleep(poll_interval)
|
|
236
|
+
continue
|
|
237
|
+
try:
|
|
238
|
+
handler(msg)
|
|
239
|
+
# `done` so quando o handler NAO decidiu nada. Isto marcava
|
|
240
|
+
# done incondicionalmente, entao um handler que chamava
|
|
241
|
+
# `nack(msg, requeue=True)` — "devolve para a fila" — via a
|
|
242
|
+
# mensagem ser DESTRUIDA logo depois: o nack a punha em
|
|
243
|
+
# 'ready' e a linha seguinte a punha em 'done'. Perda de
|
|
244
|
+
# dado no broker que e o padrao.
|
|
245
|
+
self._finish_if_untouched(msg.id)
|
|
246
|
+
except Exception:
|
|
247
|
+
# Put it back rather than losing it; the handler failing is
|
|
248
|
+
# not evidence the message is bad.
|
|
249
|
+
self._mark_ready(msg.id)
|
|
250
|
+
time.sleep(poll_interval)
|
|
251
|
+
|
|
252
|
+
def _finish_if_untouched(self, message_id: str) -> None:
|
|
253
|
+
"""Confirma a mensagem so se o handler nao a tiver movido.
|
|
254
|
+
|
|
255
|
+
`WHERE state = 'inflight'` e a guarda: se o handler chamou ack (done)
|
|
256
|
+
ou nack (ready, ou movida para a DLQ), o estado ja mudou e este
|
|
257
|
+
UPDATE nao casa com nada.
|
|
258
|
+
"""
|
|
259
|
+
conn = self._conn()
|
|
260
|
+
try:
|
|
261
|
+
conn.execute(
|
|
262
|
+
"UPDATE quantum_messages SET state='done' "
|
|
263
|
+
"WHERE id = ? AND state = 'inflight'",
|
|
264
|
+
(message_id,),
|
|
265
|
+
)
|
|
266
|
+
conn.commit()
|
|
267
|
+
finally:
|
|
268
|
+
conn.close()
|
|
269
|
+
|
|
270
|
+
def _start_sub(self, destination: str, kind: str, handler: Callable) -> str:
|
|
271
|
+
sub_id = str(uuid.uuid4())
|
|
272
|
+
with self._lock:
|
|
273
|
+
self._subs[sub_id] = {'destination': destination, 'kind': kind}
|
|
274
|
+
t = threading.Thread(
|
|
275
|
+
target=self._poll_loop, args=(sub_id, destination, kind, handler),
|
|
276
|
+
daemon=True,
|
|
277
|
+
)
|
|
278
|
+
self._threads[sub_id] = t
|
|
279
|
+
t.start()
|
|
280
|
+
return sub_id
|
|
281
|
+
|
|
282
|
+
def subscribe(self, topic: str, handler: Callable) -> str:
|
|
283
|
+
return self._start_sub(topic, 'topic', handler)
|
|
284
|
+
|
|
285
|
+
def consume(self, queue: str, handler: Callable, prefetch: int = 1) -> str:
|
|
286
|
+
return self._start_sub(queue, 'queue', handler)
|
|
287
|
+
|
|
288
|
+
def unsubscribe(self, subscription_id: str) -> None:
|
|
289
|
+
with self._lock:
|
|
290
|
+
self._subs.pop(subscription_id, None)
|
|
291
|
+
self._threads.pop(subscription_id, None)
|
|
292
|
+
|
|
293
|
+
def ack(self, message: Message) -> None:
|
|
294
|
+
self._mark_done(message.id)
|
|
295
|
+
|
|
296
|
+
def nack(self, message: Message, requeue: bool = True) -> None:
|
|
297
|
+
if requeue:
|
|
298
|
+
self._mark_ready(message.id)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# requeue=False means "this message is bad" — it belongs in the dead
|
|
302
|
+
# letter queue, which is the whole point of declaring one. This
|
|
303
|
+
# marked it done and the message VANISHED: declare_queue stored the
|
|
304
|
+
# dlq column and nothing ever read it, so a queue with a DLQ behaved
|
|
305
|
+
# exactly like a queue without one. The memory adapter routes it; the
|
|
306
|
+
# test that should have caught the difference asserted
|
|
307
|
+
# `len(dlq_messages) >= 0`.
|
|
308
|
+
dlq = self._dlq_for(message.queue) if message.queue else None
|
|
309
|
+
if dlq:
|
|
310
|
+
self._store(dlq, 'queue', Message(
|
|
311
|
+
body=message.body,
|
|
312
|
+
headers={**(message.headers or {}),
|
|
313
|
+
'x-dead-letter-from': message.queue or ''},
|
|
314
|
+
reply_to=message.reply_to,
|
|
315
|
+
correlation_id=message.correlation_id,
|
|
316
|
+
))
|
|
317
|
+
self._mark_done(message.id)
|
|
318
|
+
|
|
319
|
+
def _dlq_for(self, queue: str) -> Optional[str]:
|
|
320
|
+
"""The dead letter queue declared for `queue`, if any."""
|
|
321
|
+
conn = self._conn()
|
|
322
|
+
try:
|
|
323
|
+
row = conn.execute(
|
|
324
|
+
"SELECT dlq FROM quantum_queues WHERE name = ?", (queue,)
|
|
325
|
+
).fetchone()
|
|
326
|
+
finally:
|
|
327
|
+
conn.close()
|
|
328
|
+
return row['dlq'] if row and row['dlq'] else None
|
|
329
|
+
|
|
330
|
+
# -- queue administration ---------------------------------------------
|
|
331
|
+
|
|
332
|
+
def declare_queue(self, name: str, durable: bool = True,
|
|
333
|
+
dead_letter_queue: str = None, ttl: int = None,
|
|
334
|
+
max_length: int = None, dlq: str = None,
|
|
335
|
+
exclusive: bool = False, auto_delete: bool = False) -> None:
|
|
336
|
+
"""Declara uma fila.
|
|
337
|
+
|
|
338
|
+
O parametro se chamava `dlq`, e a classe base chama
|
|
339
|
+
`dead_letter_queue`. MessageQueueService passa o nome da base, entao
|
|
340
|
+
NENHUMA fila podia ser declarada pelo servico: TypeError. E como este
|
|
341
|
+
adapter virou o broker padrao, a DLQ que eu tinha acabado de ligar era
|
|
342
|
+
inalcancavel pelo caminho normal.
|
|
343
|
+
|
|
344
|
+
`dlq=` continua aceito porque os testes e o codigo interno o usam.
|
|
345
|
+
`exclusive` e `auto_delete` sao aceitos e IGNORADOS: o servico os
|
|
346
|
+
passa sempre, e nao ha conexao exclusiva nem fila efemera num broker
|
|
347
|
+
de arquivo. Levantar TypeError por eles impedia declarar qualquer
|
|
348
|
+
fila; fingir que os implementa seria pior.
|
|
349
|
+
"""
|
|
350
|
+
dead_letter_queue = dead_letter_queue or dlq
|
|
351
|
+
conn = self._conn()
|
|
352
|
+
try:
|
|
353
|
+
conn.execute(
|
|
354
|
+
"INSERT OR IGNORE INTO quantum_queues (name, durable, dlq, created_at) "
|
|
355
|
+
"VALUES (?,?,?,?)",
|
|
356
|
+
(name, 1 if durable else 0, dead_letter_queue,
|
|
357
|
+
datetime.now().isoformat()),
|
|
358
|
+
)
|
|
359
|
+
conn.commit()
|
|
360
|
+
finally:
|
|
361
|
+
conn.close()
|
|
362
|
+
|
|
363
|
+
def purge_queue(self, name: str) -> int:
|
|
364
|
+
conn = self._conn()
|
|
365
|
+
try:
|
|
366
|
+
n = conn.execute(
|
|
367
|
+
"DELETE FROM quantum_messages WHERE destination = ? AND kind = 'queue'",
|
|
368
|
+
(name,),
|
|
369
|
+
).rowcount
|
|
370
|
+
conn.commit()
|
|
371
|
+
return n
|
|
372
|
+
finally:
|
|
373
|
+
conn.close()
|
|
374
|
+
|
|
375
|
+
def delete_queue(self, name: str) -> None:
|
|
376
|
+
conn = self._conn()
|
|
377
|
+
try:
|
|
378
|
+
conn.execute("DELETE FROM quantum_messages WHERE destination = ?", (name,))
|
|
379
|
+
conn.execute("DELETE FROM quantum_queues WHERE name = ?", (name,))
|
|
380
|
+
conn.commit()
|
|
381
|
+
finally:
|
|
382
|
+
conn.close()
|
|
383
|
+
|
|
384
|
+
def get_queue_info(self, name: str) -> QueueInfo:
|
|
385
|
+
conn = self._conn()
|
|
386
|
+
try:
|
|
387
|
+
ready = conn.execute(
|
|
388
|
+
"SELECT COUNT(*) FROM quantum_messages "
|
|
389
|
+
"WHERE destination = ? AND kind = 'queue' AND state = 'ready'",
|
|
390
|
+
(name,),
|
|
391
|
+
).fetchone()[0]
|
|
392
|
+
row = conn.execute(
|
|
393
|
+
"SELECT durable FROM quantum_queues WHERE name = ?", (name,)
|
|
394
|
+
).fetchone()
|
|
395
|
+
finally:
|
|
396
|
+
conn.close()
|
|
397
|
+
return QueueInfo(
|
|
398
|
+
name=name,
|
|
399
|
+
message_count=ready,
|
|
400
|
+
consumer_count=sum(
|
|
401
|
+
1 for s in self._subs.values() if s['destination'] == name
|
|
402
|
+
),
|
|
403
|
+
durable=bool(row['durable']) if row else True,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def list_queues(self) -> List[str]:
|
|
407
|
+
conn = self._conn()
|
|
408
|
+
try:
|
|
409
|
+
return [r[0] for r in conn.execute(
|
|
410
|
+
"SELECT DISTINCT destination FROM quantum_messages WHERE kind='queue' "
|
|
411
|
+
"UNION SELECT name FROM quantum_queues"
|
|
412
|
+
)]
|
|
413
|
+
finally:
|
|
414
|
+
conn.close()
|