localqueue 1.0.0__cp312-cp312-win_amd64.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.
localqueue/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """localqueue: fila persistente local em SQLite com ACK, lease e retry."""
2
+
3
+ from localqueue.core import JsonSerializer, SimpleQueue
4
+ from localqueue.exceptions import Empty, LeaseExpired, LocalQueueError
5
+ from localqueue.job import Job
6
+ from localqueue.worker import Worker
7
+
8
+ __all__ = [
9
+ "Empty",
10
+ "Job",
11
+ "JsonSerializer",
12
+ "LeaseExpired",
13
+ "LocalQueueError",
14
+ "SimpleQueue",
15
+ "Worker",
16
+ ]
localqueue/core.py ADDED
@@ -0,0 +1,272 @@
1
+ """Facade Python para a fila persistente localqueue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import time as _time
8
+ from pathlib import Path
9
+ from typing import Any, Optional, Protocol
10
+
11
+ from localqueue import localqueue as _native
12
+ from localqueue.exceptions import Empty, LeaseExpired, LocalQueueError
13
+ from localqueue.job import Job
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ class Serializer(Protocol):
19
+ """Protocolo para serializadores compatíveis."""
20
+
21
+ def dumps(self, obj: Any) -> bytes: ...
22
+
23
+ def loads(self, data: bytes) -> Any: ...
24
+
25
+
26
+ class JsonSerializer:
27
+ """Serializador JSON padrão."""
28
+
29
+ def dumps(self, obj: Any) -> bytes:
30
+ return json.dumps(obj).encode("utf-8")
31
+
32
+ def loads(self, data: bytes) -> Any:
33
+ return json.loads(data.decode("utf-8"))
34
+
35
+
36
+ class SimpleQueue:
37
+ """Fila persistente baseada em SQLite com ACK/NACK, lease e retry.
38
+
39
+ O motor transacional é implementado em Rust (extensão nativa), garantindo
40
+ atomicidade mesmo com múltiplos processos/threads.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ path: str,
46
+ name: str = "default",
47
+ *,
48
+ lease_seconds: float = 60.0,
49
+ max_retries: int = 3,
50
+ fsync: bool = False,
51
+ serializer: Optional[Serializer] = None,
52
+ ) -> None:
53
+ """Inicializa a fila.
54
+
55
+ :param path: diretório onde o banco SQLite será armazenado.
56
+ :param name: nome da fila.
57
+ :param lease_seconds: tempo de lease para cada job retirado.
58
+ :param max_retries: número máximo de tentativas antes de dead-letter.
59
+ :param fsync: quando ``True`` usa ``PRAGMA synchronous=FULL``.
60
+ :param serializer: serializador com métodos ``dumps`` e ``loads``.
61
+ """
62
+ if not lease_seconds > 0:
63
+ raise ValueError("'lease_seconds' deve ser positivo")
64
+ if max_retries < 0:
65
+ raise ValueError("'max_retries' deve ser não negativo")
66
+
67
+ self.path = Path(path)
68
+ self.name = name
69
+ self.lease_seconds = lease_seconds
70
+ self.max_retries = max_retries
71
+ self.serializer = serializer or JsonSerializer()
72
+
73
+ self.path.mkdir(parents=True, exist_ok=True)
74
+ db_path = self.path / "localqueue.db"
75
+
76
+ self._native: Optional[_native.NativeQueue] = _native.NativeQueue(
77
+ str(db_path),
78
+ name,
79
+ max_attempts=max_retries + 1,
80
+ fsync=fsync,
81
+ )
82
+
83
+ def _get_native(self) -> "_native.NativeQueue":
84
+ native = self._native
85
+ if native is None:
86
+ raise LocalQueueError("fila fechada")
87
+ return native
88
+
89
+ def put(self, data: Any, job_id: Optional[str] = None) -> int:
90
+ """Adiciona um item à fila.
91
+
92
+ :param data: payload a ser enfileirado.
93
+ :param job_id: identificador único opcional para deduplicação.
94
+ :return: id interno do item na fila.
95
+ """
96
+ payload = self.serializer.dumps(data)
97
+ return self._get_native().put(payload, job_id)
98
+
99
+ def get(
100
+ self, block: bool = True, timeout: Optional[float] = None
101
+ ) -> Job:
102
+ """Retira um item da fila com lease.
103
+
104
+ :param block: se ``True``, bloqueia até haver um item disponível.
105
+ :param timeout: tempo máximo de espera em segundos.
106
+ :return: instância de :class:`Job`.
107
+ :raises Empty: se ``block=False`` e a fila estiver vazia, ou se
108
+ ``block=True`` e ``timeout`` expirar.
109
+ """
110
+ lease_ms = int(self.lease_seconds * 1000)
111
+
112
+ if not block:
113
+ lease = self._get_native().get(lease_ms)
114
+ if lease is None:
115
+ raise Empty("fila vazia")
116
+ return self._to_job(lease)
117
+
118
+ if timeout is not None and timeout < 0:
119
+ raise ValueError("'timeout' deve ser não negativo")
120
+
121
+ start = _time.monotonic()
122
+ sleep = 0.01
123
+ max_sleep = 0.25
124
+ while True:
125
+ lease = self._get_native().get(lease_ms)
126
+ if lease is not None:
127
+ return self._to_job(lease)
128
+
129
+ if timeout is not None:
130
+ elapsed = _time.monotonic() - start
131
+ if elapsed >= timeout:
132
+ raise Empty("fila vazia")
133
+
134
+ _time.sleep(sleep)
135
+ sleep = min(sleep * 1.5, max_sleep)
136
+
137
+ def get_nowait(self) -> Job:
138
+ """Variação não bloqueante de :meth:`get`."""
139
+ return self.get(block=False)
140
+
141
+ def ack(self, job: Job) -> None:
142
+ """Confirma o processamento bem-sucedido de um job."""
143
+ self._get_native().ack(job.id, job.receipt)
144
+
145
+ def nack(
146
+ self,
147
+ job: Job,
148
+ *,
149
+ delay: float = 0.0,
150
+ last_error: Optional[str] = None,
151
+ ) -> None:
152
+ """Devolve o job à fila (erro transitório).
153
+
154
+ Se o job já atingiu ``max_retries``, ele é movido para dead-letter.
155
+ """
156
+ delay_ms = int(delay * 1000)
157
+ self._get_native().nack(job.id, job.receipt, delay_ms, last_error)
158
+
159
+ def fail(self, job: Job, last_error: Optional[str] = None) -> None:
160
+ """Marca o job como falha definitiva (dead-letter)."""
161
+ self._get_native().fail(job.id, job.receipt, last_error)
162
+
163
+ def extend_lease(self, job: Job, seconds: float) -> None:
164
+ """Estende o lease de um job.
165
+
166
+ Levanta :class:`LeaseExpired` se o lease já tiver expirado.
167
+ """
168
+ extend_ms = int(seconds * 1000)
169
+ new_expiration = self._get_native().extend_lease(
170
+ job.id, job.receipt, extend_ms
171
+ )
172
+ job.lease_expires_at = new_expiration / 1000.0
173
+
174
+ def reclaim_expired_leases(self) -> int:
175
+ """Recupera jobs cujo lease expirou.
176
+
177
+ :return: número de leases recuperados.
178
+ """
179
+ return self._get_native().reclaim_expired(None)
180
+
181
+ def stats(self) -> dict[str, int]:
182
+ """Retorna estatísticas da fila."""
183
+ stats = self._get_native().stats()
184
+ return {
185
+ "ready": stats.ready,
186
+ "processing": stats.processing,
187
+ "acked": stats.acked,
188
+ "failed": stats.failed,
189
+ }
190
+
191
+ def _to_job(self, lease: "_native.Lease") -> Job:
192
+ data = self.serializer.loads(lease.payload)
193
+ return Job(
194
+ id=lease.id,
195
+ data=data,
196
+ attempts=max(0, lease.attempts - 1),
197
+ receipt=lease.receipt,
198
+ lease_expires_at=lease.lease_until / 1000.0,
199
+ queue=self,
200
+ )
201
+
202
+ def purge(self, older_than: float, *, include_failed: bool = False) -> int:
203
+ """Remove mensagens antigas da fila.
204
+
205
+ :param older_than: idade máxima em segundos. Mensagens mais antigas
206
+ serão removidas.
207
+ :param include_failed: quando ``True``, também remove mensagens
208
+ ``failed``. Por padrão apenas ``acked`` são removidas.
209
+ :return: número de mensagens removidas.
210
+ """
211
+ if older_than < 0:
212
+ raise ValueError("'older_than' deve ser não negativo")
213
+
214
+ older_than_ms = int(older_than * 1000)
215
+ removed = 0
216
+ removed += self._get_native().purge(older_than_ms, 2) # acked
217
+ if include_failed:
218
+ removed += self._get_native().purge(older_than_ms, 3) # failed
219
+ return removed
220
+
221
+ def list_failed(
222
+ self, limit: int = 100, offset: int = 0
223
+ ) -> list[dict[str, Any]]:
224
+ """Lista mensagens na dead-letter.
225
+
226
+ :param limit: número máximo de mensagens.
227
+ :param offset: deslocamento para paginação.
228
+ :return: lista de dicionários com informações das mensagens.
229
+ """
230
+ if limit < 0:
231
+ raise ValueError("'limit' deve ser não negativo")
232
+ if offset < 0:
233
+ raise ValueError("'offset' deve ser não negativo")
234
+
235
+ failed = self._get_native().list_failed(limit, offset)
236
+ return [
237
+ {
238
+ "id": msg.id,
239
+ "data": self.serializer.loads(msg.payload),
240
+ "attempts": msg.attempts,
241
+ "last_error": msg.last_error,
242
+ "created_at": msg.created_at / 1000.0,
243
+ "updated_at": msg.updated_at / 1000.0,
244
+ }
245
+ for msg in failed
246
+ ]
247
+
248
+ def retry_failed(self, message_id: int) -> None:
249
+ """Move uma mensagem da dead-letter de volta para a fila.
250
+
251
+ :param message_id: id da mensagem a ser retentada.
252
+ """
253
+ self._get_native().retry_failed(message_id)
254
+
255
+ def vacuum(self) -> None:
256
+ """Compacta todo o ``localqueue.db`` compartilhado pelas filas.
257
+
258
+ A operação pode disputar o lock do SQLite com workers ativos.
259
+ """
260
+ self._get_native().vacuum()
261
+
262
+ def close(self) -> None:
263
+ """Fecha a conexão com o banco."""
264
+ if self._native is not None:
265
+ self._native.close()
266
+ self._native = None
267
+
268
+ def __enter__(self) -> SimpleQueue:
269
+ return self
270
+
271
+ def __exit__(self, *exc: Any) -> None:
272
+ self.close()
@@ -0,0 +1,5 @@
1
+ """Exceções usadas pelo localqueue."""
2
+
3
+ from localqueue.localqueue import Empty, LeaseExpired, LocalQueueError
4
+
5
+ __all__ = ["Empty", "LeaseExpired", "LocalQueueError"]
localqueue/job.py ADDED
@@ -0,0 +1,25 @@
1
+ """Modelo de job retornado pela fila."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class Job:
11
+ """Representa um item retirado da fila para processamento."""
12
+
13
+ id: int
14
+ data: Any
15
+ attempts: int
16
+ receipt: str
17
+ lease_expires_at: float
18
+ queue: "SimpleQueue" # type: ignore[name-defined] # noqa: F821
19
+
20
+ def extend_lease(self, seconds: float) -> None:
21
+ """Estende o lease do job.
22
+
23
+ Levanta :class:`LeaseExpired` se o lease já tiver expirado.
24
+ """
25
+ self.queue.extend_lease(self, seconds)
@@ -0,0 +1,58 @@
1
+ """Type stubs para o módulo nativo localqueue.localqueue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ class Lease:
8
+ id: int
9
+ payload: bytes
10
+ attempts: int
11
+ receipt: str
12
+ lease_until: int
13
+
14
+ class Stats:
15
+ ready: int
16
+ processing: int
17
+ acked: int
18
+ failed: int
19
+
20
+ class FailedMessage:
21
+ id: int
22
+ payload: bytes
23
+ attempts: int
24
+ last_error: Optional[str]
25
+ created_at: int
26
+ updated_at: int
27
+
28
+ class NativeQueue:
29
+ def __init__(
30
+ self,
31
+ path: str,
32
+ queue: str,
33
+ max_attempts: int = 3,
34
+ fsync: bool = False,
35
+ ) -> None: ...
36
+ def put(self, payload: bytes, job_id: Optional[str] = None) -> int: ...
37
+ def get(self, lease_ms: int) -> Optional[Lease]: ...
38
+ def ack(self, id: int, receipt: str) -> None: ...
39
+ def nack(
40
+ self,
41
+ id: int,
42
+ receipt: str,
43
+ delay_ms: int = 0,
44
+ last_error: Optional[str] = None,
45
+ ) -> None: ...
46
+ def fail(self, id: int, receipt: str, last_error: Optional[str] = None) -> None: ...
47
+ def extend_lease(self, id: int, receipt: str, extend_ms: int) -> int: ...
48
+ def reclaim_expired(self, now: Optional[int] = None) -> int: ...
49
+ def stats(self) -> Stats: ...
50
+ def purge(self, older_than_ms: int, status: Optional[int] = None) -> int: ...
51
+ def list_failed(self, limit: int = 100, offset: int = 0) -> list[FailedMessage]: ...
52
+ def retry_failed(self, id: int) -> None: ...
53
+ def vacuum(self) -> None: ...
54
+ def close(self) -> None: ...
55
+
56
+ class LocalQueueError(Exception): ...
57
+ class Empty(LocalQueueError): ...
58
+ class LeaseExpired(LocalQueueError): ...
localqueue/worker.py ADDED
@@ -0,0 +1,156 @@
1
+ """Worker genérico para processar jobs do localqueue."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Any, Callable, Optional
7
+
8
+ from localqueue.core import SimpleQueue
9
+ from localqueue.exceptions import Empty, LeaseExpired
10
+ from localqueue.job import Job
11
+
12
+ log = logging.getLogger(__name__)
13
+
14
+
15
+ class Worker:
16
+ """Worker simples que consome jobs de uma :class:`SimpleQueue`.
17
+
18
+ O handler recebe o ``Job`` completo. Ele pode levantar exceções para
19
+ sinalizar erro:
20
+
21
+ * Qualquer exceção não listada em ``permanent_errors``: o job volta para
22
+ a fila (ou dead-letter se atingir ``max_retries``).
23
+ * Exceção em ``permanent_errors``: o job vai para dead-letter.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ queue: SimpleQueue,
29
+ handler: Callable[[Job], Any],
30
+ *,
31
+ permanent_errors: Optional[tuple[type[BaseException], ...]] = None,
32
+ poll_interval: float = 1.0,
33
+ heartbeat_interval: Optional[float] = None,
34
+ ) -> None:
35
+ """Inicializa o worker.
36
+
37
+ :param queue: fila a ser consumida.
38
+ :param handler: função que processa o job.
39
+ :param permanent_errors: tupla de exceções que devem mover o job para
40
+ dead-letter.
41
+ :param poll_interval: intervalo entre tentativas quando a fila está
42
+ vazia.
43
+ :param heartbeat_interval: se definido, renova o lease
44
+ automaticamente durante o processamento.
45
+ """
46
+ if not poll_interval > 0:
47
+ raise ValueError("'poll_interval' deve ser positivo")
48
+ if heartbeat_interval is not None and not heartbeat_interval > 0:
49
+ raise ValueError("'heartbeat_interval' deve ser positivo")
50
+
51
+ self.queue = queue
52
+ self.handler = handler
53
+ self.permanent_errors = permanent_errors or ()
54
+ self.poll_interval = poll_interval
55
+ self.heartbeat_interval = heartbeat_interval
56
+ self._stop = False
57
+
58
+ def run(self) -> None:
59
+ """Inicia o loop de consumo."""
60
+ while not self._stop:
61
+ try:
62
+ job = self.queue.get(block=True, timeout=self.poll_interval)
63
+ except Empty:
64
+ continue
65
+
66
+ self._process(job)
67
+
68
+ def run_once(self) -> bool:
69
+ """Processa no máximo um job.
70
+
71
+ :return: ``True`` se um job foi processado, ``False`` se a fila estava
72
+ vazia.
73
+ """
74
+ try:
75
+ job = self.queue.get(block=False)
76
+ except Empty:
77
+ return False
78
+ self._process(job)
79
+ return True
80
+
81
+ def _process(self, job: Job) -> None:
82
+ log.info("Processando job %s (tentativa %d)", job.id, job.attempts)
83
+ try:
84
+ if self.heartbeat_interval is not None:
85
+ result = self._run_with_heartbeat(job)
86
+ else:
87
+ result = self.handler(job)
88
+ except LeaseExpired:
89
+ log.warning(
90
+ "Lease do job %s foi perdido; resultado será descartado",
91
+ job.id,
92
+ )
93
+ return
94
+ except self.permanent_errors:
95
+ log.exception("Erro permanente no job %s", job.id)
96
+ self._transition(self.queue.fail, job)
97
+ except Exception:
98
+ log.exception("Erro transitório no job %s; devolvendo à fila", job.id)
99
+ self._transition(self.queue.nack, job)
100
+ else:
101
+ log.debug("Job %s processado com sucesso: %s", job.id, result)
102
+ self._transition(self.queue.ack, job)
103
+
104
+ def _transition(
105
+ self, operation: Callable[[Job], None], job: Job
106
+ ) -> None:
107
+ try:
108
+ operation(job)
109
+ except LeaseExpired:
110
+ log.warning(
111
+ "Lease do job %s foi perdido antes da transição; "
112
+ "resultado será descartado",
113
+ job.id,
114
+ )
115
+
116
+ def _run_with_heartbeat(self, job: Job) -> Any:
117
+ """Executa o handler renovando o lease periodicamente."""
118
+ import threading
119
+
120
+ result: list[Any] = []
121
+ error: list[BaseException] = []
122
+ done = threading.Event()
123
+ lease_lost = False
124
+
125
+ def target() -> None:
126
+ try:
127
+ result.append(self.handler(job))
128
+ except BaseException as exc: # noqa: BLE001
129
+ error.append(exc)
130
+ finally:
131
+ done.set()
132
+
133
+ thread = threading.Thread(target=target, daemon=True)
134
+ thread.start()
135
+
136
+ while not done.wait(self.heartbeat_interval):
137
+ try:
138
+ job.extend_lease(self.queue.lease_seconds)
139
+ except Exception:
140
+ log.warning("Perdeu o lease do job %s", job.id)
141
+ lease_lost = True
142
+ break
143
+
144
+ # Espera o handler terminar, mesmo se o lease foi perdido.
145
+ done.wait()
146
+
147
+ if lease_lost:
148
+ raise LeaseExpired(f"job {job.id} perdeu o lease durante o processamento")
149
+
150
+ if error:
151
+ raise error[0]
152
+ return result[0] if result else None
153
+
154
+ def stop(self) -> None:
155
+ """Sinaliza para o worker parar no próximo ciclo."""
156
+ self._stop = True
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: localqueue
3
+ Version: 1.0.0
4
+ Requires-Dist: persist-queue==1.1.0 ; extra == 'benchmark'
5
+ Requires-Dist: pytest>=8.0 ; extra == 'dev'
6
+ Requires-Dist: maturin>=1.14 ; extra == 'dev'
7
+ Provides-Extra: benchmark
8
+ Provides-Extra: dev
9
+ Summary: Fila persistente local em SQLite com ACK, lease e retry.
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
12
+
13
+ # localqueue
14
+
15
+ Fila persistente local em SQLite com ACK, lease e retry.
16
+
17
+ O motor transacional é implementado em **Rust** (extensão nativa via pyo3/maturin),
18
+ garantindo atomicidade mesmo com múltiplos processos e threads. A facade em
19
+ **Python** mantém uma API simples e agradável.
20
+
21
+ ## Características
22
+
23
+ - Persistência em disco via SQLite (sobrevive a reinícios).
24
+ - Semântica de ACK/NACK/falha com **receipt/fencing token**.
25
+ - Lease/timeout: jobs não confirmados dentro do prazo voltam para a fila.
26
+ - Retry com limite máximo; após isso vão para dead-letter.
27
+ - Deduplicação opcional por `job_id`.
28
+ - Extensão de lease (`job.extend_lease(...)`) e heartbeat no worker.
29
+ - Estatísticas consistentes mesmo em multiprocessos.
30
+ - Serialização JSON por padrão (legível e interoperável).
31
+ - Suporte a múltiplas filas no mesmo banco (`name="foo"`, `name="bar"`).
32
+
33
+ ## Arquitetura
34
+
35
+ ```text
36
+ localqueue/
37
+ ├── src/ # Rust: schema, storage, queue, error, lib (pyo3)
38
+ ├── python/localqueue/ # Python: SimpleQueue, Job, Worker, exceções
39
+ ├── Cargo.toml
40
+ └── pyproject.toml # build via maturin
41
+ ```
42
+
43
+ Toda a máquina de estados vive em uma única tabela SQLite (`messages`), com
44
+ transações `BEGIN IMMEDIATE` para atomicidade. Não há camadas auxiliares de
45
+ estado: cada operação (`put`, `get`, `ack`, `nack`, `fail`) é uma única
46
+ transação.
47
+
48
+ ## Uso rápido
49
+
50
+ ```python
51
+ from localqueue import SimpleQueue
52
+
53
+ with SimpleQueue("./data", lease_seconds=30, max_retries=3) as q:
54
+ q.put({"type": "deploy", "app": "jarvis", "revision": "abc123"})
55
+
56
+ job = q.get()
57
+ try:
58
+ process(job.data)
59
+ except Exception:
60
+ q.nack(job)
61
+ else:
62
+ q.ack(job)
63
+ ```
64
+
65
+ ## Worker
66
+
67
+ ```python
68
+ from localqueue import SimpleQueue, Worker
69
+
70
+ def deploy(job):
71
+ print(f"Deploying {job.data['app']}@{job.data['revision']}")
72
+ # Opcional: renova lease para jobs longos
73
+ job.extend_lease(60)
74
+
75
+ q = SimpleQueue("./data")
76
+ worker = Worker(q, deploy, heartbeat_interval=10)
77
+ worker.run()
78
+ ```
79
+
80
+ ## Multithreading / multiprocessos
81
+
82
+ A fila é segura para uso com múltiplas threads e processos. O SQLite (em WAL)
83
+ serializa escritas, mas `BEGIN IMMEDIATE` e `busy_timeout` garantem que
84
+ operações concorrentes não corrompam o estado.
85
+
86
+ ## Idempotência
87
+
88
+ Mesmo com fencing token, um job pode ser entregue mais de uma vez (por
89
+ exemplo, se o worker concluir um efeito externo e morrer antes de executar
90
+ `ack`). A fila oferece semântica at-least-once, não exactly-once.
91
+ Recomenda-se que os handlers sejam idempotentes — use `job_id` para
92
+ deduplicação quando necessário.
93
+
94
+ As garantias completas de durabilidade, leases, retries e fencing estão em
95
+ [`docs/guarantees.md`](docs/guarantees.md).
96
+
97
+ ## API resumida
98
+
99
+ * ``put(data, job_id=None)`` – enfileira um item.
100
+ * ``get(block=True, timeout=None)`` – retira um item com lease.
101
+ * ``get_nowait()`` – variação não bloqueante.
102
+ * ``ack(job)`` – confirma processamento.
103
+ * ``nack(job, delay=0, last_error=None)`` – devolve à fila (erro transitório).
104
+ * ``fail(job, last_error=None)`` – envia para dead-letter.
105
+ * ``extend_lease(job, seconds)`` – renova o lease de um job.
106
+ * ``reclaim_expired_leases()`` – recupera leases expirados manualmente.
107
+ * ``stats()`` – retorna contagens de ready, processing, acked e failed.
108
+
109
+ ## Manutenção
110
+
111
+ * ``purge(older_than, include_failed=False)`` – remove mensagens antigas.
112
+ * ``list_failed(limit=100, offset=0)`` – lista mensagens na dead-letter.
113
+ * ``retry_failed(message_id)`` – move mensagem failed de volta para ready.
114
+ * ``vacuum()`` – compacta todo o arquivo compartilhado ``localqueue.db``. Pode
115
+ disputar o lock do SQLite com workers ativos, portanto prefira executá-lo em
116
+ uma janela de manutenção.
117
+
118
+ ## Desenvolvimento
119
+
120
+ ```bash
121
+ uv sync --extra dev
122
+ maturin develop
123
+ pytest
124
+ ```
125
+
126
+ ## Build para distribuição
127
+
128
+ ```bash
129
+ maturin build --release
130
+ ```
131
+
@@ -0,0 +1,11 @@
1
+ localqueue/__init__.py,sha256=t4A3Jck_OlW_tPU_TEx0Jd9-hXhgyGDpbI6-1U3b3FU,424
2
+ localqueue/core.py,sha256=3v2SgWab_EwYtjAdQ4v1zM1KqZ3xOFIZy-2FV4CAm0g,9206
3
+ localqueue/exceptions.py,sha256=6QLLFOaNDTGUW7sEXd1geLgeub_Dcpma3pJvf77J93Y,174
4
+ localqueue/job.py,sha256=3hPuysli4MMVMp0TrHGgCk3ZF8CBMZae4ElGvg0OB-E,626
5
+ localqueue/localqueue.cp312-win_amd64.pyd,sha256=Oc3-clPGdjm6f90wOMpBRoRI9Ec984fyCquB5P9RqqM,2016256
6
+ localqueue/localqueue.pyi,sha256=Sk8LiP68YKOaDhX0_mwViw44vshRakK-koQg5xeyAhE,1690
7
+ localqueue/worker.py,sha256=Lo9teBb8CAuMsQECKV4_kVo1M_fAfiOEAMoHDu9JWdI,5274
8
+ localqueue-1.0.0.dist-info/METADATA,sha256=SPAz2jtddAlJLzI69HcYlbvsmx53c2oJIsuHb8lw7-8,4415
9
+ localqueue-1.0.0.dist-info/WHEEL,sha256=SN7W5s0Ji01-9u_6QL7cEnscsXvljOVQDFMt-2tILwI,97
10
+ localqueue-1.0.0.dist-info/sboms/localqueue.cyclonedx.json,sha256=68KV1N48gKG3FVCOIRkp8YUZY6cGLZKueEtFT0X7DeQ,36852
11
+ localqueue-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.14.1)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-win_amd64