queueio 0.6.0__tar.gz → 0.8.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 (116) hide show
  1. {queueio-0.6.0 → queueio-0.8.0}/.devcontainer/docker-compose.yml +4 -1
  2. queueio-0.8.0/.github/workflows/build.yml +92 -0
  3. {queueio-0.6.0 → queueio-0.8.0}/.github/workflows/docs.yml +1 -1
  4. {queueio-0.6.0 → queueio-0.8.0}/.gitignore +2 -0
  5. {queueio-0.6.0 → queueio-0.8.0}/CHANGELOG.md +40 -1
  6. {queueio-0.6.0 → queueio-0.8.0}/PKG-INFO +19 -8
  7. {queueio-0.6.0 → queueio-0.8.0}/README.md +12 -4
  8. {queueio-0.6.0 → queueio-0.8.0}/mkdocs.yml +1 -1
  9. {queueio-0.6.0 → queueio-0.8.0}/pyproject.toml +15 -7
  10. queueio-0.8.0/pyrightconfig.json +5 -0
  11. {queueio-0.6.0 → queueio-0.8.0}/queueio/__init__.py +2 -2
  12. {queueio-0.6.0 → queueio-0.8.0}/queueio/__main__.py +27 -48
  13. queueio-0.8.0/queueio/backend.py +18 -0
  14. {queueio-0.6.0 → queueio-0.8.0}/queueio/broker.py +7 -13
  15. {queueio-0.6.0 → queueio-0.8.0}/queueio/broker_test.py +29 -24
  16. queueio-0.8.0/queueio/conftest.py +98 -0
  17. {queueio-0.6.0 → queueio-0.8.0}/queueio/freethreading_test.py +5 -1
  18. {queueio-0.6.0 → queueio-0.8.0}/queueio/journal.py +0 -6
  19. {queueio-0.6.0 → queueio-0.8.0}/queueio/monitor.py +3 -3
  20. queueio-0.8.0/queueio/pika/__init__.py +31 -0
  21. queueio-0.8.0/queueio/pika/broker.py +83 -0
  22. queueio-0.8.0/queueio/pika/broker_test.py +38 -0
  23. {queueio-0.6.0 → queueio-0.8.0}/queueio/pika/journal.py +13 -12
  24. queueio-0.8.0/queueio/pika/journal_test.py +16 -0
  25. {queueio-0.6.0 → queueio-0.8.0}/queueio/pika/receiver.py +1 -0
  26. {queueio-0.6.0 → queueio-0.8.0}/queueio/pika/threadsafe.py +40 -1
  27. queueio-0.8.0/queueio/pika/threadsafe_test.py +20 -0
  28. queueio-0.8.0/queueio/psycopg/README.md +136 -0
  29. queueio-0.8.0/queueio/psycopg/broker-queries.md +135 -0
  30. queueio-0.8.0/queueio/psycopg/journal-queries.md +172 -0
  31. queueio-0.8.0/queueio/psycopg/journal.py +97 -0
  32. queueio-0.8.0/queueio/psycopg/journal_test.py +40 -0
  33. queueio-0.8.0/queueio/psycopg/receiver-queries.md +254 -0
  34. {queueio-0.6.0 → queueio-0.8.0}/queueio/queueio.py +52 -42
  35. queueio-0.8.0/queueio/queueio_test.py +330 -0
  36. {queueio-0.6.0 → queueio-0.8.0}/queueio/queuespec.md +4 -4
  37. {queueio-0.6.0 → queueio-0.8.0}/queueio/queuevar.md +1 -0
  38. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/basic_test.py +2 -4
  39. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django_test.py +3 -1
  40. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/expanded_test.py +2 -4
  41. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/priority_test.py +2 -4
  42. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/queuevar_test.py +2 -4
  43. {queueio-0.6.0 → queueio-0.8.0}/queueio/stream.py +2 -1
  44. queueio-0.8.0/queueio/stub/__init__.py +21 -0
  45. {queueio-0.6.0 → queueio-0.8.0}/queueio/stub/broker.py +21 -13
  46. queueio-0.8.0/queueio/stub/broker_test.py +15 -0
  47. {queueio-0.6.0 → queueio-0.8.0}/queueio/stub/journal.py +14 -6
  48. queueio-0.8.0/queueio/stub/journal_test.py +12 -0
  49. {queueio-0.6.0 → queueio-0.8.0}/queueio/stub/receiver.py +5 -4
  50. {queueio-0.6.0 → queueio-0.8.0}/queueio/worker.py +3 -2
  51. queueio-0.6.0/.github/workflows/build.yml +0 -42
  52. queueio-0.6.0/pyrightconfig.json +0 -4
  53. queueio-0.6.0/queueio/conftest.py +0 -32
  54. queueio-0.6.0/queueio/pika/broker.py +0 -68
  55. queueio-0.6.0/queueio/pika/broker_test.py +0 -23
  56. queueio-0.6.0/queueio/pika/journal_test.py +0 -20
  57. queueio-0.6.0/queueio/queue.py +0 -183
  58. queueio-0.6.0/queueio/queue_test.py +0 -729
  59. queueio-0.6.0/queueio/queueio_test.py +0 -558
  60. queueio-0.6.0/queueio/select.py +0 -170
  61. queueio-0.6.0/queueio/stub/__init__.py +0 -0
  62. queueio-0.6.0/queueio/stub/broker_test.py +0 -22
  63. queueio-0.6.0/queueio/stub/journal_test.py +0 -19
  64. {queueio-0.6.0 → queueio-0.8.0}/.devcontainer/devcontainer.json +0 -0
  65. {queueio-0.6.0 → queueio-0.8.0}/.github/workflows/publish.yml +0 -0
  66. {queueio-0.6.0 → queueio-0.8.0}/CNAME +0 -0
  67. {queueio-0.6.0 → queueio-0.8.0}/LICENSE +0 -0
  68. {queueio-0.6.0 → queueio-0.8.0}/dill.pyi +0 -0
  69. {queueio-0.6.0 → queueio-0.8.0}/docs_overrides/assets/images/favicon.svg +0 -0
  70. {queueio-0.6.0 → queueio-0.8.0}/docs_overrides/assets/images/logo.svg +0 -0
  71. {queueio-0.6.0 → queueio-0.8.0}/docs_overrides/assets/stylesheets/extra.css +0 -0
  72. {queueio-0.6.0 → queueio-0.8.0}/docs_overrides/partials/footer.html +0 -0
  73. {queueio-0.6.0 → queueio-0.8.0}/docs_overrides/partials/javascripts/palette.html +0 -0
  74. {queueio-0.6.0 → queueio-0.8.0}/logo.svg +0 -0
  75. {queueio-0.6.0 → queueio-0.8.0}/philosophy.md +0 -0
  76. {queueio-0.6.0 → queueio-0.8.0}/queueio/consumer.py +0 -0
  77. {queueio-0.6.0 → queueio-0.8.0}/queueio/continuation.py +0 -0
  78. /queueio-0.6.0/queueio/django.md → /queueio-0.8.0/queueio/django/README.md +0 -0
  79. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/__init__.py +0 -0
  80. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/apps.py +0 -0
  81. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/djp.py +0 -0
  82. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/management/__init__.py +0 -0
  83. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/management/commands/__init__.py +0 -0
  84. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/management/commands/runserver.py +0 -0
  85. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/management/commands/runserver_plus.py +0 -0
  86. {queueio-0.6.0 → queueio-0.8.0}/queueio/django/runserver_test.py +0 -0
  87. {queueio-0.6.0 → queueio-0.8.0}/queueio/event.py +0 -0
  88. {queueio-0.6.0 → queueio-0.8.0}/queueio/gather.py +0 -0
  89. {queueio-0.6.0 → queueio-0.8.0}/queueio/id.py +0 -0
  90. {queueio-0.6.0 → queueio-0.8.0}/queueio/invocation.md +0 -0
  91. {queueio-0.6.0 → queueio-0.8.0}/queueio/invocation.py +0 -0
  92. {queueio-0.6.0 → queueio-0.8.0}/queueio/journal_test.py +0 -0
  93. {queueio-0.6.0 → queueio-0.8.0}/queueio/message.py +0 -0
  94. {queueio-0.6.0 → queueio-0.8.0}/queueio/pause.py +0 -0
  95. {queueio-0.6.0/queueio/pika → queueio-0.8.0/queueio/psycopg}/__init__.py +0 -0
  96. {queueio-0.6.0 → queueio-0.8.0}/queueio/queuespec.py +0 -0
  97. {queueio-0.6.0 → queueio-0.8.0}/queueio/queuespec_test.py +0 -0
  98. {queueio-0.6.0 → queueio-0.8.0}/queueio/queuevar.py +0 -0
  99. {queueio-0.6.0 → queueio-0.8.0}/queueio/receiver.py +0 -0
  100. {queueio-0.6.0 → queueio-0.8.0}/queueio/registry.py +0 -0
  101. {queueio-0.6.0 → queueio-0.8.0}/queueio/result.py +0 -0
  102. {queueio-0.6.0 → queueio-0.8.0}/queueio/routine.py +0 -0
  103. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/__init__.py +0 -0
  104. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/basic.py +0 -0
  105. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django/__init__.py +0 -0
  106. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django/apps.py +0 -0
  107. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django/job.py +0 -0
  108. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django/models.py +0 -0
  109. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django/settings.py +0 -0
  110. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django/settings_djp.py +0 -0
  111. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/django/urls.py +0 -0
  112. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/expanded.py +0 -0
  113. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/priority.py +0 -0
  114. {queueio-0.6.0 → queueio-0.8.0}/queueio/samples/queuevar.py +0 -0
  115. {queueio-0.6.0 → queueio-0.8.0}/queueio/suspension.py +0 -0
  116. {queueio-0.6.0 → queueio-0.8.0}/queueio/thread.py +0 -0
@@ -6,5 +6,8 @@ services:
6
6
  command: sleep infinity
7
7
  rabbitmq:
8
8
  image: rabbitmq:management
9
+ environment:
10
+ RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: "-rabbit permit_deprecated_features #{global_qos=>true}"
9
11
  ports:
10
- - 15672:15672
12
+ - 127.0.0.1:5672:5672
13
+ - 15672:15672
@@ -0,0 +1,92 @@
1
+ name: Build
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ timeout-minutes: 5
9
+ permissions:
10
+ contents: read
11
+ id-token: write
12
+ services:
13
+ rabbitmq:
14
+ image: rabbitmq
15
+ env:
16
+ RABBITMQ_DEFAULT_USER: guest
17
+ RABBITMQ_DEFAULT_PASS: guest
18
+ RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: "-rabbit permit_deprecated_features #{global_qos=>true}"
19
+ ports:
20
+ - 5672/tcp
21
+ options: >-
22
+ --health-cmd "rabbitmq-diagnostics -q ping"
23
+ --health-interval 10s
24
+ --health-timeout 5s
25
+ --health-retries 5
26
+ rabbitmq_no_global_qos:
27
+ image: rabbitmq
28
+ env:
29
+ RABBITMQ_DEFAULT_USER: guest
30
+ RABBITMQ_DEFAULT_PASS: guest
31
+ RABBITMQ_SERVER_ADDITIONAL_ERL_ARGS: "-rabbit permit_deprecated_features #{global_qos=>false}"
32
+ ports:
33
+ - 5672/tcp
34
+ options: >-
35
+ --health-cmd "rabbitmq-diagnostics -q ping"
36
+ --health-interval 10s
37
+ --health-timeout 5s
38
+ --health-retries 5
39
+ lavinmq:
40
+ # 2.10.3 and later starve consumers sharing a channel with global prefetch.
41
+ # https://github.com/cloudamqp/lavinmq/issues/2628
42
+ image: cloudamqp/lavinmq:2.10.2
43
+ ports:
44
+ - 5672/tcp
45
+ options: >-
46
+ --health-cmd "lavinmqctl status"
47
+ --health-interval 10s
48
+ --health-timeout 5s
49
+ --health-retries 5
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - name: Install uv
53
+ uses: astral-sh/setup-uv@v3
54
+ - name: Set up Python
55
+ run: uv python install 3.15t
56
+ - name: Install dependencies
57
+ run: uv sync --all-groups
58
+ - name: Check formatting
59
+ run: uv run ruff format --check
60
+ - name: Run ruff linting
61
+ run: uv run ruff check
62
+ - name: Run basedpyright
63
+ run: uv run basedpyright
64
+ - name: Run tests
65
+ env:
66
+ QUEUEIO_BROKER: amqp://guest:guest@localhost:${{ job.services.rabbitmq.ports['5672'] }}/
67
+ run: uv run pytest --cov --cov-report=
68
+ timeout-minutes: 2
69
+ - name: Test broker without channel-wide QoS
70
+ env:
71
+ QUEUEIO_BROKER: amqp://guest:guest@localhost:${{ job.services.rabbitmq_no_global_qos.ports['5672'] }}/
72
+ run: uv run pytest -m no_global_qos --cov --cov-append --cov-report=
73
+ - name: Test LavinMQ
74
+ env:
75
+ QUEUEIO_BROKER: amqp://guest:guest@localhost:${{ job.services.lavinmq.ports['5672'] }}/
76
+ run: uv run pytest --cov --cov-append --cov-report=
77
+ timeout-minutes: 2
78
+ - name: Report coverage
79
+ run: |
80
+ uv run coverage report
81
+ uv run coverage xml
82
+ - name: Upload coverage to Codecov
83
+ uses: codecov/codecov-action@v5
84
+ with:
85
+ files: coverage.xml
86
+ disable_search: true
87
+ use_oidc: true
88
+ fail_ci_if_error: true
89
+ - name: Verify built package
90
+ run: |
91
+ uv build
92
+ uvx --from dist/queueio-*.whl queueio --help
@@ -21,7 +21,7 @@ jobs:
21
21
  steps:
22
22
  - uses: actions/checkout@v4
23
23
  - uses: astral-sh/setup-uv@v5
24
- - run: uv python install 3.14t
24
+ - run: uv python install 3.15t
25
25
  - run: uv sync --group dev
26
26
  - run: uv run mkdocs build --strict
27
27
  - uses: actions/upload-pages-artifact@v3
@@ -6,6 +6,8 @@ dist/
6
6
  wheels/
7
7
  *.egg-info
8
8
  .coverage*
9
+ coverage.xml
10
+ htmlcov/
9
11
 
10
12
  # Virtual environments
11
13
  .venv
@@ -8,6 +8,43 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com).
8
8
  [Unreleased]
9
9
  ------------
10
10
 
11
+ [0.8.0] - 2026-11-04
12
+ --------------------
13
+
14
+ ### Breaking Changes
15
+
16
+ - Python 3.15 or later is now required; Python 3.14 is no longer supported.
17
+
18
+ ### Changed
19
+
20
+ - Load the monitor UI lazily so other CLI commands avoid importing Textual.
21
+ - Use [noio](https://pypi.org/project/noio/) for internal queues and selection,
22
+ replacing `queueio.queue` and `queueio.select`.
23
+
24
+ ### Fixed
25
+
26
+ - A failed connection to the broker no longer leaves a thread running
27
+ that prevents the process from exiting.
28
+
29
+ [0.7.0] - 2026-03-09
30
+ --------------------
31
+
32
+ ### Breaking Changes
33
+
34
+ - The `pika` configuration key in `[tool.queueio]` has been renamed to `broker`.
35
+ - The `QUEUEIO_PIKA` environment variable has been renamed to `QUEUEIO_BROKER`.
36
+ - Removed `Broker.create(queue)`, in favor of `Broker.sync([queue])`.
37
+ - Removed `Broker.delete(queue)`, without replacement.
38
+ - Removed `QueueIO.create(queue)`, in favor of `QueueIO.sync([queue])`.
39
+ - Removed `QueueIO.delete(queue)`, without replacement.
40
+
41
+ ### Added
42
+
43
+ - Added `Broker.sync()` to back `queueio sync`.
44
+ - Added `QueueIO.sync()` to back `queueio sync`.
45
+ - `Backend` ABC with `broker()` and `journal()` methods
46
+ that return independent context managers with their own lifecycles.
47
+
11
48
  [0.6.0] - 2026-02-16
12
49
  --------------------
13
50
 
@@ -106,7 +143,9 @@ Thank you to Nick Anderegg for allowing me to use the queueio name for this proj
106
143
  - The queuespec syntax to `queue run` to consume multiple queues with shared capacity.
107
144
  - `queueio monitor` command to monitor activity in the queueio system.
108
145
 
109
- [Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.6.0...HEAD
146
+ [Unreleased]: https://github.com/ryanhiebert/queueio/compare/tag/0.8.0...HEAD
147
+ [0.8.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.7.0...tag/0.8.0
148
+ [0.7.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.6.0...tag/0.7.0
110
149
  [0.6.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.5.0...tag/0.6.0
111
150
  [0.5.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.4.0...tag/0.5.0
112
151
  [0.4.0]: https://github.com/ryanhiebert/queueio/compare/tag/0.3.0...tag/0.4.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: queueio
3
- Version: 0.6.0
3
+ Version: 0.8.0
4
4
  Summary: Python background queues with an async twist
5
5
  Author-email: Ryan Hiebert <ryan@ryanhiebert.com>
6
6
  License-Expression: MIT
@@ -10,17 +10,20 @@ Classifier: Development Status :: 3 - Alpha
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Programming Language :: Python :: 3.15
14
14
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
15
  Classifier: Topic :: System :: Distributed Computing
16
- Requires-Python: >=3.14
16
+ Requires-Python: >=3.15
17
17
  Requires-Dist: dill>=0.3.9
18
+ Requires-Dist: noio>=0.3
18
19
  Requires-Dist: pika>=1.3.2
19
20
  Requires-Dist: textual>=3.2.0
20
21
  Requires-Dist: typer>=0.15.4
21
22
  Provides-Extra: django
22
23
  Requires-Dist: django-cmd>=3.0; extra == 'django'
23
24
  Requires-Dist: django>=6.0; extra == 'django'
25
+ Provides-Extra: psycopg
26
+ Requires-Dist: psycopg>=3.3; extra == 'psycopg'
24
27
  Description-Content-Type: text/markdown
25
28
 
26
29
  ![queueio](https://raw.githubusercontent.com/ryanhiebert/queueio/main/logo.svg)
@@ -71,17 +74,25 @@ Add the configuration to your `pyproject.toml`:
71
74
 
72
75
  ```toml
73
76
  [tool.queueio]
74
- # Configure RabbitMQ
75
- pika = "amqp://guest:guest@localhost:5672/"
77
+ # Configure the broker (currently supports amqp:// URIs)
78
+ broker = "amqp://guest:guest@localhost:5672/"
76
79
  # Register the modules that the worker should load to find your routines
77
80
  register = ["basic"]
78
81
  ```
79
82
 
80
- The pika configuration can be overridden with an environment variable
83
+ The broker configuration can be overridden with an environment variable
81
84
  to allow a project to be deployed in multiple environments.
82
85
 
83
86
  ```sh
84
- QUEUEIO_PIKA='amqp://guest:guest@localhost:5672/'
87
+ QUEUEIO_BROKER='amqp://guest:guest@localhost:5672/'
88
+ ```
89
+
90
+ QueueIO requires RabbitMQ's global QoS for shared concurrency limits and
91
+ suspending tasks. On RabbitMQ 4.3, enable it in `rabbitmq.conf` and restart
92
+ the broker:
93
+
94
+ ```ini
95
+ deprecated_features.permit.global_qos = true
85
96
  ```
86
97
 
87
98
  Sync the queues to the broker:
@@ -46,17 +46,25 @@ Add the configuration to your `pyproject.toml`:
46
46
 
47
47
  ```toml
48
48
  [tool.queueio]
49
- # Configure RabbitMQ
50
- pika = "amqp://guest:guest@localhost:5672/"
49
+ # Configure the broker (currently supports amqp:// URIs)
50
+ broker = "amqp://guest:guest@localhost:5672/"
51
51
  # Register the modules that the worker should load to find your routines
52
52
  register = ["basic"]
53
53
  ```
54
54
 
55
- The pika configuration can be overridden with an environment variable
55
+ The broker configuration can be overridden with an environment variable
56
56
  to allow a project to be deployed in multiple environments.
57
57
 
58
58
  ```sh
59
- QUEUEIO_PIKA='amqp://guest:guest@localhost:5672/'
59
+ QUEUEIO_BROKER='amqp://guest:guest@localhost:5672/'
60
+ ```
61
+
62
+ QueueIO requires RabbitMQ's global QoS for shared concurrency limits and
63
+ suspending tasks. On RabbitMQ 4.3, enable it in `rabbitmq.conf` and restart
64
+ the broker:
65
+
66
+ ```ini
67
+ deprecated_features.permit.global_qos = true
60
68
  ```
61
69
 
62
70
  Sync the queues to the broker:
@@ -37,5 +37,5 @@ nav:
37
37
  - Invocation: queueio/invocation.md
38
38
  - QueueSpec: queueio/queuespec.md
39
39
  - QueueVar: queueio/queuevar.md
40
- - Django: queueio/django.md
40
+ - Django: queueio/django/README.md
41
41
  - Changelog: CHANGELOG.md
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "queueio"
3
- version = "0.6.0"
3
+ version = "0.8.0"
4
4
  description = "Python background queues with an async twist"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -15,13 +15,14 @@ classifiers = [
15
15
  "Intended Audience :: Developers",
16
16
  "License :: OSI Approved :: MIT License",
17
17
  "Programming Language :: Python :: 3",
18
- "Programming Language :: Python :: 3.14",
18
+ "Programming Language :: Python :: 3.15",
19
19
  "Topic :: Software Development :: Libraries :: Python Modules",
20
20
  "Topic :: System :: Distributed Computing",
21
21
  ]
22
- requires-python = ">=3.14"
22
+ requires-python = ">=3.15"
23
23
  dependencies = [
24
24
  "dill>=0.3.9",
25
+ "noio>=0.3",
25
26
  "pika>=1.3.2",
26
27
  "textual>=3.2.0",
27
28
  "typer>=0.15.4",
@@ -29,6 +30,7 @@ dependencies = [
29
30
 
30
31
  [project.optional-dependencies]
31
32
  django = ["django>=6.0", "django-cmd>=3.0"]
33
+ psycopg = ["psycopg>=3.3"]
32
34
 
33
35
  [project.entry-points.djp]
34
36
  queueio = "queueio.django.djp"
@@ -38,18 +40,20 @@ queueio = "queueio.__main__:app"
38
40
 
39
41
  [dependency-groups]
40
42
  dev = [
41
- "basedpyright>=1.32.1",
43
+ "basedpyright>=1.40.2",
42
44
  "django-extensions",
43
45
  "djp",
44
46
  "queueio[django]",
47
+ "queueio[psycopg]",
45
48
  "mkdocs-material>=9.7.1",
46
49
  "mkdocs-same-dir>=0.1.3",
47
50
  "pytest>=8.4.1",
48
- "pytest-cov>=6.0.0",
51
+ "pytest-cov>=7.0.0",
49
52
  "pytest-timeout>=2.4.0",
50
- "ruff>=0.9.6",
53
+ "ruff>=0.17.0",
51
54
  "textual-dev>=1.7.0",
52
55
  "types-pika>=1.2.0b1",
56
+ "psycopg>=3.3.3",
53
57
  ]
54
58
 
55
59
  [tool.ruff.lint]
@@ -65,15 +69,19 @@ register = [
65
69
  "queueio.samples.priority",
66
70
  "queueio.samples.queuevar",
67
71
  ]
68
- pika = "amqp://localhost:5672"
72
+ broker = "amqp://localhost:5672"
69
73
 
70
74
  [tool.pytest.ini_options]
75
+ markers = ["no_global_qos: requires a broker without channel-wide QoS"]
76
+ addopts = "-m 'not no_global_qos'"
71
77
  timeout = 30
72
78
  norecursedirs = ["site"]
73
79
 
74
80
  [tool.coverage.run]
75
81
  branch = true
76
82
  source = ["queueio"]
83
+ patch = ["subprocess"]
84
+ sigterm = true
77
85
 
78
86
  [build-system]
79
87
  requires = ["hatchling"]
@@ -0,0 +1,5 @@
1
+ {
2
+ "pythonVersion": "3.15",
3
+ "typeCheckingMode": "standard",
4
+ "exclude": ["site", ".venv"]
5
+ }
@@ -2,7 +2,7 @@ from contextlib import contextmanager
2
2
 
3
3
  from .gather import gather as gather
4
4
  from .pause import pause as pause
5
- from .queueio import QueueIO as RealQueueIO
5
+ from .queueio import QueueIO as QueueIO
6
6
  from .queueio import priority as priority
7
7
  from .queuevar import QueueVar as QueueVar
8
8
  from .registry import routine as routine
@@ -10,5 +10,5 @@ from .registry import routine as routine
10
10
 
11
11
  @contextmanager
12
12
  def activate():
13
- with RealQueueIO().activate():
13
+ with QueueIO.default() as queueio, queueio.activate():
14
14
  yield
@@ -1,14 +1,13 @@
1
- from contextlib import suppress
2
1
  from typing import Annotated
3
2
 
4
3
  import typer
5
4
  from typer import Argument
6
5
  from typer import Typer
7
6
 
8
- from .monitor import Monitor
9
7
  from .queueio import QueueIO
10
8
  from .queuespec import QueueSpec
11
9
  from .worker import Worker
10
+ lazy from .monitor import Monitor
12
11
 
13
12
  app = Typer()
14
13
 
@@ -32,8 +31,7 @@ app.add_typer(
32
31
  @routine_app.command("list")
33
32
  def routine_list():
34
33
  """Show all registered routines."""
35
- queueio = QueueIO()
36
- try:
34
+ with QueueIO.default() as queueio:
37
35
  routines = queueio.routines()
38
36
 
39
37
  if not routines:
@@ -53,8 +51,6 @@ def routine_list():
53
51
  print(f"{'-' * name_width}-+-{'-' * path_width}")
54
52
  for routine, path in zip(routines, function_paths, strict=False):
55
53
  print(f"{routine.name:<{name_width}} | {path:<{path_width}}")
56
- finally:
57
- queueio.shutdown()
58
54
 
59
55
 
60
56
  @app.command(rich_help_panel="Commands")
@@ -64,17 +60,16 @@ def monitor(raw: bool = False):
64
60
  Show a live view of queueio activity. Use --raw for detailed event output.
65
61
  """
66
62
  if raw:
67
- queueio = QueueIO()
68
- events = queueio.subscribe({object})
69
- try:
70
- while True:
71
- print(events.get())
72
- except KeyboardInterrupt:
73
- print("Shutting down gracefully.")
74
- finally:
75
- queueio.shutdown()
63
+ with QueueIO.default() as queueio:
64
+ events = queueio.subscribe({object})
65
+ try:
66
+ while True:
67
+ print(events.get())
68
+ except KeyboardInterrupt:
69
+ print("Shutting down gracefully.")
76
70
  else:
77
- Monitor().run()
71
+ with QueueIO.default() as queueio:
72
+ Monitor(queueio).run()
78
73
 
79
74
 
80
75
  @app.command(rich_help_panel="Commands")
@@ -94,8 +89,8 @@ def run(
94
89
  The worker will process invocations from the specified queue,
95
90
  as many at a time as specified by the concurrency.
96
91
  """
97
- queueio = QueueIO()
98
- Worker(queueio, queuespec)()
92
+ with QueueIO.default() as queueio:
93
+ Worker(queueio, queuespec)()
99
94
 
100
95
 
101
96
  @app.command(rich_help_panel="Commands")
@@ -103,14 +98,13 @@ def sync(
103
98
  recreate: Annotated[
104
99
  bool,
105
100
  typer.Option(
106
- help="Delete and recreate queues that have incompatible arguments. "
107
- "WARNING: This will lose any pending messages in those queues.",
101
+ help="Recreate all resources for the broker and journal. "
102
+ "WARNING: This will lose pending messages.",
108
103
  ),
109
104
  ] = False,
110
105
  ):
111
- """Sync known queues to the broker."""
112
- queueio = QueueIO()
113
- try:
106
+ """Sync resources for the broker and journal."""
107
+ with QueueIO.default() as queueio:
114
108
  routines = queueio.routines()
115
109
 
116
110
  if not routines:
@@ -119,33 +113,21 @@ def sync(
119
113
 
120
114
  queues = sorted({routine.queue for routine in routines})
121
115
 
122
- print(f"Syncing queues for {len(routines)} routine(s):")
123
- if recreate:
124
- for queue in queues:
125
- print(f" Recreating queue: {queue}")
126
- with suppress(Exception):
127
- queueio.delete(queue=queue)
128
-
129
- failed = []
116
+ print(f"Discovered queues for {len(routines)} routine(s):")
130
117
  for queue in queues:
131
- print(f" Ensuring queue exists: {queue}")
132
- try:
133
- queueio.create(queue=queue)
134
- except Exception:
135
- failed.append(queue)
118
+ print(f" {queue}")
136
119
 
137
- if failed:
120
+ try:
121
+ queueio.sync(queues, recreate=recreate)
122
+ except Exception:
138
123
  print(
139
- f"\nError: {len(failed)} queue(s) have incompatible arguments: "
140
- f"{', '.join(failed)}\n"
141
- f"Re-run with --recreate to delete and recreate them.\n"
142
- f"WARNING: This will lose any pending messages in those queues."
124
+ "\nError: Failed to sync resources.\n"
125
+ "Re-run with --recreate to recreate them.\n"
126
+ "WARNING: This will lose pending messages."
143
127
  )
144
- raise typer.Exit(1)
128
+ raise typer.Exit(1) from None
145
129
 
146
130
  print(f"Successfully synced {len(queues)} queue(s)")
147
- finally:
148
- queueio.shutdown()
149
131
 
150
132
 
151
133
  @queue_app.command("purge")
@@ -164,8 +146,7 @@ def queue_purge(
164
146
  This will remove all pending messages from the given queues.
165
147
  Use with caution as this operation cannot be undone.
166
148
  """
167
- queueio = QueueIO()
168
- try:
149
+ with QueueIO.default() as queueio:
169
150
  queue_list = [q.strip() for q in queues.split(",") if q.strip()]
170
151
  if not queue_list:
171
152
  print("Error: No valid queue names provided")
@@ -176,8 +157,6 @@ def queue_purge(
176
157
  queueio.purge(queue=queue)
177
158
 
178
159
  print(f"Successfully purged {len(queue_list)} queue(s)")
179
- finally:
180
- queueio.shutdown()
181
160
 
182
161
 
183
162
  if __name__ == "__main__":
@@ -0,0 +1,18 @@
1
+ from abc import ABC
2
+ from abc import abstractmethod
3
+ from contextlib import AbstractContextManager
4
+
5
+ from .broker import Broker
6
+ from .journal import Journal
7
+
8
+
9
+ class Backend(ABC):
10
+ """A backend provides context-managed access to a broker and journal."""
11
+
12
+ @abstractmethod
13
+ def broker(self) -> AbstractContextManager[Broker]:
14
+ raise NotImplementedError("Subclasses must implement this method.")
15
+
16
+ @abstractmethod
17
+ def journal(self) -> AbstractContextManager[Journal]:
18
+ raise NotImplementedError("Subclasses must implement this method.")
@@ -1,5 +1,6 @@
1
1
  from abc import ABC
2
2
  from abc import abstractmethod
3
+ from collections.abc import Iterable
3
4
 
4
5
  from .queuespec import QueueSpec
5
6
  from .receiver import Receiver
@@ -13,10 +14,13 @@ class Broker(ABC):
13
14
  at-least-once delivery.
14
15
  """
15
16
 
16
- @classmethod
17
17
  @abstractmethod
18
- def from_uri(cls, uri: str, /):
19
- """Create a broker instance from a URI."""
18
+ def sync(self, queues: Iterable[str], *, recreate: bool = False):
19
+ """Ensure the given queues are ready to use.
20
+
21
+ If recreate is True, destroy and recreate all resources,
22
+ losing any pending messages.
23
+ """
20
24
  raise NotImplementedError("Subclasses must implement this method.")
21
25
 
22
26
  @abstractmethod
@@ -24,16 +28,6 @@ class Broker(ABC):
24
28
  """Enqueue a message."""
25
29
  raise NotImplementedError("Subclasses must implement this method.")
26
30
 
27
- @abstractmethod
28
- def create(self, *, queue: str):
29
- """Create a queue if it doesn't exist."""
30
- raise NotImplementedError("Subclasses must implement this method.")
31
-
32
- @abstractmethod
33
- def delete(self, *, queue: str):
34
- """Delete a queue."""
35
- raise NotImplementedError("Subclasses must implement this method.")
36
-
37
31
  @abstractmethod
38
32
  def purge(self, *, queue: str):
39
33
  """Purge all messages from the queue."""