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.
Files changed (262) hide show
  1. quantum/__init__.py +1 -0
  2. quantum/cli/__init__.py +2 -0
  3. quantum/cli/commands/__init__.py +15 -0
  4. quantum/cli/commands/build.py +417 -0
  5. quantum/cli/commands/dev.py +185 -0
  6. quantum/cli/commands/docs.py +329 -0
  7. quantum/cli/commands/lint.py +523 -0
  8. quantum/cli/commands/migrate.py +527 -0
  9. quantum/cli/commands/new.py +622 -0
  10. quantum/cli/commands/serve.py +190 -0
  11. quantum/cli/commands/test.py +193 -0
  12. quantum/cli/deploy.py +810 -0
  13. quantum/cli/hot_reload.py +951 -0
  14. quantum/cli/jobs.py +356 -0
  15. quantum/cli/mq.py +582 -0
  16. quantum/cli/pkg.py +390 -0
  17. quantum/cli/runner.py +547 -0
  18. quantum/cli/server_process.py +159 -0
  19. quantum/cli/utils.py +334 -0
  20. quantum/compiler/__init__.py +30 -0
  21. quantum/compiler/base_generator.py +367 -0
  22. quantum/compiler/cli.py +295 -0
  23. quantum/compiler/expression_transformer.py +444 -0
  24. quantum/compiler/javascript/__init__.py +10 -0
  25. quantum/compiler/javascript/generator.py +659 -0
  26. quantum/compiler/optimizer.py +270 -0
  27. quantum/compiler/python/__init__.py +10 -0
  28. quantum/compiler/python/generator.py +883 -0
  29. quantum/compiler/python/runtime.py +863 -0
  30. quantum/compiler/transpiler.py +330 -0
  31. quantum/core/__init__.py +3 -0
  32. quantum/core/ast_nodes.py +2611 -0
  33. quantum/core/expression_diagnostics.py +87 -0
  34. quantum/core/expression_stdlib.py +148 -0
  35. quantum/core/expressions.py +591 -0
  36. quantum/core/features/agents/src/__init__.py +30 -0
  37. quantum/core/features/agents/src/ast_node.py +540 -0
  38. quantum/core/features/conditionals/src/__init__.py +8 -0
  39. quantum/core/features/conditionals/src/ast_node.py +68 -0
  40. quantum/core/features/data_fetching/src/__init__.py +21 -0
  41. quantum/core/features/data_fetching/src/ast_node.py +312 -0
  42. quantum/core/features/data_fetching/src/desktop_adapter.py +351 -0
  43. quantum/core/features/data_fetching/src/html_adapter.py +474 -0
  44. quantum/core/features/data_fetching/src/parser.py +225 -0
  45. quantum/core/features/data_import/src/__init__.py +0 -0
  46. quantum/core/features/data_import/src/ast_node.py +291 -0
  47. quantum/core/features/data_import/src/runtime.py +538 -0
  48. quantum/core/features/dump/src/__init__.py +12 -0
  49. quantum/core/features/dump/src/ast_node.py +106 -0
  50. quantum/core/features/dump/src/parser.py +61 -0
  51. quantum/core/features/dump/src/runtime.py +246 -0
  52. quantum/core/features/functions/src/__init__.py +8 -0
  53. quantum/core/features/functions/src/ast_node.py +149 -0
  54. quantum/core/features/game_engine_2d/src/__init__.py +19 -0
  55. quantum/core/features/game_engine_2d/src/ast_nodes.py +1719 -0
  56. quantum/core/features/game_engine_2d/src/parser.py +983 -0
  57. quantum/core/features/invocation/src/__init__.py +0 -0
  58. quantum/core/features/invocation/src/ast_node.py +146 -0
  59. quantum/core/features/invocation/src/runtime.py +327 -0
  60. quantum/core/features/knowledge_base/src/__init__.py +6 -0
  61. quantum/core/features/knowledge_base/src/ast_node.py +113 -0
  62. quantum/core/features/knowledge_base/src/parser.py +82 -0
  63. quantum/core/features/logging/src/__init__.py +12 -0
  64. quantum/core/features/logging/src/ast_node.py +111 -0
  65. quantum/core/features/logging/src/parser.py +50 -0
  66. quantum/core/features/logging/src/runtime.py +190 -0
  67. quantum/core/features/loops/src/__init__.py +8 -0
  68. quantum/core/features/loops/src/ast_node.py +60 -0
  69. quantum/core/features/query/src/__init__.py +0 -0
  70. quantum/core/features/query/src/database_service.py +322 -0
  71. quantum/core/features/query/src/query_validators.py +20 -0
  72. quantum/core/features/state_management/src/__init__.py +11 -0
  73. quantum/core/features/state_management/src/ast_node.py +228 -0
  74. quantum/core/features/terminal_engine/src/__init__.py +21 -0
  75. quantum/core/features/terminal_engine/src/ast_nodes.py +560 -0
  76. quantum/core/features/terminal_engine/src/parser.py +361 -0
  77. quantum/core/features/testing_engine/src/__init__.py +41 -0
  78. quantum/core/features/testing_engine/src/ast_nodes.py +1212 -0
  79. quantum/core/features/testing_engine/src/parser.py +604 -0
  80. quantum/core/features/theming/src/__init__.py +48 -0
  81. quantum/core/features/theming/src/ast_node.py +137 -0
  82. quantum/core/features/theming/src/presets.py +405 -0
  83. quantum/core/features/ui_engine/src/__init__.py +20 -0
  84. quantum/core/features/ui_engine/src/ast_nodes.py +1854 -0
  85. quantum/core/features/ui_engine/src/parser.py +1106 -0
  86. quantum/core/features/websocket/src/__init__.py +24 -0
  87. quantum/core/features/websocket/src/ast_node.py +247 -0
  88. quantum/core/html_compat.py +299 -0
  89. quantum/core/parser.py +1235 -0
  90. quantum/core/parser_registry.py +213 -0
  91. quantum/core/parsers/__init__.py +76 -0
  92. quantum/core/parsers/ai/__init__.py +12 -0
  93. quantum/core/parsers/ai/agent_parser.py +106 -0
  94. quantum/core/parsers/ai/knowledge_parser.py +86 -0
  95. quantum/core/parsers/ai/llm_parser.py +78 -0
  96. quantum/core/parsers/ai/team_parser.py +88 -0
  97. quantum/core/parsers/base.py +322 -0
  98. quantum/core/parsers/composition/__init__.py +10 -0
  99. quantum/core/parsers/composition/import_parser.py +50 -0
  100. quantum/core/parsers/composition/slot_parser.py +48 -0
  101. quantum/core/parsers/control_flow/__init__.py +11 -0
  102. quantum/core/parsers/control_flow/if_parser.py +68 -0
  103. quantum/core/parsers/control_flow/loop_parser.py +105 -0
  104. quantum/core/parsers/control_flow/set_parser.py +89 -0
  105. quantum/core/parsers/data/__init__.py +12 -0
  106. quantum/core/parsers/data/data_parser.py +217 -0
  107. quantum/core/parsers/data/invoke_parser.py +116 -0
  108. quantum/core/parsers/data/query_parser.py +188 -0
  109. quantum/core/parsers/data/transaction_parser.py +77 -0
  110. quantum/core/parsers/events/__init__.py +9 -0
  111. quantum/core/parsers/events/dispatch_event_parser.py +44 -0
  112. quantum/core/parsers/forms/__init__.py +11 -0
  113. quantum/core/parsers/forms/action_parser.py +54 -0
  114. quantum/core/parsers/forms/flash_parser.py +34 -0
  115. quantum/core/parsers/forms/redirect_parser.py +33 -0
  116. quantum/core/parsers/functions/__init__.py +11 -0
  117. quantum/core/parsers/functions/function_parser.py +137 -0
  118. quantum/core/parsers/functions/param_parser.py +66 -0
  119. quantum/core/parsers/functions/return_parser.py +31 -0
  120. quantum/core/parsers/html/__init__.py +10 -0
  121. quantum/core/parsers/html/component_call_parser.py +130 -0
  122. quantum/core/parsers/html/html_parser.py +115 -0
  123. quantum/core/parsers/jobs/__init__.py +11 -0
  124. quantum/core/parsers/jobs/job_parser.py +71 -0
  125. quantum/core/parsers/jobs/schedule_parser.py +62 -0
  126. quantum/core/parsers/jobs/thread_parser.py +57 -0
  127. quantum/core/parsers/messaging/__init__.py +17 -0
  128. quantum/core/parsers/messaging/message_ack_parser.py +30 -0
  129. quantum/core/parsers/messaging/message_nack_parser.py +30 -0
  130. quantum/core/parsers/messaging/message_parser.py +114 -0
  131. quantum/core/parsers/messaging/queue_parser.py +61 -0
  132. quantum/core/parsers/messaging/websocket_parser.py +121 -0
  133. quantum/core/parsers/persistence/__init__.py +9 -0
  134. quantum/core/parsers/persistence/persist_parser.py +64 -0
  135. quantum/core/parsers/routing/__init__.py +9 -0
  136. quantum/core/parsers/routing/route_parser.py +41 -0
  137. quantum/core/parsers/scripting/__init__.py +12 -0
  138. quantum/core/parsers/scripting/pyclass_parser.py +62 -0
  139. quantum/core/parsers/scripting/pydecorator_parser.py +68 -0
  140. quantum/core/parsers/scripting/pyimport_parser.py +52 -0
  141. quantum/core/parsers/scripting/python_parser.py +49 -0
  142. quantum/core/parsers/services/__init__.py +12 -0
  143. quantum/core/parsers/services/dump_parser.py +52 -0
  144. quantum/core/parsers/services/file_parser.py +48 -0
  145. quantum/core/parsers/services/log_parser.py +46 -0
  146. quantum/core/parsers/services/mail_parser.py +65 -0
  147. quantum/core/tiers.py +82 -0
  148. quantum/packages/__init__.py +28 -0
  149. quantum/packages/manager.py +413 -0
  150. quantum/packages/manifest.py +351 -0
  151. quantum/packages/registry.py +399 -0
  152. quantum/packages/resolver.py +336 -0
  153. quantum/plugins/__init__.py +33 -0
  154. quantum/plugins/hooks.py +329 -0
  155. quantum/plugins/loader.py +479 -0
  156. quantum/plugins/manifest.py +336 -0
  157. quantum/plugins/registry.py +371 -0
  158. quantum/runtime/__init__.py +28 -0
  159. quantum/runtime/action_handler.py +443 -0
  160. quantum/runtime/adapters/__init__.py +88 -0
  161. quantum/runtime/adapters/memory_adapter.py +690 -0
  162. quantum/runtime/adapters/rabbitmq_adapter.py +715 -0
  163. quantum/runtime/adapters/redis_adapter.py +582 -0
  164. quantum/runtime/adapters/sqlite_adapter.py +414 -0
  165. quantum/runtime/agent_service.py +1133 -0
  166. quantum/runtime/api_server.py +86 -0
  167. quantum/runtime/ast_cache.py +506 -0
  168. quantum/runtime/auth_service.py +267 -0
  169. quantum/runtime/component.py +990 -0
  170. quantum/runtime/component_composer.py +319 -0
  171. quantum/runtime/component_resolver.py +174 -0
  172. quantum/runtime/database_service.py +598 -0
  173. quantum/runtime/email_service.py +162 -0
  174. quantum/runtime/error_handler.py +295 -0
  175. quantum/runtime/execution_context.py +286 -0
  176. quantum/runtime/executor_registry.py +171 -0
  177. quantum/runtime/executors/__init__.py +71 -0
  178. quantum/runtime/executors/ai/__init__.py +12 -0
  179. quantum/runtime/executors/ai/agent_executor.py +217 -0
  180. quantum/runtime/executors/ai/knowledge_executor.py +114 -0
  181. quantum/runtime/executors/ai/llm_executor.py +153 -0
  182. quantum/runtime/executors/ai/team_executor.py +171 -0
  183. quantum/runtime/executors/base.py +262 -0
  184. quantum/runtime/executors/control_flow/__init__.py +11 -0
  185. quantum/runtime/executors/control_flow/if_executor.py +93 -0
  186. quantum/runtime/executors/control_flow/loop_executor.py +307 -0
  187. quantum/runtime/executors/control_flow/set_executor.py +412 -0
  188. quantum/runtime/executors/data/__init__.py +12 -0
  189. quantum/runtime/executors/data/data_executor.py +145 -0
  190. quantum/runtime/executors/data/invoke_executor.py +176 -0
  191. quantum/runtime/executors/data/query_executor.py +256 -0
  192. quantum/runtime/executors/data/transaction_executor.py +91 -0
  193. quantum/runtime/executors/jobs/__init__.py +11 -0
  194. quantum/runtime/executors/jobs/job_executor.py +190 -0
  195. quantum/runtime/executors/jobs/schedule_executor.py +132 -0
  196. quantum/runtime/executors/jobs/thread_executor.py +127 -0
  197. quantum/runtime/executors/messaging/__init__.py +17 -0
  198. quantum/runtime/executors/messaging/message_ack_executor.py +51 -0
  199. quantum/runtime/executors/messaging/message_executor.py +174 -0
  200. quantum/runtime/executors/messaging/queue_executor.py +103 -0
  201. quantum/runtime/executors/messaging/websocket_executor.py +197 -0
  202. quantum/runtime/executors/scripting/__init__.py +11 -0
  203. quantum/runtime/executors/scripting/pyclass_executor.py +90 -0
  204. quantum/runtime/executors/scripting/pyimport_executor.py +81 -0
  205. quantum/runtime/executors/scripting/python_executor.py +249 -0
  206. quantum/runtime/executors/services/__init__.py +12 -0
  207. quantum/runtime/executors/services/dump_executor.py +72 -0
  208. quantum/runtime/executors/services/file_executor.py +89 -0
  209. quantum/runtime/executors/services/log_executor.py +77 -0
  210. quantum/runtime/executors/services/mail_executor.py +81 -0
  211. quantum/runtime/expression_cache.py +498 -0
  212. quantum/runtime/file_upload_service.py +326 -0
  213. quantum/runtime/function_registry.py +118 -0
  214. quantum/runtime/game_builder.py +166 -0
  215. quantum/runtime/game_code_generator.py +2371 -0
  216. quantum/runtime/game_templates.py +2006 -0
  217. quantum/runtime/godot_code_generator.py +4681 -0
  218. quantum/runtime/godot_templates.py +1449 -0
  219. quantum/runtime/job_executor.py +1599 -0
  220. quantum/runtime/knowledge_service.py +500 -0
  221. quantum/runtime/llm_cache.py +100 -0
  222. quantum/runtime/llm_providers.py +704 -0
  223. quantum/runtime/llm_service.py +287 -0
  224. quantum/runtime/logging_setup.py +140 -0
  225. quantum/runtime/message_broker.py +364 -0
  226. quantum/runtime/message_queue_service.py +571 -0
  227. quantum/runtime/param_validation.py +184 -0
  228. quantum/runtime/pypy_compat.py +315 -0
  229. quantum/runtime/python_bridge.py +698 -0
  230. quantum/runtime/query_validators.py +304 -0
  231. quantum/runtime/renderer.py +733 -0
  232. quantum/runtime/service_container.py +444 -0
  233. quantum/runtime/terminal_builder.py +76 -0
  234. quantum/runtime/terminal_code_generator.py +607 -0
  235. quantum/runtime/terminal_templates.py +243 -0
  236. quantum/runtime/testing_builder.py +77 -0
  237. quantum/runtime/testing_code_generator.py +833 -0
  238. quantum/runtime/testing_templates.py +85 -0
  239. quantum/runtime/ui_builder.py +188 -0
  240. quantum/runtime/ui_desktop_adapter.py +1730 -0
  241. quantum/runtime/ui_desktop_templates.py +307 -0
  242. quantum/runtime/ui_html_adapter.py +2691 -0
  243. quantum/runtime/ui_html_templates.py +2297 -0
  244. quantum/runtime/ui_mobile_adapter.py +1832 -0
  245. quantum/runtime/ui_mobile_templates.py +1003 -0
  246. quantum/runtime/ui_textual_adapter.py +1866 -0
  247. quantum/runtime/ui_textual_templates.py +45 -0
  248. quantum/runtime/ui_tokens.py +465 -0
  249. quantum/runtime/ui_validator.py +365 -0
  250. quantum/runtime/validators.py +256 -0
  251. quantum/runtime/web_server.py +1766 -0
  252. quantum/runtime/websocket_adapter.py +501 -0
  253. quantum/runtime/websocket_service.py +585 -0
  254. quantum/runtime/websocket_transport.py +289 -0
  255. quantum/runtime/wsgi.py +101 -0
  256. quantum/utils/__init__.py +1 -0
  257. quantum_framework-0.9.0.dist-info/METADATA +244 -0
  258. quantum_framework-0.9.0.dist-info/RECORD +262 -0
  259. quantum_framework-0.9.0.dist-info/WHEEL +5 -0
  260. quantum_framework-0.9.0.dist-info/entry_points.txt +2 -0
  261. quantum_framework-0.9.0.dist-info/licenses/LICENSE +21 -0
  262. 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()