localqueue 0.3.3__tar.gz → 0.4.0__tar.gz

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 (53) hide show
  1. {localqueue-0.3.3 → localqueue-0.4.0}/CHANGELOG.md +29 -0
  2. {localqueue-0.3.3 → localqueue-0.4.0}/PKG-INFO +3 -1
  3. {localqueue-0.3.3 → localqueue-0.4.0}/README.md +2 -0
  4. localqueue-0.4.0/docs/develop.md +137 -0
  5. {localqueue-0.3.3 → localqueue-0.4.0}/docs/index.md +2 -0
  6. localqueue-0.4.0/docs/release.md +47 -0
  7. localqueue-0.4.0/docs/use-cases.md +174 -0
  8. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/cli.py +10 -0
  9. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/services/queue_worker.py +2 -0
  10. {localqueue-0.3.3 → localqueue-0.4.0}/pyproject.toml +1 -1
  11. {localqueue-0.3.3 → localqueue-0.4.0}/tests/test_cli.py +57 -0
  12. {localqueue-0.3.3 → localqueue-0.4.0}/uv.lock +1 -1
  13. localqueue-0.3.3/docs/release.md +0 -111
  14. {localqueue-0.3.3 → localqueue-0.4.0}/.gitignore +0 -0
  15. {localqueue-0.3.3 → localqueue-0.4.0}/LICENSE +0 -0
  16. {localqueue-0.3.3 → localqueue-0.4.0}/docs/api.md +0 -0
  17. {localqueue-0.3.3 → localqueue-0.4.0}/docs/compare.md +0 -0
  18. {localqueue-0.3.3 → localqueue-0.4.0}/docs/operational-maturity.md +0 -0
  19. {localqueue-0.3.3 → localqueue-0.4.0}/docs/queues.md +0 -0
  20. {localqueue-0.3.3 → localqueue-0.4.0}/docs/retries.md +0 -0
  21. {localqueue-0.3.3 → localqueue-0.4.0}/docs/stability.md +0 -0
  22. {localqueue-0.3.3 → localqueue-0.4.0}/examples/email_worker.py +0 -0
  23. {localqueue-0.3.3 → localqueue-0.4.0}/examples/enqueue_email.py +0 -0
  24. {localqueue-0.3.3 → localqueue-0.4.0}/examples/process_webhook.sh +0 -0
  25. {localqueue-0.3.3 → localqueue-0.4.0}/examples/retry_demo.py +0 -0
  26. {localqueue-0.3.3 → localqueue-0.4.0}/examples/sqlite_concurrency_benchmark.py +0 -0
  27. {localqueue-0.3.3 → localqueue-0.4.0}/examples/sqlite_process_harness.py +0 -0
  28. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/__init__.py +0 -0
  29. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/cli_commands.py +0 -0
  30. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/cli_support.py +0 -0
  31. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/cli_worker_commands.py +0 -0
  32. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/failure.py +0 -0
  33. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/paths.py +0 -0
  34. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/queue.py +0 -0
  35. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/__init__.py +0 -0
  36. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/stores/__init__.py +0 -0
  37. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/stores/_shared.py +0 -0
  38. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/stores/base.py +0 -0
  39. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/stores/lmdb.py +0 -0
  40. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/stores/memory.py +0 -0
  41. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/stores/sqlite.py +0 -0
  42. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/retry/tenacity.py +0 -0
  43. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/services/__init__.py +0 -0
  44. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/stores/__init__.py +0 -0
  45. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/stores/_shared.py +0 -0
  46. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/stores/base.py +0 -0
  47. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/stores/lmdb.py +0 -0
  48. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/stores/memory.py +0 -0
  49. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/stores/sqlite.py +0 -0
  50. {localqueue-0.3.3 → localqueue-0.4.0}/localqueue/worker.py +0 -0
  51. {localqueue-0.3.3 → localqueue-0.4.0}/tests/test_process_harness.py +0 -0
  52. {localqueue-0.3.3 → localqueue-0.4.0}/tests/test_queue.py +0 -0
  53. {localqueue-0.3.3 → localqueue-0.4.0}/tests/test_retry.py +0 -0
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0](https://github.com/brunoportis/localqueue/compare/v0.3.4...v0.4.0) (2026-04-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * add release-please automation and CLI --version option ([f8db976](https://github.com/brunoportis/localqueue/commit/f8db9767f45fc1eb5055ef0e7eb946aae3af3129))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **release:** target uv.lock version explicitly ([3e71f44](https://github.com/brunoportis/localqueue/commit/3e71f4450e50de69488b18c04b018a6af00bf962))
14
+ * **release:** target uv.lock version explicitly ([31b45ce](https://github.com/brunoportis/localqueue/commit/31b45ce2ba3deea070d8d49c8bebebadcbc9fb37))
15
+ * **release:** track uv.lock in release-please ([897b86a](https://github.com/brunoportis/localqueue/commit/897b86aa33d9b38cba82da7b0e2623473488b94d))
16
+ * **release:** track uv.lock in release-please ([637ef7f](https://github.com/brunoportis/localqueue/commit/637ef7fca945f1153b643f27b0d6df6ccc39f778))
17
+ * **workflow:** configure git identity for release sync ([9872e7a](https://github.com/brunoportis/localqueue/commit/9872e7a85d1382a968f344444009e721da926e50))
18
+ * **workflow:** configure git identity for release sync ([2ef15fd](https://github.com/brunoportis/localqueue/commit/2ef15fdd3b5a877569b46980ef795442c1f3089c))
19
+ * **workflow:** sync uv.lock in release-please ([f26b1fa](https://github.com/brunoportis/localqueue/commit/f26b1fac2603f5ae0016ce994f5065291b6a6ad0))
20
+ * **workflow:** sync uv.lock in release-please ([193b4c6](https://github.com/brunoportis/localqueue/commit/193b4c6d5dba208ec43d7ea6ada402c5c4f1c2a5))
21
+
22
+
23
+ ### Documentation
24
+
25
+ * **use-cases:** simplify CLI examples ([84b92f2](https://github.com/brunoportis/localqueue/commit/84b92f2a420ddeb46dc8db2e6c1493fc45430bff))
26
+
27
+ ## 0.3.4
28
+
29
+ - Keep `queue exec` and `queue process` alive in `--forever --block` mode when the queue starts empty.
30
+ - Preserve the batch empty-queue exit behavior for non-forever workers.
31
+
3
32
  ## 0.3.3
4
33
 
5
34
  - Split queue and retry store implementations into dedicated modules.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: localqueue
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Durable local queues for Python, with persistent retry state powered by Tenacity.
5
5
  Project-URL: Homepage, https://brunoportis.github.io/localqueue/
6
6
  Project-URL: Documentation, https://brunoportis.github.io/localqueue/
@@ -61,6 +61,8 @@ def send_email(job: dict[str, str]) -> None:
61
61
 
62
62
  Full docs live at [brunoportis.github.io/localqueue](https://brunoportis.github.io/localqueue/). The source docs are in [`docs/`](docs/).
63
63
 
64
+ For development and release workflow details, see [`docs/develop.md`](docs/develop.md).
65
+
64
66
  ## Install
65
67
 
66
68
  ```bash
@@ -26,6 +26,8 @@ def send_email(job: dict[str, str]) -> None:
26
26
 
27
27
  Full docs live at [brunoportis.github.io/localqueue](https://brunoportis.github.io/localqueue/). The source docs are in [`docs/`](docs/).
28
28
 
29
+ For development and release workflow details, see [`docs/develop.md`](docs/develop.md).
30
+
29
31
  ## Install
30
32
 
31
33
  ```bash
@@ -0,0 +1,137 @@
1
+ ---
2
+ icon: lucide/hammer
3
+ ---
4
+
5
+ # Development Guide
6
+
7
+ This page describes the day-to-day development flow for `localqueue`: how to
8
+ write commits, how versions are released, what quality gates should pass before
9
+ merging, and what happens to docs and deployments.
10
+
11
+ ## Commit style
12
+
13
+ Use Conventional Commits.
14
+
15
+ Preferred shapes:
16
+
17
+ ```text
18
+ feat: add queue health summary
19
+ fix: keep forever workers alive on empty queues
20
+ docs: describe release flow
21
+ chore: refresh release-please config
22
+ test: cover version output in the CLI
23
+ ```
24
+
25
+ Keep commit messages short and specific. Use a scope only when it helps the
26
+ history, for example `fix(cli): ...` or `docs(release): ...`.
27
+
28
+ Why this matters here:
29
+
30
+ - `release-please` uses commit history to decide the next version.
31
+ - clean commit messages produce cleaner changelogs.
32
+ - release PRs become easier to review when each change has a clear intent.
33
+
34
+ ## Versioning
35
+
36
+ Versions follow SemVer-style `MAJOR.MINOR.PATCH`.
37
+
38
+ Before `1.0.0`, use this rule:
39
+
40
+ | Change | Version bump |
41
+ | --- | --- |
42
+ | Bug fix only | patch |
43
+ | New CLI/API feature | minor |
44
+ | Breaking user-facing behavior | minor, with a clear changelog note |
45
+
46
+ In this repo:
47
+
48
+ - `release-please` opens or updates the release PR.
49
+ - merging that release PR creates the tag and the GitHub Release.
50
+ - the release tag stays in the form `v<version>`.
51
+ - `pyproject.toml` is updated by the release automation, not by hand.
52
+
53
+ ## Quality gates before merge
54
+
55
+ Run the project checks before opening or merging a release-worthy change.
56
+
57
+ ```bash
58
+ env UV_CACHE_DIR=/tmp/uv-cache uv run ruff format .
59
+ env UV_CACHE_DIR=/tmp/uv-cache uv run ruff format --check .
60
+ env UV_CACHE_DIR=/tmp/uv-cache uv run pytest -q
61
+ env UV_CACHE_DIR=/tmp/uv-cache uv run ruff check .
62
+ env UV_CACHE_DIR=/tmp/uv-cache uv run basedpyright
63
+ env UV_CACHE_DIR=/tmp/uv-cache uv run zensical build --clean
64
+ env UV_CACHE_DIR=/tmp/uv-cache uv lock --check
65
+ env UV_CACHE_DIR=/tmp/uv-cache uv build
66
+ ```
67
+
68
+ Practical notes:
69
+
70
+ - `pytest` is the main behavior gate.
71
+ - `ruff format` applies the formatting fix locally.
72
+ - `ruff format --check` verifies formatting stays stable before a merge.
73
+ - `ruff check` and `basedpyright` keep the codebase clean and typed where it
74
+ matters.
75
+ - `zensical build --clean` catches docs breakage before it lands.
76
+ - `uv lock --check` confirms the lockfile is current.
77
+ - `uv build` verifies the package artifacts still build cleanly.
78
+
79
+ ## Documentation workflow
80
+
81
+ Docs should stay close to the code and the release flow.
82
+
83
+ - Update `docs/` when the CLI, public API, or release process changes.
84
+ - Keep `docs/release.md` focused on release automation and tagging.
85
+ - Keep `docs/operational-maturity.md` focused on runtime guarantees and
86
+ production boundaries.
87
+ - Update `docs/index.md` and the docs navigation when a new top-level doc is
88
+ added.
89
+
90
+ Good documentation changes usually include:
91
+
92
+ - a short user-facing description
93
+ - exact commands
94
+ - concrete examples
95
+ - the current expected release or deployment behavior
96
+
97
+ ## Deployments
98
+
99
+ The docs site is built from `docs/` with `zensical`.
100
+
101
+ The release workflow currently handles:
102
+
103
+ - PyPI publication
104
+ - GHCR image publication
105
+
106
+ The docs workflow currently handles:
107
+
108
+ - building the docs site on pushes and pull requests
109
+ - deploying the site from GitHub Actions on push to the configured branches
110
+
111
+ If a change affects packaging, release tags, or public docs, validate both the
112
+ package build and the docs build before merging.
113
+
114
+ ## Local release checklist
115
+
116
+ When preparing a release or release PR, verify:
117
+
118
+ 1. commits follow Conventional Commits
119
+ 2. tests pass
120
+ 3. lint and typecheck pass
121
+ 4. docs build
122
+ 5. package build succeeds
123
+ 6. the release notes and changelog reflect the change
124
+
125
+ ## Secrets and CI
126
+
127
+ The current release automation expects:
128
+
129
+ - `RELEASE_PLEASE_TOKEN` for `release-please`
130
+ - `SONAR_TOKEN` for SonarCloud, if the Sonar workflow is enabled
131
+
132
+ Keep secrets in repository settings, not in the repo.
133
+
134
+ ## Daily rule
135
+
136
+ If a change would confuse the next release PR, fix the commit message, docs, or
137
+ tests before merging it.
@@ -65,10 +65,12 @@ See [Compare](compare.md) for a short decision guide.
65
65
 
66
66
  ## Read more
67
67
 
68
+ - [Use cases](use-cases.md)
68
69
  - [Persistent queues](queues.md)
69
70
  - [Persistent retries](retries.md)
70
71
  - [Stability](stability.md)
71
72
  - [Compare](compare.md)
72
73
  - [API reference](api.md)
73
74
  - [Operational maturity](operational-maturity.md)
75
+ - [Development guide](develop.md)
74
76
  - [Release checklist](release.md)
@@ -0,0 +1,47 @@
1
+ ---
2
+ icon: lucide/package-check
3
+ ---
4
+
5
+ # Release
6
+
7
+ This project uses `release-please` to automate release PRs, changelog updates,
8
+ version bumps, Git tags, and GitHub Releases. The PyPI and GHCR publication
9
+ still happens in the existing release workflow after the release tag is
10
+ created.
11
+
12
+ ## Versioning
13
+
14
+ Use SemVer-style versions:
15
+
16
+ ```text
17
+ MAJOR.MINOR.PATCH
18
+ ```
19
+
20
+ Before `1.0.0`, use this convention:
21
+
22
+ | Change | Version bump |
23
+ | --- | --- |
24
+ | Bug fix only | patch, for example `0.1.1` |
25
+ | New CLI/API feature | minor, for example `0.2.0` |
26
+ | Breaking user-facing behavior | minor before `1.0.0`, with a clear changelog note |
27
+
28
+ ## How it works
29
+
30
+ 1. Merge Conventional Commit changes into `master`.
31
+ 2. `release-please` opens or updates a release PR with the changelog and the
32
+ version bump.
33
+ 3. Merge the release PR.
34
+ 4. `release-please` tags the merge commit and creates the GitHub Release.
35
+ 5. The existing `Release` workflow publishes the artifacts to PyPI and GHCR.
36
+
37
+ ## Setup notes
38
+
39
+ 1. The workflow expects a repository secret named `RELEASE_PLEASE_TOKEN`.
40
+ 2. The release tag format stays `v<version>` so the current publish workflow can
41
+ keep matching `v*` tags.
42
+ 3. Keep commit messages in Conventional Commit form so `release-please` can
43
+ classify changes correctly.
44
+
45
+ ## Current Release
46
+
47
+ The current planned release is `0.3.4`.
@@ -0,0 +1,174 @@
1
+ ---
2
+ icon: lucide/briefcase
3
+ ---
4
+
5
+ # Use Cases
6
+
7
+ These examples are meant to be copied and run from any directory.
8
+ They keep state under `/tmp/localqueue-use-cases` so they do not interfere with
9
+ your normal localqueue data.
10
+
11
+ ## Command style
12
+
13
+ The examples below use the direct CLI form:
14
+
15
+ ```bash
16
+ localqueue queue exec ...
17
+ ```
18
+
19
+ Install `localqueue[cli]` first if the `localqueue` command is not available.
20
+ If you prefer `pipx run` or `uvx`, translate the command locally after you
21
+ understand the workflow.
22
+
23
+ ## Setup
24
+
25
+ Create a scratch directory and a small worker script once:
26
+
27
+ ```bash
28
+ mkdir -p /tmp/localqueue-use-cases
29
+ cat > /tmp/localqueue-use-cases/email_worker.py <<'PY'
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import sys
34
+
35
+
36
+ payload = json.load(sys.stdin)
37
+ address = payload["to"]
38
+ if payload.get("fail"):
39
+ raise ConnectionError(f"could not deliver email to {address}")
40
+ print(f"sent email to {address}")
41
+ PY
42
+ ```
43
+
44
+ ## 1. Local email spooler
45
+
46
+ This is the simplest shape for `localqueue`: one process enqueues work and
47
+ another process handles it later on the same machine.
48
+
49
+ Enqueue one email job:
50
+
51
+ ```bash
52
+ localqueue queue add emails \
53
+ --store-path /tmp/localqueue-use-cases/emails.sqlite3 \
54
+ --value '{"to":"user@example.com"}'
55
+ ```
56
+
57
+ Inspect the queue before processing:
58
+
59
+ ```bash
60
+ localqueue queue stats emails \
61
+ --store-path /tmp/localqueue-use-cases/emails.sqlite3
62
+ ```
63
+
64
+ Process one queued message with the worker script:
65
+
66
+ ```bash
67
+ localqueue queue exec emails \
68
+ --store-path /tmp/localqueue-use-cases/emails.sqlite3 \
69
+ -- python /tmp/localqueue-use-cases/email_worker.py
70
+ ```
71
+
72
+ Check that the queue is empty again:
73
+
74
+ ```bash
75
+ localqueue queue stats emails \
76
+ --store-path /tmp/localqueue-use-cases/emails.sqlite3
77
+ ```
78
+
79
+ Use this pattern when your app should accept work quickly and let a local
80
+ worker perform the external side effect later.
81
+
82
+ ## 2. Dead-letter and recover a failed command
83
+
84
+ This recipe shows a practical operator loop: enqueue a job that fails, inspect
85
+ the dead-letter list, then requeue it after fixing the payload.
86
+
87
+ Create a failing job:
88
+
89
+ ```bash
90
+ localqueue queue add emails \
91
+ --store-path /tmp/localqueue-use-cases/failing-emails.sqlite3 \
92
+ --value '{"to":"broken@example.com","fail":true}'
93
+ ```
94
+
95
+ Process it with a small retry budget so it reaches dead-letter quickly:
96
+
97
+ ```bash
98
+ localqueue queue exec emails \
99
+ --store-path /tmp/localqueue-use-cases/failing-emails.sqlite3 \
100
+ --retry-store-path /tmp/localqueue-use-cases/failing-emails-retries.sqlite3 \
101
+ --max-tries 2 \
102
+ -- python /tmp/localqueue-use-cases/email_worker.py
103
+ ```
104
+
105
+ Inspect the dead-letter summary and full record:
106
+
107
+ ```bash
108
+ localqueue queue dead emails \
109
+ --store-path /tmp/localqueue-use-cases/failing-emails.sqlite3 \
110
+ --summary
111
+
112
+ localqueue queue dead emails \
113
+ --store-path /tmp/localqueue-use-cases/failing-emails.sqlite3
114
+ ```
115
+
116
+ If you want to replay everything in the dead-letter queue:
117
+
118
+ ```bash
119
+ localqueue queue requeue-dead emails \
120
+ --store-path /tmp/localqueue-use-cases/failing-emails.sqlite3 \
121
+ --all
122
+ ```
123
+
124
+ This is a good fit for webhook delivery, notification pipelines, and other
125
+ single-host jobs where operators need to inspect and replay failed work from
126
+ the terminal.
127
+
128
+ ## 3. Persistent retries without a queue
129
+
130
+ Sometimes another system already delivers the job and all you need is retry
131
+ state that survives restarts. In that case, use `localqueue.retry` directly.
132
+
133
+ Run this once in an environment where `localqueue` is importable.
134
+
135
+ ```bash
136
+ python - <<'PY'
137
+ from localqueue.retry import PersistentRetryExhausted, persistent_retry
138
+
139
+
140
+ @persistent_retry(
141
+ key="invoice:1001",
142
+ store_path="/tmp/localqueue-use-cases/retries.sqlite3",
143
+ max_tries=3,
144
+ )
145
+ def flaky() -> None:
146
+ print("calling remote API")
147
+ raise ConnectionError("temporary upstream failure")
148
+
149
+
150
+ try:
151
+ flaky()
152
+ except PersistentRetryExhausted as exc:
153
+ print(f"exhausted: {exc.key} after {exc.attempts} attempts")
154
+ except Exception as exc:
155
+ print(f"failed this run: {exc}")
156
+ PY
157
+ ```
158
+
159
+ Run the same command again. The retry state is reused because the key is
160
+ stable and the attempt store is on disk. After the budget is exhausted,
161
+ `PersistentRetryExhausted` is raised before the wrapped function runs again.
162
+
163
+ You can later inspect or prune exhausted retry records:
164
+
165
+ ```bash
166
+ localqueue retry prune \
167
+ --retry-store-path /tmp/localqueue-use-cases/retries.sqlite3 \
168
+ --dry-run \
169
+ --older-than 0
170
+ ```
171
+
172
+ Use this shape when work already arrives from somewhere else, such as a cron
173
+ trigger, another queue, or an HTTP handler, and you only want durable retry
174
+ budgets.
@@ -8,6 +8,7 @@ from contextlib import contextmanager
8
8
  from pathlib import Path
9
9
  from typing import Any, Iterator, TYPE_CHECKING
10
10
 
11
+ from . import __version__
11
12
  from .cli_commands import (
12
13
  CommandRegistryContext,
13
14
  register_config_commands,
@@ -75,6 +76,15 @@ def _build_app(typer: Any, yaml: Any, console: Any, err_console: Any) -> Any:
75
76
  queue_app = typer.Typer(no_args_is_help=True)
76
77
  retry_app = typer.Typer(no_args_is_help=True)
77
78
  config_app = typer.Typer(no_args_is_help=True)
79
+
80
+ @app.callback(invoke_without_command=True)
81
+ def root_callback(
82
+ version: bool = typer.Option(False, "--version", help="Show version and exit"),
83
+ ) -> None:
84
+ if version:
85
+ console.print(__version__)
86
+ raise typer.Exit()
87
+
78
88
  command_context = CommandRegistryContext(
79
89
  typer=typer,
80
90
  console=console,
@@ -352,6 +352,8 @@ def process_queue_messages(
352
352
  )
353
353
  )
354
354
  if empty_queue:
355
+ if options.forever and options.block:
356
+ continue
355
357
  return handle_empty_queue(err_console, processed)
356
358
  assert iteration is not None
357
359
  if handle_processed_queue_result(
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "localqueue"
3
- version = "0.3.3"
3
+ version = "0.4.0"
4
4
  description = "Durable local queues for Python, with persistent retry state powered by Tenacity."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -58,6 +58,7 @@ from localqueue.cli_worker_commands import (
58
58
  )
59
59
  from localqueue.services.queue_worker import (
60
60
  QueueIterationContext as _QueueIterationContext,
61
+ QueueIterationResult as _QueueIterationResult,
61
62
  QueueWorkerOptions as _QueueWorkerOptions,
62
63
  _CommandExecutionError,
63
64
  _CommandNotFoundError,
@@ -214,6 +215,13 @@ class CliTests(unittest.TestCase):
214
215
  build_app.assert_called_once()
215
216
  app.assert_called_once_with(args=["--help"])
216
217
 
218
+ def test_root_version_option_prints_version(self) -> None:
219
+ with mock.patch("localqueue.cli.__version__", "9.9.9"):
220
+ result = self._invoke(["--version"])
221
+
222
+ self.assertEqual(result.exit_code, 0)
223
+ self.assertEqual(result.stdout.strip(), "9.9.9")
224
+
217
225
  def test_read_value_uses_argument_before_stdin(self) -> None:
218
226
  self.assertEqual(_read_value("{}"), "{}")
219
227
 
@@ -1747,6 +1755,55 @@ class CliTests(unittest.TestCase):
1747
1755
  self.assertEqual(console.values, [])
1748
1756
  self.assertEqual(err_console.messages, ["[yellow]queue is empty[/yellow]"])
1749
1757
 
1758
+ def test_process_queue_messages_keeps_waiting_when_forever_blocking_is_empty(
1759
+ self,
1760
+ ) -> None:
1761
+ queue = PersistentQueue("emails", store=MemoryQueueStore())
1762
+ console = _JsonConsole()
1763
+ err_console = _JsonConsole()
1764
+ shutdown = _ShutdownState()
1765
+
1766
+ def stop_after_second_poll(
1767
+ context: _QueueIterationContext,
1768
+ ) -> tuple[_QueueIterationResult | None, SQLiteAttemptStore | None, bool]:
1769
+ if shutdown.requested:
1770
+ return None, None, True
1771
+ shutdown.requested = True
1772
+ self.assertTrue(context.forever)
1773
+ self.assertTrue(context.block)
1774
+ return None, None, True
1775
+
1776
+ with mock.patch(
1777
+ "localqueue.services.queue_worker.process_queue_iteration",
1778
+ side_effect=stop_after_second_poll,
1779
+ ) as process_iteration:
1780
+ result = _process_queue_messages(
1781
+ queue,
1782
+ lambda payload: payload,
1783
+ console=console,
1784
+ err_console=err_console,
1785
+ shutdown=shutdown,
1786
+ options=_QueueWorkerOptions(
1787
+ retry_store_path=self._retry_store_path(),
1788
+ max_jobs=1,
1789
+ forever=True,
1790
+ max_tries=1,
1791
+ worker_id="worker-a",
1792
+ block=True,
1793
+ timeout=None,
1794
+ idle_sleep=0.001,
1795
+ release_delay=0.0,
1796
+ dead_letter_on_exhaustion=True,
1797
+ log_events=False,
1798
+ mode="process",
1799
+ ),
1800
+ )
1801
+
1802
+ self.assertEqual(result, 0)
1803
+ self.assertEqual(console.values, [{"state": "stopped", "processed": 0}])
1804
+ self.assertEqual(err_console.messages, [])
1805
+ self.assertEqual(process_iteration.call_count, 1)
1806
+
1750
1807
  def test_process_queue_iteration_handles_forever_empty_queue_without_blocking(
1751
1808
  self,
1752
1809
  ) -> None:
@@ -200,7 +200,7 @@ wheels = [
200
200
 
201
201
  [[package]]
202
202
  name = "localqueue"
203
- version = "0.3.3"
203
+ version = "0.4.0"
204
204
  source = { editable = "." }
205
205
  dependencies = [
206
206
  { name = "tenacity" },
@@ -1,111 +0,0 @@
1
- ---
2
- icon: lucide/package-check
3
- ---
4
-
5
- # Release
6
-
7
- This project currently uses a manual versioning flow. The release version lives
8
- in `pyproject.toml`, and each release should have a matching `CHANGELOG.md`
9
- entry and Git tag.
10
-
11
- ## Versioning
12
-
13
- Use SemVer-style versions:
14
-
15
- ```text
16
- MAJOR.MINOR.PATCH
17
- ```
18
-
19
- Before `1.0.0`, use this convention:
20
-
21
- | Change | Version bump |
22
- | --- | --- |
23
- | Bug fix only | patch, for example `0.1.1` |
24
- | New CLI/API feature | minor, for example `0.2.0` |
25
- | Breaking user-facing behavior | minor before `1.0.0`, with a clear changelog note |
26
-
27
- ## Checklist
28
-
29
- 1. Update `version` in `pyproject.toml`.
30
- 2. Run `uv lock` and commit the updated `uv.lock`.
31
- 3. Update `CHANGELOG.md`.
32
- 4. Confirm `requires-python`, license metadata, and PyPI classifiers still match
33
- supported Python versions and the intended release policy.
34
- 5. Run the test suite:
35
-
36
- ```bash
37
- uv run pytest
38
- ```
39
-
40
- 6. Run lint and typecheck:
41
-
42
- ```bash
43
- uv run ruff check .
44
- uv run basedpyright
45
- ```
46
-
47
- 7. Build the docs:
48
-
49
- ```bash
50
- uv run zensical build --clean
51
- ```
52
-
53
- 8. Confirm the lockfile is current:
54
-
55
- ```bash
56
- uv lock --check
57
- ```
58
-
59
- 9. Build the release artifacts:
60
-
61
- ```bash
62
- uv build
63
- ```
64
-
65
- 10. Inspect the artifacts:
66
-
67
- ```bash
68
- tar -tzf dist/localqueue-<version>.tar.gz | less
69
- python -m zipfile -l dist/localqueue-<version>-py3-none-any.whl
70
- ```
71
-
72
- 11. Install the wheel in a clean virtual environment:
73
-
74
- ```bash
75
- uv venv /tmp/localqueue-release-venv
76
- uv pip install --python /tmp/localqueue-release-venv/bin/python \
77
- "dist/localqueue-<version>-py3-none-any.whl[all]"
78
- /tmp/localqueue-release-venv/bin/localqueue --help
79
- ```
80
-
81
- 12. Run a local smoke test:
82
-
83
- ```bash
84
- uv run python examples/enqueue_email.py user@example.com \
85
- --store-path /tmp/localqueue-smoke
86
-
87
- uv run localqueue queue process emails examples.email_worker:send_email \
88
- --store-path /tmp/localqueue-smoke \
89
- --retry-store-path /tmp/localqueue-smoke-retries.sqlite3 \
90
- --worker-id worker-smoke \
91
- --max-tries 3
92
- ```
93
-
94
- 13. Create the release tag:
95
-
96
- ```bash
97
- git tag v<version>
98
- ```
99
-
100
- 14. Publish the artifacts.
101
-
102
- This release workflow also publishes the CLI image to GitHub Container
103
- Registry as `ghcr.io/brunoportis/localqueue:<tag>` and `:latest`.
104
-
105
- 15. Confirm the docs deployment finished successfully in the `Documentation`
106
- workflow. GitHub Pages must be configured to deploy from GitHub Actions in
107
- the repository settings.
108
-
109
- ## Current Release
110
-
111
- The current planned release is `0.3.3`.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes